mirror of
https://github.com/amark/gun.git
synced 2025-11-23 22:15:55 +00:00
relay-sqlite-example (#1403)
* relay-sqlite-example Manager+Adapter * Update README.md
This commit is contained in:
parent
4b43fa7f2c
commit
938697dee9
26
examples/relay-sqlite-example/README.md
Normal file
26
examples/relay-sqlite-example/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# gun-relay-sqlite
|
||||
|
||||
Manager+Adapter Stable version
|
||||
|
||||
Support iOS & Android
|
||||
|
||||
# Quickly build a relay manager and Gun storage adapter with sqlite persistence.
|
||||
|
||||
```base
|
||||
yarn install
|
||||
```
|
||||
|
||||
```base
|
||||
yarn build
|
||||
```
|
||||
|
||||
```base
|
||||
npx cap sync
|
||||
```
|
||||
|
||||
```base
|
||||
npx cap open ios | android
|
||||
```
|
||||
|
||||
# Preview
|
||||

|
||||
43
examples/relay-sqlite-example/capacitor.config.ts
Normal file
43
examples/relay-sqlite-example/capacitor.config.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { CapacitorConfig } from '@capacitor/cli';
|
||||
import { CapacitorHttp } from '@capacitor/core';
|
||||
import { KeyboardResize, KeyboardStyle } from '@capacitor/keyboard'
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'com.gun.relay',
|
||||
appName: 'Relay',
|
||||
webDir: 'dist',
|
||||
|
||||
|
||||
plugins: {
|
||||
CapacitorHttp: {
|
||||
enabled: true,
|
||||
},
|
||||
|
||||
Keyboard: {
|
||||
resize: KeyboardResize.None,
|
||||
resizeOnFullScreen: true,
|
||||
|
||||
},
|
||||
CapacitorSQLite: {
|
||||
migrate: true,
|
||||
iosDatabaseLocation: 'Library/CapacitorDatabase',
|
||||
iosIsEncryption: true,
|
||||
iosKeychainPrefix: 'gundb',
|
||||
iosBiometric: {
|
||||
biometricAuth: false,
|
||||
biometricTitle : "Biometric login for capacitor sqlite"
|
||||
},
|
||||
androidIsEncryption: true,
|
||||
androidBiometric: {
|
||||
biometricAuth : false,
|
||||
biometricTitle : "Biometric login for capacitor sqlite",
|
||||
biometricSubTitle : "Log in using your biometric"
|
||||
},
|
||||
electronIsEncryption: true,
|
||||
electronWindowsLocation: "C:\\ProgramData\\CapacitorDatabases",
|
||||
electronMacLocation: "~/Databases/",
|
||||
electronLinuxLocation: "Databases"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
31
examples/relay-sqlite-example/index.html
Normal file
31
examples/relay-sqlite-example/index.html
Normal file
@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Gun-Relay-sqlite</title>
|
||||
|
||||
<base href="/" />
|
||||
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
|
||||
<!-- add to homescreen for ios -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-title" content="Gun-Relay-sqlite" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
204
examples/relay-sqlite-example/package.json
Normal file
204
examples/relay-sqlite-example/package.json
Normal file
@ -0,0 +1,204 @@
|
||||
{
|
||||
"name": "gun-relay-sqlite",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "npm run copy:sql:wasm && vite --host",
|
||||
"build:web": "npm run copy:sql:wasm && npm run build",
|
||||
"build:native": "npm run remove:sql:wasm && npm run build",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"ionic:serve:before": "npm run copy:sql:wasm",
|
||||
"copy:sql:wasm": "copyfiles -u 3 node_modules/sql.js/dist/sql-wasm.wasm public/assets",
|
||||
"remove:sql:wasm": "rimraf public/assets/sql-wasm.wasm",
|
||||
"ios:start": "npm run remove:sql:wasm && npm run build:native && npx cap sync && npx cap copy && npx cap open ios",
|
||||
"android:start": "npm run remove:sql:wasm && npm run build:native && npx cap sync && npx cap copy && npx cap open android",
|
||||
"electron:install": "cd electron && npm install && cd ..",
|
||||
"electron:prepare": "npm run remove:sql:wasm && npm run build && npx cap sync @capacitor-community/electron && npx cap copy @capacitor-community/electron",
|
||||
"electron:start": "npm run electron:prepare && cd electron && npm run electron:start",
|
||||
"clean:vite:cache": "vite clean",
|
||||
"test:e2e": "cypress run",
|
||||
"test:unit": "vitest",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/core": "^19.1.6",
|
||||
"@aparajita/capacitor-biometric-auth": "^9.0.0",
|
||||
"@aparajita/capacitor-secure-storage": "^6.0.1",
|
||||
"@capacitor-community/barcode-scanner": "^4.0.1",
|
||||
"@capacitor-community/electron": "^4.1.2",
|
||||
"@capacitor-community/media": "^8.0.0",
|
||||
"@capacitor-community/speech-recognition": "^6.0.1",
|
||||
"@capacitor-community/sqlite": "^5.2.3",
|
||||
"@capacitor/action-sheet": "^7.0.0",
|
||||
"@capacitor/android": "^7.0.0",
|
||||
"@capacitor/app": "^7.0.0",
|
||||
"@capacitor/assets": "latest",
|
||||
"@capacitor/background-runner": "^2.1.0",
|
||||
"@capacitor/browser": "^7.0.0",
|
||||
"@capacitor/camera": "^7.0.0",
|
||||
"@capacitor/clipboard": "^7.0.0",
|
||||
"@capacitor/core": "^7.0.0",
|
||||
"@capacitor/device": "^7.0.0",
|
||||
"@capacitor/dialog": "^7.0.0",
|
||||
"@capacitor/filesystem": "^7.0.0",
|
||||
"@capacitor/geolocation": "^7.0.0",
|
||||
"@capacitor/haptics": "^7.0.0",
|
||||
"@capacitor/ios": "^7.0.0",
|
||||
"@capacitor/keyboard": "^7.0.0",
|
||||
"@capacitor/local-notifications": "^7.0.0",
|
||||
"@capacitor/network": "^7.0.0",
|
||||
"@capacitor/preferences": "^7.0.0",
|
||||
"@capacitor/push-notifications": "^7.0.0",
|
||||
"@capacitor/status-bar": "^7.0.0",
|
||||
"@capacitor/toast": "^7.0.0",
|
||||
"@ffmpeg/core": "^0.12.10",
|
||||
"@ffmpeg/ffmpeg": "^0.12.15",
|
||||
"@ffmpeg/util": "^0.12.2",
|
||||
"@gun-vue/composables": "^0.24.2",
|
||||
"@gun-vue/gun-es": "^0.3.1240",
|
||||
"@iconify/tailwind": "latest",
|
||||
"@ionic-native/background-mode": "^5.36.0",
|
||||
"@ionic-native/core": "^5.36.0",
|
||||
"@ionic-native/fingerprint-aio": "^5.36.0",
|
||||
"@ionic-native/media": "^5.36.0",
|
||||
"@ionic-native/qr-scanner": "^5.36.0",
|
||||
"@ionic-native/secure-storage": "^5.36.0",
|
||||
"@ionic-native/sqlite": "^5.36.0",
|
||||
"@ionic/cli": "latest",
|
||||
"@ionic/pwa-elements": "^3.2.2",
|
||||
"@ionic/storage": "^4.0.0",
|
||||
"@ionic/storage-angular": "^4.0.0",
|
||||
"@ionic/vue": "^7.0.0",
|
||||
"@ionic/vue-router": "^7.0.0",
|
||||
"@joyid/capacitor-native-passkey": "^0.0.3",
|
||||
"@peculiar/webcrypto": "^1.5.0",
|
||||
"@rollup/plugin-inject": "^5.0.5",
|
||||
"@scure/bip39": "^1.6.0",
|
||||
"@simplewebauthn/browser": "^13.1.0",
|
||||
"@tresjs/core": "latest",
|
||||
"@tweenjs/tween.js": "latest",
|
||||
"@types/gun": "^0.9.6",
|
||||
"@types/text-encoding": "^0.0.40",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/webrtc": "^0.0.44",
|
||||
"@vue/language-plugin-pug": "^2.2.2",
|
||||
"@vueuse/core": "latest",
|
||||
"@vueuse/gesture": "^2.0.0",
|
||||
"add": "^2.0.6",
|
||||
"animate.css": "latest",
|
||||
"autopass": "^2.1.0",
|
||||
"axios": "latest",
|
||||
"b4a": "^1.6.7",
|
||||
"better-scroll": "^2.5.1",
|
||||
"btoa": "^1.2.1",
|
||||
"buffer": "^6.0.3",
|
||||
"capacitor-voice-recorder": "^7.0.5",
|
||||
"cordova-plugin-background-mode": "^0.7.3",
|
||||
"cordova-plugin-device": "^3.0.0",
|
||||
"cordova-plugin-fingerprint-aio": "^6.0.0",
|
||||
"cordova-plugin-secure-storage-echo": "^5.1.1",
|
||||
"cordova-sqlite-storage": "^7.0.0",
|
||||
"corestore": "^7.1.0",
|
||||
"crypto-browserify": "^3.12.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"cryptojs": "^2.5.3",
|
||||
"elliptic": "^6.6.1",
|
||||
"ffmpeg": "^0.0.4",
|
||||
"gsap": "^3.13.0",
|
||||
"gun": "^0.2020.1240",
|
||||
"gun-avatar": "^2.2.2",
|
||||
"gun-flint": "^0.0.28",
|
||||
"ionicons": "^7.4.0",
|
||||
"jsqr": "latest",
|
||||
"localforage-cordovasqlitedriver": "^1.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"native-run": "^2.0.1",
|
||||
"node-forge": "^1.3.1",
|
||||
"nvm": "latest",
|
||||
"peerjs": "^1.5.4",
|
||||
"photoswipe": "^5.4.4",
|
||||
"pinia": "^3.0.1",
|
||||
"pinyin-pro": "^3.26.0",
|
||||
"postcss": "latest",
|
||||
"postcss-loader": "latest",
|
||||
"pug": "^3.0.3",
|
||||
"qrcode.vue": "latest",
|
||||
"qrcodejs2": "latest",
|
||||
"sass": "latest",
|
||||
"socket.io-client": "latest",
|
||||
"stats.js": "latest",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"swiper": "^11.2.6",
|
||||
"tailwind": "latest",
|
||||
"text-encoding": "^0.7.0",
|
||||
"three": "latest",
|
||||
"three-stdlib": "^2.35.16",
|
||||
"tweakpane": "latest",
|
||||
"uint8arrays": "latest",
|
||||
"unplugin-auto-import": "^19.0.0",
|
||||
"uuid": "^11.1.0",
|
||||
"vite-plugin-pwa": "latest",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"vue": "latest",
|
||||
"vue-chartjs": "latest",
|
||||
"vue-count-to": "^1.0.13",
|
||||
"vue-i18n": "latest",
|
||||
"vue-image-lightbox": "^7.2.0",
|
||||
"vue-lazyload": "^3.0.0",
|
||||
"vue-router": "^4.1.6",
|
||||
"vue-virtual-scroller": "^2.0.0-beta.8",
|
||||
"vue3-carousel": "^0.15.0",
|
||||
"vue3-photo-preview": "^0.3.0",
|
||||
"vue3-qrcode-reader": "latest",
|
||||
"vue3-touch-events": "latest",
|
||||
"webrtc-adapter": "^9.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/cli": "^7.0.0",
|
||||
"@iconify-json/tabler": "latest",
|
||||
"@iconify/json": "latest",
|
||||
"@tsconfig/node22": "latest",
|
||||
"@types/jsdom": "latest",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/stats.js": "latest",
|
||||
"@types/three": "latest",
|
||||
"@unocss/preset-icons": "^65.4.2",
|
||||
"@unocss/preset-uno": "^65.4.2",
|
||||
"@vitejs/plugin-legacy": "^4.0.2",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vitest/eslint-plugin": "latest",
|
||||
"@vue/eslint-config-prettier": "latest",
|
||||
"@vue/eslint-config-typescript": "^11.0.2",
|
||||
"@vue/test-utils": "^2.3.0",
|
||||
"@vue/tsconfig": "latest",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cypress": "^13.1.0",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-plugin-oxlint": "latest",
|
||||
"eslint-plugin-vue": "^9.9.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"npm-run-all2": "latest",
|
||||
"oxlint": "latest",
|
||||
"prettier": "latest",
|
||||
"process": "^0.11.10",
|
||||
"rimraf": "^5.0.1",
|
||||
"tailwindcss": "latest",
|
||||
"terser": "^5.37.0",
|
||||
"typescript": "^5.1.6",
|
||||
"unocss": "^65.4.2",
|
||||
"unplugin-vue-components": "latest",
|
||||
"vite": "^latest",
|
||||
"vite-plugin-node-polyfills": "^0.23.0",
|
||||
"vite-plugin-pages": "latest",
|
||||
"vite-plugin-vue-devtools": "latest",
|
||||
"vitest": "latest",
|
||||
"vue-tsc": "latest"
|
||||
},
|
||||
"description": "gun-sqlite-relay",
|
||||
"main": "index.js",
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
131
examples/relay-sqlite-example/src/App.vue
Normal file
131
examples/relay-sqlite-example/src/App.vue
Normal file
@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<ion-app>
|
||||
|
||||
|
||||
|
||||
|
||||
<ion-router-outlet />
|
||||
|
||||
|
||||
|
||||
</ion-app>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import { IonApp, IonRouterOutlet } from '@ionic/vue';
|
||||
|
||||
|
||||
|
||||
function setupNetworkListener() {
|
||||
let debounceTimer: NodeJS.Timeout;
|
||||
window.addEventListener('online', async () => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(async () => {
|
||||
|
||||
|
||||
|
||||
}, 500);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 保存原始 console.warn
|
||||
const originalConsoleWarn = console.warn;
|
||||
|
||||
|
||||
function filterGunWarnings(...args: any[]) {
|
||||
const message = args[0]?.toString() || '';
|
||||
if (message.includes('Deprecated internal utility will break in next version')) {
|
||||
return; // 忽略 Gun.js 警告
|
||||
}
|
||||
originalConsoleWarn.apply(console, args);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
console.warn = filterGunWarnings;
|
||||
await setupNetworkListener();
|
||||
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('online', () => {});
|
||||
window.removeEventListener('offline', () => {});
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 0px; /* 滚动条宽度 */
|
||||
background-color: transparent; /* 透明背景 */
|
||||
}
|
||||
|
||||
/* 滚动条滑块 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: transparent; /* 半透明的滑块颜色 */
|
||||
border-radius: 4px; /* 圆角 */
|
||||
}
|
||||
|
||||
/* 滚动条轨道 */
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent; /* 透明轨道 */
|
||||
}
|
||||
|
||||
|
||||
.ion-page,
|
||||
ion-content {
|
||||
background: transparent !important;
|
||||
--background: transparent !important;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* touch-action: none;
|
||||
touch-action: pan-x pan-y;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none; */
|
||||
background-color: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
</style>
|
||||
638
examples/relay-sqlite-example/src/components/RelayMode.vue
Normal file
638
examples/relay-sqlite-example/src/components/RelayMode.vue
Normal file
@ -0,0 +1,638 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useNetworkStatus } from '@/composables/useNetworkStatus';
|
||||
import {
|
||||
IonIcon,
|
||||
IonToggle,
|
||||
IonModal,
|
||||
IonHeader,
|
||||
IonToolbar,
|
||||
IonTitle,
|
||||
IonContent,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonInput,
|
||||
IonButton,
|
||||
IonButtons
|
||||
} from '@ionic/vue';
|
||||
import { addCircleSharp, closeCircleSharp, searchSharp, closeSharp, refreshOutline } from 'ionicons/icons';
|
||||
import StorageService from '@/services/storageService';
|
||||
import { getCurrentInstance } from 'vue';
|
||||
|
||||
|
||||
const appInstance = getCurrentInstance();
|
||||
const storageServ = appInstance?.appContext.config.globalProperties.$storageServ as StorageService;
|
||||
|
||||
|
||||
const {
|
||||
networkStatus,
|
||||
peersStatus,
|
||||
currentMode,
|
||||
peerStatuses,
|
||||
peersList,
|
||||
enabledPeer,
|
||||
addPeer,
|
||||
removePeer,
|
||||
enablePeer,
|
||||
disablePeer,
|
||||
peersNotes,
|
||||
savePeerNote,
|
||||
} = useNetworkStatus(storageServ);
|
||||
|
||||
|
||||
const newPeerUrl = ref('');
|
||||
const searchQuery = ref('');
|
||||
const selectedPeer = ref<string | null>(null);
|
||||
const isModalOpen = ref(false);
|
||||
const notes = ref<Record<string, string>>({});
|
||||
const isResetting = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const savedNotes = localStorage.getItem('relayNotes');
|
||||
if (savedNotes) {
|
||||
const notes = JSON.parse(savedNotes);
|
||||
for (const [peer, note] of Object.entries(notes)) {
|
||||
await savePeerNote(peer as string, note as string);
|
||||
}
|
||||
localStorage.removeItem('relayNotes'); // 迁移后删除
|
||||
console.log('Migrated relay notes to SQLite');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to migrate relay notes:', e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
watch(peersNotes, async (newNotes) => {
|
||||
for (const [peer, note] of Object.entries(newNotes)) {
|
||||
await savePeerNote(peer, note);
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
|
||||
const sortedPeers = computed(() => {
|
||||
return peersList.value
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
if (a === enabledPeer.value) return -1;
|
||||
if (b === enabledPeer.value) return 1;
|
||||
return 0;
|
||||
})
|
||||
.filter((peer) => {
|
||||
const searchLower = searchQuery.value.toLowerCase();
|
||||
const peerLower = peer.toLowerCase();
|
||||
const noteLower = (peersNotes.value[peer] || '').toLowerCase();
|
||||
return peerLower.includes(searchLower) || noteLower.includes(searchLower);
|
||||
});
|
||||
});
|
||||
|
||||
const openModal = (peer: string) => {
|
||||
selectedPeer.value = peer;
|
||||
isModalOpen.value = true;
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
isModalOpen.value = false;
|
||||
selectedPeer.value = null;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="liquid-container" >
|
||||
<div class="status-bar" :class="['indicator', networkStatus]">
|
||||
|
||||
<div >
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="add-peer">
|
||||
<input
|
||||
v-model="newPeerUrl"
|
||||
placeholder="Enter relay URL"
|
||||
@keyup.enter="addPeer(newPeerUrl)"
|
||||
/>
|
||||
<ion-icon
|
||||
:icon="addCircleSharp"
|
||||
class="addlink"
|
||||
@click="addPeer(newPeerUrl);newPeerUrl = ''"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="peer-list">
|
||||
<div class="peer-list-header">
|
||||
<h3>Gun Relays</h3>
|
||||
<div class="search-container">
|
||||
<ion-icon :icon="searchSharp" class="search-icon" />
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
placeholder="Search relays..."
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="peer-scroll-container">
|
||||
<div
|
||||
v-for="peer in sortedPeers"
|
||||
:key="peer"
|
||||
class="peer-item"
|
||||
@click="openModal(peer)"
|
||||
>
|
||||
<div class="peer-header">
|
||||
<div :class="['status', peerStatuses[peer]]">
|
||||
{{ peerStatuses[peer] || 'Checking' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="peer-content">
|
||||
<span class="peer-url">{{ notes[peer] || peer }}</span>
|
||||
</div>
|
||||
<div class="peer-actions">
|
||||
<ion-toggle
|
||||
@click.stop
|
||||
:checked="enabledPeer === peer"
|
||||
@ionChange="enabledPeer === peer ? disablePeer() : enablePeer(peer)"
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ion-modal :is-open="isModalOpen" @didDismiss="closeModal">
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-item lines="none" v-if="selectedPeer">
|
||||
<ion-label position="stacked">Relay URL</ion-label>
|
||||
<ion-input :value="selectedPeer" readonly class="readonly-input" />
|
||||
</ion-item>
|
||||
<ion-item lines="none" v-if="selectedPeer">
|
||||
<ion-label position="stacked">Status</ion-label>
|
||||
<div :class="['status', peerStatuses[selectedPeer]]" class="status-display">
|
||||
{{ peerStatuses[selectedPeer] || 'Checking' }}
|
||||
</div>
|
||||
</ion-item>
|
||||
<ion-item lines="none" v-if="selectedPeer">
|
||||
<ion-label position="stacked">Note</ion-label>
|
||||
<ion-input
|
||||
v-model="notes[selectedPeer]"
|
||||
placeholder=""
|
||||
clear-input
|
||||
style=" --padding-start: 12px;
|
||||
--padding-end: 12px;"
|
||||
/>
|
||||
</ion-item>
|
||||
<div class="modal-actions">
|
||||
<ion-button
|
||||
expand="block"
|
||||
color="danger"
|
||||
@click="removePeer(selectedPeer!); closeModal()"
|
||||
>
|
||||
Remove Relay
|
||||
</ion-button>
|
||||
<ion-button
|
||||
expand="block"
|
||||
color="medium"
|
||||
@click="closeModal"
|
||||
>
|
||||
Close
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.liquid-container {
|
||||
padding: 16px 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', sans-serif;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: transparent;
|
||||
/* background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(0, 0, 0, 0.05)); */
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
padding: 6px 8px;
|
||||
background: rgba(130, 130, 130, 0.15);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.status-item:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
|
||||
.reset-button-container {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.reset-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, rgba(102, 204, 255, 0.2), rgba(102, 204, 255, 0.4));
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
gap: 6px;
|
||||
color: inherit;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.reset-button:hover {
|
||||
background: linear-gradient(135deg, rgba(102, 204, 255, 0.3), rgba(102, 204, 255, 0.5));
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.reset-button:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.reset-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.reset-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.rotating {
|
||||
animation: spin 1.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
margin-right: 6px;
|
||||
/* color: #333; */
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: 100%;
|
||||
/* padding: 4px 8px; */
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
backdrop-filter: blur(4px);
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.indicator.online,
|
||||
.indicator.connected,
|
||||
.indicator.relay {
|
||||
background: linear-gradient(135deg, #88ff88, #55ccaa);
|
||||
color: #2a2a2a;
|
||||
}
|
||||
|
||||
.indicator.offline,
|
||||
.indicator.disconnected,
|
||||
.indicator.direct {
|
||||
background: linear-gradient(135deg, #ff7777, #cc5555);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.add-peer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.add-peer input {
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: rgba(150, 150, 150, 0.2);
|
||||
font-size: 14px;
|
||||
/* color: #333; */
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.add-peer input:hover,
|
||||
.add-peer input:focus {
|
||||
background: rgba(150, 150, 150, 0.25);
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-1px);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.addlink {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* color: #66ccff; */
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.addlink:hover {
|
||||
/* color: #88ddff; */
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.peer-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.peer-list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.peer-list h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
/* color: #333; */
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.search-container {
|
||||
position: relative;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #777;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px 8px 36px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: rgba(150, 150, 150, 0.2);
|
||||
font-size: 14px;
|
||||
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.search-input:hover,
|
||||
.search-input:focus {
|
||||
background: rgba(150, 150, 150, 0.25);
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-1px);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.peer-scroll-container {
|
||||
border-radius: 12px;
|
||||
max-height: 539px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.peer-scroll-container::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.peer-scroll-container::-webkit-scrollbar-track {
|
||||
background: rgba(150, 150, 150, 0.2);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.peer-scroll-container::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(180deg, #66ccff, #88ddff);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.peer-scroll-container::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(180deg, #55bbff, #77ccff);
|
||||
}
|
||||
|
||||
.peer-item {
|
||||
padding: 12px;
|
||||
background: linear-gradient(135deg, rgba(150, 150, 150, 0.15), rgba(255, 255, 255, 0.05));
|
||||
border-radius: 12px;
|
||||
margin-bottom: 12px;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.peer-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.peer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 4px 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
backdrop-filter: blur(4px);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.status.connected {
|
||||
background: linear-gradient(135deg, #88ff88, #55ccaa);
|
||||
color: #2a2a2a;
|
||||
}
|
||||
|
||||
.status.disconnected {
|
||||
background: linear-gradient(135deg, #ff7777, #cc5555);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.status.checking {
|
||||
background: linear-gradient(135deg, #ffcc66, #ffaa33);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.peer-content {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.peer-url {
|
||||
display: block;
|
||||
word-break: break-all;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
padding: 6px 10px;
|
||||
background: rgba(150, 150, 150, 0.1);
|
||||
border-radius: 8px;
|
||||
/* color: #444; */
|
||||
}
|
||||
|
||||
.peer-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
ion-toggle {
|
||||
/* --background: rgba(150, 150, 150, 0.2);
|
||||
--background-checked: linear-gradient(135deg, #66ccff, #88ddff);
|
||||
--handle-background: #fff;
|
||||
--handle-background-checked: #fff; */
|
||||
width: 48px;
|
||||
height: 24px;
|
||||
/* --handle-width: 20px;
|
||||
--handle-height: 20px; */
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
ion-modal {
|
||||
--border-radius: 12px;
|
||||
--max-width: 400px;
|
||||
--max-height: 46%;
|
||||
|
||||
--backdrop-opacity: 0.4;
|
||||
}
|
||||
|
||||
/* ion-header {
|
||||
--background: rgba(150, 150, 150, 0.1);
|
||||
} */
|
||||
|
||||
ion-toolbar {
|
||||
--background: transparent;
|
||||
--border-width: 0;
|
||||
|
||||
}
|
||||
|
||||
ion-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
ion-buttons {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
ion-button {
|
||||
--background-activated: transparent;
|
||||
}
|
||||
|
||||
ion-button ion-icon {
|
||||
font-size: 24px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
ion-button:hover ion-icon {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.ion-padding {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
ion-item {
|
||||
--background: transparent;
|
||||
--padding-start: 0;
|
||||
--inner-padding-end: 0;
|
||||
/* margin-bottom: 16px; */
|
||||
}
|
||||
|
||||
ion-label {
|
||||
/* color: #333 !important; */
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
ion-input {
|
||||
--background: rgba(150, 150, 150, 0.15);
|
||||
--padding-start: 12px;
|
||||
--padding-end: 12px;
|
||||
--padding-top: 8px;
|
||||
--padding-bottom: 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
|
||||
}
|
||||
|
||||
.readonly-input {
|
||||
--padding-start: 12px;
|
||||
--padding-end: 12px;
|
||||
|
||||
--color: #666;
|
||||
--background: rgba(150, 150, 150, 0.1);
|
||||
|
||||
}
|
||||
|
||||
.status-display {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.modal-actions ion-button {
|
||||
--border-radius: 8px;
|
||||
--padding-start: 16px;
|
||||
--padding-end: 16px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal-actions ion-button[color="danger"] {
|
||||
--background: #ff6666;
|
||||
--background-hover: #ff8888;
|
||||
--background-activated: #ff5555;
|
||||
}
|
||||
|
||||
.modal-actions ion-button[color="medium"] {
|
||||
--background: #ccc;
|
||||
--background-hover: #ddd;
|
||||
--background-activated: #bbb;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,202 @@
|
||||
import { ref, Ref } from 'vue';
|
||||
import { Flint, NodeAdapter } from 'gun-flint';
|
||||
import StorageService from '../services/storageService';
|
||||
import { ISQLiteService } from '../services/sqliteService';
|
||||
import { IDbVersionService } from '../services/dbVersionService';
|
||||
|
||||
// 日志工具
|
||||
// const log = {
|
||||
// debug: (msg: string, ...args: any[]) => console.debug(`[Gun-SQLite-Adapter] ${msg}`, ...args),
|
||||
// info: (msg: string, ...args: any[]) => console.info(`[Gun-SQLite-Adapter] ${msg}`, ...args),
|
||||
// warn: (msg: string, ...args: any[]) => console.warn(`[Gun-SQLite-Adapter] ${msg}`, ...args),
|
||||
// error: (msg: string, ...args: any[]) => console.error(`[Gun-SQLite-Adapter] ${msg}`, ...args),
|
||||
// };
|
||||
|
||||
// 请求队列管理,防止重复查询
|
||||
class RequestQueue {
|
||||
private queue: Map<string, { resolve: (data: any) => void; reject: (err: any) => void }[]> = new Map();
|
||||
private debounceTimers: Map<string, NodeJS.Timeout> = new Map();
|
||||
private storageServ: StorageService;
|
||||
|
||||
constructor(storageServ: StorageService) {
|
||||
this.storageServ = storageServ;
|
||||
}
|
||||
|
||||
async get(key: string): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const handlers = this.queue.get(key) || [];
|
||||
handlers.push({ resolve, reject });
|
||||
this.queue.set(key, handlers);
|
||||
|
||||
if (!this.debounceTimers.has(key)) {
|
||||
const timer = setTimeout(async () => {
|
||||
const handlers = this.queue.get(key) || [];
|
||||
this.queue.delete(key);
|
||||
this.debounceTimers.delete(key);
|
||||
|
||||
try {
|
||||
if (!this.storageServ.db) throw new Error('Database connection not available');
|
||||
const result = await this.storageServ.query('SELECT value FROM gun_nodes WHERE key = ?', [key]);
|
||||
const data = result.values && result.values.length > 0 ? JSON.parse(result.values[0].value) : null;
|
||||
handlers.forEach(h => h.resolve(data));
|
||||
} catch (err) {
|
||||
//log.error(`Failed to get key=${key}:`, err);
|
||||
handlers.forEach(h => h.reject(err));
|
||||
}
|
||||
}, 50);
|
||||
this.debounceTimers.set(key, timer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async put(soul: string, node: any): Promise<void> {
|
||||
await this.storageServ.run(
|
||||
'INSERT OR REPLACE INTO gun_nodes (key, value, timestamp) VALUES (?, ?, ?)',
|
||||
[soul, JSON.stringify(node), Date.now()]
|
||||
);
|
||||
}
|
||||
|
||||
async batchPut(nodes: Record<string, any>): Promise<void> {
|
||||
const updates: [string, string, number][] = [];
|
||||
for (const soul in nodes) {
|
||||
updates.push([soul, JSON.stringify(nodes[soul]), Date.now()]);
|
||||
}
|
||||
await this.storageServ.run(
|
||||
'INSERT OR REPLACE INTO gun_nodes (key, value, timestamp) VALUES ' + updates.map(() => '(?, ?, ?)').join(','),
|
||||
updates.flat()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 定义适配器接口
|
||||
interface GunAdapter {
|
||||
opt?: (context: any, options: any) => void;
|
||||
get: (key: string, done: (err: Error | null, node: any) => void) => void;
|
||||
put: (node: any, done: (err: Error | null) => void) => void;
|
||||
}
|
||||
|
||||
export interface IGunSQLiteAdapter {
|
||||
initialize(): Promise<void>;
|
||||
getAdapter(): GunAdapter;
|
||||
isReady: Ref<boolean>;
|
||||
}
|
||||
|
||||
// 单例实例
|
||||
let instance: IGunSQLiteAdapter | null = null;
|
||||
|
||||
export function useGunSQLiteAdapter(
|
||||
sqliteService: ISQLiteService,
|
||||
dbVersionService: IDbVersionService,
|
||||
storageService: StorageService
|
||||
): IGunSQLiteAdapter {
|
||||
if (instance) return instance;
|
||||
|
||||
const isReady: Ref<boolean> = ref(false);
|
||||
const storageServ = storageService;
|
||||
let queue: RequestQueue | null = null;
|
||||
|
||||
async function initialize() {
|
||||
if (isReady.value) return;
|
||||
try {
|
||||
//log.info('Initializing Gun SQLite adapter...');
|
||||
// 检查 StorageService 是否已初始化数据库
|
||||
if (!storageServ.db) {
|
||||
//log.warn('StorageService database not initialized, initializing now...');
|
||||
const dbName = 'talkflowdb';
|
||||
const loadToVersion = storageServ.loadToVersion || 2;
|
||||
storageServ.db = await sqliteService.openDatabase(dbName, loadToVersion, false);
|
||||
} else {
|
||||
const isOpen = await storageServ.db.isDBOpen();
|
||||
if (!isOpen) {
|
||||
//log.warn('Database not open, reopening...');
|
||||
await storageServ.db.open();
|
||||
}
|
||||
}
|
||||
|
||||
// 创建 gun_nodes 表
|
||||
const result = await storageServ.execute(`
|
||||
CREATE TABLE IF NOT EXISTS gun_nodes (
|
||||
key TEXT PRIMARY KEY NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
timestamp INTEGER DEFAULT (strftime('%s', 'now'))
|
||||
)
|
||||
`);
|
||||
if (result.changes && result.changes.changes >= 0) {
|
||||
//log.info('gun_nodes table created or already exists');
|
||||
} else {
|
||||
throw new Error('Failed to create gun_nodes table: no changes returned');
|
||||
}
|
||||
|
||||
queue = new RequestQueue(storageServ);
|
||||
isReady.value = true;
|
||||
//log.info('Gun SQLite adapter initialized successfully');
|
||||
} catch (err) {
|
||||
//log.error('Failed to initialize Gun SQLite adapter:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const adapterCore = {
|
||||
storageServ,
|
||||
queue,
|
||||
opt: async function (context: any, options: any) {
|
||||
// log.info('Adapter opt called:', { context, options });
|
||||
await initialize();
|
||||
return options;
|
||||
},
|
||||
get: async function (key: string, done: (err: Error | null, node: any) => void) {
|
||||
try {
|
||||
if (!isReady.value) await initialize();
|
||||
if (!queue) throw new Error('Adapter not initialized');
|
||||
const data = await queue.get(key);
|
||||
done(null, data);
|
||||
} catch (err) {
|
||||
//log.error(`Get error for key=${key}:`, err);
|
||||
done(err instanceof Error ? err : new Error('Unknown error'), null);
|
||||
}
|
||||
},
|
||||
put: async function (node: any, done: (err: Error | null) => void) {
|
||||
try {
|
||||
if (!isReady.value) await initialize();
|
||||
if (!queue) throw new Error('Adapter not initialized');
|
||||
if (typeof node !== 'object' || node === null) throw new Error('Invalid node');
|
||||
const souls = Object.keys(node).length > 1 ? Object.keys(node) : [node._?.['#'] || node._.id];
|
||||
if (!souls[0]) throw new Error('Missing soul in node');
|
||||
if (souls.length > 1) {
|
||||
await queue.batchPut(node);
|
||||
} else {
|
||||
await queue.put(souls[0], node[souls[0]] || node);
|
||||
}
|
||||
done(null);
|
||||
} catch (err) {
|
||||
//log.error('Put error:', err);
|
||||
done(err instanceof Error ? err : new Error('Unknown error'));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const adapter = new NodeAdapter(adapterCore);
|
||||
Flint.register(adapter);
|
||||
|
||||
const gunSQLiteAdapter: IGunSQLiteAdapter = {
|
||||
initialize,
|
||||
getAdapter: () => adapter,
|
||||
isReady,
|
||||
};
|
||||
|
||||
instance = gunSQLiteAdapter;
|
||||
return gunSQLiteAdapter;
|
||||
}
|
||||
|
||||
export function getGunSQLiteAdapter(
|
||||
sqliteService: ISQLiteService,
|
||||
dbVersionService: IDbVersionService,
|
||||
storageService: StorageService
|
||||
): IGunSQLiteAdapter {
|
||||
if (!instance) {
|
||||
instance = useGunSQLiteAdapter(sqliteService, dbVersionService, storageService);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
export default getGunSQLiteAdapter;
|
||||
121
examples/relay-sqlite-example/src/composables/useNetwork.ts
Normal file
121
examples/relay-sqlite-example/src/composables/useNetwork.ts
Normal file
@ -0,0 +1,121 @@
|
||||
|
||||
import { ref } from 'vue';
|
||||
import Gun, { IGunInstance } from 'gun';
|
||||
|
||||
// 模块级别的单例状态
|
||||
const isOnline = ref(navigator.onLine);
|
||||
const peersConnected = ref(false);
|
||||
const checkInterval = 60000; // 每 60 秒检查一次
|
||||
|
||||
// 单例初始化标志和变量
|
||||
let initialized = false;
|
||||
let intervalId: number | null = null;
|
||||
let currentGunInstance: IGunInstance<any> | null = null;
|
||||
let instance: ReturnType<typeof createNetwork> | null = null;
|
||||
|
||||
function createNetwork(gunInstance: IGunInstance<any>) {
|
||||
// 检查 Gun.js 对等节点,反复尝试直到成功
|
||||
async function checkPeers(): Promise<boolean> {
|
||||
const maxAttempts = 3;
|
||||
let attempt = 0;
|
||||
const retryDelay = 1000; // 每次尝试间隔 1 秒
|
||||
|
||||
while (attempt < maxAttempts) {
|
||||
attempt++;
|
||||
const result = await new Promise<boolean>((resolve) => {
|
||||
let alive = false;
|
||||
const off = gunInstance.get('~public').once((data) => {
|
||||
alive = true;
|
||||
off.off();
|
||||
resolve(true);
|
||||
});
|
||||
setTimeout(() => {
|
||||
if (!alive) {
|
||||
resolve(false);
|
||||
}
|
||||
}, 5000); // 10 秒超时
|
||||
});
|
||||
|
||||
if (result) {
|
||||
peersConnected.value = true;
|
||||
return true;
|
||||
} else {
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||
}
|
||||
}
|
||||
return false; // 理论上不会到达这里,因为 maxAttempts 是 Infinity
|
||||
}
|
||||
|
||||
async function updateNetworkStatus() {
|
||||
isOnline.value = navigator.onLine;
|
||||
peersConnected.value = await checkPeers();
|
||||
}
|
||||
|
||||
function handleOnline() {
|
||||
updateNetworkStatus();
|
||||
}
|
||||
|
||||
function handleOffline() {
|
||||
isOnline.value = false;
|
||||
peersConnected.value = false;
|
||||
}
|
||||
|
||||
function startChecking() {
|
||||
updateNetworkStatus();
|
||||
intervalId = window.setInterval(updateNetworkStatus, checkInterval);
|
||||
}
|
||||
|
||||
function stopChecking() {
|
||||
if (intervalId !== null) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 单例初始化逻辑
|
||||
if (!initialized || currentGunInstance !== gunInstance) {
|
||||
if (initialized && currentGunInstance !== gunInstance) {
|
||||
// 如果 Gun 实例变了,清理旧的事件监听器和定时器
|
||||
stopChecking();
|
||||
window.removeEventListener('online', handleOnline);
|
||||
window.removeEventListener('offline', handleOffline);
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
currentGunInstance = gunInstance;
|
||||
|
||||
window.addEventListener('online', handleOnline);
|
||||
window.addEventListener('offline', handleOffline);
|
||||
startChecking();
|
||||
}
|
||||
|
||||
return {
|
||||
isOnline,
|
||||
peersConnected,
|
||||
updateNetworkStatus,
|
||||
checkPeers,
|
||||
};
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export function useNetwork(gunInstance: IGunInstance<any>) {
|
||||
if (!instance || currentGunInstance !== gunInstance) {
|
||||
instance = createNetwork(gunInstance);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
// 清理函数(可选,用于测试或应用卸载时)
|
||||
export function cleanupNetwork() {
|
||||
if (instance) {
|
||||
window.removeEventListener('online', instance.updateNetworkStatus);
|
||||
window.removeEventListener('offline', instance.updateNetworkStatus);
|
||||
if (intervalId !== null) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
instance = null;
|
||||
initialized = false;
|
||||
currentGunInstance = null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,558 @@
|
||||
import { ref, watch } from 'vue';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useNetwork } from '@/composables/useNetwork';
|
||||
import StorageService from '@/services/storageService';
|
||||
import Gun from 'gun';
|
||||
import 'gun/sea';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 模块级别的单例状态
|
||||
const networkStatus = ref<'online' | 'offline'>('online');
|
||||
const peersStatus = ref<'connected' | 'disconnected'>('disconnected');
|
||||
const currentMode = ref<'direct' | 'relay'>('direct');
|
||||
const peerStatuses = ref<Record<string, 'connected' | 'disconnected'>>({});
|
||||
|
||||
// 单例初始化标志
|
||||
let initialized = false;
|
||||
let instance: ReturnType<typeof createNetworkStatus> | null = null;
|
||||
|
||||
function createNetworkStatus(storageService: StorageService) {
|
||||
|
||||
const { showToast } = useToast();
|
||||
const { isOnline, peersConnected, updateNetworkStatus, checkPeers } = useNetwork(gun);
|
||||
const peersNotes = ref<Record<string, string>>({});
|
||||
|
||||
const peersList = ref<string[]>([
|
||||
'https://peer.wallie.io/gun',
|
||||
'https://gun.defucc.me/gun',
|
||||
'https://talkflow.team/gun',
|
||||
'https://gun-manhattan.herokuapp.com/gun',
|
||||
'https://gundb-relay-mlccl.ondigitalocean.app/gun',
|
||||
|
||||
|
||||
]);
|
||||
|
||||
const enabledPeer = ref<string>(peersList.value[0]);
|
||||
|
||||
let gun = Gun({
|
||||
|
||||
peers: [ enabledPeer.value],
|
||||
radisk: true,
|
||||
localStorage: false,
|
||||
gunSQLiteAdapter: {
|
||||
key: 'gundb',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// 确保 storageService 已初始化
|
||||
async function ensureStorageReady() {
|
||||
try {
|
||||
if (!storageService.db || !(await storageService.db.isDBOpen())) {
|
||||
console.log('[useNetworkStatus] Initializing StorageService database...');
|
||||
await storageService.initializeDatabase();
|
||||
if (!storageService.db) {
|
||||
throw new Error('StorageService 初始化后仍无数据库连接');
|
||||
}
|
||||
console.log('[useNetworkStatus] StorageService database initialized');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[useNetworkStatus] Failed to initialize StorageService:', err);
|
||||
showToast('数据库初始化失败,请重试', 'error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存 enabledPeer 到 SQLite
|
||||
async function saveEnabledPeer() {
|
||||
try {
|
||||
await ensureStorageReady();
|
||||
await storageService.run('UPDATE network_peers SET is_enabled = 0');
|
||||
if (enabledPeer.value) {
|
||||
await storageService.run(
|
||||
'INSERT OR REPLACE INTO network_peers (url, is_enabled, note) VALUES (?, ?, ?)',
|
||||
[enabledPeer.value, 1, peersNotes.value[enabledPeer.value] || '']
|
||||
);
|
||||
}
|
||||
console.log(`Enabled peer saved: ${enabledPeer.value}`);
|
||||
} catch (err) {
|
||||
console.error('[useNetworkStatus] Failed to save enabled peer:', err);
|
||||
showToast('无法保存启用节点', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 保存节点备注
|
||||
async function savePeerNote(peer: string, note: string) {
|
||||
try {
|
||||
await ensureStorageReady();
|
||||
await storageService.run(
|
||||
'UPDATE network_peers SET note = ? WHERE url = ?',
|
||||
[note, peer]
|
||||
);
|
||||
peersNotes.value[peer] = note;
|
||||
console.log(`Peer note saved: ${peer} -> ${note}`);
|
||||
} catch (err) {
|
||||
console.error('[useNetworkStatus] Failed to save peer note:', err);
|
||||
showToast('无法保存节点备注', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 从 SQLite 加载 Peer 配置和备注
|
||||
// async function loadPeers() {
|
||||
// try {
|
||||
// await ensureStorageReady();
|
||||
// const result = await storageService.query('SELECT url, is_enabled, note FROM network_peers');
|
||||
// const peers = result.values || [];
|
||||
// peersList.value = peers.map((peer: { url: string }) => peer.url);
|
||||
// peersNotes.value = peers.reduce((acc: Record<string, string>, peer: { url: string; note: string }) => {
|
||||
// acc[peer.url] = peer.note || '';
|
||||
// return acc;
|
||||
// }, {});
|
||||
// const enabled = peers.find((peer: { is_enabled: number }) => peer.is_enabled === 1);
|
||||
// if (enabled && peersList.value.includes(enabled.url)) {
|
||||
// enabledPeer.value = enabled.url;
|
||||
// } else if (peersList.value.length > 0) {
|
||||
// enabledPeer.value = peersList.value[0];
|
||||
// await saveEnabledPeer();
|
||||
// }
|
||||
// gun.opt({ peers: peersList.value });
|
||||
// } catch (err) {
|
||||
// console.error('[useNetworkStatus] Failed to load peers:', err);
|
||||
// showToast('无法加载节点列表', 'error');
|
||||
// }
|
||||
// }
|
||||
async function loadPeers() {
|
||||
try {
|
||||
await ensureStorageReady();
|
||||
const result = await storageService.query('SELECT url, is_enabled, note FROM network_peers');
|
||||
const peers = result.values || [];
|
||||
|
||||
if (peers.length === 0) {
|
||||
// 如果 SQLite 表为空,插入 TalkFlowCore 的预设节点
|
||||
console.log('[useNetworkStatus] network_peers 表为空,插入预设节点');
|
||||
const { peersList: defaultPeersList } = getTalkFlowCore(); // 获取预设节点
|
||||
for (const peerUrl of defaultPeersList.value) {
|
||||
await storageService.run(
|
||||
'INSERT OR IGNORE INTO network_peers (url, is_enabled, note) VALUES (?, ?, ?)',
|
||||
[peerUrl, 0, '']
|
||||
);
|
||||
}
|
||||
// 重新查询以确保数据已插入
|
||||
const newResult = await storageService.query('SELECT url, is_enabled, note FROM network_peers');
|
||||
peersList.value = newResult.values.map((peer: { url: string }) => peer.url);
|
||||
} else {
|
||||
// 使用 SQLite 中的节点
|
||||
peersList.value = peers.map((peer: { url: string }) => peer.url);
|
||||
}
|
||||
|
||||
// 加载备注
|
||||
peersNotes.value = peers.reduce((acc: Record<string, string>, peer: { url: string; note: string }) => {
|
||||
acc[peer.url] = peer.note || '';
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// 设置 enabledPeer
|
||||
const enabled = peers.find((peer: { is_enabled: number }) => peer.is_enabled === 1);
|
||||
if (enabled && peersList.value.includes(enabled.url)) {
|
||||
enabledPeer.value = enabled.url;
|
||||
} else if (peersList.value.length > 0) {
|
||||
enabledPeer.value = peersList.value[0];
|
||||
await saveEnabledPeer();
|
||||
}
|
||||
|
||||
// 更新 Gun 配置
|
||||
gun.opt({ peers: peersList.value });
|
||||
console.log('[useNetworkStatus] Gun 配置节点:', peersList.value);
|
||||
} catch (err) {
|
||||
console.error('[useNetworkStatus] 加载节点失败:', err);
|
||||
showToast('无法加载节点列表', 'error');
|
||||
}
|
||||
}
|
||||
// 更新网络和 Peer 状态
|
||||
async function updateStatus() {
|
||||
networkStatus.value = isOnline.value ? 'online' : 'offline';
|
||||
peersStatus.value = peersConnected.value ? 'connected' : 'disconnected';
|
||||
currentMode.value = peersConnected.value && enabledPeer.value ? 'relay' : 'direct';
|
||||
await updatePeerStatuses();
|
||||
}
|
||||
|
||||
// 检查单个 Peer 的状态(用于 UI 展示)
|
||||
async function checkPeerStatus(peer: string): Promise<'connected' | 'disconnected'> {
|
||||
return new Promise((resolve) => {
|
||||
const tempGun = Gun({ peers: [peer] });
|
||||
let connected = false;
|
||||
tempGun.on('hi', () => {
|
||||
connected = true;
|
||||
resolve('connected');
|
||||
});
|
||||
setTimeout(() => {
|
||||
if (!connected) resolve('disconnected');
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新所有 Peer 的状态(用于 UI 展示)
|
||||
async function updatePeerStatuses() {
|
||||
for (const peer of peersList.value) {
|
||||
const status = await checkPeerStatus(peer);
|
||||
peerStatuses.value[peer] = status;
|
||||
}
|
||||
}
|
||||
|
||||
// 用户手动启用某个 Peer
|
||||
async function enablePeer(peer: string) {
|
||||
if (!peersList.value.includes(peer)) {
|
||||
showToast(`节点 ${peer} 不在列表中`, 'warning');
|
||||
return;
|
||||
}
|
||||
if (enabledPeer.value === peer) {
|
||||
showToast(`${peer} 已是启用状态`, 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
enabledPeer.value = peer;
|
||||
await saveEnabledPeer();
|
||||
gun.opt({ peers: peersList.value, priorityPeer: peer });
|
||||
showToast(`启用节点: ${peer}`, 'success');
|
||||
|
||||
const connected = await checkPeers();
|
||||
if (!connected) {
|
||||
showToast(`无法连接到 ${peer},将回退到其他节点`, 'warning');
|
||||
}
|
||||
await updateStatus();
|
||||
}
|
||||
|
||||
// 用户禁用当前启用的 Peer
|
||||
async function disablePeer() {
|
||||
if (!enabledPeer.value) {
|
||||
showToast('无启用的节点', 'info');
|
||||
return;
|
||||
}
|
||||
const oldPeer = enabledPeer.value;
|
||||
enabledPeer.value = peersList.value.length > 1 ? peersList.value.find(p => p !== oldPeer) || '' : '';
|
||||
await saveEnabledPeer();
|
||||
gun.opt({ peers: peersList.value });
|
||||
showToast(`禁用节点: ${oldPeer}`, 'success');
|
||||
await updateStatus();
|
||||
}
|
||||
|
||||
// 添加 Peer(允许任意输入)
|
||||
async function addPeer(url: string) {
|
||||
if (!url) {
|
||||
showToast('请输入节点地址', 'warning');
|
||||
return;
|
||||
}
|
||||
const trimmedUrl = url.trim();
|
||||
if (peersList.value.includes(trimmedUrl)) {
|
||||
showToast('该节点已存在', 'warning');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ensureStorageReady();
|
||||
console.log('[useNetworkStatus] 添加节点:', trimmedUrl);
|
||||
await storageService.run(
|
||||
'INSERT INTO network_peers (url, is_enabled, note) VALUES (?, ?, ?)',
|
||||
[trimmedUrl, 0, '']
|
||||
);
|
||||
|
||||
// 重新查询 network_peers 表,确保数据一致
|
||||
const result = await storageService.query('SELECT url FROM network_peers');
|
||||
peersList.value = result.values.map((peer: { url: string }) => peer.url);
|
||||
console.log('[useNetworkStatus] 更新 peersList:', peersList.value);
|
||||
|
||||
gun.opt({ peers: peersList.value });
|
||||
showToast(`节点已添加: ${trimmedUrl}`, 'success');
|
||||
await updatePeerStatuses();
|
||||
} catch (err: any) {
|
||||
console.error('[useNetworkStatus] Failed to add peer:', err);
|
||||
showToast(`添加节点失败: ${err.message || '未知错误'}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 移除 Peer
|
||||
async function removePeer(peer: string) {
|
||||
try {
|
||||
await ensureStorageReady();
|
||||
if (enabledPeer.value === peer) {
|
||||
await disablePeer();
|
||||
}
|
||||
await storageService.run('DELETE FROM network_peers WHERE url = ?', [peer]);
|
||||
peersList.value = peersList.value.filter(p => p !== peer);
|
||||
delete peerStatuses.value[peer];
|
||||
gun.opt({ peers: peersList.value });
|
||||
showToast(`删除节点 ${peer}`, 'success');
|
||||
await updatePeerStatuses();
|
||||
} catch (err) {
|
||||
console.error('[useNetworkStatus] Failed to remove peer:', err);
|
||||
showToast('删除节点失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 处理网络状态变化
|
||||
function handleOnline() {
|
||||
updateNetworkStatus();
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
function handleOffline() {
|
||||
updateNetworkStatus();
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
// 初始化逻辑,只执行一次
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
loadPeers();
|
||||
updateStatus();
|
||||
|
||||
// 添加网络事件监听
|
||||
window.addEventListener('online', handleOnline);
|
||||
window.addEventListener('offline', handleOffline);
|
||||
|
||||
// 监听 peersList 和 enabledPeer 的变化
|
||||
watch(peersList, () => {
|
||||
updatePeerStatuses();
|
||||
});
|
||||
watch(enabledPeer, () => {
|
||||
saveEnabledPeer();
|
||||
updateStatus();
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
networkStatus,
|
||||
peersStatus,
|
||||
currentMode,
|
||||
peerStatuses,
|
||||
peersList,
|
||||
enabledPeer,
|
||||
addPeer,
|
||||
removePeer,
|
||||
enablePeer,
|
||||
disablePeer,
|
||||
updateStatus,
|
||||
peersNotes,
|
||||
savePeerNote,
|
||||
};
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export function useNetworkStatus(storageService: StorageService) {
|
||||
if (!instance) {
|
||||
instance = createNetworkStatus(storageService);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
// import { ref, watch } from 'vue';
|
||||
// import { useToast } from '@/composables/useToast';
|
||||
// import { useNetwork } from '@/composables/useNetwork';
|
||||
|
||||
// // 模块级别的单例状态
|
||||
// const networkStatus = ref<'online' | 'offline'>('online');
|
||||
// const peersStatus = ref<'connected' | 'disconnected'>('disconnected');
|
||||
// const currentMode = ref<'direct' | 'relay'>('direct');
|
||||
// const peerStatuses = ref<Record<string, 'connected' | 'disconnected'>>({});
|
||||
|
||||
// // 单例初始化标志
|
||||
// let initialized = false;
|
||||
// let instance: ReturnType<typeof createNetworkStatus> | null = null;
|
||||
|
||||
// // 持久化 enabledPeer 的本地存储键
|
||||
// const ENABLED_PEER_KEY = 'enabledPeer';
|
||||
|
||||
// function createNetworkStatus() {
|
||||
// const { gun, peersList, enabledPeer } = getTalkFlowCore();
|
||||
// const { showToast } = useToast();
|
||||
// const { isOnline, peersConnected, updateNetworkStatus, checkPeers } = useNetwork(gun);
|
||||
|
||||
// // 保存 enabledPeer 到 localStorage
|
||||
// function saveEnabledPeer() {
|
||||
// localStorage.setItem(ENABLED_PEER_KEY, enabledPeer.value);
|
||||
// }
|
||||
|
||||
// // 从 localStorage 加载 Peer 配置
|
||||
// function loadPeers() {
|
||||
// const savedPeers = localStorage.getItem('peers');
|
||||
// if (savedPeers) {
|
||||
// peersList.value = JSON.parse(savedPeers);
|
||||
// }
|
||||
// const savedPeer = localStorage.getItem(ENABLED_PEER_KEY);
|
||||
// if (savedPeer && peersList.value.includes(savedPeer)) {
|
||||
// enabledPeer.value = savedPeer;
|
||||
// } else if (peersList.value.length > 0) {
|
||||
// enabledPeer.value = peersList.value[0];
|
||||
// saveEnabledPeer();
|
||||
// }
|
||||
// // 初始化时使用完整的 peersList
|
||||
// gun.opt({ peers: peersList.value });
|
||||
// }
|
||||
|
||||
// // 更新网络和 Peer 状态
|
||||
// async function updateStatus() {
|
||||
// networkStatus.value = isOnline.value ? 'online' : 'offline';
|
||||
// peersStatus.value = peersConnected.value ? 'connected' : 'disconnected';
|
||||
// currentMode.value = peersConnected.value && enabledPeer.value ? 'relay' : 'direct';
|
||||
// await updatePeerStatuses();
|
||||
// }
|
||||
|
||||
// // 检查单个 Peer 的状态(用于 UI 展示)
|
||||
// async function checkPeerStatus(peer: string): Promise<'connected' | 'disconnected'> {
|
||||
// return new Promise((resolve) => {
|
||||
// const tempGun = Gun({ peers: [peer] });
|
||||
// let connected = false;
|
||||
// tempGun.on('hi', () => {
|
||||
// connected = true;
|
||||
// resolve('connected');
|
||||
// });
|
||||
// setTimeout(() => {
|
||||
// if (!connected) resolve('disconnected');
|
||||
// }, 5000);
|
||||
// });
|
||||
// }
|
||||
|
||||
// // 更新所有 Peer 的状态(用于 UI 展示)
|
||||
// async function updatePeerStatuses() {
|
||||
// for (const peer of peersList.value) {
|
||||
// const status = await checkPeerStatus(peer);
|
||||
// peerStatuses.value[peer] = status;
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 用户手动启用某个 Peer
|
||||
// async function enablePeer(peer: string) {
|
||||
// if (!peersList.value.includes(peer)) {
|
||||
// showToast(`Peer ${peer} not in list`, 'warning');
|
||||
// return;
|
||||
// }
|
||||
// if (enabledPeer.value === peer) {
|
||||
// showToast(`${peer} is already enabled`, 'info');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// enabledPeer.value = peer;
|
||||
// saveEnabledPeer();
|
||||
// gun.opt({ peers: peersList.value, priorityPeer: peer }); // 自定义选项,优先尝试该 Peer
|
||||
// showToast(`Enabled peer: ${peer}`, 'success');
|
||||
|
||||
// // 检查连接状态
|
||||
// const connected = await checkPeers();
|
||||
// if (!connected) {
|
||||
// showToast(`Failed to connect to ${peer}, falling back to other peers`, 'warning');
|
||||
// }
|
||||
// await updateStatus();
|
||||
// }
|
||||
|
||||
// // 用户禁用当前启用的 Peer
|
||||
// function disablePeer() {
|
||||
// if (!enabledPeer.value) {
|
||||
// showToast('No peer enabled', 'info');
|
||||
// return;
|
||||
// }
|
||||
// const oldPeer = enabledPeer.value;
|
||||
// enabledPeer.value = peersList.value.length > 1 ? peersList.value.find(p => p !== oldPeer) || '' : '';
|
||||
// saveEnabledPeer();
|
||||
// gun.opt({ peers: peersList.value }); // 重置为完整列表
|
||||
// showToast(`Disabled peer: ${oldPeer}`, 'success');
|
||||
// updateStatus();
|
||||
// }
|
||||
|
||||
// // 添加 Peer
|
||||
// function addPeer(url: string) {
|
||||
// if (!url) {
|
||||
// showToast('Please enter the node URL', 'warning');
|
||||
// return;
|
||||
// }
|
||||
// if (peersList.value.includes(url)) {
|
||||
// showToast('This node already exists.', 'warning');
|
||||
// return;
|
||||
// }
|
||||
// peersList.value.push(url);
|
||||
// localStorage.setItem('peers', JSON.stringify(peersList.value));
|
||||
// gun.opt({ peers: peersList.value });
|
||||
// showToast(`Node added: ${url}`, 'success');
|
||||
// updatePeerStatuses();
|
||||
// }
|
||||
|
||||
// // 移除 Peer
|
||||
// function removePeer(peer: string) {
|
||||
// if (enabledPeer.value === peer) {
|
||||
// disablePeer();
|
||||
// }
|
||||
// peersList.value = peersList.value.filter(p => p !== peer);
|
||||
// delete peerStatuses.value[peer];
|
||||
// localStorage.setItem('peers', JSON.stringify(peersList.value));
|
||||
// gun.opt({ peers: peersList.value });
|
||||
// showToast(`Deleted node ${peer}`, 'success');
|
||||
// updatePeerStatuses();
|
||||
// }
|
||||
|
||||
// // 处理网络状态变化
|
||||
// function handleOnline() {
|
||||
// updateNetworkStatus();
|
||||
// updateStatus();
|
||||
// }
|
||||
|
||||
// function handleOffline() {
|
||||
// updateNetworkStatus();
|
||||
// updateStatus();
|
||||
// }
|
||||
|
||||
// // 初始化逻辑,只执行一次
|
||||
// if (!initialized) {
|
||||
// initialized = true;
|
||||
// loadPeers();
|
||||
// updateStatus();
|
||||
|
||||
// // 添加网络事件监听
|
||||
// window.addEventListener('online', handleOnline);
|
||||
// window.addEventListener('offline', handleOffline);
|
||||
|
||||
// // 监听 peersList 和 enabledPeer 的变化
|
||||
// watch(peersList, () => {
|
||||
// // gun.opt({ peers: peersList.value });
|
||||
// updatePeerStatuses();
|
||||
// });
|
||||
// watch(enabledPeer, () => {
|
||||
// saveEnabledPeer();
|
||||
// updateStatus();
|
||||
// });
|
||||
// }
|
||||
|
||||
// return {
|
||||
// networkStatus,
|
||||
// peersStatus,
|
||||
// currentMode,
|
||||
// peerStatuses,
|
||||
// peersList,
|
||||
// enabledPeer,
|
||||
// addPeer,
|
||||
// removePeer,
|
||||
// enablePeer,
|
||||
// disablePeer,
|
||||
// updateStatus,
|
||||
// };
|
||||
// }
|
||||
|
||||
// // 导出单例
|
||||
// export function useNetworkStatus() {
|
||||
// if (!instance) {
|
||||
// instance = createNetworkStatus();
|
||||
// }
|
||||
// return instance;
|
||||
// }
|
||||
|
||||
// 清理函数(用于测试或应用卸载时)
|
||||
// export function cleanupNetworkStatus() {
|
||||
// if (instance) {
|
||||
// window.removeEventListener('online', instance.updateStatus);
|
||||
// window.removeEventListener('offline', instance.updateStatus);
|
||||
// instance = null;
|
||||
// initialized = false;
|
||||
// }
|
||||
// }
|
||||
88
examples/relay-sqlite-example/src/composables/useToast.ts
Normal file
88
examples/relay-sqlite-example/src/composables/useToast.ts
Normal file
@ -0,0 +1,88 @@
|
||||
// src/composables/useToast.ts
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { Directory, Encoding, Filesystem } from '@capacitor/filesystem';
|
||||
|
||||
type ToastType = 'info' | 'success' | 'error' | 'warning';
|
||||
|
||||
interface ToastMessage {
|
||||
id: number;
|
||||
text: string;
|
||||
type: ToastType;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
const messages = ref<ToastMessage[]>([]);
|
||||
let idCounter = 0;
|
||||
const isEnabled = ref(false); // 默认开启提示
|
||||
const SETTINGS_FILE = 'toast_settings.json';
|
||||
|
||||
async function loadSettings(): Promise<{ isToastEnabled: boolean }> {
|
||||
const defaultSettings = { isToastEnabled: false };
|
||||
try {
|
||||
const result = await Filesystem.readFile({
|
||||
path: SETTINGS_FILE,
|
||||
directory: Directory.Data,
|
||||
encoding: Encoding.UTF8,
|
||||
});
|
||||
const data = typeof result.data === 'string' ? result.data : await result.data.text();
|
||||
return JSON.parse(data) || defaultSettings;
|
||||
} catch (err) {
|
||||
console.log('未找到提示设置文件,使用默认值');
|
||||
return defaultSettings;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings(): Promise<void> {
|
||||
try {
|
||||
await Filesystem.writeFile({
|
||||
path: SETTINGS_FILE,
|
||||
data: JSON.stringify({ isToastEnabled: isEnabled.value }),
|
||||
directory: Directory.Data,
|
||||
encoding: Encoding.UTF8,
|
||||
});
|
||||
console.log('提示设置已保存:', { isToastEnabled: isEnabled.value });
|
||||
} catch (err) {
|
||||
console.error('保存提示设置失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
export function showToast(msg: string, msgType: ToastType = 'info', customDuration = 3000) {
|
||||
if (!isEnabled.value) return; // 如果关闭则不显示
|
||||
|
||||
const toast = {
|
||||
id: idCounter++,
|
||||
text: msg,
|
||||
type: msgType,
|
||||
duration: customDuration,
|
||||
};
|
||||
messages.value.push(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
messages.value = messages.value.filter(m => m.id !== toast.id);
|
||||
}, customDuration);
|
||||
}
|
||||
|
||||
function hideToast(id: number) {
|
||||
messages.value = messages.value.filter(m => m.id !== id);
|
||||
}
|
||||
|
||||
function toggleToast(enabled: boolean) {
|
||||
isEnabled.value = enabled;
|
||||
saveSettings(); // 保存设置
|
||||
}
|
||||
|
||||
// 初始化加载设置
|
||||
onMounted(async () => {
|
||||
const settings = await loadSettings();
|
||||
isEnabled.value = settings.isToastEnabled;
|
||||
});
|
||||
|
||||
export function useToast() {
|
||||
return {
|
||||
messages,
|
||||
isEnabled,
|
||||
showToast,
|
||||
hideToast,
|
||||
toggleToast,
|
||||
};
|
||||
}
|
||||
97
examples/relay-sqlite-example/src/main.ts
Normal file
97
examples/relay-sqlite-example/src/main.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './src/App.vue'
|
||||
import router from './router';
|
||||
|
||||
import { IonicVue } from '@ionic/vue';
|
||||
|
||||
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* Core CSS required for Ionic components to work properly */
|
||||
import '@ionic/vue/css/core.css';
|
||||
|
||||
/* Basic CSS for apps built with Ionic */
|
||||
import '@ionic/vue/css/normalize.css';
|
||||
import '@ionic/vue/css/structure.css';
|
||||
import '@ionic/vue/css/typography.css';
|
||||
import '@ionic/vue/css/ionic.bundle.css'
|
||||
/* Optional CSS utils that can be commented out */
|
||||
import '@ionic/vue/css/padding.css';
|
||||
import '@ionic/vue/css/float-elements.css';
|
||||
import '@ionic/vue/css/text-alignment.css';
|
||||
import '@ionic/vue/css/text-transformation.css';
|
||||
import '@ionic/vue/css/flex-utils.css';
|
||||
import '@ionic/vue/css/display.css';
|
||||
|
||||
/* Theme variables */
|
||||
import './theme/variables.css';
|
||||
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { JeepSqlite } from 'jeep-sqlite/dist/components/jeep-sqlite';
|
||||
import { defineCustomElements as pwaElements} from '@ionic/pwa-elements/loader';
|
||||
import SqliteService from './services/sqliteService';
|
||||
import DbVersionService from './services/dbVersionService';
|
||||
import StorageService from './services/storageService';
|
||||
import InitializeAppService from './services/initializeAppService';
|
||||
|
||||
|
||||
|
||||
|
||||
pwaElements(window);
|
||||
customElements.define('jeep-sqlite', JeepSqlite);
|
||||
const platform = Capacitor.getPlatform();
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
.use(IonicVue)
|
||||
.use(useRouter)
|
||||
|
||||
.use(router);
|
||||
|
||||
|
||||
// Set the platform as global properties on the app
|
||||
app.config.globalProperties.$platform = platform;
|
||||
|
||||
// Define and instantiate the required services
|
||||
const sqliteServ = new SqliteService();
|
||||
const dbVersionServ = new DbVersionService();
|
||||
const storageServ = new StorageService(sqliteServ, dbVersionServ);
|
||||
// Set the services as global properties on the app
|
||||
app.config.globalProperties.$sqliteServ = sqliteServ;
|
||||
app.config.globalProperties.$dbVersionServ = dbVersionServ;
|
||||
app.config.globalProperties.$storageServ = storageServ;
|
||||
|
||||
//Define and instantiate the InitializeAppService
|
||||
const initAppServ = new InitializeAppService(sqliteServ, storageServ);
|
||||
|
||||
const mountApp = () => {
|
||||
initAppServ.initializeApp()
|
||||
.then(() => {
|
||||
router.isReady().then(() => {
|
||||
|
||||
app.mount('#app');
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('App Initialization error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
if (platform !== "web") {
|
||||
mountApp();
|
||||
} else {
|
||||
window.addEventListener('DOMContentLoaded', async () => {
|
||||
const jeepEl = document.createElement("jeep-sqlite");
|
||||
document.body.appendChild(jeepEl);
|
||||
customElements.whenDefined('jeep-sqlite').then(() => {
|
||||
mountApp();
|
||||
})
|
||||
.catch ((err) => {
|
||||
console.error('jeep-sqlite creation error:', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
313
examples/relay-sqlite-example/src/pages/index.vue
Normal file
313
examples/relay-sqlite-example/src/pages/index.vue
Normal file
@ -0,0 +1,313 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import {
|
||||
IonPage, IonHeader, IonToolbar, IonButtons, IonBackButton, IonTitle, IonContent, IonIcon,
|
||||
IonModal, IonToggle
|
||||
} from '@ionic/vue';
|
||||
import { addCircleSharp, closeCircleSharp, helpCircleOutline, closeOutline } from 'ionicons/icons';
|
||||
import RelayMode from '@/components/RelayMode.vue';
|
||||
|
||||
|
||||
const showHelpModal = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ion-page>
|
||||
<ion-header :translucent="true" collapse="fade">
|
||||
<ion-toolbar class="liquid-toolbar">
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button text="Discover" color="dark"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Network Status</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button color="dark" @click="showHelpModal = true">
|
||||
<ion-icon :icon="helpCircleOutline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content :fullscreen="true" :scroll-y="false">
|
||||
|
||||
<RelayMode/>
|
||||
|
||||
|
||||
|
||||
</ion-content>
|
||||
</ion-page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.addlink {
|
||||
width: 39px;
|
||||
height: 39px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Ionic Toolbar */
|
||||
.liquid-toolbar {
|
||||
--border-color: transparent;
|
||||
}
|
||||
|
||||
/* Ionic Content */
|
||||
.liquid-content {
|
||||
--background: transparent;
|
||||
--padding-start: 0;
|
||||
--padding-end: 0;
|
||||
--padding-top: 0;
|
||||
--padding-bottom: 0;
|
||||
}
|
||||
|
||||
/* Original Container Styles */
|
||||
.liquid-container {
|
||||
padding: 15px 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', sans-serif;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
background: rgba(130, 130, 130, 0.1);
|
||||
padding: 10px;
|
||||
border-radius: 15px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.status-item:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 500;
|
||||
margin-right: 10px;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.indicator {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.indicator.online,
|
||||
.indicator.connected,
|
||||
.indicator.relay {
|
||||
background: linear-gradient(45deg, #99ff99, #66ffcc);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.indicator.offline,
|
||||
.indicator.disconnected,
|
||||
.indicator.direct {
|
||||
background: linear-gradient(45deg, #ff6666, #ff9999);
|
||||
}
|
||||
|
||||
.add-peer {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.add-peer input {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
background: rgba(134, 134, 134, 0.25);
|
||||
font-size: 14px;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.add-peer input:hover,
|
||||
.add-peer input:focus {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.peer-list {
|
||||
margin-top: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.peer-list h3 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 15px;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.peer-scroll-container {
|
||||
border-radius: 10px;
|
||||
max-height: 390px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.peer-scroll-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.peer-scroll-container::-webkit-scrollbar-track {
|
||||
background: rgba(125, 125, 125, 0.451);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.peer-scroll-container::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(45deg, #66ccff, #99eeff);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.peer-scroll-container::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(45deg, #00b7ff, #66ddff);
|
||||
}
|
||||
|
||||
.peer-item {
|
||||
padding: 15px;
|
||||
background: linear-gradient(135deg, rgba(127, 127, 127, 0.15), rgba(255, 255, 255, 0.05));
|
||||
border-radius: 15px;
|
||||
margin-bottom: 15px;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.peer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
backdrop-filter: blur(5px);
|
||||
animation: pulse 2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.status.connected {
|
||||
background: linear-gradient(45deg, #99ff99, #66ffcc);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status.disconnected {
|
||||
background: linear-gradient(45deg, #ff6666, #ff9999);
|
||||
}
|
||||
|
||||
.status.checking {
|
||||
background: linear-gradient(45deg, #ffcc66, #ffdd99);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.peer-content {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.peer-url {
|
||||
display: block;
|
||||
word-break: break-all;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
padding: 5px 10px;
|
||||
background: rgba(124, 124, 124, 0.1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.peer-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
ion-toggle {
|
||||
--background: rgba(128, 128, 128, 0.2);
|
||||
--background-checked: #66ccff;
|
||||
--handle-background: #fff;
|
||||
--handle-background-checked: #fff;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.remove-icon {
|
||||
color: #ff6666;
|
||||
font-size: 30px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.remove-icon:hover {
|
||||
transform: scale(1.1);
|
||||
color: #ff9999;
|
||||
}
|
||||
|
||||
|
||||
.help-modal {
|
||||
--border-radius: 16px;
|
||||
|
||||
}
|
||||
|
||||
.help-modal ion-toolbar {
|
||||
--border-width: 0;
|
||||
--background: transparent;
|
||||
}
|
||||
|
||||
.help-content {
|
||||
padding: 0 0 20px;
|
||||
}
|
||||
|
||||
.help-content h2 {
|
||||
font-size: 1.25rem;
|
||||
margin: 20px 0 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.help-content h3 {
|
||||
font-size: 1.1rem;
|
||||
margin: 15px 0 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.help-content ion-list {
|
||||
background: transparent;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.help-content ion-item {
|
||||
--background: transparent;
|
||||
--padding-start: 0;
|
||||
--inner-padding-end: 0;
|
||||
}
|
||||
|
||||
.help-content ion-label {
|
||||
color: #666 !important;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
ion-button[color="dark"] {
|
||||
--background: #333;
|
||||
--border-radius: 12px;
|
||||
height: 44px;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
ion-button[color="dark"]:hover {
|
||||
--background: #444;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
||||
46
examples/relay-sqlite-example/src/router/index.ts
Normal file
46
examples/relay-sqlite-example/src/router/index.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { createRouter, createWebHistory } from '@ionic/vue-router'
|
||||
import { createAnimation } from '@ionic/vue'
|
||||
import routes from 'virtual:generated-pages'
|
||||
// import { useChatFlow } from '@/composables/TalkFlowCore'
|
||||
|
||||
// const { isLoggedIn } = useChatFlow()
|
||||
|
||||
const customRoutes = [
|
||||
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/pages/index.vue'),
|
||||
}
|
||||
|
||||
|
||||
|
||||
...routes,
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: customRoutes,
|
||||
})
|
||||
|
||||
|
||||
// 导航守卫
|
||||
// router.beforeEach((to, from, next) => {
|
||||
// // 如果用户没有登录,且访问的不是 'i18n.vue' 页面,则重定向到 'i18n.vue'
|
||||
// if (!isLoggedIn.value && to.path !== '/') {
|
||||
// next('/'); // 重定向到 i18n 页面
|
||||
// } else {
|
||||
// next(); // 允许访问目标路由
|
||||
// }
|
||||
// });
|
||||
|
||||
// router.beforeEach(async (to, from, next) => {
|
||||
// const { isLoggedIn } = await import('@/composables/TalkFlowCore');
|
||||
// if (!isLoggedIn.value && to.path !== '/') {
|
||||
// next('/');
|
||||
// } else {
|
||||
// next();
|
||||
// }
|
||||
// });
|
||||
|
||||
|
||||
export default router
|
||||
@ -0,0 +1,19 @@
|
||||
export interface IDbVersionService {
|
||||
setDbVersion(dbName: string, version: number): void
|
||||
getDbVersion(dbName: string):number| undefined
|
||||
};
|
||||
class DbVersionService implements IDbVersionService {
|
||||
dbNameVersionDict: Map<string, number> = new Map();
|
||||
|
||||
setDbVersion(dbName: string, version: number) {
|
||||
this.dbNameVersionDict.set(dbName, version);
|
||||
console.log(`设置数据库 ${dbName} 版本为: ${version}`);
|
||||
}
|
||||
|
||||
getDbVersion(dbName: string): number | undefined {
|
||||
const version = this.dbNameVersionDict.get(dbName);
|
||||
console.log(`获取数据库 ${dbName} 版本: ${version}`);
|
||||
return version;
|
||||
}
|
||||
}
|
||||
export default DbVersionService;
|
||||
@ -0,0 +1,9 @@
|
||||
import SQLiteService from './sqliteService';
|
||||
import DbVersionService from './dbVersionService';
|
||||
import StorageService from './storageService';
|
||||
|
||||
const sqliteServ = new SQLiteService();
|
||||
const dbVerServ = new DbVersionService();
|
||||
const storageServ = new StorageService(sqliteServ, dbVerServ);
|
||||
|
||||
export { sqliteServ, dbVerServ, storageServ };
|
||||
@ -0,0 +1,45 @@
|
||||
import {ISQLiteService } from '../services/sqliteService';
|
||||
import {IStorageService } from '../services/storageService';
|
||||
|
||||
export interface IInitializeAppService {
|
||||
initializeApp(): Promise<boolean>
|
||||
};
|
||||
|
||||
class InitializeAppService implements IInitializeAppService {
|
||||
appInit = false;
|
||||
sqliteServ!: ISQLiteService;
|
||||
storageServ!: IStorageService;
|
||||
platform!: string;
|
||||
static platform: string;
|
||||
|
||||
constructor(sqliteService: ISQLiteService, storageService: IStorageService) {
|
||||
this.sqliteServ = sqliteService;
|
||||
this.storageServ = storageService;
|
||||
this.platform = this.sqliteServ.getPlatform();
|
||||
}
|
||||
async initializeApp(): Promise<boolean> {
|
||||
if (!this.appInit) {
|
||||
try {
|
||||
console.log('开始应用初始化');
|
||||
if (this.platform === 'web') {
|
||||
await this.sqliteServ.initWebStore();
|
||||
console.log('Web 存储初始化完成');
|
||||
}
|
||||
await this.storageServ.initializeDatabase();
|
||||
console.log('数据库初始化完成');
|
||||
// if (this.platform === 'web') {
|
||||
// await this.sqliteServ.saveToStore(this.storageServ.getDatabaseName());
|
||||
// console.log('数据库保存到 Web 存储完成');
|
||||
// }
|
||||
this.appInit = true;
|
||||
console.log('应用初始化成功');
|
||||
} catch (error: any) {
|
||||
const msg = error.message ? error.message : error;
|
||||
console.error(`initializeAppError.initializeApp: ${msg}`, error);
|
||||
throw new Error(`initializeAppError.initializeApp: ${msg}`);
|
||||
}
|
||||
}
|
||||
return this.appInit;
|
||||
}
|
||||
}
|
||||
export default InitializeAppService;
|
||||
114
examples/relay-sqlite-example/src/services/sqliteService.ts
Normal file
114
examples/relay-sqlite-example/src/services/sqliteService.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { CapacitorSQLite, SQLiteConnection, SQLiteDBConnection, capSQLiteUpgradeOptions } from '@capacitor-community/sqlite';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
|
||||
export interface ISQLiteService {
|
||||
getPlatform(): string
|
||||
initWebStore(): Promise<void>
|
||||
addUpgradeStatement(options: capSQLiteUpgradeOptions): Promise<void>
|
||||
openDatabase(dbName: string, loadToVersion: number, readOnly: boolean): Promise<SQLiteDBConnection>
|
||||
closeDatabase(dbName: string, readOnly: boolean): Promise<void>
|
||||
saveToStore(dbName: string): Promise<void>
|
||||
saveToLocalDisk(dbName: string): Promise<void>
|
||||
isConnection(dbName: string, readOnly: boolean): Promise<boolean>
|
||||
};
|
||||
|
||||
class SQLiteService implements ISQLiteService {
|
||||
platform = Capacitor.getPlatform();
|
||||
sqlitePlugin = CapacitorSQLite;
|
||||
sqliteConnection = new SQLiteConnection(CapacitorSQLite);
|
||||
dbNameVersionDict: Map<string, number> = new Map();
|
||||
|
||||
getPlatform(): string {
|
||||
return this.platform;
|
||||
}
|
||||
async initWebStore() : Promise<void> {
|
||||
try {
|
||||
await this.sqliteConnection.initWebStore();
|
||||
} catch(error: any) {
|
||||
const msg = error.message ? error.message : error;
|
||||
throw new Error(`sqliteService.initWebStore: ${msg}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
async addUpgradeStatement(options: capSQLiteUpgradeOptions): Promise<void> {
|
||||
try {
|
||||
await this.sqlitePlugin.addUpgradeStatement(options);
|
||||
} catch(error: any) {
|
||||
const msg = error.message ? error.message : error;
|
||||
throw new Error(`sqliteService.addUpgradeStatement: ${msg}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
async openDatabase(dbName:string, loadToVersion: number,
|
||||
readOnly: boolean): Promise<SQLiteDBConnection> {
|
||||
this.dbNameVersionDict.set(dbName, loadToVersion);
|
||||
let encrypted = false;
|
||||
const mode = encrypted ? "secret" : "no-encryption";
|
||||
try {
|
||||
let db: SQLiteDBConnection;
|
||||
const retCC = (await this.sqliteConnection.checkConnectionsConsistency()).result;
|
||||
let isConn = (await this.sqliteConnection.isConnection(dbName, readOnly)).result;
|
||||
if(retCC && isConn) {
|
||||
db = await this.sqliteConnection.retrieveConnection(dbName, readOnly);
|
||||
} else {
|
||||
db = await this.sqliteConnection
|
||||
.createConnection(dbName, encrypted, mode, loadToVersion, readOnly);
|
||||
}
|
||||
const jeepSQlEL = document.querySelector("jeep-sqlite")
|
||||
|
||||
await db.open();
|
||||
const res = await db.isDBOpen();
|
||||
return db;
|
||||
|
||||
} catch(error: any) {
|
||||
const msg = error.message ? error.message : error;
|
||||
throw new Error(`sqliteService.openDatabase: ${msg}`);
|
||||
}
|
||||
|
||||
}
|
||||
async isConnection(dbName:string, readOnly: boolean): Promise<boolean> {
|
||||
try {
|
||||
const isConn = (await this.sqliteConnection.isConnection(dbName, readOnly)).result;
|
||||
if (isConn != undefined) {
|
||||
return isConn
|
||||
} else {
|
||||
throw new Error(`sqliteService.isConnection undefined`);
|
||||
}
|
||||
|
||||
} catch(error: any) {
|
||||
const msg = error.message ? error.message : error;
|
||||
throw new Error(`sqliteService.isConnection: ${msg}`);
|
||||
}
|
||||
}
|
||||
async closeDatabase(dbName:string, readOnly: boolean):Promise<void> {
|
||||
try {
|
||||
const isConn = (await this.sqliteConnection.isConnection(dbName, readOnly)).result;
|
||||
if(isConn) {
|
||||
await this.sqliteConnection.closeConnection(dbName, readOnly);
|
||||
}
|
||||
return;
|
||||
} catch(error: any) {
|
||||
const msg = error.message ? error.message : error;
|
||||
throw new Error(`sqliteService.closeDatabase: ${msg}`);
|
||||
}
|
||||
}
|
||||
async saveToStore(dbName: string): Promise<void> {
|
||||
try {
|
||||
await this.sqliteConnection.saveToStore(dbName);
|
||||
return;
|
||||
} catch(error: any) {
|
||||
const msg = error.message ? error.message : error;
|
||||
throw new Error(`sqliteService.saveToStore: ${msg}`);
|
||||
}
|
||||
}
|
||||
async saveToLocalDisk(dbName: string): Promise<void> {
|
||||
try {
|
||||
await this.sqliteConnection.saveToLocalDisk(dbName);
|
||||
return;
|
||||
} catch(err:any) {
|
||||
const msg = err.message ? err.message : err;
|
||||
throw new Error(`sqliteService.saveToLocalDisk: ${msg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
export default SQLiteService;
|
||||
153
examples/relay-sqlite-example/src/services/storageService.ts
Normal file
153
examples/relay-sqlite-example/src/services/storageService.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { CapacitorSQLite, SQLiteDBConnection } from '@capacitor-community/sqlite';
|
||||
import { getCurrentInstance } from 'vue';
|
||||
import { ISQLiteService } from './sqliteService';
|
||||
import { IDbVersionService } from './dbVersionService';
|
||||
|
||||
|
||||
export interface IStorageService {
|
||||
initializeDatabase(): Promise<void>;
|
||||
|
||||
|
||||
query(sql: string, params?: any[]): Promise<any>;
|
||||
run(sql: string, params?: any[]): Promise<any>;
|
||||
execute(sql: string): Promise<any>;
|
||||
|
||||
}
|
||||
|
||||
class StorageService implements IStorageService {
|
||||
versionUpgrades = [
|
||||
|
||||
{
|
||||
toVersion: 1,
|
||||
statements: [
|
||||
|
||||
`CREATE TABLE IF NOT EXISTS network_peers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
url TEXT UNIQUE,
|
||||
is_enabled INTEGER DEFAULT 0,
|
||||
note TEXT DEFAULT ''
|
||||
);`,
|
||||
|
||||
`CREATE TABLE IF NOT EXISTS gun_nodes (
|
||||
key TEXT PRIMARY KEY NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
timestamp INTEGER DEFAULT (strftime('%s', 'now'))
|
||||
);`,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
loadToVersion = 1;
|
||||
momentsVersion = 2;
|
||||
db!: SQLiteDBConnection;
|
||||
database: string = 'gundb';
|
||||
sqliteServ!: ISQLiteService;
|
||||
dbVerServ!: IDbVersionService;
|
||||
isInitCompleted = new BehaviorSubject(false);
|
||||
appInstance = getCurrentInstance();
|
||||
platform!: string;
|
||||
private isInitialized = false;
|
||||
|
||||
constructor(sqliteService: ISQLiteService, dbVersionService: IDbVersionService) {
|
||||
this.sqliteServ = sqliteService;
|
||||
this.dbVerServ = dbVersionService;
|
||||
this.platform = this.appInstance?.appContext.config.globalProperties.$platform || 'web';
|
||||
}
|
||||
|
||||
async initializeDatabase(): Promise<void> {
|
||||
if (this.isInitialized) {
|
||||
console.log('数据库已初始化,跳过重复调用');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('开始初始化数据库:', this.database);
|
||||
await this.sqliteServ.addUpgradeStatement({
|
||||
database: this.database,
|
||||
upgrade: this.versionUpgrades,
|
||||
});
|
||||
console.log('核心升级语句已添加');
|
||||
|
||||
this.db = await this.sqliteServ.openDatabase(this.database, this.loadToVersion, false);
|
||||
console.log('数据库已打开,目标版本:', this.loadToVersion);
|
||||
|
||||
const currentVersionResult = await this.db.getVersion();
|
||||
const currentVersion: number = currentVersionResult.version ?? 0;
|
||||
console.log('当前数据库版本:', currentVersion);
|
||||
|
||||
for (const upgrade of this.versionUpgrades) {
|
||||
console.log(`执行核心升级到版本 ${upgrade.toVersion}`);
|
||||
for (const stmt of upgrade.statements) {
|
||||
try {
|
||||
await this.db.execute(stmt);
|
||||
console.log('执行语句成功:', stmt);
|
||||
} catch (err) {
|
||||
console.error('执行语句失败:', stmt, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.dbVerServ.setDbVersion(this.database, this.loadToVersion);
|
||||
console.log('核心数据库版本已设置为:', this.loadToVersion);
|
||||
|
||||
|
||||
|
||||
if (this.platform === 'web') {
|
||||
try {
|
||||
await this.sqliteServ.saveToStore(this.database);
|
||||
console.log('数据库已保存到 Web 存储');
|
||||
} catch (err) {
|
||||
console.warn('Web 存储保存失败(非致命错误):', err);
|
||||
}
|
||||
}
|
||||
|
||||
const tablesAfter = await this.db.query("SELECT name FROM sqlite_master WHERE type='table'");
|
||||
console.log('初始化后的表:', tablesAfter.values);
|
||||
this.isInitCompleted.next(true);
|
||||
this.isInitialized = true;
|
||||
console.log('SQLite 数据库初始化成功');
|
||||
} catch (error: any) {
|
||||
console.error(`storageService.initializeDatabase: ${error.message || error}`, error);
|
||||
throw new Error(`storageService.initializeDatabase: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
async query(sql: string, params: any[] = []): Promise<any> {
|
||||
try {
|
||||
return await this.db.query(sql, params);
|
||||
} catch (err) {
|
||||
console.error(`执行查询 ${sql} 失败:`, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async run(sql: string, params: any[] = []): Promise<any> {
|
||||
try {
|
||||
return await this.db.run(sql, params);
|
||||
} catch (err) {
|
||||
console.error(`执行语句 ${sql} 失败:`, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async execute(sql: string): Promise<any> {
|
||||
try {
|
||||
return await this.db.execute(sql);
|
||||
} catch (err) {
|
||||
console.error(`执行语句 ${sql} 失败:`, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default StorageService;
|
||||
|
||||
|
||||
|
||||
237
examples/relay-sqlite-example/src/theme/variables.css
Normal file
237
examples/relay-sqlite-example/src/theme/variables.css
Normal file
@ -0,0 +1,237 @@
|
||||
/* Ionic Variables and Theming. For more info, please see:
|
||||
http://ionicframework.com/docs/theming/ */
|
||||
|
||||
/** Ionic CSS Variables **/
|
||||
:root {
|
||||
--ion-background-color: #ffffff;
|
||||
/** primary **/
|
||||
--ion-color-primary: #3880ff;
|
||||
--ion-color-primary-rgb: 56, 128, 255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-primary-shade: #3171e0;
|
||||
--ion-color-primary-tint: #4c8dff;
|
||||
|
||||
/** secondary **/
|
||||
--ion-color-secondary: #3dc2ff;
|
||||
--ion-color-secondary-rgb: 61, 194, 255;
|
||||
--ion-color-secondary-contrast: #e1dfdf;
|
||||
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-secondary-shade: #36abe0;
|
||||
--ion-color-secondary-tint: #50c8ff;
|
||||
|
||||
/** tertiary **/
|
||||
--ion-color-tertiary: #5260ff;
|
||||
--ion-color-tertiary-rgb: 82, 96, 255;
|
||||
--ion-color-tertiary-contrast: #e3e3e3;
|
||||
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-tertiary-shade: #4854e0;
|
||||
--ion-color-tertiary-tint: #6370ff;
|
||||
|
||||
/** success **/
|
||||
--ion-color-success: #2dd36f;
|
||||
--ion-color-success-rgb: 45, 211, 111;
|
||||
--ion-color-success-contrast: #ededed;
|
||||
--ion-color-success-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-success-shade: #28ba62;
|
||||
--ion-color-success-tint: #42d77d;
|
||||
|
||||
/** warning #222428 **/
|
||||
--ion-color-warning: #ffc409;
|
||||
--ion-color-warning-rgb: 255, 196, 9;
|
||||
--ion-color-warning-contrast: #222428;
|
||||
--ion-color-warning-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-warning-shade: #e0ac08;
|
||||
--ion-color-warning-tint: #ffca22;
|
||||
|
||||
/** danger **/
|
||||
--ion-color-danger: #eb445a;
|
||||
--ion-color-danger-rgb: 235, 68, 90;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-danger-shade: #cf3c4f;
|
||||
--ion-color-danger-tint: #ed576b;
|
||||
|
||||
/** dark #000000 **/
|
||||
--ion-color-dark: #000000;
|
||||
--ion-color-dark-rgb: 34, 36, 40;
|
||||
--ion-color-dark-contrast: #e1e1e1;
|
||||
--ion-color-dark-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-dark-shade: #1e2023;
|
||||
--ion-color-dark-tint: #383a3e;
|
||||
|
||||
/** medium **/
|
||||
--ion-color-medium: #92949c;
|
||||
--ion-color-medium-rgb: 146, 148, 156;
|
||||
--ion-color-medium-contrast: #e6e6e6;
|
||||
--ion-color-medium-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-medium-shade: #808289;
|
||||
--ion-color-medium-tint: #9d9fa6;
|
||||
|
||||
/** light **/
|
||||
--ion-color-light: #e8e8e8;
|
||||
--ion-color-light-rgb: 244, 245, 248;
|
||||
--ion-color-light-contrast: #000000;
|
||||
--ion-color-light-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-light-shade: #d7d8da;
|
||||
--ion-color-light-tint: #f5f6f9;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
/*
|
||||
* Dark Colors
|
||||
* -------------------------------------------
|
||||
*/
|
||||
|
||||
body {
|
||||
--ion-color-primary: #428cff;
|
||||
--ion-color-primary-rgb: 66,140,255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255,255,255;
|
||||
--ion-color-primary-shade: #3a7be0;
|
||||
--ion-color-primary-tint: #5598ff;
|
||||
|
||||
--ion-color-secondary: #50c8ff;
|
||||
--ion-color-secondary-rgb: 80,200,255;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255,255,255;
|
||||
--ion-color-secondary-shade: #46b0e0;
|
||||
--ion-color-secondary-tint: #62ceff;
|
||||
|
||||
--ion-color-tertiary: #6a64ff;
|
||||
--ion-color-tertiary-rgb: 106,100,255;
|
||||
--ion-color-tertiary-contrast: #ffffff;
|
||||
--ion-color-tertiary-contrast-rgb: 255,255,255;
|
||||
--ion-color-tertiary-shade: #5d58e0;
|
||||
--ion-color-tertiary-tint: #7974ff;
|
||||
|
||||
--ion-color-success: #2fdf75;
|
||||
--ion-color-success-rgb: 47,223,117;
|
||||
--ion-color-success-contrast: #000000;
|
||||
--ion-color-success-contrast-rgb: 0,0,0;
|
||||
--ion-color-success-shade: #29c467;
|
||||
--ion-color-success-tint: #44e283;
|
||||
|
||||
--ion-color-warning: #ffd534;
|
||||
--ion-color-warning-rgb: 255,213,52;
|
||||
--ion-color-warning-contrast: #000000;
|
||||
--ion-color-warning-contrast-rgb: 0,0,0;
|
||||
--ion-color-warning-shade: #e0bb2e;
|
||||
--ion-color-warning-tint: #ffd948;
|
||||
|
||||
--ion-color-danger: #ff4961;
|
||||
--ion-color-danger-rgb: 255,73,97;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255,255,255;
|
||||
--ion-color-danger-shade: #e04055;
|
||||
--ion-color-danger-tint: #ff5b71;
|
||||
|
||||
--ion-color-dark: #dfdfdf;
|
||||
--ion-color-dark-rgb: 244,245,248;
|
||||
--ion-color-dark-contrast: #141414;
|
||||
--ion-color-dark-contrast-rgb: 0,0,0;
|
||||
--ion-color-dark-shade: #b5b5b5;
|
||||
--ion-color-dark-tint: #dedfe0;
|
||||
|
||||
--ion-color-medium: #989aa2;
|
||||
--ion-color-medium-rgb: 152,154,162;
|
||||
--ion-color-medium-contrast: #000000;
|
||||
--ion-color-medium-contrast-rgb: 0,0,0;
|
||||
--ion-color-medium-shade: #86888f;
|
||||
--ion-color-medium-tint: #a2a4ab;
|
||||
|
||||
--ion-color-light: #222428;
|
||||
--ion-color-light-rgb: 34,36,40;
|
||||
--ion-color-light-contrast: #ffffff;
|
||||
--ion-color-light-contrast-rgb: 255,255,255;
|
||||
--ion-color-light-shade: #1e2023;
|
||||
--ion-color-light-tint: #383a3e;
|
||||
}
|
||||
|
||||
/*
|
||||
* iOS Dark Theme #111111
|
||||
* -------------------------------------------
|
||||
*/
|
||||
|
||||
.ios body {
|
||||
--ion-background-color: #000000;
|
||||
--ion-background-color-rgb: 0,0,0;
|
||||
|
||||
--ion-text-color: #bfcbc7;
|
||||
--ion-text-color-rgb: 255,255,255;
|
||||
|
||||
--ion-color-step-50: #0d0d0d;
|
||||
--ion-color-step-100: #1a1a1a;
|
||||
--ion-color-step-150: #262626;
|
||||
--ion-color-step-200: #333333;
|
||||
--ion-color-step-250: #404040;
|
||||
--ion-color-step-300: #4d4d4d;
|
||||
--ion-color-step-350: #595959;
|
||||
--ion-color-step-400: #666666;
|
||||
--ion-color-step-450: #737373;
|
||||
--ion-color-step-500: #808080;
|
||||
--ion-color-step-550: #8c8c8c;
|
||||
--ion-color-step-600: #999999;
|
||||
--ion-color-step-650: #a6a6a6;
|
||||
--ion-color-step-700: #b3b3b3;
|
||||
--ion-color-step-750: #bfbfbf;
|
||||
--ion-color-step-800: #cccccc;
|
||||
--ion-color-step-850: #d9d9d9;
|
||||
--ion-color-step-900: #e6e6e6;
|
||||
--ion-color-step-950: #f2f2f2;
|
||||
|
||||
--ion-item-background: #000000;
|
||||
|
||||
--ion-card-background: #1c1c1d;
|
||||
}
|
||||
|
||||
.ios ion-modal {
|
||||
--ion-background-color: var(--ion-color-step-50);
|
||||
--ion-toolbar-background: var(--ion-color-step-100);
|
||||
--ion-toolbar-border-color: var(--ion-color-step-150);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Material Design Dark Theme
|
||||
* -------------------------------------------
|
||||
*/
|
||||
|
||||
.md body {
|
||||
--ion-background-color: #000000;
|
||||
--ion-background-color-rgb: 18,18,18;
|
||||
|
||||
--ion-text-color: #dfdede;
|
||||
--ion-text-color-rgb: 255,255,255;
|
||||
|
||||
--ion-border-color: #222222;
|
||||
|
||||
--ion-color-step-50: #1e1e1e;
|
||||
--ion-color-step-100: #2a2a2a;
|
||||
--ion-color-step-150: #363636;
|
||||
--ion-color-step-200: #414141;
|
||||
--ion-color-step-250: #4d4d4d;
|
||||
--ion-color-step-300: #595959;
|
||||
--ion-color-step-350: #656565;
|
||||
--ion-color-step-400: #717171;
|
||||
--ion-color-step-450: #7d7d7d;
|
||||
--ion-color-step-500: #898989;
|
||||
--ion-color-step-550: #949494;
|
||||
--ion-color-step-600: #a0a0a0;
|
||||
--ion-color-step-650: #acacac;
|
||||
--ion-color-step-700: #b8b8b8;
|
||||
--ion-color-step-750: #c4c4c4;
|
||||
--ion-color-step-800: #d0d0d0;
|
||||
--ion-color-step-850: #dbdbdb;
|
||||
--ion-color-step-900: #e7e7e7;
|
||||
--ion-color-step-950: #f3f3f3;
|
||||
|
||||
--ion-item-background: #000000;
|
||||
|
||||
--ion-toolbar-background: #000000;
|
||||
|
||||
--ion-tab-bar-background: #000000;
|
||||
|
||||
--ion-card-background: #1e1e1e;
|
||||
}
|
||||
}
|
||||
23
examples/relay-sqlite-example/tsconfig.json
Normal file
23
examples/relay-sqlite-example/tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"bare-plugin": ["./bare-plugin/dist/plugin"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
9
examples/relay-sqlite-example/tsconfig.node.json
Normal file
9
examples/relay-sqlite-example/tsconfig.node.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
73
examples/relay-sqlite-example/vite.config.ts
Normal file
73
examples/relay-sqlite-example/vite.config.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import legacy from '@vitejs/plugin-legacy'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
import { defineConfig } from 'vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Pages from 'vite-plugin-pages'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
// import inject from "@rollup/plugin-inject";
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
legacy(),
|
||||
Components(),
|
||||
// inject({
|
||||
// Buffer: ["buffer", "Buffer"],
|
||||
// }),
|
||||
AutoImport({
|
||||
|
||||
imports: ['vue', 'vue-router'],
|
||||
|
||||
|
||||
dirs: [
|
||||
'src/composables',
|
||||
|
||||
],
|
||||
|
||||
|
||||
dts: 'src/auto-imports.d.ts',
|
||||
|
||||
|
||||
}),
|
||||
Pages(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
workbox: {
|
||||
maximumFileSizeToCacheInBytes: 5000 * 1024 * 1024,
|
||||
},
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
crypto: 'crypto-browserify',
|
||||
buffer: "buffer",
|
||||
|
||||
},
|
||||
},
|
||||
define: {
|
||||
global: "window",
|
||||
},
|
||||
build: {
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
external: ['text-encoding'],
|
||||
output:{
|
||||
|
||||
manualChunks(id) {
|
||||
|
||||
if (id.includes('node_modules')) {
|
||||
return id.toString().split('node_modules/')[1].split('/')[0].toString();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// test: {
|
||||
// globals: true,
|
||||
// environment: 'jsdom'
|
||||
// }
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user