From 938697dee98d95a7d46d45aa2b836a3e62997ac7 Mon Sep 17 00:00:00 2001
From: ponzS <148766667+ponzS@users.noreply.github.com>
Date: Sun, 15 Jun 2025 16:39:24 +0800
Subject: [PATCH] relay-sqlite-example (#1403)
* relay-sqlite-example
Manager+Adapter
* Update README.md
---
examples/relay-sqlite-example/README.md | 26 +
.../relay-sqlite-example/capacitor.config.ts | 43 ++
examples/relay-sqlite-example/index.html | 31 +
examples/relay-sqlite-example/package.json | 204 ++++++
examples/relay-sqlite-example/src/App.vue | 131 ++++
.../src/components/RelayMode.vue | 638 ++++++++++++++++++
.../src/composables/GunStorageAdapter.ts | 202 ++++++
.../src/composables/useNetwork.ts | 121 ++++
.../src/composables/useNetworkStatus.ts | 558 +++++++++++++++
.../src/composables/useToast.ts | 88 +++
examples/relay-sqlite-example/src/main.ts | 97 +++
.../relay-sqlite-example/src/pages/index.vue | 313 +++++++++
.../relay-sqlite-example/src/router/index.ts | 46 ++
.../src/services/dbVersionService.ts | 19 +
.../src/services/globalServices.ts | 9 +
.../src/services/initializeAppService.ts | 45 ++
.../src/services/sqliteService.ts | 114 ++++
.../src/services/storageService.ts | 153 +++++
.../src/theme/variables.css | 237 +++++++
examples/relay-sqlite-example/tsconfig.json | 23 +
.../relay-sqlite-example/tsconfig.node.json | 9 +
examples/relay-sqlite-example/vite.config.ts | 73 ++
22 files changed, 3180 insertions(+)
create mode 100644 examples/relay-sqlite-example/README.md
create mode 100644 examples/relay-sqlite-example/capacitor.config.ts
create mode 100644 examples/relay-sqlite-example/index.html
create mode 100644 examples/relay-sqlite-example/package.json
create mode 100644 examples/relay-sqlite-example/src/App.vue
create mode 100644 examples/relay-sqlite-example/src/components/RelayMode.vue
create mode 100644 examples/relay-sqlite-example/src/composables/GunStorageAdapter.ts
create mode 100644 examples/relay-sqlite-example/src/composables/useNetwork.ts
create mode 100644 examples/relay-sqlite-example/src/composables/useNetworkStatus.ts
create mode 100644 examples/relay-sqlite-example/src/composables/useToast.ts
create mode 100644 examples/relay-sqlite-example/src/main.ts
create mode 100644 examples/relay-sqlite-example/src/pages/index.vue
create mode 100644 examples/relay-sqlite-example/src/router/index.ts
create mode 100644 examples/relay-sqlite-example/src/services/dbVersionService.ts
create mode 100644 examples/relay-sqlite-example/src/services/globalServices.ts
create mode 100644 examples/relay-sqlite-example/src/services/initializeAppService.ts
create mode 100644 examples/relay-sqlite-example/src/services/sqliteService.ts
create mode 100644 examples/relay-sqlite-example/src/services/storageService.ts
create mode 100644 examples/relay-sqlite-example/src/theme/variables.css
create mode 100644 examples/relay-sqlite-example/tsconfig.json
create mode 100644 examples/relay-sqlite-example/tsconfig.node.json
create mode 100644 examples/relay-sqlite-example/vite.config.ts
diff --git a/examples/relay-sqlite-example/README.md b/examples/relay-sqlite-example/README.md
new file mode 100644
index 00000000..c8970f0e
--- /dev/null
+++ b/examples/relay-sqlite-example/README.md
@@ -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
+
diff --git a/examples/relay-sqlite-example/capacitor.config.ts b/examples/relay-sqlite-example/capacitor.config.ts
new file mode 100644
index 00000000..65dc8e13
--- /dev/null
+++ b/examples/relay-sqlite-example/capacitor.config.ts
@@ -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;
diff --git a/examples/relay-sqlite-example/index.html b/examples/relay-sqlite-example/index.html
new file mode 100644
index 00000000..e1432398
--- /dev/null
+++ b/examples/relay-sqlite-example/index.html
@@ -0,0 +1,31 @@
+
+
+
+
+ Gun-Relay-sqlite
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/relay-sqlite-example/package.json b/examples/relay-sqlite-example/package.json
new file mode 100644
index 00000000..3c735543
--- /dev/null
+++ b/examples/relay-sqlite-example/package.json
@@ -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"
+}
diff --git a/examples/relay-sqlite-example/src/App.vue b/examples/relay-sqlite-example/src/App.vue
new file mode 100644
index 00000000..55eb7f15
--- /dev/null
+++ b/examples/relay-sqlite-example/src/App.vue
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/relay-sqlite-example/src/components/RelayMode.vue b/examples/relay-sqlite-example/src/components/RelayMode.vue
new file mode 100644
index 00000000..23676a9a
--- /dev/null
+++ b/examples/relay-sqlite-example/src/components/RelayMode.vue
@@ -0,0 +1,638 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Relay URL
+
+
+
+ Status
+
+ {{ peerStatuses[selectedPeer] || 'Checking' }}
+
+
+
+ Note
+
+
+
+
+ Remove Relay
+
+
+ Close
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/relay-sqlite-example/src/composables/GunStorageAdapter.ts b/examples/relay-sqlite-example/src/composables/GunStorageAdapter.ts
new file mode 100644
index 00000000..4dd3e585
--- /dev/null
+++ b/examples/relay-sqlite-example/src/composables/GunStorageAdapter.ts
@@ -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 void; reject: (err: any) => void }[]> = new Map();
+ private debounceTimers: Map = new Map();
+ private storageServ: StorageService;
+
+ constructor(storageServ: StorageService) {
+ this.storageServ = storageServ;
+ }
+
+ async get(key: string): Promise {
+ 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 {
+ await this.storageServ.run(
+ 'INSERT OR REPLACE INTO gun_nodes (key, value, timestamp) VALUES (?, ?, ?)',
+ [soul, JSON.stringify(node), Date.now()]
+ );
+ }
+
+ async batchPut(nodes: Record): Promise {
+ 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;
+ getAdapter(): GunAdapter;
+ isReady: Ref;
+}
+
+// 单例实例
+let instance: IGunSQLiteAdapter | null = null;
+
+export function useGunSQLiteAdapter(
+ sqliteService: ISQLiteService,
+ dbVersionService: IDbVersionService,
+ storageService: StorageService
+): IGunSQLiteAdapter {
+ if (instance) return instance;
+
+ const isReady: Ref = 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;
\ No newline at end of file
diff --git a/examples/relay-sqlite-example/src/composables/useNetwork.ts b/examples/relay-sqlite-example/src/composables/useNetwork.ts
new file mode 100644
index 00000000..3aef2af3
--- /dev/null
+++ b/examples/relay-sqlite-example/src/composables/useNetwork.ts
@@ -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 | null = null;
+let instance: ReturnType | null = null;
+
+function createNetwork(gunInstance: IGunInstance) {
+ // 检查 Gun.js 对等节点,反复尝试直到成功
+ async function checkPeers(): Promise {
+ const maxAttempts = 3;
+ let attempt = 0;
+ const retryDelay = 1000; // 每次尝试间隔 1 秒
+
+ while (attempt < maxAttempts) {
+ attempt++;
+ const result = await new Promise((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) {
+ 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;
+ }
+}
\ No newline at end of file
diff --git a/examples/relay-sqlite-example/src/composables/useNetworkStatus.ts b/examples/relay-sqlite-example/src/composables/useNetworkStatus.ts
new file mode 100644
index 00000000..2d29fcf0
--- /dev/null
+++ b/examples/relay-sqlite-example/src/composables/useNetworkStatus.ts
@@ -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>({});
+
+// 单例初始化标志
+let initialized = false;
+let instance: ReturnType | null = null;
+
+function createNetworkStatus(storageService: StorageService) {
+
+ const { showToast } = useToast();
+ const { isOnline, peersConnected, updateNetworkStatus, checkPeers } = useNetwork(gun);
+ const peersNotes = ref>({});
+
+ const peersList = ref([
+ '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(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, 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, 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>({});
+
+// // 单例初始化标志
+// let initialized = false;
+// let instance: ReturnType | 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;
+// }
+// }
\ No newline at end of file
diff --git a/examples/relay-sqlite-example/src/composables/useToast.ts b/examples/relay-sqlite-example/src/composables/useToast.ts
new file mode 100644
index 00000000..6388220f
--- /dev/null
+++ b/examples/relay-sqlite-example/src/composables/useToast.ts
@@ -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([]);
+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 {
+ 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,
+ };
+}
\ No newline at end of file
diff --git a/examples/relay-sqlite-example/src/main.ts b/examples/relay-sqlite-example/src/main.ts
new file mode 100644
index 00000000..7f36d093
--- /dev/null
+++ b/examples/relay-sqlite-example/src/main.ts
@@ -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);
+ });
+ });
+}
\ No newline at end of file
diff --git a/examples/relay-sqlite-example/src/pages/index.vue b/examples/relay-sqlite-example/src/pages/index.vue
new file mode 100644
index 00000000..1c1ad7ff
--- /dev/null
+++ b/examples/relay-sqlite-example/src/pages/index.vue
@@ -0,0 +1,313 @@
+
+
+
+
+
+
+
+
+
+ Network Status
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/relay-sqlite-example/src/router/index.ts b/examples/relay-sqlite-example/src/router/index.ts
new file mode 100644
index 00000000..cc4dad45
--- /dev/null
+++ b/examples/relay-sqlite-example/src/router/index.ts
@@ -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
diff --git a/examples/relay-sqlite-example/src/services/dbVersionService.ts b/examples/relay-sqlite-example/src/services/dbVersionService.ts
new file mode 100644
index 00000000..31e63c00
--- /dev/null
+++ b/examples/relay-sqlite-example/src/services/dbVersionService.ts
@@ -0,0 +1,19 @@
+export interface IDbVersionService {
+ setDbVersion(dbName: string, version: number): void
+ getDbVersion(dbName: string):number| undefined
+};
+class DbVersionService implements IDbVersionService {
+ dbNameVersionDict: Map = 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;
\ No newline at end of file
diff --git a/examples/relay-sqlite-example/src/services/globalServices.ts b/examples/relay-sqlite-example/src/services/globalServices.ts
new file mode 100644
index 00000000..7bfd943d
--- /dev/null
+++ b/examples/relay-sqlite-example/src/services/globalServices.ts
@@ -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 };
\ No newline at end of file
diff --git a/examples/relay-sqlite-example/src/services/initializeAppService.ts b/examples/relay-sqlite-example/src/services/initializeAppService.ts
new file mode 100644
index 00000000..6806ec01
--- /dev/null
+++ b/examples/relay-sqlite-example/src/services/initializeAppService.ts
@@ -0,0 +1,45 @@
+import {ISQLiteService } from '../services/sqliteService';
+import {IStorageService } from '../services/storageService';
+
+export interface IInitializeAppService {
+ initializeApp(): Promise
+};
+
+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 {
+ 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;
diff --git a/examples/relay-sqlite-example/src/services/sqliteService.ts b/examples/relay-sqlite-example/src/services/sqliteService.ts
new file mode 100644
index 00000000..8ce2e34e
--- /dev/null
+++ b/examples/relay-sqlite-example/src/services/sqliteService.ts
@@ -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
+ addUpgradeStatement(options: capSQLiteUpgradeOptions): Promise
+ openDatabase(dbName: string, loadToVersion: number, readOnly: boolean): Promise
+ closeDatabase(dbName: string, readOnly: boolean): Promise
+ saveToStore(dbName: string): Promise
+ saveToLocalDisk(dbName: string): Promise
+ isConnection(dbName: string, readOnly: boolean): Promise
+};
+
+class SQLiteService implements ISQLiteService {
+ platform = Capacitor.getPlatform();
+ sqlitePlugin = CapacitorSQLite;
+ sqliteConnection = new SQLiteConnection(CapacitorSQLite);
+ dbNameVersionDict: Map = new Map();
+
+ getPlatform(): string {
+ return this.platform;
+ }
+ async initWebStore() : Promise {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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;
diff --git a/examples/relay-sqlite-example/src/services/storageService.ts b/examples/relay-sqlite-example/src/services/storageService.ts
new file mode 100644
index 00000000..af8a8eb0
--- /dev/null
+++ b/examples/relay-sqlite-example/src/services/storageService.ts
@@ -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;
+
+
+ query(sql: string, params?: any[]): Promise;
+ run(sql: string, params?: any[]): Promise;
+ execute(sql: string): Promise;
+
+}
+
+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 {
+ 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 {
+ try {
+ return await this.db.query(sql, params);
+ } catch (err) {
+ console.error(`执行查询 ${sql} 失败:`, err);
+ throw err;
+ }
+ }
+
+ async run(sql: string, params: any[] = []): Promise {
+ try {
+ return await this.db.run(sql, params);
+ } catch (err) {
+ console.error(`执行语句 ${sql} 失败:`, err);
+ throw err;
+ }
+ }
+
+ async execute(sql: string): Promise {
+ try {
+ return await this.db.execute(sql);
+ } catch (err) {
+ console.error(`执行语句 ${sql} 失败:`, err);
+ throw err;
+ }
+ }
+}
+
+export default StorageService;
+
+
+
diff --git a/examples/relay-sqlite-example/src/theme/variables.css b/examples/relay-sqlite-example/src/theme/variables.css
new file mode 100644
index 00000000..b43c3b73
--- /dev/null
+++ b/examples/relay-sqlite-example/src/theme/variables.css
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/examples/relay-sqlite-example/tsconfig.json b/examples/relay-sqlite-example/tsconfig.json
new file mode 100644
index 00000000..04f4cadf
--- /dev/null
+++ b/examples/relay-sqlite-example/tsconfig.json
@@ -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" }]
+}
diff --git a/examples/relay-sqlite-example/tsconfig.node.json b/examples/relay-sqlite-example/tsconfig.node.json
new file mode 100644
index 00000000..9d31e2ae
--- /dev/null
+++ b/examples/relay-sqlite-example/tsconfig.node.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/examples/relay-sqlite-example/vite.config.ts b/examples/relay-sqlite-example/vite.config.ts
new file mode 100644
index 00000000..bd19d492
--- /dev/null
+++ b/examples/relay-sqlite-example/vite.config.ts
@@ -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'
+ // }
+})