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