web_lib

Common web application libraries
git clone https://radroots.dev/git/web_lib.git
Log | Files | Refs | LICENSE

commit ec594f693e29974c897af21a8cfab83f1c0e4b97
parent 7d25ff2aad22d878ac7ba91b44ee1a5ca926b6f1
Author: triesap <137732411+triesap@users.noreply.github.com>
Date:   Wed, 16 Oct 2024 21:08:16 +0000

client: refactor client api classes for use with `tauri` crate, update tsconfig

Diffstat:
Mclient/README.md | 2+-
Mclient/package.json | 31+++++++++++--------------------
Dclient/src/capacitor/bluetooth-le.ts | 60------------------------------------------------------------
Dclient/src/capacitor/browser.ts | 10----------
Dclient/src/capacitor/camera.ts | 130-------------------------------------------------------------------------------
Dclient/src/capacitor/date-picker.ts | 16----------------
Dclient/src/capacitor/device.ts | 20--------------------
Dclient/src/capacitor/dialog.ts | 50--------------------------------------------------
Dclient/src/capacitor/geolocation.ts | 38--------------------------------------
Dclient/src/capacitor/haptics.ts | 34----------------------------------
Dclient/src/capacitor/http.ts | 29-----------------------------
Dclient/src/capacitor/index.ts | 127-------------------------------------------------------------------------------
Dclient/src/capacitor/keystore.ts | 42------------------------------------------
Dclient/src/capacitor/network.ts | 20--------------------
Dclient/src/capacitor/preferences.ts | 29-----------------------------
Dclient/src/capacitor/settings.ts | 45---------------------------------------------
Dclient/src/capacitor/share.ts | 29-----------------------------
Dclient/src/capacitor/sqlite.ts | 667-------------------------------------------------------------------------------
Dclient/src/capacitor/sqlite_lib.ts | 122-------------------------------------------------------------------------------
Dclient/src/capacitor/wifi.ts | 52----------------------------------------------------
Dclient/src/capacitor/window.ts | 38--------------------------------------
Aclient/src/database/tauri.ts | 360+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aclient/src/database/types.ts | 26++++++++++++++++++++++++++
Aclient/src/dialog/tauri.ts | 30++++++++++++++++++++++++++++++
Aclient/src/dialog/types.ts | 19+++++++++++++++++++
Aclient/src/geolocation/tauri.ts | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aclient/src/geolocation/types.ts | 28++++++++++++++++++++++++++++
Aclient/src/haptics/tauri.ts | 33+++++++++++++++++++++++++++++++++
Aclient/src/haptics/types.ts | 12++++++++++++
Aclient/src/http/tauri.ts | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Aclient/src/http/types.ts | 28++++++++++++++++++++++++++++
Mclient/src/index.ts | 28++++++++++++++++++++++++++--
Aclient/src/keyring/tauri.ts | 26++++++++++++++++++++++++++
Aclient/src/keyring/types.ts | 8++++++++
Aclient/src/keystore/tauri.ts | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aclient/src/keystore/types.ts | 14++++++++++++++
Aclient/src/map/tauri.ts | 40++++++++++++++++++++++++++++++++++++++++
Aclient/src/map/types.ts | 7+++++++
Aclient/src/nostr/client.ts | 17+++++++++++++++++
Mclient/src/nostr/events.ts | 2+-
Dclient/src/nostr/index.ts | 17-----------------
Mclient/src/nostr/lib.ts | 31++++++++++++++++++++-----------
Aclient/src/nostr/types.ts | 27+++++++++++++++++++++++++++
Aclient/src/notification/tauri.ts | 31+++++++++++++++++++++++++++++++
Aclient/src/notification/types.ts | 10++++++++++
Mclient/src/types.ts | 285-------------------------------------------------------------------------------
Aclient/src/window/tauri.ts | 17+++++++++++++++++
Aclient/src/window/types.ts | 8++++++++
Mclient/tsconfig.json | 3++-
49 files changed, 992 insertions(+), 1896 deletions(-)

diff --git a/client/README.md b/client/README.md @@ -1 +1 @@ -# capacitor +# client diff --git a/client/package.json b/client/package.json @@ -3,34 +3,25 @@ "version": "0.0.0", "private": true, "license": "GPLv3", + "type": "module", + "module": "index.ts", "scripts": { - "build": "tsc", + "build": "just build && tsc", "dev": "tsc -w" }, "dependencies": { - "@capacitor/browser": "^6.0.0", - "@capacitor/camera": "^6.0.0", - "@capacitor/core": "^6.1.2", - "@capacitor/device": "^6.0.0", - "@capacitor/dialog": "^6.0.0", - "@capacitor/filesystem": "^6.0.0", - "@capacitor/geolocation": "^6.0.0", - "@capacitor/haptics": "^6.0.0", - "@capacitor/network": "^6.0.0", - "@capacitor/preferences": "^6.0.0", - "@capacitor/share": "^6.0.0", - "@capacitor/splash-screen": "^6.0.0", - "@capacitor/status-bar": "^6.0.0", "@noble/hashes": "^1.4.0", "@nostr-dev-kit/ndk": "^2.10.0", - "@radroots/capacitor-bluetooth-le": "workspace:*", - "@radroots/capacitor-date-picker": "workspace:*", - "@radroots/capacitor-native-settings": "workspace:*", - "@radroots/capacitor-secure-storage": "workspace:*", - "@radroots/capacitor-sqlite": "workspace:*", - "@radroots/capacitor-wifi": "workspace:*", "@radroots/models": "workspace:*", + "@radroots/tauri-plugin-map-display": "workspace:*", "@radroots/utils": "workspace:*", + "@tauri-apps/api": "2.0.0-rc.5", + "@tauri-apps/plugin-dialog": "^2.0.0-rc", + "@tauri-apps/plugin-geolocation": "^2.0.0-rc", + "@tauri-apps/plugin-haptics": "^2.0.0-rc", + "@tauri-apps/plugin-http": "^2.0.0-rc", + "@tauri-apps/plugin-notification": "^2.0.0-rc", + "@tauri-apps/plugin-store": "^2.0.0-rc", "nostr-tools": "^2.7.2" }, "devDependencies": { diff --git a/client/src/capacitor/bluetooth-le.ts b/client/src/capacitor/bluetooth-le.ts @@ -1,59 +0,0 @@ -import { BleClient } from '@radroots/capacitor-bluetooth-le'; -import type { IClientBluetoothLe, IClientBluetoothLeScanResult } from '../types'; - -export class CapacitorClientBluetoothLe implements IClientBluetoothLe { - private _scan_results: IClientBluetoothLeScanResult[] = []; - - private update_scan_results(result: IClientBluetoothLeScanResult): void { - const existing_result_i = this._scan_results.findIndex(i => i.device.deviceId === result.device.deviceId); - if (existing_result_i !== -1) this._scan_results[existing_result_i] = result; - else this._scan_results.push(result); - } - - public async enabled(): Promise<boolean> { - try { - const res = await BleClient.isEnabled(); - return res; - } catch (e) { - return false; - }; - } - - public async initialize(): Promise<boolean> { - try { - await BleClient.initialize({ - androidNeverForLocation: false - }); - if (!this.enabled()) await BleClient.requestEnable(); - return true; - } catch (e) { - return false; - }; - } - - public async scan(): Promise<boolean> { - try { - await BleClient.requestLEScan( - { allowDuplicates: true }, - (result) => this.update_scan_results(result) - ); - return true; - } catch (e) { - return false; - }; - } - - public async select_device(device_id: string): Promise<IClientBluetoothLeScanResult | undefined> { - try { - const res = this._scan_results.find(i => i.device.deviceId === device_id); - return res; - } catch (e) { }; - } - - public async select_devices(): Promise<IClientBluetoothLeScanResult[] | undefined> { - try { - const res = this._scan_results.length ? this._scan_results : []; - return res; - } catch (e) { }; - } -}; -\ No newline at end of file diff --git a/client/src/capacitor/browser.ts b/client/src/capacitor/browser.ts @@ -1,10 +0,0 @@ -import { Browser } from '@capacitor/browser'; -import type { IClientBrowser } from '../types'; - -export class CapacitorClientBrowser implements IClientBrowser { - public async open(url: string): Promise<void> { - try { - await Browser.open({ url }); - } catch (e) { }; - } -} diff --git a/client/src/capacitor/camera.ts b/client/src/capacitor/camera.ts @@ -1,130 +0,0 @@ - -import { Camera, CameraResultType } from '@capacitor/camera'; -import { handle_error, type ErrorResponse } from '@radroots/utils'; -import type { IClientCamera, OsPhoto, OsPhotoGallery, OsPhotoGallerySelectOptions, OsPhotoSelectOptions, OsPhotosPermissions } from '../types'; - -export class CapacitorClientCamera implements IClientCamera { - private parse_camera_result_type(value: string): CameraResultType { - switch (value) { - case 'uri': - return CameraResultType.Uri; - case 'base64': - return CameraResultType.Base64; - case 'dataUrl': - return CameraResultType.DataUrl; - default: - return CameraResultType.Uri; - } - } - - - public async enabled(): Promise<OsPhotosPermissions | ErrorResponse> { - try { - const { camera, photos } = await Camera.checkPermissions(); - return { - camera, - photos - }; - } catch (e) { - return handle_error(e); - }; - } - - public async request_enabled(): Promise<OsPhotosPermissions | ErrorResponse> { - try { - const { camera, photos } = await Camera.requestPermissions({ - permissions: ['camera', 'photos'] - }); - return { - camera, - photos - }; - } catch (e) { - return handle_error(e); - }; - } - - public async get_photo(opts: OsPhotoSelectOptions): Promise<OsPhoto | ErrorResponse> { - try { - const { - quality, - allow_editing: allowEditing, - result_type, - save_to_gallery: saveToGallery, - width, - height, - correct_orientation: correctOrientation, - prompt_label_header: promptLabelHeader, - prompt_label_cancel: promptLabelCancel, - prompt_label_photo: promptLabelPhoto, - prompt_label_picture: promptLabelPicture - } = opts; - - - const res = await Camera.getPhoto({ - quality, - allowEditing, - resultType: this.parse_camera_result_type(result_type), - saveToGallery, - width, - height, - correctOrientation, - promptLabelHeader, - promptLabelCancel, - promptLabelPhoto, - promptLabelPicture - }); - - const { - base64String: base64_string, - dataUrl: data_url, - path, - webPath: web_path, - exif, - format, - saved - } = res; - - return { - base64_string, - data_url, - path, - web_path, - exif, - format, - saved - }; - } catch (e) { - return handle_error(e); - }; - } - - public async get_photos(opts: OsPhotoGallerySelectOptions): Promise<OsPhotoGallery[] | ErrorResponse> { - try { - const { - quality, - width, - height, - correct_orientation: correctOrientation, - limit, - } = opts; - - const { photos } = await Camera.pickImages({ - quality, - width, - height, - correctOrientation, - limit, - }); - - return photos.map(i => ({ - path: i.path, - web_path: i.webPath, - exif: i.exif, - format: i.format - })); - } catch (e) { - return handle_error(e); - }; - } -} diff --git a/client/src/capacitor/date-picker.ts b/client/src/capacitor/date-picker.ts @@ -1,16 +0,0 @@ -import { DatePicker } from '@radroots/capacitor-date-picker'; -import type { IClientDatePicker, IClientDatePickerPresent } from '../types'; - -export class CapacitorClientDatePicker implements IClientDatePicker { - public async present(opts: IClientDatePickerPresent): Promise<string | undefined> { - try { - const res = await DatePicker.present({ - mode: opts.mode, - ios: { - style: "wheels", - }, - }); - if (typeof res.value === `string`) return res.value; - } catch (e) { }; - }; -} diff --git a/client/src/capacitor/device.ts b/client/src/capacitor/device.ts @@ -1,20 +0,0 @@ -import { Device } from '@capacitor/device'; -import type { CapacitorDeviceBatteryInfo, CapacitorDeviceInfo, IClientDevice } from '../types'; - - -export class CapacitorClientDevice implements IClientDevice { - public async info(): Promise<CapacitorDeviceInfo | undefined> { - try { - const res = await Device.getInfo(); - return res; - } catch (e) { }; - } - - public async battery(): Promise<CapacitorDeviceBatteryInfo | undefined> { - try { - const res = await Device.getBatteryInfo(); - return res; - } catch (e) { }; - - } -} diff --git a/client/src/capacitor/dialog.ts b/client/src/capacitor/dialog.ts @@ -1,50 +0,0 @@ -import { type ConfirmOptions, Dialog } from "@capacitor/dialog"; -import type { IClientDialog, IClientDialogConfirmOpts, IClientDialogPrompt } from "../types"; - -export class CapacitorClientDialog implements IClientDialog { - public async alert(message: string): Promise<boolean> { - try { - await Dialog.alert({ message }); - return true; - } catch (e) { - return false; - }; - } - - public async confirm(opts: IClientDialogConfirmOpts): Promise<boolean> { - try { - const message = typeof opts === `string` ? opts : opts.message; - const options: ConfirmOptions = { - message - }; - - if (typeof opts !== `string`) { - if (opts.cancel_label) options.cancelButtonTitle = opts.cancel_label; - if (opts.ok_label) options.okButtonTitle = opts.ok_label; - } - const res = await Dialog.confirm(options); - if (res && typeof res.value === `boolean`) return res.value; - return false; - } catch (e) { - return false; - }; - } - - public async prompt(opts: IClientDialogPrompt): Promise<string | false> { - try { - const { title, message, ok_button_title: okButtonTitle, cancel_button_title: cancelButtonTitle, input_placeholder: inputPlaceholder, input_text: inputText } = opts; - const res = await Dialog.prompt({ - title, - message, - okButtonTitle, - cancelButtonTitle, - inputPlaceholder, - inputText - }); - if (typeof res.value === `string` && res.cancelled === false) return res.value; - return false; - } catch (e) { - return false; - }; - } -} diff --git a/client/src/capacitor/geolocation.ts b/client/src/capacitor/geolocation.ts @@ -1,38 +0,0 @@ -import { Geolocation, type Position } from '@capacitor/geolocation'; -import { err_msg, type ErrorMessage, handle_error } from '@radroots/utils'; -import type { IClientGeolocation, IClientGeolocationPosition, IGeolocationErrorMessage } from '../types'; -import { fmt_location_coords } from '../utils'; - -export class CapacitorClientGeolocation implements IClientGeolocation { - private parse_geolocation_position(position: Position): IClientGeolocationPosition { - const pos: IClientGeolocationPosition = { - lat: fmt_location_coords(position.coords.latitude), - lng: fmt_location_coords(position.coords.longitude), - accuracy: position.coords.accuracy || undefined, - altitude: position.coords.altitude || undefined, - altitude_accuracy: position.coords.altitudeAccuracy || undefined - }; - return pos; - } - - public async current(): Promise<IClientGeolocationPosition | ErrorMessage<IGeolocationErrorMessage>> { - try { - const position = await Geolocation.getCurrentPosition(); - return this.parse_geolocation_position(position); - } catch (e) { - const { error } = handle_error(e); - if (error.includes(`The operation couldn’t be completed`)) return err_msg(`permissions-required`); - return err_msg(`*`); - }; - } - - public async has_permissions(): Promise<boolean> { - try { - const permissions = await Geolocation.checkPermissions(); - return permissions.location === `granted`; - } catch (e) { - return false; - }; - } -} - diff --git a/client/src/capacitor/haptics.ts b/client/src/capacitor/haptics.ts @@ -1,34 +0,0 @@ -import { Haptics, ImpactStyle } from '@capacitor/haptics'; -import type { IClientHaptics } from '../types'; - -export class CapacitorClientHaptics implements IClientHaptics { - public impact = async (mod?: 'less' | 'more'): Promise<void> => { - try { - await Haptics.impact({ style: mod ? mod === `more` ? ImpactStyle.Heavy : ImpactStyle.Light : ImpactStyle.Medium }); - } catch (e) { }; - }; - - public vibrate = async (duration?: number): Promise<void> => { - try { - await Haptics.vibrate(duration ? { duration } : undefined); - } catch (e) { }; - }; - - public selection_start = async (): Promise<void> => { - try { - await Haptics.selectionStart(); - } catch (e) { }; - }; - - public selection_changed = async (): Promise<void> => { - try { - await Haptics.selectionChanged(); - } catch (e) { }; - }; - - public selection_end = async (): Promise<void> => { - try { - await Haptics.selectionEnd(); - } catch (e) { }; - }; -} diff --git a/client/src/capacitor/http.ts b/client/src/capacitor/http.ts @@ -1,28 +0,0 @@ -import { CapacitorHttp, type HttpOptions } from '@capacitor/core'; -import { err_msg, type ErrorMessage } from '@radroots/utils'; -import type { IClientHttp, IClientHttpOpts, IClientHttpResponse } from '../types'; - -export class CapacitorClientHttp implements IClientHttp { - public async fetch(opts: IClientHttpOpts): Promise<IClientHttpResponse | ErrorMessage<string>> { - try { - const { url, params, data, headers, read_timeout: readTimeout, connect_timeout: connectTimeout } = opts; - const options: HttpOptions = { - url, - params, - data, - headers, - readTimeout, - connectTimeout, - }; - if (opts.method && opts.method === `post`) { - const res: IClientHttpResponse = await CapacitorHttp.post(options); - return res; - } else { - const res: IClientHttpResponse = await CapacitorHttp.get(options); - return res; - } - } catch (e) { - return err_msg(String(e)); - }; - } -} -\ No newline at end of file diff --git a/client/src/capacitor/index.ts b/client/src/capacitor/index.ts @@ -1,126 +0,0 @@ - -import { Capacitor } from "@capacitor/core"; -import { ClientNostr } from "../nostr"; -import type { IClient, IClientBluetoothLe, IClientBrowser, IClientCamera, IClientDatePicker, IClientDevice, IClientDialog, IClientGeolocation, IClientHaptics, IClientHttp, IClientKeystore, IClientNetwork, IClientPlatform, IClientPreferences, IClientShare, IClientWifi, IClientWindow } from "../types"; -import { parse_platform } from "../utils"; -import { CapacitorClientBluetoothLe } from "./bluetooth-le"; -import { CapacitorClientBrowser } from "./browser"; -import { CapacitorClientCamera } from "./camera"; -import { CapacitorClientDatePicker } from "./date-picker"; -import { CapacitorClientDevice } from "./device"; -import { CapacitorClientDialog } from "./dialog"; -import { CapacitorClientGeolocation } from "./geolocation"; -import { CapacitorClientHaptics } from "./haptics"; -import { CapacitorClientHttp } from "./http"; -import { CapacitorClientKeystore } from "./keystore"; -import { CapacitorClientNetwork } from "./network"; -import { CapacitorClientPreferences } from "./preferences"; -import { CapacitorClientSettings } from "./settings"; -import { CapacitorClientShare } from "./share"; -import { CapacitorClientSQLite, type ICapacitorClientSQLite } from "./sqlite"; -import { CapacitorClientWifi } from "./wifi"; -import { CapacitorClientWindow } from "./window"; - -export class ClientCapacitor implements IClient { - private _nostr: ClientNostr = new ClientNostr(); - private _platform: IClientPlatform = parse_platform(Capacitor.getPlatform()); - private _keystore: IClientKeystore = new CapacitorClientKeystore(); - private _device: IClientDevice = new CapacitorClientDevice(); - private _haptics: IClientHaptics = new CapacitorClientHaptics(); - private _network: IClientNetwork = new CapacitorClientNetwork(); - private _preferences: IClientPreferences = new CapacitorClientPreferences(); - private _share: IClientShare = new CapacitorClientShare(); - private _wifi: IClientWifi = new CapacitorClientWifi(); - private _dialog: IClientDialog = new CapacitorClientDialog(); - private _browser: IClientBrowser = new CapacitorClientBrowser(); - private _dates: IClientDatePicker = new CapacitorClientDatePicker(); - private _geo: IClientGeolocation = new CapacitorClientGeolocation(); - private _http: IClientHttp = new CapacitorClientHttp(); - private _window: IClientWindow = new CapacitorClientWindow(); - private _ble: IClientBluetoothLe = new CapacitorClientBluetoothLe(); - private _camera: IClientCamera = new CapacitorClientCamera(); - private _settings: CapacitorClientSettings = new CapacitorClientSettings(); - private _db: CapacitorClientSQLite; - - constructor(opts: { - sqlite: ICapacitorClientSQLite; - }) { - this._db = new CapacitorClientSQLite(opts.sqlite); - } - - public get nostr() { - return this._nostr; - } - - public get platform() { - return this._platform; - } - - public get keystore() { - return this._keystore; - } - - public get device() { - return this._device; - } - - public get haptics() { - return this._haptics; - } - - public get network() { - return this._network; - } - - public get preferences() { - return this._preferences; - } - - public get share() { - return this._share; - } - - public get wifi() { - return this._wifi; - } - - public get dialog() { - return this._dialog; - } - - public get browser() { - return this._browser; - } - - public get dates() { - return this._dates; - } - - public get geo() { - return this._geo; - } - - public get http() { - return this._http; - } - - public get window() { - return this._window; - } - - public get ble() { - return this._ble; - } - - public get camera() { - return this._camera; - } - - public get db() { - return this._db; - } - - public get settings() { - return this._settings; - } -}; -\ No newline at end of file diff --git a/client/src/capacitor/keystore.ts b/client/src/capacitor/keystore.ts @@ -1,42 +0,0 @@ -import { SecureStorage } from "@radroots/capacitor-secure-storage"; -import type { IClientKeystore } from "../types"; - -export class CapacitorClientKeystore implements IClientKeystore { - public async init() { - await SecureStorage.setKeyPrefix("radroots-"); - await SecureStorage.setSynchronize(false); - } - - public async set(key: string, val: string): Promise<boolean> { - try { - await SecureStorage.set(key, val, true, false); - return true; - } catch (e) { - return false; - } - } - - public async get(key: string): Promise<string | undefined> { - try { - const res = await SecureStorage.get(key, true, false); - if (typeof res === `string`) return res; - } catch (e) { } - } - - public async keys(): Promise<string[] | undefined> { - try { - const res = await SecureStorage.keys(); - if (res && res.length) return res; - } catch (e) { } - } - - - public async remove(key: string): Promise<boolean> { - try { - const res = await SecureStorage.remove(key, false); - return res; - } catch (e) { - return false; - } - } -} diff --git a/client/src/capacitor/network.ts b/client/src/capacitor/network.ts @@ -1,20 +0,0 @@ -import { Network } from '@capacitor/network'; -import type { IClientNetwork, IClientNetworkConnection } from '../types'; - -export class CapacitorClientNetwork implements IClientNetwork { - public async status(): Promise<IClientNetworkConnection | undefined> { - try { - const { connected, connectionType: connection_type } = await Network.getStatus(); - return { connected, connection_type }; - } catch (e) { }; - } - - public async close(): Promise<boolean> { - try { - await Network.removeAllListeners(); - return true; - } catch (e) { - return false; - }; - } -} diff --git a/client/src/capacitor/preferences.ts b/client/src/capacitor/preferences.ts @@ -1,29 +0,0 @@ -import { Preferences } from '@capacitor/preferences'; -import type { IClientPreferences } from '../types'; - -export class CapacitorClientPreferences implements IClientPreferences { - public async set(key: string, value: string): Promise<boolean> { - try { - await Preferences.set({ key, value }); - return true; - } catch (e) { - return false; - }; - } - - public async get(key: string): Promise<string | undefined> { - try { - const res = await Preferences.get({ key }); - if (typeof res.value === 'string') return res.value; - } catch (e) { }; - } - - public async remove(key: string): Promise<boolean> { - try { - await Preferences.remove({ key }); - return true; - } catch (e) { - return false; - }; - } -} diff --git a/client/src/capacitor/settings.ts b/client/src/capacitor/settings.ts @@ -1,45 +0,0 @@ -import { AndroidSettings, IOSSettings, NativeSettings } from '@radroots/capacitor-native-settings'; -import type { IClientSettings, IClientSettingsOpen, } from '../types'; - -export class CapacitorClientSettings implements IClientSettings { - private async open_android(setting: keyof typeof AndroidSettings): Promise<boolean> { - try { - const res = await NativeSettings.openAndroid({ - option: AndroidSettings[setting], - }); - if (typeof res.status === `boolean`) return res.status; - else return false; - } catch (e) { - return false; - }; - }; - - private async open_ios(setting: keyof typeof IOSSettings): Promise<boolean> { - try { - const res = await NativeSettings.openIOS({ - option: IOSSettings[setting], - }); - if (typeof res.status === `boolean`) return res.status; - else return false; - } catch (e) { - return false; - }; - }; - - public async open(opts: IClientSettingsOpen): Promise<boolean> { - try { - if (`android` in opts) { - const { android: { setting } } = opts; - const res = await this.open_android(setting); - return res; - } else { - const { ios: { setting } } = opts; - const res = await this.open_ios(setting); - return res; - } - - } catch (e) { - return false; - }; - }; -} diff --git a/client/src/capacitor/share.ts b/client/src/capacitor/share.ts @@ -1,29 +0,0 @@ -import { Share } from '@capacitor/share'; -import type { IClientShare, IClientShareOpenOpts } from '../types'; - -export class CapacitorClientShare implements IClientShare { - public async status(): Promise<boolean> { - try { - const res = await Share.canShare(); - if (res && typeof res.value === `boolean`) return res.value; - return false; - } catch (e) { - return false; - }; - } - - public async open(opts: IClientShareOpenOpts): Promise<void> { - try { - const { title, text, url, files, dialog_title: dialogTitle } = opts; - await Share.share({ - title, - text, - url, - files, - dialogTitle - }); - } catch (e) { }; - } -} - - diff --git a/client/src/capacitor/sqlite.ts b/client/src/capacitor/sqlite.ts @@ -1,666 +0,0 @@ -import { SQLiteDBConnection, type capSQLiteChanges, type DBSQLiteValues } from "@radroots/capacitor-sqlite"; -import { type IModelsQueryBindValue, type IModelsQueryBindValueTuple, type IModelsQueryParam, type IModelsQueryValue, type ModelsUniqueConstraintMessages, type ILocationGcsAddResolve,type ILocationGcsDeleteResolve,type ILocationGcsGetResolve,type ILocationGcsUpdateResolve, parse_location_gcs_form_fields, location_gcs_sort, type ILocationGcsGetList, type ILocationGcsGet, type ILocationGcsUpdate, type ILocationGcsQueryBindValues, type ILocationGcsQueryBindValuesKey, type ILocationGcsQueryBindValuesTuple, parse_location_gcs, parse_location_gcs_list, type LocationGcs, type LocationGcsFields, type LocationGcsFormFields, LocationGcsSchema, LocationGcsUpdateSchema, type ITradeProductAddResolve,type ITradeProductDeleteResolve,type ITradeProductGetResolve,type ITradeProductUpdateResolve, parse_trade_product_form_fields, trade_product_sort, type ITradeProductGetList, type ITradeProductGet, type ITradeProductUpdate, type ITradeProductQueryBindValues, type ITradeProductQueryBindValuesKey, type ITradeProductQueryBindValuesTuple, parse_trade_product, parse_trade_product_list, type TradeProduct, type TradeProductFields, type TradeProductFormFields, TradeProductSchema, TradeProductUpdateSchema, type INostrProfileAddResolve,type INostrProfileDeleteResolve,type INostrProfileGetResolve,type INostrProfileUpdateResolve, parse_nostr_profile_form_fields, nostr_profile_sort, type INostrProfileGetList, type INostrProfileGet, type INostrProfileUpdate, type INostrProfileQueryBindValues, type INostrProfileQueryBindValuesKey, type INostrProfileQueryBindValuesTuple, parse_nostr_profile, parse_nostr_profile_list, type NostrProfile, type NostrProfileFields, type NostrProfileFormFields, NostrProfileSchema, NostrProfileUpdateSchema, NostrProfileMetadataSchema, type INostrRelayAddResolve,type INostrRelayDeleteResolve,type INostrRelayGetResolve,type INostrRelayUpdateResolve, parse_nostr_relay_form_fields, nostr_relay_sort, type INostrRelayGetList, type INostrRelayGet, type INostrRelayUpdate, type INostrRelayQueryBindValues, type INostrRelayQueryBindValuesKey, type INostrRelayQueryBindValuesTuple, parse_nostr_relay, parse_nostr_relay_list, type NostrRelay, type NostrRelayFields, type NostrRelayFormFields, NostrRelaySchema, NostrRelayUpdateSchema } from "@radroots/models"; -import { err_msg, time_created_on, uuidv4, type ErrorMessage } from "@radroots/utils"; -import { sqlite_svc, sqlite_version_svc, type IISQLiteServiceOpenDatabase } from "./sqlite_lib"; - -export type ICapacitorClientSQLiteMessage = - | ModelsUniqueConstraintMessages - | "*-validate" - | "*-result" - | "*-fields" - | "*-open" - | "*-connect" - | "*-connection" - | "*-exe-result" - | "*-exe" - | "*-sel-result" - | "*-sel" - | "*"; - -export type ICapacitorClientSQLiteUpgrade = { toVersion: number; statements: string[]; }; -export type ICapacitorClientSQLite = { - database: string; - upgrade: ICapacitorClientSQLiteUpgrade[]; -}; - -export class CapacitorClientSQLite { - private _platform = sqlite_svc.platform; - private _conn: SQLiteDBConnection | null = null; - private _database: string; - private _upgrade: ICapacitorClientSQLiteUpgrade[]; - private _version: number; - - constructor(opts: ICapacitorClientSQLite) { - const { database, upgrade } = opts; - this._database = database; - this._upgrade = upgrade; - this._version = upgrade[upgrade.length - 1].toVersion; - } - - private append_logs(error_msg: ICapacitorClientSQLiteMessage, bind_values: any, query: string, e: any): ICapacitorClientSQLiteMessage { - sqlite_svc.logs.push({ - key: error_msg, - bind_values, - query, - e, - }); - return error_msg; - } - - private handle_errors(error_msg: ICapacitorClientSQLiteMessage, bind_values: IModelsQueryBindValue[], query: string, e: any): ErrorMessage<ICapacitorClientSQLiteMessage> { - const err = this.append_logs(error_msg, bind_values, query, e); - if (String(e).includes("UNIQUE constraint failed: location_gcs.geohash")) return err_msg("*-location-gcs-geohash-unique"); - else if (String(e).includes("UNIQUE constraint failed: nostr_relay.url")) return err_msg("*-nostr-relay-url-unique"); - return err_msg(err); - } - - private filter_bind_value_fields(fields: IModelsQueryBindValueTuple[]): IModelsQueryBindValueTuple[] { - return fields.filter(([_, v]) => !!v); - } - - private async execute(query: string, bind_values: IModelsQueryBindValue[]): Promise<capSQLiteChanges | ICapacitorClientSQLiteMessage> { - try { - if (!this._conn) return "*-connection"; - const result = await this._conn.run(query, bind_values.length ? bind_values : undefined); - if (sqlite_svc.platform === "web" && this._database) await sqlite_svc.save_to_store(this._database); - if (result) return result; - return this.append_logs("*-exe-result", bind_values, query, result); - } catch (e) { - return this.append_logs("*-exe", bind_values, query, e); - }; - } - - private async select(query: string, bind_values: IModelsQueryBindValue[]): Promise<DBSQLiteValues | ICapacitorClientSQLiteMessage> { - try { - if (!this._conn) return "*-connection"; - const result = await this._conn.query(query, bind_values.length ? bind_values : undefined); - if (result) return result; - return this.append_logs("*-sel-result", bind_values, query, result); - } catch (e) { - return this.append_logs("*-sel", bind_values, query, e); - }; - } - - private async open(opts: IISQLiteServiceOpenDatabase): Promise<undefined | ErrorMessage<ICapacitorClientSQLiteMessage>> { - try { - if (this._platform === "web") await sqlite_svc.init_web_store(); - await sqlite_svc.add_upgrade({ - database: opts.database, - upgrade: opts.upgrade - }); - const conn = await sqlite_svc.open_db(opts.database, opts.version, false); - sqlite_version_svc.set_version(opts.database, opts.version); - if (!conn) return err_msg("*-connection"); - if (opts.platform === "web") await sqlite_svc.save_to_store(opts.database); - this._conn = conn; - } catch (e) { - return this.handle_errors("*-open", [], "open()", e); - }; - } - - public async connect(): Promise<true | ErrorMessage<ICapacitorClientSQLiteMessage>> { - try { - const { - _platform: platform, - _database: database, - _upgrade: upgrade, - _version: version - } = this; - await this.open({ platform, database, upgrade, version }).then(async () => { - if (this._platform === "web") await sqlite_svc.save_to_store(database); - }); - return true; - } catch (e) { - return this.handle_errors("*-connect", [], "connect()", e); - }; - } - - private location_gcs_add_validate(fields: LocationGcsFormFields): LocationGcsFields | string[] { - const fields_r = Object.entries(fields).filter(([_, v]) => !!v).reduce((acc: Record<string, IModelsQueryValue>, i) => { - const [key, val] = parse_location_gcs_form_fields(i); - acc[key] = val; - return acc; - }, {}); - const schema = LocationGcsSchema; - const parsed_schema = schema.safeParse(fields_r); - if (!parsed_schema.success) return parsed_schema.error.issues.map(i => i.message); - else return { - ...parsed_schema.data - }; - } - - public async location_gcs_add(opts: LocationGcsFormFields): Promise<ILocationGcsAddResolve<ICapacitorClientSQLiteMessage>> { - const err_s = this.location_gcs_add_validate(opts); - if (Array.isArray(err_s)) return { err_s }; - const fields = Object.entries(err_s); - if (!fields.length) return err_msg("*-fields"); - const id = uuidv4(); - const bind_values_tup: IModelsQueryBindValueTuple[] = [ - ["id", id], - ["created_at", time_created_on()] - ]; - for (const field of this.filter_bind_value_fields(fields)) bind_values_tup.push(field); - const bind_values = bind_values_tup.map(([_, v]) => v); - const query = `INSERT INTO location_gcs (${bind_values_tup.map(([k]) => k).join(", ")}) VALUES (${bind_values_tup.map((_, index) => `$${1 + index}`).join(", ")});`; - try { - const result = await this.execute(query, bind_values); - if (typeof result !== "string" && typeof result.changes?.changes === "number" && result.changes.changes > 0) return { id }; - else if (typeof result === "string") return err_msg(result); - return err_msg("*-result"); - } catch (e) { - return this.handle_errors("*", bind_values, query, ["location_gcs_add", opts, e]); - }; - } - - - private location_gcs_query_bind_values = (opts: ILocationGcsQueryBindValues): ILocationGcsQueryBindValuesTuple => { - if ("id" in opts) return ["id", opts.id]; - else return ["geohash", opts.geohash]; - } - - private location_gcs_get_query_list = (opts: ILocationGcsGetList): IModelsQueryParam => { - const sort = location_gcs_sort[opts.sort || "newest"]; - let query = ""; - const bind_values: IModelsQueryBindValue[] = []; - if (opts.list[0] === "all") { - query = `SELECT * FROM location_gcs ORDER BY ${sort};`; - } - if (!query) throw new Error("Error: Missing query (location_gcs_get_query_list)") - return { - query, - bind_values - }; - } - - private location_gcs_get_parse_opts = (opts: ILocationGcsGet): IModelsQueryParam => { - if ("list" in opts) return this.location_gcs_get_query_list(opts); - else { - const [bv_k, bv_v] = this.location_gcs_query_bind_values(opts); - return { - query: `SELECT * FROM location_gcs WHERE ${bv_k} = $1;`, - bind_values: [bv_v] - }; - }; - } - - public async location_gcs_get(opts: ILocationGcsGet): Promise<ILocationGcsGetResolve<ICapacitorClientSQLiteMessage>> { - const { query, bind_values } = this.location_gcs_get_parse_opts(opts); - try { - const response = await this.select(query, bind_values); - if (typeof response === "string") return err_msg(response); - const results = parse_location_gcs_list(response); - if (Array.isArray(results)) return { results }; - return err_msg("*-result"); - } catch (e) { - return this.handle_errors("*", bind_values, query, ["location_gcs_get", opts, e]); - }; - } - - public async location_gcs_delete(opts: ILocationGcsQueryBindValues): Promise<ILocationGcsDeleteResolve<ICapacitorClientSQLiteMessage>> { - const [bv_k, bv_v] = this.location_gcs_query_bind_values(opts); - const bind_values = [bv_v]; - const query = `DELETE FROM location_gcs WHERE ${bv_k} = $1;`; - try { - const response = await this.execute(query, bind_values); - if (typeof response === "string") return err_msg(response); - else if (typeof response.changes?.changes === "number" && response.changes.changes > 0) return true; - return err_msg("*-result"); - } catch (e) { - return this.handle_errors("*", bind_values, query, ["location_gcs_delete", opts, e]); - }; - } - - private location_gcs_update_validate(fields: Partial<LocationGcsFormFields>): Partial<LocationGcsFields> | string[] { - const fields_r = Object.entries(fields).filter(([_, v]) => !!v).reduce((acc: Record<string, IModelsQueryValue>, i) => { - const [key, val] = parse_location_gcs_form_fields(i); - acc[key] = val; - return acc; - }, {}); - const schema = LocationGcsUpdateSchema; - const parsed_schema = schema.safeParse(fields_r); - if (!parsed_schema.success) return parsed_schema.error.issues.map(i => i.message); - else return { - ...parsed_schema.data - }; - } - - public async location_gcs_update(opts: ILocationGcsUpdate): Promise<ILocationGcsUpdateResolve<ICapacitorClientSQLiteMessage>> { - const err_s = this.location_gcs_update_validate(opts.fields); - if (Array.isArray(err_s)) return { err_s }; - const fields = this.filter_bind_value_fields(Object.entries(err_s)); - if (!fields.length) return err_msg("*-fields"); - const [bv_k, bv_v] = this.location_gcs_query_bind_values(opts.on); - const bind_values = [...fields.map(([_, v]) => v), bv_v]; - const query = `UPDATE location_gcs SET ${fields.map(([k], index) => `${k} = $${1 + index}`).join(", ")} WHERE ${bv_k} = $${bind_values.length};`; - try { - const response = await this.execute(query, bind_values); - if (typeof response === "string") return err_msg(response); - else if (typeof response.changes?.changes === "number" && response.changes.changes > 0) return true; - return err_msg("*-result"); - } catch (e) { - return this.handle_errors("*", bind_values, query, ["location_gcs_update", opts, e]); - }; - } - - private trade_product_add_validate(fields: TradeProductFormFields): TradeProductFields | string[] { - const fields_r = Object.entries(fields).filter(([_, v]) => !!v).reduce((acc: Record<string, IModelsQueryValue>, i) => { - const [key, val] = parse_trade_product_form_fields(i); - acc[key] = val; - return acc; - }, {}); - const schema = TradeProductSchema; - const parsed_schema = schema.safeParse(fields_r); - if (!parsed_schema.success) return parsed_schema.error.issues.map(i => i.message); - else return { - ...parsed_schema.data - }; - } - - public async trade_product_add(opts: TradeProductFormFields): Promise<ITradeProductAddResolve<ICapacitorClientSQLiteMessage>> { - const err_s = this.trade_product_add_validate(opts); - if (Array.isArray(err_s)) return { err_s }; - const fields = Object.entries(err_s); - if (!fields.length) return err_msg("*-fields"); - const id = uuidv4(); - const bind_values_tup: IModelsQueryBindValueTuple[] = [ - ["id", id], - ["created_at", time_created_on()] - ]; - for (const field of this.filter_bind_value_fields(fields)) bind_values_tup.push(field); - const bind_values = bind_values_tup.map(([_, v]) => v); - const query = `INSERT INTO trade_product (${bind_values_tup.map(([k]) => k).join(", ")}) VALUES (${bind_values_tup.map((_, index) => `$${1 + index}`).join(", ")});`; - try { - const result = await this.execute(query, bind_values); - if (typeof result !== "string" && typeof result.changes?.changes === "number" && result.changes.changes > 0) return { id }; - else if (typeof result === "string") return err_msg(result); - return err_msg("*-result"); - } catch (e) { - return this.handle_errors("*", bind_values, query, ["trade_product_add", opts, e]); - }; - } - - - private trade_product_query_bind_values = (opts: ITradeProductQueryBindValues): ITradeProductQueryBindValuesTuple => { - return ["id", opts.id]; - } - - private trade_product_get_query_list = (opts: ITradeProductGetList): IModelsQueryParam => { - const sort = trade_product_sort[opts.sort || "newest"]; - let query = ""; - const bind_values: IModelsQueryBindValue[] = []; - if (opts.list[0] === "all") { - query = `SELECT * FROM trade_product ORDER BY ${sort};`; - } - if (!query) throw new Error("Error: Missing query (trade_product_get_query_list)") - return { - query, - bind_values - }; - } - - private trade_product_get_parse_opts = (opts: ITradeProductGet): IModelsQueryParam => { - if ("list" in opts) return this.trade_product_get_query_list(opts); - else { - const [bv_k, bv_v] = this.trade_product_query_bind_values(opts); - return { - query: `SELECT * FROM trade_product WHERE ${bv_k} = $1;`, - bind_values: [bv_v] - }; - }; - } - - public async trade_product_get(opts: ITradeProductGet): Promise<ITradeProductGetResolve<ICapacitorClientSQLiteMessage>> { - const { query, bind_values } = this.trade_product_get_parse_opts(opts); - try { - const response = await this.select(query, bind_values); - if (typeof response === "string") return err_msg(response); - const results = parse_trade_product_list(response); - if (Array.isArray(results)) return { results }; - return err_msg("*-result"); - } catch (e) { - return this.handle_errors("*", bind_values, query, ["trade_product_get", opts, e]); - }; - } - - public async trade_product_delete(opts: ITradeProductQueryBindValues): Promise<ITradeProductDeleteResolve<ICapacitorClientSQLiteMessage>> { - const [bv_k, bv_v] = this.trade_product_query_bind_values(opts); - const bind_values = [bv_v]; - const query = `DELETE FROM trade_product WHERE ${bv_k} = $1;`; - try { - const response = await this.execute(query, bind_values); - if (typeof response === "string") return err_msg(response); - else if (typeof response.changes?.changes === "number" && response.changes.changes > 0) return true; - return err_msg("*-result"); - } catch (e) { - return this.handle_errors("*", bind_values, query, ["trade_product_delete", opts, e]); - }; - } - - private trade_product_update_validate(fields: Partial<TradeProductFormFields>): Partial<TradeProductFields> | string[] { - const fields_r = Object.entries(fields).filter(([_, v]) => !!v).reduce((acc: Record<string, IModelsQueryValue>, i) => { - const [key, val] = parse_trade_product_form_fields(i); - acc[key] = val; - return acc; - }, {}); - const schema = TradeProductUpdateSchema; - const parsed_schema = schema.safeParse(fields_r); - if (!parsed_schema.success) return parsed_schema.error.issues.map(i => i.message); - else return { - ...parsed_schema.data - }; - } - - public async trade_product_update(opts: ITradeProductUpdate): Promise<ITradeProductUpdateResolve<ICapacitorClientSQLiteMessage>> { - const err_s = this.trade_product_update_validate(opts.fields); - if (Array.isArray(err_s)) return { err_s }; - const fields = this.filter_bind_value_fields(Object.entries(err_s)); - if (!fields.length) return err_msg("*-fields"); - const [bv_k, bv_v] = this.trade_product_query_bind_values(opts.on); - const bind_values = [...fields.map(([_, v]) => v), bv_v]; - const query = `UPDATE trade_product SET ${fields.map(([k], index) => `${k} = $${1 + index}`).join(", ")} WHERE ${bv_k} = $${bind_values.length};`; - try { - const response = await this.execute(query, bind_values); - if (typeof response === "string") return err_msg(response); - else if (typeof response.changes?.changes === "number" && response.changes.changes > 0) return true; - return err_msg("*-result"); - } catch (e) { - return this.handle_errors("*", bind_values, query, ["trade_product_update", opts, e]); - }; - } - - private nostr_profile_add_validate(fields: NostrProfileFormFields): NostrProfileFields | string[] { - const fields_r = Object.entries(fields).filter(([_, v]) => !!v).reduce((acc: Record<string, IModelsQueryValue>, i) => { - const [key, val] = parse_nostr_profile_form_fields(i); - acc[key] = val; - return acc; - }, {}); - const schema = NostrProfileSchema.and(NostrProfileMetadataSchema); - const parsed_schema = schema.safeParse(fields_r); - if (!parsed_schema.success) return parsed_schema.error.issues.map(i => i.message); - else return { - ...parsed_schema.data - }; - } - - public async nostr_profile_add(opts: NostrProfileFormFields): Promise<INostrProfileAddResolve<ICapacitorClientSQLiteMessage>> { - const err_s = this.nostr_profile_add_validate(opts); - if (Array.isArray(err_s)) return { err_s }; - const fields = Object.entries(err_s); - if (!fields.length) return err_msg("*-fields"); - const id = uuidv4(); - const bind_values_tup: IModelsQueryBindValueTuple[] = [ - ["id", id], - ["created_at", time_created_on()] - ]; - for (const field of this.filter_bind_value_fields(fields)) bind_values_tup.push(field); - const bind_values = bind_values_tup.map(([_, v]) => v); - const query = `INSERT INTO nostr_profile (${bind_values_tup.map(([k]) => k).join(", ")}) VALUES (${bind_values_tup.map((_, index) => `$${1 + index}`).join(", ")});`; - try { - const result = await this.execute(query, bind_values); - if (typeof result !== "string" && typeof result.changes?.changes === "number" && result.changes.changes > 0) return { id }; - else if (typeof result === "string") return err_msg(result); - return err_msg("*-result"); - } catch (e) { - return this.handle_errors("*", bind_values, query, ["nostr_profile_add", opts, e]); - }; - } - - - private nostr_profile_query_bind_values = (opts: INostrProfileQueryBindValues): INostrProfileQueryBindValuesTuple => { - if ("id" in opts) return ["id", opts.id]; - else return ["public_key", opts.public_key]; - } - - private nostr_profile_get_query_list = (opts: INostrProfileGetList): IModelsQueryParam => { - const sort = nostr_profile_sort[opts.sort || "newest"]; - let query = ""; - const bind_values: IModelsQueryBindValue[] = []; - if (opts.list[0] === "all") { - query = `SELECT * FROM nostr_profile ORDER BY ${sort};`; - } else if (opts.list[0] === "on_relay") { - query = `SELECT pr.* FROM nostr_profile pr JOIN nostr_profile_relay np_rl ON pr.id = np_rl.tb_pr_rl_0 WHERE np_rl.tb_pr_rl_1 = $1;`; - bind_values.push(opts.list[1].id); - } else if (opts.list[0] === "off_relay") { - query = `SELECT pr.* FROM nostr_profile pr WHERE NOT EXISTS (SELECT 1 FROM nostr_profile_relay pr_rl WHERE pr_rl.tb_pr_rl_0 = pr.id AND pr_rl.tb_pr_rl_1 = $1);`; - bind_values.push(opts.list[1].id); - } - if (!query) throw new Error("Error: Missing query (nostr_profile_get_query_list)") - return { - query, - bind_values - }; - } - - private nostr_profile_get_parse_opts = (opts: INostrProfileGet): IModelsQueryParam => { - if ("list" in opts) return this.nostr_profile_get_query_list(opts); - else { - const [bv_k, bv_v] = this.nostr_profile_query_bind_values(opts); - return { - query: `SELECT * FROM nostr_profile WHERE ${bv_k} = $1;`, - bind_values: [bv_v] - }; - }; - } - - public async nostr_profile_get(opts: INostrProfileGet): Promise<INostrProfileGetResolve<ICapacitorClientSQLiteMessage>> { - const { query, bind_values } = this.nostr_profile_get_parse_opts(opts); - try { - const response = await this.select(query, bind_values); - if (typeof response === "string") return err_msg(response); - const results = parse_nostr_profile_list(response); - if (Array.isArray(results)) return { results }; - return err_msg("*-result"); - } catch (e) { - return this.handle_errors("*", bind_values, query, ["nostr_profile_get", opts, e]); - }; - } - - public async nostr_profile_delete(opts: INostrProfileQueryBindValues): Promise<INostrProfileDeleteResolve<ICapacitorClientSQLiteMessage>> { - const [bv_k, bv_v] = this.nostr_profile_query_bind_values(opts); - const bind_values = [bv_v]; - const query = `DELETE FROM nostr_profile WHERE ${bv_k} = $1;`; - try { - const response = await this.execute(query, bind_values); - if (typeof response === "string") return err_msg(response); - else if (typeof response.changes?.changes === "number" && response.changes.changes > 0) return true; - return err_msg("*-result"); - } catch (e) { - return this.handle_errors("*", bind_values, query, ["nostr_profile_delete", opts, e]); - }; - } - - private nostr_profile_update_validate(fields: Partial<NostrProfileFormFields>): Partial<NostrProfileFields> | string[] { - const fields_r = Object.entries(fields).filter(([_, v]) => !!v).reduce((acc: Record<string, IModelsQueryValue>, i) => { - const [key, val] = parse_nostr_profile_form_fields(i); - acc[key] = val; - return acc; - }, {}); - const schema = NostrProfileUpdateSchema.and(NostrProfileMetadataSchema); - const parsed_schema = schema.safeParse(fields_r); - if (!parsed_schema.success) return parsed_schema.error.issues.map(i => i.message); - else return { - ...parsed_schema.data - }; - } - - public async nostr_profile_update(opts: INostrProfileUpdate): Promise<INostrProfileUpdateResolve<ICapacitorClientSQLiteMessage>> { - const err_s = this.nostr_profile_update_validate(opts.fields); - if (Array.isArray(err_s)) return { err_s }; - const fields = this.filter_bind_value_fields(Object.entries(err_s)); - if (!fields.length) return err_msg("*-fields"); - const [bv_k, bv_v] = this.nostr_profile_query_bind_values(opts.on); - const bind_values = [...fields.map(([_, v]) => v), bv_v]; - const query = `UPDATE nostr_profile SET ${fields.map(([k], index) => `${k} = $${1 + index}`).join(", ")} WHERE ${bv_k} = $${bind_values.length};`; - try { - const response = await this.execute(query, bind_values); - if (typeof response === "string") return err_msg(response); - else if (typeof response.changes?.changes === "number" && response.changes.changes > 0) return true; - return err_msg("*-result"); - } catch (e) { - return this.handle_errors("*", bind_values, query, ["nostr_profile_update", opts, e]); - }; - } - - private nostr_relay_add_validate(fields: NostrRelayFormFields): NostrRelayFields | string[] { - const fields_r = Object.entries(fields).filter(([_, v]) => !!v).reduce((acc: Record<string, IModelsQueryValue>, i) => { - const [key, val] = parse_nostr_relay_form_fields(i); - acc[key] = val; - return acc; - }, {}); - const schema = NostrRelaySchema; - const parsed_schema = schema.safeParse(fields_r); - if (!parsed_schema.success) return parsed_schema.error.issues.map(i => i.message); - else return { - ...parsed_schema.data - }; - } - - public async nostr_relay_add(opts: NostrRelayFormFields): Promise<INostrRelayAddResolve<ICapacitorClientSQLiteMessage>> { - const err_s = this.nostr_relay_add_validate(opts); - if (Array.isArray(err_s)) return { err_s }; - const fields = Object.entries(err_s); - if (!fields.length) return err_msg("*-fields"); - const id = uuidv4(); - const bind_values_tup: IModelsQueryBindValueTuple[] = [ - ["id", id], - ["created_at", time_created_on()] - ]; - for (const field of this.filter_bind_value_fields(fields)) bind_values_tup.push(field); - const bind_values = bind_values_tup.map(([_, v]) => v); - const query = `INSERT INTO nostr_relay (${bind_values_tup.map(([k]) => k).join(", ")}) VALUES (${bind_values_tup.map((_, index) => `$${1 + index}`).join(", ")});`; - try { - const result = await this.execute(query, bind_values); - if (typeof result !== "string" && typeof result.changes?.changes === "number" && result.changes.changes > 0) return { id }; - else if (typeof result === "string") return err_msg(result); - return err_msg("*-result"); - } catch (e) { - return this.handle_errors("*", bind_values, query, ["nostr_relay_add", opts, e]); - }; - } - - - private nostr_relay_query_bind_values = (opts: INostrRelayQueryBindValues): INostrRelayQueryBindValuesTuple => { - if ("id" in opts) return ["id", opts.id]; - else return ["url", opts.url]; - } - - private nostr_relay_get_query_list = (opts: INostrRelayGetList): IModelsQueryParam => { - const sort = nostr_relay_sort[opts.sort || "newest"]; - let query = ""; - const bind_values: IModelsQueryBindValue[] = []; - if (opts.list[0] === "all") { - query = `SELECT * FROM nostr_relay ORDER BY ${sort};`; - } else if (opts.list[0] === "on_profile") { - query = `SELECT rl.* FROM nostr_relay rl JOIN nostr_profile_relay pr_rl ON rl.id = pr_rl.tb_pr_rl_1 JOIN nostr_profile pr ON pr.id = pr_rl.tb_pr_rl_0 WHERE pr.public_key = $1 ORDER BY ${sort};`; - bind_values.push(opts.list[1].public_key); - } else if (opts.list[0] === "off_profile") { - query = `SELECT rl.* FROM nostr_relay rl LEFT JOIN nostr_profile_relay pr_rl ON rl.id = pr_rl.tb_pr_rl_1 LEFT JOIN nostr_profile pr ON pr.id = pr_rl.tb_pr_rl_0 WHERE pr.public_key <> $1 ORDER BY ${sort};`; - bind_values.push(opts.list[1].public_key); - } - if (!query) throw new Error("Error: Missing query (nostr_relay_get_query_list)") - return { - query, - bind_values - }; - } - - private nostr_relay_get_parse_opts = (opts: INostrRelayGet): IModelsQueryParam => { - if ("list" in opts) return this.nostr_relay_get_query_list(opts); - else { - const [bv_k, bv_v] = this.nostr_relay_query_bind_values(opts); - return { - query: `SELECT * FROM nostr_relay WHERE ${bv_k} = $1;`, - bind_values: [bv_v] - }; - }; - } - - public async nostr_relay_get(opts: INostrRelayGet): Promise<INostrRelayGetResolve<ICapacitorClientSQLiteMessage>> { - const { query, bind_values } = this.nostr_relay_get_parse_opts(opts); - try { - const response = await this.select(query, bind_values); - if (typeof response === "string") return err_msg(response); - const results = parse_nostr_relay_list(response); - if (Array.isArray(results)) return { results }; - return err_msg("*-result"); - } catch (e) { - return this.handle_errors("*", bind_values, query, ["nostr_relay_get", opts, e]); - }; - } - - public async nostr_relay_delete(opts: INostrRelayQueryBindValues): Promise<INostrRelayDeleteResolve<ICapacitorClientSQLiteMessage>> { - const [bv_k, bv_v] = this.nostr_relay_query_bind_values(opts); - const bind_values = [bv_v]; - const query = `DELETE FROM nostr_relay WHERE ${bv_k} = $1;`; - try { - const response = await this.execute(query, bind_values); - if (typeof response === "string") return err_msg(response); - else if (typeof response.changes?.changes === "number" && response.changes.changes > 0) return true; - return err_msg("*-result"); - } catch (e) { - return this.handle_errors("*", bind_values, query, ["nostr_relay_delete", opts, e]); - }; - } - - private nostr_relay_update_validate(fields: Partial<NostrRelayFormFields>): Partial<NostrRelayFields> | string[] { - const fields_r = Object.entries(fields).filter(([_, v]) => !!v).reduce((acc: Record<string, IModelsQueryValue>, i) => { - const [key, val] = parse_nostr_relay_form_fields(i); - acc[key] = val; - return acc; - }, {}); - const schema = NostrRelayUpdateSchema; - const parsed_schema = schema.safeParse(fields_r); - if (!parsed_schema.success) return parsed_schema.error.issues.map(i => i.message); - else return { - ...parsed_schema.data - }; - } - - public async nostr_relay_update(opts: INostrRelayUpdate): Promise<INostrRelayUpdateResolve<ICapacitorClientSQLiteMessage>> { - const err_s = this.nostr_relay_update_validate(opts.fields); - if (Array.isArray(err_s)) return { err_s }; - const fields = this.filter_bind_value_fields(Object.entries(err_s)); - if (!fields.length) return err_msg("*-fields"); - const [bv_k, bv_v] = this.nostr_relay_query_bind_values(opts.on); - const bind_values = [...fields.map(([_, v]) => v), bv_v]; - const query = `UPDATE nostr_relay SET ${fields.map(([k], index) => `${k} = $${1 + index}`).join(", ")} WHERE ${bv_k} = $${bind_values.length};`; - try { - const response = await this.execute(query, bind_values); - if (typeof response === "string") return err_msg(response); - else if (typeof response.changes?.changes === "number" && response.changes.changes > 0) return true; - return err_msg("*-result"); - } catch (e) { - return this.handle_errors("*", bind_values, query, ["nostr_relay_update", opts, e]); - }; - } - - public async set_nostr_profile_relay(opts: { nostr_profile: INostrProfileQueryBindValues; nostr_relay: INostrRelayQueryBindValues; }): Promise<true | ErrorMessage<ICapacitorClientSQLiteMessage>> { - const bv_np = this.nostr_profile_query_bind_values(opts.nostr_profile) - const bv_nr = this.nostr_relay_query_bind_values(opts.nostr_relay) - const bind_values = [bv_np[1], bv_nr[1]]; - const query = `INSERT INTO nostr_profile_relay (tb_pr_rl_0, tb_pr_rl_1) VALUES ((SELECT id FROM nostr_profile WHERE ${bv_np[0]} = $1), (SELECT id FROM nostr_relay WHERE ${bv_nr[0]} = $2));`; - try { - const response = await this.execute(query, bind_values); - if (typeof response === "string") return err_msg(response); - else if (typeof response.changes?.changes === "number" && response.changes.changes > 0) return true; - return err_msg("*-result"); - } catch (e) { - return this.handle_errors("*", bind_values, query, ["set_nostr_profile_relay", opts, e]); - }; - }; - - public async unset_nostr_profile_relay(opts: { nostr_profile: INostrProfileQueryBindValues; nostr_relay: INostrRelayQueryBindValues; }): Promise<true | ErrorMessage<ICapacitorClientSQLiteMessage>> { - const bv_np = this.nostr_profile_query_bind_values(opts.nostr_profile) - const bv_nr = this.nostr_relay_query_bind_values(opts.nostr_relay) - const bind_values = [bv_np[1], bv_nr[1]]; - const query = `DELETE FROM nostr_profile_relay WHERE tb_pr_rl_0 = (SELECT id FROM nostr_profile WHERE ${bv_np[0]} = $1) AND tb_pr_rl_1 = (SELECT id FROM nostr_relay WHERE ${bv_nr[0]} = $2);`; - try { - const response = await this.execute(query, bind_values); - if (typeof response === "string") return err_msg(response); - else if (typeof response.changes?.changes === "number" && response.changes.changes > 0) return true; - return err_msg("*-result"); - } catch (e) { - return this.handle_errors("*", bind_values, query, ["unset_nostr_profile_relay", opts, e]); - }; - }; -}; -\ No newline at end of file diff --git a/client/src/capacitor/sqlite_lib.ts b/client/src/capacitor/sqlite_lib.ts @@ -1,121 +0,0 @@ -import { Capacitor } from "@capacitor/core"; -import { CapacitorSQLite, type capSQLiteUpgradeOptions, type capSQLiteVersionUpgrade, SQLiteConnection, SQLiteDBConnection } from "@radroots/capacitor-sqlite"; -import type { IModelsQueryBindValue } from "@radroots/models"; -import { handle_error } from "@radroots/utils"; - -export type ISQLiteServiceDatabaseLog = { key: string, bind_values: IModelsQueryBindValue[], query: string, e: any }; -export type IISQLiteServiceOpenDatabase = { - platform: string; - database: string; - upgrade: capSQLiteVersionUpgrade[]; - version: number; -}; - -export class CapacitorClientSQLiteVersionService { - version_map: Map<string, number> = new Map(); - - set_version(db_name: string, version: number) { - this.version_map.set(db_name, version); - } - - get_version(db_name: string): number | undefined { - const version = this.version_map.get(db_name); - return version; - } -}; - -export class CapacitorClientSQLiteService { - private _platform = Capacitor.getPlatform(); - private _plugin = CapacitorSQLite; - private _conn = new SQLiteConnection(CapacitorSQLite); - private _db_version_dict: Map<string, number> = new Map(); - private _logs: ISQLiteServiceDatabaseLog[] = []; - - public get logs() { - return this._logs; - } - - public get platform() { - return this._platform; - } - - public async init_web_store(): Promise<void> { - try { - await this._conn.initWebStore(); - } catch (e) { - const { error } = handle_error(e); - throw new Error(`Error: CapacitorClientSQLiteService init_web_store: ${error}`); - } - } - - public async add_upgrade(options: capSQLiteUpgradeOptions): Promise<void> { - try { - await this._plugin.addUpgradeStatement(options); - } catch (e) { - const { error } = handle_error(e); - throw new Error(`Error: CapacitorClientSQLiteService add_upgrade: ${error}`); - } - } - - public async open_db(db_name: string, loadToVersion: number, read_only: boolean, encryption_passphrase?: string): Promise<SQLiteDBConnection> { - this._db_version_dict.set(db_name, loadToVersion); - const mode = encryption_passphrase ? "secret" : "no-encryption"; - try { - let db: SQLiteDBConnection; - const ret_cc = (await this._conn.checkConnectionsConsistency()).result; - const is_conn = (await this._conn.isConnection(db_name, read_only)).result; - if (ret_cc && is_conn) db = await this._conn.retrieveConnection(db_name, read_only); - else db = await this._conn - .createConnection(db_name, !!encryption_passphrase, mode, loadToVersion, read_only); - await db.open(); - const res = (await db.isDBOpen()).result!; - if (!res) throw new Error('Error: CapacitorClientSQLiteService open_db: database not opened') - return db; - } catch (e) { - const { error } = handle_error(e); - throw new Error(`Error: CapacitorClientSQLiteService open_db: ${error}`); - } - } - - public async close_db(db_name: string, read_only: boolean): Promise<void> { - try { - const is_conn = (await this._conn.isConnection(db_name, read_only)).result; - if (is_conn) await this._conn.closeConnection(db_name, read_only); - } catch (e) { - const { error } = handle_error(e); - throw new Error(`Error: CapacitorClientSQLiteService close_db: ${error}`); - } - } - - public async save_to_store(db_name: string): Promise<void> { - try { - await this._conn.saveToStore(db_name); - } catch (e) { - const { error } = handle_error(e); - throw new Error(`Error: CapacitorClientSQLiteService save_to_store: ${error}`); - } - } - - public async save_to_disk(db_name: string): Promise<void> { - try { - await this._conn.saveToLocalDisk(db_name); - } catch (e) { - const { error } = handle_error(e); - throw new Error(`Error: CapacitorClientSQLiteService save_to_disk: ${error}`); - } - } - - public async is_conn(db_name: string, read_only: boolean): Promise<boolean> { - try { - const is_conn = (await this._conn.isConnection(db_name, read_only)).result; - if (is_conn !== undefined) return is_conn; - throw new Error(`Error: CapacitorClientSQLiteService is_conn undefined`); - } catch (e) { - const { error } = handle_error(e); - throw new Error(`Error: CapacitorClientSQLiteService is_conn: ${error}`); - } - } -}; - -export const sqlite_version_svc = new CapacitorClientSQLiteVersionService(); -export const sqlite_svc = new CapacitorClientSQLiteService(); -\ No newline at end of file diff --git a/client/src/capacitor/wifi.ts b/client/src/capacitor/wifi.ts @@ -1,52 +0,0 @@ -import { Wifi } from '@radroots/capacitor-wifi'; -import type { IClientWifi, IClientWifiConnectResult, IClientWifiCurrentResult, IClientWifiPermissionsStatus, IClientWifiScanResult } from '../types'; - -export class CapacitorClientWifi implements IClientWifi { - public scan = async (): Promise<IClientWifiScanResult | undefined> => { - try { - const res = Wifi.scanWifi(); - return res; - } catch (e) { }; - }; - - public current = async (): Promise<IClientWifiCurrentResult | undefined> => { - try { - const res = Wifi.getCurrentWifi(); - return res; - } catch (e) { }; - }; - - public connect = async (ssid: string, password: string): Promise<IClientWifiConnectResult | undefined> => { - try { - const res = Wifi.connectToWifiBySsidAndPassword({ ssid, password }); - return res; - } catch (e) { }; - }; - - public connect_prefix = async (ssidPrefix: string, password: string): Promise<IClientWifiConnectResult | undefined> => { - try { - const res = Wifi.connectToWifiBySsidPrefixAndPassword({ ssidPrefix, password }); - return res; - } catch (e) { }; - }; - - public disconnect = async (): Promise<void> => { - try { - await Wifi.disconnectAndForget(); - } catch (e) { }; - }; - - public check_permissions = async (): Promise<IClientWifiPermissionsStatus | undefined> => { - try { - const res = await Wifi.checkPermissions(); - return res; - } catch (e) { }; - }; - - public request_permissions = async (): Promise<IClientWifiPermissionsStatus | undefined> => { - try { - const res = await Wifi.requestPermissions(); - return res; - } catch (e) { }; - }; -} diff --git a/client/src/capacitor/window.ts b/client/src/capacitor/window.ts @@ -1,38 +0,0 @@ -import { SplashScreen } from '@capacitor/splash-screen'; -import { StatusBar, Style } from '@capacitor/status-bar'; -import type { IClientWindow } from '../types'; - -export class CapacitorClientWindow implements IClientWindow { - public async splash_hide(): Promise<void> { - try { - await SplashScreen.hide(); - } catch (e) { }; - } - - public async splash_show(showDuration?: number): Promise<void> { - try { - await SplashScreen.show({ - showDuration, - autoHide: showDuration ? true : false, - }); - } catch (e) { }; - } - - public async status_hide(): Promise<void> { - try { - await StatusBar.hide(); - } catch (e) { }; - } - - public async status_show(): Promise<void> { - try { - await StatusBar.show(); - } catch (e) { }; - } - - public async status_style(style: 'light' | 'dark'): Promise<void> { - try { - await StatusBar.setStyle({ style: style === 'light' ? Style.Light : Style.Dark }); - } catch (e) { }; - } -} diff --git a/client/src/database/tauri.ts b/client/src/database/tauri.ts @@ -0,0 +1,359 @@ + +import { type INostrProfileQueryBindValues, type INostrRelayQueryBindValues, type ILocationGcsAdd, type ILocationGcsAddResolve, type ILocationGcsGet, type ILocationGcsGetResolve, type ILocationGcsDelete, type ILocationGcsDeleteResolve, type ILocationGcsUpdate, type ILocationGcsUpdateResolve, type ITradeProductAdd, type ITradeProductAddResolve, type ITradeProductGet, type ITradeProductGetResolve, type ITradeProductDelete, type ITradeProductDeleteResolve, type ITradeProductUpdate, type ITradeProductUpdateResolve, type INostrProfileAdd, type INostrProfileAddResolve, type INostrProfileGet, type INostrProfileGetResolve, type INostrProfileDelete, type INostrProfileDeleteResolve, type INostrProfileUpdate, type INostrProfileUpdateResolve, type INostrRelayAdd, type INostrRelayAddResolve, type INostrRelayGet, type INostrRelayGetResolve, type INostrRelayDelete, type INostrRelayDeleteResolve, type INostrRelayUpdate, type INostrRelayUpdateResolve, type IModelsQueryBindValue, type IModelsQueryValue, type IModelsQueryBindValueTuple, type LocationGcsFields, type LocationGcsFormFields, LocationGcsSchema, LocationGcsUpdateSchema, parse_location_gcs_form_fields, parse_location_gcs_list, type TradeProductFields, type TradeProductFormFields, TradeProductSchema, TradeProductUpdateSchema, parse_trade_product_form_fields, parse_trade_product_list, type NostrProfileFields, type NostrProfileFormFields, NostrProfileSchema, NostrProfileUpdateSchema, parse_nostr_profile_form_fields, parse_nostr_profile_list, type NostrRelayFields, type NostrRelayFormFields, NostrRelaySchema, NostrRelayUpdateSchema, parse_nostr_relay_form_fields, parse_nostr_relay_list } from "@radroots/models"; +import { err_msg, type ErrorMessage, type ResultPass } from "@radroots/utils"; +import { invoke } from "@tauri-apps/api/core"; +import type { IClientDb, IClientDbMessage } from "./types"; + +export class TauriClientDb implements IClientDb { + private append_logs(scope: string, opts: any, error: any): IClientDbMessage { + console.log("todo... append_logs"); + const error_msg = String(error); + return error_msg; + } + + private handle_errors(scope: string, opts: any, e: any): ErrorMessage<IClientDbMessage> { + const error = this.append_logs(scope, opts, e); + if (error.includes("UNIQUE constraint failed: location_gcs.geohash")) return err_msg("*-location-gcs-geohash-unique"); + else if (error.includes("UNIQUE constraint failed: nostr_relay.url")) return err_msg("*-nostr-relay-url-unique"); + return err_msg(error); + } + + private filter_bind_value_fields(fields: IModelsQueryBindValueTuple[]): IModelsQueryBindValueTuple[] { + return fields.filter(([_, v]) => !!v); + } + + private location_gcs_add_validate(fields: LocationGcsFormFields): LocationGcsFields | string[] { + const fields_r = Object.entries(fields).filter(([_, v]) => !!v).reduce((acc: Record<string, IModelsQueryValue>, i) => { + const [key, val] = parse_location_gcs_form_fields(i); + acc[key] = val; + return acc; + }, {}); + const schema = LocationGcsSchema; + const parsed_schema = schema.safeParse(fields_r); + if (!parsed_schema.success) return parsed_schema.error.issues.map(i => i.message); + else return { + ...parsed_schema.data + }; + } + + public async location_gcs_add(opts: ILocationGcsAdd): Promise<ILocationGcsAddResolve<IClientDbMessage>> { + const err_s = this.location_gcs_add_validate(opts); + if (Array.isArray(err_s)) return { err_s }; + const fields = this.filter_bind_value_fields(Object.entries(err_s)); + if (!fields.length) return err_msg("*-fields"); + try { + const response = await invoke<any>("model_location_gcs_add", { opts }); + if ("id" in response && typeof response.id === "string") return { id: response.id }; + else if (typeof response === "string") return err_msg(response); + return err_msg("*-result"); + } catch (e) { + return this.handle_errors("model_location_gcs_add", opts, e); + }; + } + + public async location_gcs_get(opts: ILocationGcsGet): Promise<ILocationGcsGetResolve<IClientDbMessage>> { + try { + const response = await invoke<any>("model_location_gcs_get", { opts: "list" in opts ? { list: { of: opts.list, sort: opts.sort } } : { on: { ...opts } } }); + if (typeof response === "string") return err_msg(response); + else if ("results" in response && Array.isArray(response.results)) return { results: parse_location_gcs_list(response.results) }; + return err_msg("*-result"); + } catch (e) { + return this.handle_errors("model_location_gcs_get", opts, e); + }; + } + + public async location_gcs_delete(opts: ILocationGcsDelete): Promise<ILocationGcsDeleteResolve<IClientDbMessage>> { + try { + const response = await invoke<any>("model_location_gcs_delete", { opts }); + if (response === null) return { pass: true }; + else if (typeof response === "string") return err_msg(response); + return err_msg("*-result"); + } catch (e) { + return this.handle_errors("model_location_gcs_delete", opts, e); + }; + } + private location_gcs_update_validate(fields: Partial<LocationGcsFormFields>): Partial<LocationGcsFields> | string[] { + const fields_r = Object.entries(fields).filter(([_, v]) => !!v).reduce((acc: Record<string, IModelsQueryValue>, i) => { + const [key, val] = parse_location_gcs_form_fields(i); + acc[key] = val; + return acc; + }, {}); + const schema = LocationGcsUpdateSchema; + const parsed_schema = schema.safeParse(fields_r); + if (!parsed_schema.success) return parsed_schema.error.issues.map(i => i.message); + else return { + ...parsed_schema.data + }; + } + + public async location_gcs_update(opts: ILocationGcsUpdate): Promise<ILocationGcsUpdateResolve<IClientDbMessage>> { + const err_s = this.location_gcs_update_validate(opts.fields); + if (Array.isArray(err_s)) return { err_s }; + const fields = this.filter_bind_value_fields(Object.entries(err_s)); + if (!fields.length) return err_msg("*-fields"); + try { + const response = await invoke<any>("model_location_gcs_update", { opts }); + if (response === null) return { pass: true }; + else if (typeof response === "string") return err_msg(response); + return err_msg("*-result"); + } catch (e) { + return this.handle_errors("model_location_gcs_update", opts, e); + }; + } + private trade_product_add_validate(fields: TradeProductFormFields): TradeProductFields | string[] { + const fields_r = Object.entries(fields).filter(([_, v]) => !!v).reduce((acc: Record<string, IModelsQueryValue>, i) => { + const [key, val] = parse_trade_product_form_fields(i); + acc[key] = val; + return acc; + }, {}); + const schema = TradeProductSchema; + const parsed_schema = schema.safeParse(fields_r); + if (!parsed_schema.success) return parsed_schema.error.issues.map(i => i.message); + else return { + ...parsed_schema.data + }; + } + + public async trade_product_add(opts: ITradeProductAdd): Promise<ITradeProductAddResolve<IClientDbMessage>> { + const err_s = this.trade_product_add_validate(opts); + if (Array.isArray(err_s)) return { err_s }; + const fields = this.filter_bind_value_fields(Object.entries(err_s)); + if (!fields.length) return err_msg("*-fields"); + try { + const response = await invoke<any>("model_trade_product_add", { opts }); + if ("id" in response && typeof response.id === "string") return { id: response.id }; + else if (typeof response === "string") return err_msg(response); + return err_msg("*-result"); + } catch (e) { + return this.handle_errors("model_trade_product_add", opts, e); + }; + } + + public async trade_product_get(opts: ITradeProductGet): Promise<ITradeProductGetResolve<IClientDbMessage>> { + try { + const response = await invoke<any>("model_trade_product_get", { opts: "list" in opts ? { list: { of: opts.list, sort: opts.sort } } : { on: { ...opts } } }); + if (typeof response === "string") return err_msg(response); + else if ("results" in response && Array.isArray(response.results)) return { results: parse_trade_product_list(response.results) }; + return err_msg("*-result"); + } catch (e) { + return this.handle_errors("model_trade_product_get", opts, e); + }; + } + + public async trade_product_delete(opts: ITradeProductDelete): Promise<ITradeProductDeleteResolve<IClientDbMessage>> { + try { + const response = await invoke<any>("model_trade_product_delete", { opts }); + if (response === null) return { pass: true }; + else if (typeof response === "string") return err_msg(response); + return err_msg("*-result"); + } catch (e) { + return this.handle_errors("model_trade_product_delete", opts, e); + }; + } + private trade_product_update_validate(fields: Partial<TradeProductFormFields>): Partial<TradeProductFields> | string[] { + const fields_r = Object.entries(fields).filter(([_, v]) => !!v).reduce((acc: Record<string, IModelsQueryValue>, i) => { + const [key, val] = parse_trade_product_form_fields(i); + acc[key] = val; + return acc; + }, {}); + const schema = TradeProductUpdateSchema; + const parsed_schema = schema.safeParse(fields_r); + if (!parsed_schema.success) return parsed_schema.error.issues.map(i => i.message); + else return { + ...parsed_schema.data + }; + } + + public async trade_product_update(opts: ITradeProductUpdate): Promise<ITradeProductUpdateResolve<IClientDbMessage>> { + const err_s = this.trade_product_update_validate(opts.fields); + if (Array.isArray(err_s)) return { err_s }; + const fields = this.filter_bind_value_fields(Object.entries(err_s)); + if (!fields.length) return err_msg("*-fields"); + try { + const response = await invoke<any>("model_trade_product_update", { opts }); + if (response === null) return { pass: true }; + else if (typeof response === "string") return err_msg(response); + return err_msg("*-result"); + } catch (e) { + return this.handle_errors("model_trade_product_update", opts, e); + }; + } + private nostr_profile_add_validate(fields: NostrProfileFormFields): NostrProfileFields | string[] { + const fields_r = Object.entries(fields).filter(([_, v]) => !!v).reduce((acc: Record<string, IModelsQueryValue>, i) => { + const [key, val] = parse_nostr_profile_form_fields(i); + acc[key] = val; + return acc; + }, {}); + const schema = NostrProfileSchema; + const parsed_schema = schema.safeParse(fields_r); + if (!parsed_schema.success) return parsed_schema.error.issues.map(i => i.message); + else return { + ...parsed_schema.data + }; + } + + public async nostr_profile_add(opts: INostrProfileAdd): Promise<INostrProfileAddResolve<IClientDbMessage>> { + const err_s = this.nostr_profile_add_validate(opts); + if (Array.isArray(err_s)) return { err_s }; + const fields = this.filter_bind_value_fields(Object.entries(err_s)); + if (!fields.length) return err_msg("*-fields"); + try { + const response = await invoke<any>("model_nostr_profile_add", { opts }); + if ("id" in response && typeof response.id === "string") return { id: response.id }; + else if (typeof response === "string") return err_msg(response); + return err_msg("*-result"); + } catch (e) { + return this.handle_errors("model_nostr_profile_add", opts, e); + }; + } + + public async nostr_profile_get(opts: INostrProfileGet): Promise<INostrProfileGetResolve<IClientDbMessage>> { + try { + const response = await invoke<any>("model_nostr_profile_get", { opts: "list" in opts ? { list: { of: opts.list, sort: opts.sort } } : { on: { ...opts } } }); + if (typeof response === "string") return err_msg(response); + else if ("results" in response && Array.isArray(response.results)) return { results: parse_nostr_profile_list(response.results) }; + return err_msg("*-result"); + } catch (e) { + return this.handle_errors("model_nostr_profile_get", opts, e); + }; + } + + public async nostr_profile_delete(opts: INostrProfileDelete): Promise<INostrProfileDeleteResolve<IClientDbMessage>> { + try { + const response = await invoke<any>("model_nostr_profile_delete", { opts }); + if (response === null) return { pass: true }; + else if (typeof response === "string") return err_msg(response); + return err_msg("*-result"); + } catch (e) { + return this.handle_errors("model_nostr_profile_delete", opts, e); + }; + } + private nostr_profile_update_validate(fields: Partial<NostrProfileFormFields>): Partial<NostrProfileFields> | string[] { + const fields_r = Object.entries(fields).filter(([_, v]) => !!v).reduce((acc: Record<string, IModelsQueryValue>, i) => { + const [key, val] = parse_nostr_profile_form_fields(i); + acc[key] = val; + return acc; + }, {}); + const schema = NostrProfileUpdateSchema; + const parsed_schema = schema.safeParse(fields_r); + if (!parsed_schema.success) return parsed_schema.error.issues.map(i => i.message); + else return { + ...parsed_schema.data + }; + } + + public async nostr_profile_update(opts: INostrProfileUpdate): Promise<INostrProfileUpdateResolve<IClientDbMessage>> { + const err_s = this.nostr_profile_update_validate(opts.fields); + if (Array.isArray(err_s)) return { err_s }; + const fields = this.filter_bind_value_fields(Object.entries(err_s)); + if (!fields.length) return err_msg("*-fields"); + try { + const response = await invoke<any>("model_nostr_profile_update", { opts }); + if (response === null) return { pass: true }; + else if (typeof response === "string") return err_msg(response); + return err_msg("*-result"); + } catch (e) { + return this.handle_errors("model_nostr_profile_update", opts, e); + }; + } + private nostr_relay_add_validate(fields: NostrRelayFormFields): NostrRelayFields | string[] { + const fields_r = Object.entries(fields).filter(([_, v]) => !!v).reduce((acc: Record<string, IModelsQueryValue>, i) => { + const [key, val] = parse_nostr_relay_form_fields(i); + acc[key] = val; + return acc; + }, {}); + const schema = NostrRelaySchema; + const parsed_schema = schema.safeParse(fields_r); + if (!parsed_schema.success) return parsed_schema.error.issues.map(i => i.message); + else return { + ...parsed_schema.data + }; + } + + public async nostr_relay_add(opts: INostrRelayAdd): Promise<INostrRelayAddResolve<IClientDbMessage>> { + const err_s = this.nostr_relay_add_validate(opts); + if (Array.isArray(err_s)) return { err_s }; + const fields = this.filter_bind_value_fields(Object.entries(err_s)); + if (!fields.length) return err_msg("*-fields"); + try { + const response = await invoke<any>("model_nostr_relay_add", { opts }); + if ("id" in response && typeof response.id === "string") return { id: response.id }; + else if (typeof response === "string") return err_msg(response); + return err_msg("*-result"); + } catch (e) { + return this.handle_errors("model_nostr_relay_add", opts, e); + }; + } + + public async nostr_relay_get(opts: INostrRelayGet): Promise<INostrRelayGetResolve<IClientDbMessage>> { + try { + const response = await invoke<any>("model_nostr_relay_get", { opts: "list" in opts ? { list: { of: opts.list, sort: opts.sort } } : { on: { ...opts } } }); + if (typeof response === "string") return err_msg(response); + else if ("results" in response && Array.isArray(response.results)) return { results: parse_nostr_relay_list(response.results) }; + return err_msg("*-result"); + } catch (e) { + return this.handle_errors("model_nostr_relay_get", opts, e); + }; + } + + public async nostr_relay_delete(opts: INostrRelayDelete): Promise<INostrRelayDeleteResolve<IClientDbMessage>> { + try { + const response = await invoke<any>("model_nostr_relay_delete", { opts }); + if (response === null) return { pass: true }; + else if (typeof response === "string") return err_msg(response); + return err_msg("*-result"); + } catch (e) { + return this.handle_errors("model_nostr_relay_delete", opts, e); + }; + } + private nostr_relay_update_validate(fields: Partial<NostrRelayFormFields>): Partial<NostrRelayFields> | string[] { + const fields_r = Object.entries(fields).filter(([_, v]) => !!v).reduce((acc: Record<string, IModelsQueryValue>, i) => { + const [key, val] = parse_nostr_relay_form_fields(i); + acc[key] = val; + return acc; + }, {}); + const schema = NostrRelayUpdateSchema; + const parsed_schema = schema.safeParse(fields_r); + if (!parsed_schema.success) return parsed_schema.error.issues.map(i => i.message); + else return { + ...parsed_schema.data + }; + } + + public async nostr_relay_update(opts: INostrRelayUpdate): Promise<INostrRelayUpdateResolve<IClientDbMessage>> { + const err_s = this.nostr_relay_update_validate(opts.fields); + if (Array.isArray(err_s)) return { err_s }; + const fields = this.filter_bind_value_fields(Object.entries(err_s)); + if (!fields.length) return err_msg("*-fields"); + try { + const response = await invoke<any>("model_nostr_relay_update", { opts }); + if (response === null) return { pass: true }; + else if (typeof response === "string") return err_msg(response); + return err_msg("*-result"); + } catch (e) { + return this.handle_errors("model_nostr_relay_update", opts, e); + }; + } + + public async set_nostr_profile_relay(opts: { nostr_profile: INostrProfileQueryBindValues; nostr_relay: INostrRelayQueryBindValues; }): Promise<ResultPass | ErrorMessage<IClientDbMessage>> { + try { + const response = await invoke<any>("model_set_nostr_profile_relay", { opts }); + if (response === null) return { pass: true }; + else if (typeof response === "string") return err_msg(response); + return err_msg("*-result"); + } catch (e) { + return this.handle_errors("model_set_nostr_profile_relay", opts, e); + }; + }; + + public async unset_nostr_profile_relay(opts: { nostr_profile: INostrProfileQueryBindValues; nostr_relay: INostrRelayQueryBindValues; }): Promise<ResultPass | ErrorMessage<IClientDbMessage>> { + try { + const response = await invoke<any>("model_unset_nostr_profile_relay", { opts }); + if (response === null) return { pass: true }; + else if (typeof response === "string") return err_msg(response); + return err_msg("*-result"); + } catch (e) { + return this.handle_errors("model_unset_nostr_profile_relay", opts, e); + }; + }; +} +\ No newline at end of file diff --git a/client/src/database/types.ts b/client/src/database/types.ts @@ -0,0 +1,25 @@ +import { type ILocationGcsAdd, type ILocationGcsAddResolve, type ILocationGcsGet, type ILocationGcsGetResolve, type ILocationGcsDelete, type ILocationGcsDeleteResolve, type ILocationGcsUpdate, type ILocationGcsUpdateResolve, type ITradeProductAdd, type ITradeProductAddResolve, type ITradeProductGet, type ITradeProductGetResolve, type ITradeProductDelete, type ITradeProductDeleteResolve, type ITradeProductUpdate, type ITradeProductUpdateResolve, type INostrProfileAdd, type INostrProfileAddResolve, type INostrProfileGet, type INostrProfileGetResolve, type INostrProfileDelete, type INostrProfileDeleteResolve, type INostrProfileUpdate, type INostrProfileUpdateResolve, type INostrRelayAdd, type INostrRelayAddResolve, type INostrRelayGet, type INostrRelayGetResolve, type INostrRelayDelete, type INostrRelayDeleteResolve, type INostrRelayUpdate, type INostrRelayUpdateResolve } from "@radroots/models"; + +export type IClientDbMessage = + | string + | "*-fields" + | "*-result"; + +export type IClientDb = { + location_gcs_add(opts: ILocationGcsAdd): Promise<ILocationGcsAddResolve<IClientDbMessage>>; + location_gcs_get(opts: ILocationGcsGet): Promise<ILocationGcsGetResolve<IClientDbMessage>>; + location_gcs_delete(opts: ILocationGcsDelete): Promise<ILocationGcsDeleteResolve<IClientDbMessage>>; + location_gcs_update(opts: ILocationGcsUpdate): Promise<ILocationGcsUpdateResolve<IClientDbMessage>>; + trade_product_add(opts: ITradeProductAdd): Promise<ITradeProductAddResolve<IClientDbMessage>>; + trade_product_get(opts: ITradeProductGet): Promise<ITradeProductGetResolve<IClientDbMessage>>; + trade_product_delete(opts: ITradeProductDelete): Promise<ITradeProductDeleteResolve<IClientDbMessage>>; + trade_product_update(opts: ITradeProductUpdate): Promise<ITradeProductUpdateResolve<IClientDbMessage>>; + nostr_profile_add(opts: INostrProfileAdd): Promise<INostrProfileAddResolve<IClientDbMessage>>; + nostr_profile_get(opts: INostrProfileGet): Promise<INostrProfileGetResolve<IClientDbMessage>>; + nostr_profile_delete(opts: INostrProfileDelete): Promise<INostrProfileDeleteResolve<IClientDbMessage>>; + nostr_profile_update(opts: INostrProfileUpdate): Promise<INostrProfileUpdateResolve<IClientDbMessage>>; + nostr_relay_add(opts: INostrRelayAdd): Promise<INostrRelayAddResolve<IClientDbMessage>>; + nostr_relay_get(opts: INostrRelayGet): Promise<INostrRelayGetResolve<IClientDbMessage>>; + nostr_relay_delete(opts: INostrRelayDelete): Promise<INostrRelayDeleteResolve<IClientDbMessage>>; + nostr_relay_update(opts: INostrRelayUpdate): Promise<INostrRelayUpdateResolve<IClientDbMessage>>; +}; +\ No newline at end of file diff --git a/client/src/dialog/tauri.ts b/client/src/dialog/tauri.ts @@ -0,0 +1,30 @@ +import { confirm, type ConfirmDialogOptions, message } from '@tauri-apps/plugin-dialog'; +import type { IClientDialog, IClientDialogConfirmOpts, IClientDialogKind } from "./types"; + +export class TauriClientDialog implements IClientDialog { + public async alert(msg: string, title?: string, kind?: IClientDialogKind): Promise<boolean> { + try { + await message(msg, { title: title || ``, kind: kind || `info` }); + return true; + } catch (e) { + return false; + }; + } + + public async confirm(opts: IClientDialogConfirmOpts): Promise<boolean> { + try { + const msg = typeof opts === `string` ? opts : opts.message; + const options: ConfirmDialogOptions = { title: `` }; + if (typeof opts !== `string`) { + options.title = opts.title || ``; + options.kind = opts.kind || `info`; + if (opts.cancel_label) options.cancelLabel = opts.cancel_label; + if (opts.ok_label) options.okLabel = opts.ok_label; + } + const res = await confirm(msg, options); + return res; + } catch (e) { + return false; + }; + } +} diff --git a/client/src/dialog/types.ts b/client/src/dialog/types.ts @@ -0,0 +1,18 @@ +export type IClientDialogPrompt = { + title?: string; + message: string; + ok_button_title?: string; + cancel_button_title?: string; + input_placeholder?: string; + input_text?: string; +}; + +export type IClientDialogKind = "info" | "warning" | "error"; + +export type IClientDialogConfirmOpts = string | { title?: string, kind?: IClientDialogKind; message: string; cancel_label?: string; ok_label?: string; }; + +export type IClientDialog = { + alert(message: string): Promise<boolean>; + confirm(opts: IClientDialogConfirmOpts): Promise<boolean>; + //prompt(opts: IClientDialogPrompt): Promise<string | false>; +}; +\ No newline at end of file diff --git a/client/src/geolocation/tauri.ts b/client/src/geolocation/tauri.ts @@ -0,0 +1,72 @@ +import { err_msg, handle_error, type ErrorMessage } from '@radroots/utils'; +import { + checkPermissions, + getCurrentPosition, + requestPermissions, + watchPosition, + type Position +} from '@tauri-apps/plugin-geolocation'; +import { fmt_location_coords } from '../utils'; +import type { IClientGeolocation, IClientGeolocationPermission, IClientGeolocationPosition, IClientGeolocationWatchCallback, IClientGeolocationWatchOpts, IGeolocationErrorMessage } from './types'; + +export class TauriClientGeolocation implements IClientGeolocation { + private parse_geolocation_position({ coords: pos_coords }: Position): IClientGeolocationPosition { + const position: IClientGeolocationPosition = { + lat: fmt_location_coords(pos_coords.latitude), + lng: fmt_location_coords(pos_coords.longitude), + accuracy: pos_coords.accuracy || undefined, + altitude: pos_coords.altitude || undefined, + altitude_accuracy: pos_coords.altitudeAccuracy || undefined + }; + return position; + } + + private async request_permissions(): Promise<IClientGeolocationPermission> { + return await requestPermissions(['location']); + } + + private async has_permissions(): Promise<boolean> { + try { + const permissions = await checkPermissions(); + if (permissions.location !== `granted`) { + const permission = await this.request_permissions(); + if (permission.location !== `granted`) return false + }; + return true + } catch (e) { + console.log(`e has_permissions`, e); + return false; + } + } + + + public async current(): Promise<IClientGeolocationPosition | ErrorMessage<IGeolocationErrorMessage>> { + try { + if (!(await this.has_permissions())) return err_msg(`*-permissions`); + const position = await getCurrentPosition() + return this.parse_geolocation_position(position); + } catch (e) { + const { error } = handle_error(e); + if (error.includes(`The operation couldn’t be completed`)) return err_msg(`*-permissions`); + return err_msg(`*`); + }; + } + + public async watch(opts: IClientGeolocationWatchOpts = { + timeout: 10000, + max_age: 0 + }, callback: IClientGeolocationWatchCallback): Promise<number | ErrorMessage<IGeolocationErrorMessage>> { + try { + if (!(await this.has_permissions())) return err_msg(`*-permissions`); + const position_w = await watchPosition( + { enableHighAccuracy: true, timeout: opts.timeout, maximumAge: opts.max_age }, + async (pos) => pos ? await callback(this.parse_geolocation_position(pos)) : null + ) + return position_w; + } catch (e) { + const { error } = handle_error(e); + if (error.includes(`The operation couldn’t be completed`)) err_msg(`*-permissions`); + return err_msg(`*`); + }; + } +} diff --git a/client/src/geolocation/types.ts b/client/src/geolocation/types.ts @@ -0,0 +1,27 @@ +import type { ErrorMessage } from "@radroots/utils"; +import { + type PermissionStatus +} from '@tauri-apps/plugin-geolocation'; + +export type IClientGeolocationPermission = PermissionStatus; +export type IGeolocationErrorMessage = `*-permissions` | `*`; + +export type IClientGeolocationPosition = { + lat: number; + lng: number; + accuracy: number | undefined; + altitude: number | undefined; + altitude_accuracy: number | undefined; +}; + +export type IClientGeolocationWatchOpts = { + timeout: number; + max_age: number; +}; + +export type IClientGeolocationWatchCallback = (pos: IClientGeolocationPosition | null) => Promise<void>; + +export type IClientGeolocation = { + current(): Promise<IClientGeolocationPosition | ErrorMessage<IGeolocationErrorMessage>>; + watch(opts: IClientGeolocationWatchOpts | undefined, callback: IClientGeolocationWatchCallback): Promise<number | ErrorMessage<IGeolocationErrorMessage>> +}; +\ No newline at end of file diff --git a/client/src/haptics/tauri.ts b/client/src/haptics/tauri.ts @@ -0,0 +1,33 @@ +import { + impactFeedback, + notificationFeedback, + selectionFeedback, + vibrate, +} from '@tauri-apps/plugin-haptics'; +import type { IClientHaptics, IClientHapticsFeedback, IClientHapticsImpact } from './types'; + +export class TauriClientHaptics implements IClientHaptics { + public impact = async (opts: IClientHapticsImpact = `medium`): Promise<void> => { + try { + await impactFeedback(opts); + } catch (e) { }; + }; + + public vibrate = async (duration: number = 10): Promise<void> => { + try { + await vibrate(duration); + } catch (e) { }; + }; + + public feedback = async (opts: IClientHapticsFeedback): Promise<void> => { + try { + await notificationFeedback(opts); + } catch (e) { }; + }; + + public selection = async (): Promise<void> => { + try { + await selectionFeedback(); + } catch (e) { }; + }; +} diff --git a/client/src/haptics/types.ts b/client/src/haptics/types.ts @@ -0,0 +1,11 @@ +import type { ImpactFeedbackStyle, NotificationFeedbackType } from "@tauri-apps/plugin-haptics"; + +export type IClientHapticsImpact = ImpactFeedbackStyle; +export type IClientHapticsFeedback = NotificationFeedbackType; + +export type IClientHaptics = { + impact: (opts: IClientHapticsImpact) => Promise<void>; + vibrate: (duration?: number) => Promise<void>; + feedback: (opts: IClientHapticsFeedback) => Promise<void>; + selection: () => Promise<void>; +}; +\ No newline at end of file diff --git a/client/src/http/tauri.ts b/client/src/http/tauri.ts @@ -0,0 +1,48 @@ +import { err_msg, type ErrorMessage } from '@radroots/utils'; +import { type ClientOptions, fetch } from '@tauri-apps/plugin-http'; +import type { IClientHttp, IClientHttpOpts, IClientHttpResponse } from './types'; + +const parse_headers = (headers: Headers): Record<string, string> => { + const record: Record<string, string> = {}; + headers.forEach((value, key) => record[key] = value); + return record; +}; + +const to_bodyinit = (data: any): BodyInit => { + if (typeof data === 'string') { + return data; + } else if (data instanceof FormData) { + return data; + } else if (data instanceof Blob) { + return data; + } else if (data instanceof ArrayBuffer) { + return data; + } else if (data instanceof URLSearchParams) { + return data; + } else { + return JSON.stringify(data); + } +} +export class TauriClientHttp implements IClientHttp { + public async fetch(opts: IClientHttpOpts): Promise<IClientHttpResponse | ErrorMessage<string>> { + try { + const { url } = opts; + + const options: RequestInit & ClientOptions = { + method: opts.method ? opts.method.toUpperCase() : `GET`, + } + if (opts.data) options.body = to_bodyinit(opts.data); + if (opts.headers) options.headers = opts.headers; + if (opts.connect_timeout) options.connectTimeout = opts.connect_timeout; + const response = await fetch(url, options); + return { + status: response.status, + url: response.url, + data: await response.json(), + headers: parse_headers(response.headers) + }; + } catch (e) { + return err_msg(String(e)); + }; + } +} +\ No newline at end of file diff --git a/client/src/http/types.ts b/client/src/http/types.ts @@ -0,0 +1,27 @@ +import type { ErrorMessage } from "@radroots/utils"; + +export type IClientHttpOpts = { + url: string; + method?: `get` | `post`; + params?: { + [key: string]: string | string[]; + }; + data?: any; + headers?: { + [key: string]: string; + }; + connect_timeout?: number; +}; + +export type IClientHttpResponse = { + status: number; + data: any; + headers: { + [key: string]: string; + }; + url: string; +}; + +export type IClientHttp = { + fetch(opts: IClientHttpOpts): Promise<IClientHttpResponse | ErrorMessage<string>>; +}; +\ No newline at end of file diff --git a/client/src/index.ts b/client/src/index.ts @@ -1,3 +1,27 @@ -export * from "./capacitor"; -export * from "./types"; +export * from "./database/tauri" +export * from "./database/types" +export * from "./dialog/tauri" +export * from "./dialog/types" +export * from "./geolocation/tauri" +export * from "./geolocation/types" +export * from "./haptics/tauri" +export * from "./haptics/types" +export * from "./http/tauri" +export * from "./http/types" +export * from "./keyring/tauri" +export * from "./keyring/types" +export * from "./keystore/tauri" +export * from "./keystore/types" +export * from "./map/tauri" +export * from "./map/types" +export * from "./nostr/client" +export * from "./nostr/events" +export * from "./nostr/lib" +export * from "./nostr/types" +export * from "./notification/tauri" +export * from "./notification/types" +export * from "./types" +export * from "./utils" +export * from "./window/tauri" +export * from "./window/types" diff --git a/client/src/keyring/tauri.ts b/client/src/keyring/tauri.ts @@ -0,0 +1,26 @@ +import { err_msg, type ErrorMessage } from '@radroots/utils'; +import { invoke } from "@tauri-apps/api/core"; +import type { IClientKeyring } from './types'; + +export class TauriClientKeying implements IClientKeyring { + public async set_nostr_key(secret_key_hex: string): Promise<true | ErrorMessage<string>> { + try { + const response = await invoke<any>("keyring_nostr_key_set", { secretKeyHex: secret_key_hex }); + console.log(`response set_nostr_key`, response) + return true; + } catch (e) { + return err_msg(`*`); + } + } + + public async get_nostr_key(public_key_hex: string): Promise<{ result: string } | ErrorMessage<string>> { + try { + const response = await invoke<any>("keyring_nostr_key_get", { publicKeyHex: public_key_hex }); + console.log(`response `, response); + if (response && typeof response === `string`) return { result: response }; + return err_msg(`*-result`);; + } catch (e) { + return err_msg(`*`); + } + } +} diff --git a/client/src/keyring/types.ts b/client/src/keyring/types.ts @@ -0,0 +1,7 @@ +import type { ErrorMessage } from "@radroots/utils"; + + +export type IClientKeyring = { + set_nostr_key(key: string, val: string): Promise<true | ErrorMessage<string>>; + get_nostr_key(key: string): Promise<{ result: string } | ErrorMessage<string>>; +}; +\ No newline at end of file diff --git a/client/src/keystore/tauri.ts b/client/src/keystore/tauri.ts @@ -0,0 +1,69 @@ +import { err_msg, type ErrorMessage } from '@radroots/utils'; +import { createStore, Store } from '@tauri-apps/plugin-store'; +import type { IClientKeystore, IClientKeystoreUnlisten } from './types'; + +export class TauriClientKeystore implements IClientKeystore { + private _store: Store | undefined = undefined; + private _store_path: string; + + constructor(store_path: string = 'store.bin') { + this._store_path = store_path; + } + + public async init(): Promise<void> { + this._store = await createStore(this._store_path); + } + + public async set(key: string, value: string): Promise<{ pass: true } | ErrorMessage<string>> { + try { + if (!this._store) return err_msg(`*-store`); + await this._store.set(key, { value }); + await this._store.save(); + return { pass: true }; + } catch (e) { + return err_msg(`*`); + } + } + + public async get(key: string): Promise<{ result: string } | ErrorMessage<string>> { + try { + if (!this._store) return err_msg(`*-store`); + const result = await this._store.get<{ value: any }>(key); + if (result && typeof result.value === `string`) return { result: result.value }; + return err_msg(`*-result`);; + } catch (e) { + return err_msg(`*`); + } + } + + public async keys(): Promise<{ results: string[] } | ErrorMessage<string>> { + try { + if (!this._store) return err_msg(`*-store`); + const results = await this._store.keys(); + return { results }; + } catch (e) { + return err_msg(`*`); + } + } + + public async remove(key: string): Promise<boolean | ErrorMessage<string>> { + try { + if (!this._store) return err_msg(`*-store`); + const res = await this._store.delete(key); + if (res) await this._store.save(); + return res; + } catch (e) { + return err_msg(`*`); + } + } + + public async on_key_change(key: string, callback: (value: string | null) => Promise<void>): Promise<IClientKeystoreUnlisten | ErrorMessage<string>> { + try { + if (!this._store) return err_msg(`*-store`); + const res = await this._store.onKeyChange<{ value: any }>(key, async (res) => await callback(res && `value` in res ? String(res.value) : null)); + return res; + } catch (e) { + return err_msg(`*`); + } + } +} diff --git a/client/src/keystore/types.ts b/client/src/keystore/types.ts @@ -0,0 +1,13 @@ +import type { ErrorMessage } from "@radroots/utils"; +import type { UnlistenFn } from "@tauri-apps/api/event"; + +export type IClientKeystoreUnlisten = UnlistenFn; + +export type IClientKeystore = { + init: () => Promise<void>; + set(key: string, val: string): Promise<{ pass: true } | ErrorMessage<string>>; + get(key: string): Promise<{ result: string } | ErrorMessage<string>>; + keys(): Promise<{ results: string[] } | ErrorMessage<string>>; + remove(key: string): Promise<boolean | ErrorMessage<string>>; + on_key_change(key: string, callback: (value: string | null) => Promise<void>): Promise<IClientKeystoreUnlisten | ErrorMessage<string>>; +}; +\ No newline at end of file diff --git a/client/src/map/tauri.ts b/client/src/map/tauri.ts @@ -0,0 +1,40 @@ +import { setRegion, showMap } from '@radroots/tauri-plugin-map-display'; +import type { GeolocationCoordinatesPoint } from '@radroots/utils'; +import type { IClientMap } from './types'; + +export class TauriClientMap implements IClientMap { + public show_map = async (point: GeolocationCoordinatesPoint): Promise<boolean> => { + try { + const { lat: latitude, lng: longitude } = point; + const res = await showMap({ + mapType: 'standard', + region: { + latitude, + longitude, + latitudeDelta: 0.1, + longitudeDelta: 0.1 + } + }); + return res.success; + } catch (e) { + console.log(`e show_map`, e) + return false; + }; + }; + + public set_region = async (point: GeolocationCoordinatesPoint): Promise<boolean> => { + try { + const { lat: latitude, lng: longitude } = point; + const res = await setRegion({ + latitude, + longitude, + latitudeDelta: 0.1, + longitudeDelta: 0.1 + }); + return res.success; + } catch (e) { + console.log(`e set_region`, e) + return false; + }; + }; +} diff --git a/client/src/map/types.ts b/client/src/map/types.ts @@ -0,0 +1,6 @@ +import type { GeolocationCoordinatesPoint } from "@radroots/utils"; + +export type IClientMap = { + show_map: (point: GeolocationCoordinatesPoint) => Promise<boolean>; + set_region: (point: GeolocationCoordinatesPoint) => Promise<boolean>; +}; +\ No newline at end of file diff --git a/client/src/nostr/client.ts b/client/src/nostr/client.ts @@ -0,0 +1,16 @@ +import { ClientNostrEvents } from "./events"; +import { ClientNostrLib } from "./lib"; +import type { IClientNostr } from "./types"; + +export class ClientNostr implements IClientNostr { + private _ev: ClientNostrEvents = new ClientNostrEvents(); + private _lib: ClientNostrLib = new ClientNostrLib(); + + public get ev() { + return this._ev; + } + + public get lib() { + return this._lib; + } +} +\ No newline at end of file diff --git a/client/src/nostr/events.ts b/client/src/nostr/events.ts @@ -1,5 +1,5 @@ import { type NDKEvent } from "@nostr-dev-kit/ndk"; -import type { IClientNostrEvents } from "../types"; +import type { IClientNostrEvents } from "./types"; export class ClientNostrEvents implements IClientNostrEvents { public first_tag_value(event: NDKEvent, tag_name: string): string { diff --git a/client/src/nostr/index.ts b/client/src/nostr/index.ts @@ -1,16 +0,0 @@ -import type { IClientNostr } from "../types"; -import { ClientNostrEvents } from "./events"; -import { ClientNostrLib } from "./lib"; - -export class ClientNostr implements IClientNostr { - private _ev: ClientNostrEvents = new ClientNostrEvents(); - private _lib: ClientNostrLib = new ClientNostrLib(); - - public get ev() { - return this._ev; - } - - public get lib() { - return this._lib; - } -} -\ No newline at end of file diff --git a/client/src/nostr/lib.ts b/client/src/nostr/lib.ts @@ -1,6 +1,6 @@ import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; import { Relay, generateSecretKey, getPublicKey, nip19, } from 'nostr-tools'; -import type { IClientNostrLib, IClientNostrLibRelayConnectResponse } from '../types'; +import type { IClientNostrLib, IClientNostrLibRelayConnectResponse } from './types'; export class ClientNostrLib implements IClientNostrLib { private generate_key_bytes(): Uint8Array { @@ -45,10 +45,14 @@ export class ClientNostrLib implements IClientNostrLib { * @returns nostr public key hex */ public public_key(secret_key_hex: string | undefined): string { - if (!secret_key_hex) return ``; - const bytes = this.get_key_bytes(secret_key_hex); - const hex = getPublicKey(bytes) - return hex; + try { + if (!secret_key_hex) return ``; + const bytes = this.get_key_bytes(secret_key_hex); + const hex = getPublicKey(bytes) + return hex; + } catch (e) { + return `` + } } /** @@ -66,8 +70,9 @@ export class ClientNostrLib implements IClientNostrLib { * @returns public key hex from npub */ public npub_decode(npub: string): string { - const hex = nip19.decode(npub); - if (hex && hex.type === `npub` && hex.data) return hex.data + const decode = nip19.decode(npub); + console.log(`decode `, decode) + if (decode && decode.type === `npub` && decode.data) return decode.data return ``; } @@ -87,10 +92,14 @@ export class ClientNostrLib implements IClientNostrLib { * @returns nostr secret key hex from nsec */ public nsec_decode(nsec: string): string | undefined { - if (!nsec) return undefined; - const decode = nip19.decode(nsec); - if (decode && decode.type === `nsec` && decode.data && typeof decode.data === `string`) return decode.data - return undefined; + try { + if (!nsec) return undefined; + const decode = nip19.decode(nsec); + if (decode && decode.type === `nsec` && decode.data) return bytesToHex(decode.data); + return undefined; + } catch (e) { + return undefined; + } } /** diff --git a/client/src/nostr/types.ts b/client/src/nostr/types.ts @@ -0,0 +1,27 @@ +import { NDKEvent } from "@nostr-dev-kit/ndk"; + +export type IClientNostrEvents = { + first_tag_value(event: NDKEvent, tag_name: string): string; +}; + +export type IClientNostrLibRelayConnectResponse = { + url: string; + connected: boolean; +}; + +export type IClientNostrLib = { + relay_connect(url: string): Promise<IClientNostrLibRelayConnectResponse | undefined>; + generate_key(): string; + public_key(secret_key_hex: string | undefined): string; + npub(public_key_hex: string | undefined): string; + npub_decode(npub: string): string; + nsec(secret_key_hex: string | undefined): string; + nsec_decode(nsec: string): string | undefined; + nprofile(public_key_hex: string, relays: string[]): string; + nprofile_decode(nprofile: string): [string, string[]] | undefined; +}; + +export type IClientNostr = { + ev: IClientNostrEvents; + lib: IClientNostrLib +}; diff --git a/client/src/notification/tauri.ts b/client/src/notification/tauri.ts @@ -0,0 +1,31 @@ + +import { + isPermissionGranted, + requestPermission, + sendNotification +} from '@tauri-apps/plugin-notification'; +import type { IClientNotification, IClientNotificationPermission, IClientNotificationSendOptions } from "./types"; + +export class TauriClientNotification implements IClientNotification { + public async init(): Promise<IClientNotificationPermission | undefined> { + try { + const permission = await requestPermission(); + return permission; + } catch (e) { + console.log(`e init`, e) + }; + } + + public async send(opts: string | IClientNotificationSendOptions): Promise<void> { + try { + if (!(await isPermissionGranted())) { + const permission = await this.init(); + if (permission !== 'granted') return; + }; + + sendNotification(typeof opts === `string` ? { title: `Radroots`, body: opts } : opts); + } catch (e) { + console.log(`e send`, e) + }; + } +} diff --git a/client/src/notification/types.ts b/client/src/notification/types.ts @@ -0,0 +1,9 @@ +import type { Options } from "@tauri-apps/plugin-notification"; + +export type IClientNotificationPermission = "default" | "denied" | "granted"; +export type IClientNotificationSendOptions = Options; + +export type IClientNotification = { + init(): Promise<IClientNotificationPermission | undefined>; + send(opts: string | IClientNotificationSendOptions): Promise<void>; +}; +\ No newline at end of file diff --git a/client/src/types.ts b/client/src/types.ts @@ -1,286 +1 @@ -import { type BatteryInfo, type DeviceInfo } from '@capacitor/device'; -import { type NDKEvent } from "@nostr-dev-kit/ndk"; -import { type ScanResult } from '@radroots/capacitor-bluetooth-le'; -import { IOSSettings, type AndroidSettings } from '@radroots/capacitor-native-settings'; -import { type ConnectToWifiResult, type GetCurrentWifiResult, type PermissionStatus, type ScanWifiResult } from '@radroots/capacitor-wifi'; -import type { ErrorMessage, ErrorResponse } from '@radroots/utils'; -import { CapacitorClientSQLite } from './capacitor/sqlite'; - -export type IClient = { - nostr: IClientNostr; - platform: IClientPlatform; - keystore: IClientKeystore; - device: IClientDevice; - haptics: IClientHaptics; - network: IClientNetwork; - preferences: IClientPreferences; - share: IClientShare; - wifi: IClientWifi; - dialog: IClientDialog; - browser: IClientBrowser; - dates: IClientDatePicker; - geo: IClientGeolocation; - http: IClientHttp; - window: IClientWindow; - ble: IClientBluetoothLe; - settings: IClientSettings; - db: IClientDb; -}; - -export type IClientNostr = { - ev: IClientNostrEvents; - lib: IClientNostrLib -}; - export type IClientPlatform = `androiď` | `ios` | `web`; - -export type IClientDb = CapacitorClientSQLite; - -export type IClientKeystore = { - init: () => Promise<void>; - set(key: string, val: string): Promise<boolean>; - get(key: string): Promise<string | undefined>; - keys(): Promise<string[] | undefined>; - remove(key: string): Promise<boolean>; -}; - -export type CapacitorDeviceInfo = DeviceInfo; -export type CapacitorDeviceBatteryInfo = BatteryInfo; - -export type IClientDevice = { - info(): Promise<CapacitorDeviceInfo | undefined>; - battery(): Promise<CapacitorDeviceBatteryInfo | undefined>; -}; - -export type IClientHaptics = { - impact: (mod?: "less" | "more") => Promise<void>; - vibrate: (duration?: number) => Promise<void>; - selection_start: () => Promise<void>; - selection_changed: () => Promise<void>; - selection_end: () => Promise<void>; -}; - -export type IClientNetworkConnectionType = `wifi` | `cellular` | `none` | `unknown`; - -export type IClientNetworkConnection = { - connected: boolean; - connection_type: IClientNetworkConnectionType; -}; - -export type IClientNetwork = { - status(): Promise<IClientNetworkConnection | undefined>; - close(): Promise<boolean>; -}; - -export type IClientPreferences = { - set(key: string, value: string): Promise<boolean>; - get(key: string): Promise<string | undefined>; - remove(key: string): Promise<boolean>; -}; - -export type IClientShareOpenOpts = { - title?: string; - text?: string; - url?: string; - files?: string[]; - dialog_title?: string; -}; - -export type IClientShare = { - status(): Promise<boolean>; - open(opts: IClientShareOpenOpts): Promise<void>; -}; - -export type IClientWifiPermissionsStatus = PermissionStatus; -export type IClientWifiScanResult = ScanWifiResult; -export type IClientWifiCurrentResult = GetCurrentWifiResult; -export type IClientWifiConnectResult = ConnectToWifiResult; - -export type IClientWifi = { - scan: () => Promise<IClientWifiScanResult | undefined>; - current: () => Promise<IClientWifiCurrentResult | undefined>; - connect: (ssid: string, password: string) => Promise<IClientWifiConnectResult | undefined>; - connect_prefix: (ssidPrefix: string, password: string) => Promise<IClientWifiConnectResult | undefined>; - disconnect: () => Promise<void>; - check_permissions: () => Promise<IClientWifiPermissionsStatus | undefined>; - request_permissions: () => Promise<IClientWifiPermissionsStatus | undefined>; -}; - -export type IClientDialogPrompt = { - title?: string; - message: string; - ok_button_title?: string; - cancel_button_title?: string; - input_placeholder?: string; - input_text?: string; -}; - -export type IClientDialogConfirmOpts = string | { message: string; cancel_label?: string; ok_label?: string; }; - -export type IClientDialog = { - alert(message: string): Promise<boolean>; - confirm(opts: IClientDialogConfirmOpts): Promise<boolean>; - prompt(opts: IClientDialogPrompt): Promise<string | false>; -}; - -export type IClientBrowser = { - open(url: string): Promise<void>; -}; - -export type IClientDatePickerPresentDatesMode = `date` | `time` | `dateAndTime`; - -export type IClientDatePickerPresent = { - mode: IClientDatePickerPresentDatesMode; -}; - -export type IClientDatePicker = { - present(opts: IClientDatePickerPresent): Promise<string | undefined>; -}; - -export type IClientGeolocationPosition = { - lat: number; - lng: number; - accuracy: number | undefined; - altitude: number | undefined; - altitude_accuracy: number | undefined; -}; - -export type IGeolocationErrorMessage = `permissions-required` | `*`; - -export type IClientGeolocation = { - current(): Promise<IClientGeolocationPosition | ErrorMessage<IGeolocationErrorMessage>> -}; - -export type IClientHttpOpts = { - url: string; - method?: `get` | `post`; - params?: { - [key: string]: string | string[]; - }; - data?: any; - headers?: { - [key: string]: string; - }; - read_timeout?: number; - connect_timeout?: number; -}; - -export type IClientHttpResponse = { - data: any; - status: number; - headers: { - [key: string]: string; - }; - url: string; -}; - -export type IClientHttp = { - fetch(opts: IClientHttpOpts): Promise<IClientHttpResponse | ErrorMessage<string>>; -}; - -export type IClientWindow = { - splash_hide(): Promise<void>; - splash_show(showDuration?: number): Promise<void>; - status_hide(): Promise<void>; - status_show(): Promise<void>; - status_style(style: "light" | "dark"): Promise<void>; -}; - -export type IClientBluetoothLeScanResult = ScanResult; - -export type IClientBluetoothLe = { - enabled(): Promise<boolean>; - initialize(): Promise<boolean>; - scan(): Promise<boolean>; - select_device(device_id: string): Promise<IClientBluetoothLeScanResult | undefined>; - select_devices(): Promise<IClientBluetoothLeScanResult[] | undefined>; -}; - -export type IClientCamera = { - enabled(): Promise<OsPhotosPermissions | ErrorResponse>; - request_enabled(): Promise<OsPhotosPermissions | ErrorResponse>; - get_photo(opts: OsPhotoSelectOptions): Promise<OsPhoto | ErrorResponse>; - get_photos(opts: OsPhotoGallerySelectOptions): Promise<OsPhotoGallery[] | ErrorResponse>; -}; - -export type OsPhotoSelectOptionsBase = { - quality?: number; - width?: number; - height?: number; - correct_orientation?: boolean; -}; - -export type OsPhotoSelectOptions = OsPhotoSelectOptionsBase & { - allow_editing?: boolean; - result_type: 'uri' | 'base64' | 'dataUrl'; - save_to_gallery?: boolean; - prompt_label_header?: string; - prompt_label_cancel?: string; - prompt_label_photo?: string; - prompt_label_picture?: string; -}; - -export type OsPhotoGallerySelectOptions = OsPhotoSelectOptionsBase & { - limit?: number; -}; - -export type OsPhoto = { - base64_string?: string; - data_url?: string; - path?: string; - web_path?: string; - exif?: any; - format: string; - saved: boolean; -}; - -export type OsPhotoGallery = { - path?: string; - web_path: string; - exif?: any; - format: string; -}; - -export type OsPhotosPermissions = { - camera: string; - photos: string; -}; - -export type IClientSettingsOpenAndroid = { - android: { - setting: keyof typeof AndroidSettings; - } -}; - -export type IClientSettingsOpenIos = { - ios: { - setting: keyof typeof IOSSettings; - } -}; - -export type IClientSettingsOpen = IClientSettingsOpenAndroid | IClientSettingsOpenIos; - -export type IClientSettings = { - open(opts: IClientSettingsOpen): Promise<boolean>; -}; - -export type IClientNostrEvents = { - first_tag_value(event: NDKEvent, tag_name: string): string; -}; - -export type IClientNostrLibRelayConnectResponse = { - url: string; - connected: boolean; -}; - -export type IClientNostrLib = { - relay_connect(url: string): Promise<IClientNostrLibRelayConnectResponse | undefined>; - generate_key(): string; - public_key(secret_key_hex: string | undefined): string; - npub(public_key_hex: string | undefined): string; - npub_decode(npub: string): string; - nsec(secret_key_hex: string | undefined): string; - nsec_decode(nsec: string): string | undefined; - nprofile(public_key_hex: string, relays: string[]): string; - nprofile_decode(nprofile: string): [string, string[]] | undefined; -}; diff --git a/client/src/window/tauri.ts b/client/src/window/tauri.ts @@ -0,0 +1,17 @@ +import { invoke } from "@tauri-apps/api/core"; +import type { IClientWindow } from "./types"; + +export class TauriClientWindow implements IClientWindow { + public async splash_hide(): Promise<void> { + try { + await invoke('hide_splashscreen') + } catch (e) { }; + } + + public async splash_show(showDuration?: number): Promise<void> { + try { + await invoke('show_splashscreen') + + } catch (e) { }; + } +} diff --git a/client/src/window/types.ts b/client/src/window/types.ts @@ -0,0 +1,7 @@ +export type IClientWindow = { + splash_hide(): Promise<void>; + splash_show(showDuration?: number): Promise<void>; + //status_hide(): Promise<void>; + //status_show(): Promise<void>; + //status_style(style: "light" | "dark"): Promise<void>; +}; +\ No newline at end of file diff --git a/client/tsconfig.json b/client/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "strict": true, "target": "es2021", "lib": [ "es2021", @@ -12,7 +13,7 @@ "esModuleInterop": true, }, "include": [ - "src", "src/capacitor/camera.ts", + "src" ], "exclude": [ "node_modules"