diff --git a/package-lock.json b/package-lock.json index 0cfdf5d..800945c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "fflate": "^0.8.2", "idb": "^8.0.3", "minimatch": "^10.0.2", - "octagonal-wheels": "^0.1.44", + "octagonal-wheels": "^0.1.45", "qrcode-generator": "^1.4.4", "trystero": "^0.22.0", "xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2" @@ -11151,9 +11151,9 @@ "license": "MIT" }, "node_modules/octagonal-wheels": { - "version": "0.1.44", - "resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.44.tgz", - "integrity": "sha512-sUn/dkYQ2AbMB0R8CubVd75BjkcsteW9B14ArO99F6wM5JRwOo/yPIBBoxCUFE7JjBFOfuWG21C9E3NTga6XrA==", + "version": "0.1.45", + "resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.45.tgz", + "integrity": "sha512-gXoCrwoUIXhmu57YN4BxAtBe+JaYNJNaXaZuVjqjopwYKpH5p2mn1om6KjA22rgGPiIJFXkse2U28FFXoT3/0Q==", "license": "MIT", "dependencies": { "idb": "^8.0.3" @@ -23036,9 +23036,9 @@ "dev": true }, "octagonal-wheels": { - "version": "0.1.44", - "resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.44.tgz", - "integrity": "sha512-sUn/dkYQ2AbMB0R8CubVd75BjkcsteW9B14ArO99F6wM5JRwOo/yPIBBoxCUFE7JjBFOfuWG21C9E3NTga6XrA==", + "version": "0.1.45", + "resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.45.tgz", + "integrity": "sha512-gXoCrwoUIXhmu57YN4BxAtBe+JaYNJNaXaZuVjqjopwYKpH5p2mn1om6KjA22rgGPiIJFXkse2U28FFXoT3/0Q==", "requires": { "idb": "^8.0.3" } diff --git a/package.json b/package.json index 6914468..ac4c18a 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "fflate": "^0.8.2", "idb": "^8.0.3", "minimatch": "^10.0.2", - "octagonal-wheels": "^0.1.44", + "octagonal-wheels": "^0.1.45", "qrcode-generator": "^1.4.4", "trystero": "^0.22.0", "xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2" diff --git a/src/features/P2PSync/CmdP2PReplicator.ts b/src/features/P2PSync/CmdP2PReplicator.ts index 7b85b33..211fd3a 100644 --- a/src/features/P2PSync/CmdP2PReplicator.ts +++ b/src/features/P2PSync/CmdP2PReplicator.ts @@ -29,7 +29,7 @@ import { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2"; import type { Confirm } from "../../lib/src/interfaces/Confirm.ts"; import type ObsidianLiveSyncPlugin from "../../main.ts"; import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase"; -import { getPlatformName } from "../../lib/src/PlatformAPIs/obsidian/Environment.ts"; +// import { getPlatformName } from "../../lib/src/PlatformAPIs/obsidian/Environment.ts"; import type { LiveSyncCore } from "../../main.ts"; import { TrysteroReplicator } from "../../lib/src/replication/trystero/TrysteroReplicator.ts"; import { SETTING_KEY_P2P_DEVICE_NAME } from "../../lib/src/common/types.ts"; @@ -130,7 +130,7 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase const getDB = () => this.getDB(); const getConfirm = () => this.confirm; - const getPlatform = () => this.getPlatform(); + const getPlatform = () => this.services.API.getPlatform(); const env = { get db() { return getDB(); @@ -166,9 +166,6 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase throw e; } } - getPlatform(): string { - return getPlatformName(); - } onunload(): void { removeP2PReplicatorInstance(); diff --git a/src/lib b/src/lib index cd32d3d..7c275d5 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit cd32d3d32635a536266efbf7f974b47f70b878c5 +Subproject commit 7c275d50ae3988903f6d518603450d24e979c1ff diff --git a/src/main.ts b/src/main.ts index 7be8578..fc65dc1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -67,9 +67,10 @@ import { ModuleExtraSyncObsidian } from "./modules/extraFeaturesObsidian/ModuleE import { LocalDatabaseMaintenance } from "./features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts"; import { P2PReplicator } from "./features/P2PSync/CmdP2PReplicator.ts"; import type { LiveSyncManagers } from "./lib/src/managers/LiveSyncManagers.ts"; -import { ObsidianServiceHub } from "./modules/services/ObsidianServices.ts"; -import type { InjectableServiceHub } from "./lib/src/services/InjectableServices.ts"; -import type { ServiceContext } from "./lib/src/services/ServiceHub.ts"; +import type { InjectableServiceHub } from "./lib/src/services/implements/injectable/InjectableServiceHub.ts"; +import { ObsidianServiceHub } from "./modules/services/ObsidianServiceHub.ts"; +import type { ServiceContext } from "./lib/src/services/base/ServiceBase.ts"; +// import type { InjectableServiceHub } from "./lib/src/services/InjectableServices.ts"; export default class ObsidianLiveSyncPlugin extends Plugin diff --git a/src/modules/coreFeatures/ModuleRedFlag.ts b/src/modules/coreFeatures/ModuleRedFlag.ts index 931e39f..5ed6e1e 100644 --- a/src/modules/coreFeatures/ModuleRedFlag.ts +++ b/src/modules/coreFeatures/ModuleRedFlag.ts @@ -9,10 +9,11 @@ import { } from "../../lib/src/common/types.ts"; import { AbstractModule } from "../AbstractModule.ts"; import type { LiveSyncCore } from "../../main.ts"; -import { SvelteDialogManager } from "../features/SetupWizard/ObsidianSvelteDialog.ts"; import FetchEverything from "../features/SetupWizard/dialogs/FetchEverything.svelte"; import RebuildEverything from "../features/SetupWizard/dialogs/RebuildEverything.svelte"; import { extractObject } from "octagonal-wheels/object"; +import { SvelteDialogManagerBase } from "@/lib/src/UI/svelteDialog.ts"; +import type { ServiceContext } from "@/lib/src/services/base/ServiceBase.ts"; export class ModuleRedFlag extends AbstractModule { async isFlagFileExist(path: string) { @@ -52,7 +53,10 @@ export class ModuleRedFlag extends AbstractModule { await this.deleteFlagFile(FlagFilesOriginal.FETCH_ALL); await this.deleteFlagFile(FlagFilesHumanReadable.FETCH_ALL); } - dialogManager = new SvelteDialogManager(this.core); + // dialogManager = new SvelteDialogManagerBase(this.core); + get dialogManager(): SvelteDialogManagerBase { + return this.core.services.UI.dialogManager; + } /** * Adjust setting to remote if needed. diff --git a/src/modules/essential/ModuleKeyValueDB.ts b/src/modules/essential/ModuleKeyValueDB.ts index 3c58edf..6d657e8 100644 --- a/src/modules/essential/ModuleKeyValueDB.ts +++ b/src/modules/essential/ModuleKeyValueDB.ts @@ -5,6 +5,8 @@ import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/log import { AbstractModule } from "../AbstractModule.ts"; import type { LiveSyncCore } from "../../main.ts"; import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase"; +import type { InjectableServiceHub } from "@/lib/src/services/InjectableServices.ts"; +import type { ObsidianDatabaseService } from "../services/ObsidianServices.ts"; export class ModuleKeyValueDB extends AbstractModule { async tryCloseKvDB() { @@ -77,6 +79,7 @@ export class ModuleKeyValueDB extends AbstractModule { .filter((e) => e.startsWith(prefix)) .map((e) => e.substring(prefix.length)); }, + db: Promise.resolve(getDB()), } satisfies SimpleStore; } _everyOnInitializeDatabase(db: LiveSyncLocalDB): Promise { @@ -100,12 +103,12 @@ export class ModuleKeyValueDB extends AbstractModule { } return true; } - onBindFunction(core: LiveSyncCore, services: typeof core.services): void { + onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void { services.databaseEvents.onUnloadDatabase.addHandler(this._onDBUnload.bind(this)); services.databaseEvents.onCloseDatabase.addHandler(this._onDBClose.bind(this)); services.databaseEvents.onDatabaseInitialisation.addHandler(this._everyOnInitializeDatabase.bind(this)); services.databaseEvents.onResetDatabase.addHandler(this._everyOnResetDatabase.bind(this)); - services.database.openSimpleStore.setHandler(this._getSimpleStore.bind(this)); + (services.database as ObsidianDatabaseService).openSimpleStore.setHandler(this._getSimpleStore.bind(this)); services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this)); } } diff --git a/src/modules/features/SetupManager.ts b/src/modules/features/SetupManager.ts index 83ebbe1..6691338 100644 --- a/src/modules/features/SetupManager.ts +++ b/src/modules/features/SetupManager.ts @@ -9,7 +9,6 @@ import { } from "../../lib/src/common/types.ts"; import { generatePatchObj, isObjectDifferent } from "../../lib/src/common/utils.ts"; import { AbstractObsidianModule } from "../AbstractObsidianModule.ts"; -import { SvelteDialogManager } from "./SetupWizard/ObsidianSvelteDialog.ts"; import Intro from "./SetupWizard/dialogs/Intro.svelte"; import SelectMethodNewUser from "./SetupWizard/dialogs/SelectMethodNewUser.svelte"; import SelectMethodExisting from "./SetupWizard/dialogs/SelectMethodExisting.svelte"; @@ -52,10 +51,13 @@ export const enum UserMode { * Setup Manager to handle onboarding and configuration setup */ export class SetupManager extends AbstractObsidianModule { - /** - * Dialog manager for handling Svelte dialogs - */ - private dialogManager: SvelteDialogManager = new SvelteDialogManager(this.plugin); + // /** + // * Dialog manager for handling Svelte dialogs + // */ + // private dialogManager: SvelteDialogManager = new SvelteDialogManager(this.plugin); + get dialogManager() { + return this.services.UI.dialogManager; + } /** * Starts the onboarding process diff --git a/src/modules/features/SetupWizard/ObsidianSvelteDialog.ts b/src/modules/features/SetupWizard/ObsidianSvelteDialog.ts deleted file mode 100644 index 51e57ad..0000000 --- a/src/modules/features/SetupWizard/ObsidianSvelteDialog.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { eventHub, EVENT_PLUGIN_UNLOADED } from "@/common/events"; -import { Modal } from "@/deps"; -import type ObsidianLiveSyncPlugin from "@/main"; -import { mount, unmount } from "svelte"; -import DialogHost from "@lib/UI/DialogHost.svelte"; -import { fireAndForget, promiseWithResolvers, type PromiseWithResolvers } from "octagonal-wheels/promises"; -import { LOG_LEVEL_NOTICE, Logger } from "octagonal-wheels/common/logger"; -import { - type DialogControlBase, - type DialogSvelteComponentBaseProps, - type ComponentHasResult, - setupDialogContext, - getDialogContext, - type SvelteDialogManagerBase, -} from "@/lib/src/UI/svelteDialog.ts"; - -export type DialogSvelteComponentProps = DialogSvelteComponentBaseProps & { - plugin: ObsidianLiveSyncPlugin; - services: ObsidianLiveSyncPlugin["services"]; -}; - -export type DialogControls = DialogControlBase & { - plugin: ObsidianLiveSyncPlugin; - services: ObsidianLiveSyncPlugin["services"]; -}; - -export type DialogMessageProps = Record; -// type DialogSvelteComponent = Component,any>; - -export class SvelteDialog extends Modal { - plugin: ObsidianLiveSyncPlugin; - mountedComponent?: ReturnType; - component: ComponentHasResult; - result?: T; - initialData?: U; - title: string = "Obsidian LiveSync - Setup Wizard"; - constructor(plugin: ObsidianLiveSyncPlugin, component: ComponentHasResult, initialData?: U) { - super(plugin.app); - this.plugin = plugin; - this.component = component; - this.initialData = initialData; - } - resolveResult() { - this.resultPromiseWithResolvers?.resolve(this.result); - this.resultPromiseWithResolvers = undefined; - } - resultPromiseWithResolvers?: PromiseWithResolvers; - onOpen() { - const { contentEl } = this; - contentEl.empty(); - // eslint-disable-next-line @typescript-eslint/no-this-alias - const dialog = this; - - if (this.resultPromiseWithResolvers) { - this.resultPromiseWithResolvers.reject("Dialog opened again"); - } - const pr = promiseWithResolvers(); - eventHub.once(EVENT_PLUGIN_UNLOADED, () => { - if (this.resultPromiseWithResolvers === pr) { - pr.reject("Plugin unloaded"); - this.close(); - } - }); - this.resultPromiseWithResolvers = pr; - this.mountedComponent = mount(DialogHost, { - target: contentEl, - props: { - onSetupContext: (props: DialogSvelteComponentBaseProps) => { - setupDialogContext({ - ...props, - plugin: this.plugin, - services: this.plugin.services, - }); - }, - setTitle: (title: string) => { - dialog.setTitle(title); - }, - closeDialog: () => { - dialog.close(); - }, - setResult: (result: T) => { - this.result = result; - }, - getInitialData: () => this.initialData, - mountComponent: this.component, - }, - }); - } - waitForClose(): Promise { - if (!this.resultPromiseWithResolvers) { - throw new Error("Dialog not opened yet"); - } - return this.resultPromiseWithResolvers.promise; - } - onClose() { - this.resolveResult(); - fireAndForget(async () => { - if (this.mountedComponent) { - await unmount(this.mountedComponent); - } - }); - } -} - -export async function openSvelteDialog( - plugin: ObsidianLiveSyncPlugin, - component: ComponentHasResult, - initialData?: U -): Promise { - const dialog = new SvelteDialog(plugin, component, initialData); - dialog.open(); - - return await dialog.waitForClose(); -} - -export class SvelteDialogManager implements SvelteDialogManagerBase { - plugin: ObsidianLiveSyncPlugin; - constructor(plugin: ObsidianLiveSyncPlugin) { - this.plugin = plugin; - } - async open(component: ComponentHasResult, initialData?: U): Promise { - return await openSvelteDialog(this.plugin, component, initialData); - } - async openWithExplicitCancel(component: ComponentHasResult, initialData?: U): Promise { - for (let i = 0; i < 10; i++) { - const ret = await openSvelteDialog(this.plugin, component, initialData); - if (ret !== undefined) { - return ret; - } - if (this.plugin.services.appLifecycle.hasUnloaded()) { - throw new Error("Operation cancelled due to app shutdown."); - } - Logger("Please select 'Cancel' explicitly to cancel this operation.", LOG_LEVEL_NOTICE); - } - throw new Error("Operation Forcibly cancelled by user."); - } -} - -export function getObsidianDialogContext(): DialogControls { - return getDialogContext() as DialogControls; -} diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte index d4f905b..f2270dc 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte @@ -16,8 +16,7 @@ } from "../../../../lib/src/common/types"; import { onMount } from "svelte"; - import type { GuestDialogProps } from "../../../../lib/src/UI/svelteDialog"; - import { getObsidianDialogContext } from "../ObsidianSvelteDialog"; + import { getDialogContext, type GuestDialogProps } from "../../../../lib/src/UI/svelteDialog"; import { copyTo, pickBucketSyncSettings } from "../../../../lib/src/common/utils"; const default_setting = pickBucketSyncSettings(DEFAULT_SETTINGS); @@ -39,7 +38,7 @@ } }); let error = $state(""); - const context = getObsidianDialogContext(); + const context = getDialogContext(); const isEndpointSecure = $derived.by(() => { return syncSetting.endpoint.trim().toLowerCase().startsWith("https://"); }); diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte index 2d7134b..671af71 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte @@ -17,9 +17,8 @@ } from "../../../../lib/src/common/types"; import { isCloudantURI } from "../../../../lib/src/pouchdb/utils_couchdb"; - import { getObsidianDialogContext } from "../ObsidianSvelteDialog"; import { onMount } from "svelte"; - import type { GuestDialogProps } from "../../../../lib/src/UI/svelteDialog"; + import { getDialogContext, type GuestDialogProps } from "../../../../lib/src/UI/svelteDialog"; import { copyTo, pickCouchDBSyncSettings } from "../../../../lib/src/common/utils"; import PanelCouchDBCheck from "./PanelCouchDBCheck.svelte"; @@ -40,7 +39,7 @@ }); let error = $state(""); - const context = getObsidianDialogContext(); + const context = getDialogContext(); function generateSetting() { const connSetting: CouchDBConnection = { diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte index 3ab053c..b07c157 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte @@ -22,16 +22,15 @@ import { TrysteroReplicator } from "../../../../lib/src/replication/trystero/TrysteroReplicator"; import type { ReplicatorHostEnv } from "../../../../lib/src/replication/trystero/types"; import { copyTo, pickP2PSyncSettings, type SimpleStore } from "../../../../lib/src/common/utils"; - import { getObsidianDialogContext } from "../ObsidianSvelteDialog"; import { onMount } from "svelte"; - import type { GuestDialogProps } from "../../../../lib/src/UI/svelteDialog"; + import { getDialogContext, type GuestDialogProps } from "../../../../lib/src/UI/svelteDialog"; import { SETTING_KEY_P2P_DEVICE_NAME } from "../../../../lib/src/common/types"; import ExtraItems from "../../../../lib/src/UI/components/ExtraItems.svelte"; const default_setting = pickP2PSyncSettings(DEFAULT_SETTINGS); let syncSetting = $state({ ...default_setting }); - const context = getObsidianDialogContext(); + const context = getDialogContext(); let error = $state(""); const TYPE_CANCELLED = "cancelled"; type SettingInfo = P2PConnectionInfo; @@ -104,7 +103,7 @@ processReplicatedDocs: async (docs: any[]) => { return; }, - confirm: context.plugin.confirm, + confirm: context.services.confirm, db: dummyPouch, simpleStore: store, deviceName: syncSetting.P2P_DevicePeerName || "unnamed-device", diff --git a/src/modules/main/ModuleLiveSyncMain.ts b/src/modules/main/ModuleLiveSyncMain.ts index 5fa27b3..bc7f95a 100644 --- a/src/modules/main/ModuleLiveSyncMain.ts +++ b/src/modules/main/ModuleLiveSyncMain.ts @@ -13,8 +13,8 @@ import { versionNumberString2Number } from "../../lib/src/string_and_binary/conv import { cancelAllPeriodicTask, cancelAllTasks } from "octagonal-wheels/concurrency/task"; import { stopAllRunningProcessors } from "octagonal-wheels/concurrency/processor"; import { AbstractModule } from "../AbstractModule.ts"; -import { EVENT_PLATFORM_UNLOADED } from "../../lib/src/PlatformAPIs/base/APIBase.ts"; -import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts"; +import { EVENT_PLATFORM_UNLOADED } from "@lib/events/coreEvents"; +import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub.ts"; import type { LiveSyncCore } from "../../main.ts"; import { initialiseWorkerModule } from "@/lib/src/worker/bgWorker.ts"; diff --git a/src/modules/services/ObsidianConfirm.ts b/src/modules/services/ObsidianConfirm.ts new file mode 100644 index 0000000..eb4e3b3 --- /dev/null +++ b/src/modules/services/ObsidianConfirm.ts @@ -0,0 +1,111 @@ +import { type App, type Plugin, Notice } from "@/deps"; +import { scheduleTask, memoIfNotExist, memoObject, retrieveMemoObject, disposeMemoObject } from "@/common/utils"; +import { $msg } from "@/lib/src/common/i18n"; +import type { Confirm } from "@/lib/src/interfaces/Confirm"; +import type { ObsidianServiceContext } from "@/lib/src/services/implements/obsidian/ObsidianServiceContext"; +import { + askYesNo, + askString, + confirmWithMessageWithWideButton, + askSelectString, + confirmWithMessage, +} from "../coreObsidian/UILib/dialogs"; + +export class ObsidianConfirm implements Confirm { + private _context: T; + get _app(): App { + return this._context.app; + } + get _plugin(): Plugin { + return this._context.plugin; + } + constructor(context: T) { + this._context = context; + } + askYesNo(message: string): Promise<"yes" | "no"> { + return askYesNo(this._app, message); + } + askString(title: string, key: string, placeholder: string, isPassword: boolean = false): Promise { + return askString(this._app, title, key, placeholder, isPassword); + } + + async askYesNoDialog( + message: string, + opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number } = { title: "Confirmation" } + ): Promise<"yes" | "no"> { + const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleConfirmation"); + const yesLabel = $msg("moduleInputUIObsidian.optionYes"); + const noLabel = $msg("moduleInputUIObsidian.optionNo"); + const defaultOption = opt.defaultOption === "Yes" ? yesLabel : noLabel; + const ret = await confirmWithMessageWithWideButton( + this._plugin, + opt.title || defaultTitle, + message, + [yesLabel, noLabel], + defaultOption, + opt.timeout + ); + return ret === yesLabel ? "yes" : "no"; + } + + askSelectString(message: string, items: string[]): Promise { + return askSelectString(this._app, message, items); + } + + askSelectStringDialogue( + message: string, + buttons: T, + opt: { title?: string; defaultAction: T[number]; timeout?: number } + ): Promise { + const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleSelect"); + return confirmWithMessageWithWideButton( + this._plugin, + opt.title || defaultTitle, + message, + buttons, + opt.defaultAction, + opt.timeout + ); + } + + askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void) { + const fragment = createFragment((doc) => { + const [beforeText, afterText] = dialogText.split("{HERE}", 2); + doc.createEl("span", undefined, (a) => { + a.appendText(beforeText); + a.appendChild( + a.createEl("a", undefined, (anchor) => { + anchorCallback(anchor); + }) + ); + a.appendText(afterText); + }); + }); + const popupKey = "popup-" + key; + scheduleTask(popupKey, 1000, async () => { + const popup = await memoIfNotExist(popupKey, () => new Notice(fragment, 0)); + const isShown = popup?.noticeEl?.isShown(); + if (!isShown) { + memoObject(popupKey, new Notice(fragment, 0)); + } + scheduleTask(popupKey + "-close", 20000, () => { + const popup = retrieveMemoObject(popupKey); + if (!popup) return; + if (popup?.noticeEl?.isShown()) { + popup.hide(); + } + disposeMemoObject(popupKey); + }); + }); + } + + confirmWithMessage( + title: string, + contentMd: string, + buttons: string[], + defaultAction: (typeof buttons)[number], + timeout?: number + ): Promise<(typeof buttons)[number] | false> { + return confirmWithMessage(this._plugin, title, contentMd, buttons, defaultAction, timeout); + } +} diff --git a/src/modules/services/ObsidianServiceHub.ts b/src/modules/services/ObsidianServiceHub.ts new file mode 100644 index 0000000..c01d747 --- /dev/null +++ b/src/modules/services/ObsidianServiceHub.ts @@ -0,0 +1,73 @@ +import { InjectableServiceHub } from "@/lib/src/services/implements/injectable/InjectableServiceHub"; +import { ObsidianServiceContext } from "@/lib/src/services/implements/obsidian/ObsidianServiceContext"; +import type { ServiceInstances } from "@/lib/src/services/ServiceHub"; +import type ObsidianLiveSyncPlugin from "@/main"; +import { + ObsidianAPIService, + ObsidianAppLifecycleService, + ObsidianConflictService, + ObsidianDatabaseService, + ObsidianFileProcessingService, + ObsidianReplicationService, + ObsidianReplicatorService, + ObsidianRemoteService, + ObsidianSettingService, + ObsidianTweakValueService, + ObsidianVaultService, + ObsidianTestService, + ObsidianDatabaseEventService, + ObsidianPathService, + ObsidianConfigService, +} from "./ObsidianServices"; +import { ObsidianUIService } from "./ObsidianUIService"; + +// InjectableServiceHub + +export class ObsidianServiceHub extends InjectableServiceHub { + constructor(plugin: ObsidianLiveSyncPlugin) { + const context = new ObsidianServiceContext(plugin.app, plugin, plugin); + + const API = new ObsidianAPIService(context); + const appLifecycle = new ObsidianAppLifecycleService(context); + const conflict = new ObsidianConflictService(context); + const database = new ObsidianDatabaseService(context); + const fileProcessing = new ObsidianFileProcessingService(context); + const replication = new ObsidianReplicationService(context); + const replicator = new ObsidianReplicatorService(context); + const remote = new ObsidianRemoteService(context); + const setting = new ObsidianSettingService(context); + const tweakValue = new ObsidianTweakValueService(context); + const vault = new ObsidianVaultService(context); + const test = new ObsidianTestService(context); + const databaseEvents = new ObsidianDatabaseEventService(context); + const path = new ObsidianPathService(context); + const config = new ObsidianConfigService(context, vault); + const ui = new ObsidianUIService(context, { + appLifecycle, + config, + replicator, + }); + + // Using 'satisfies' to ensure all services are provided + const serviceInstancesToInit = { + appLifecycle: appLifecycle, + conflict: conflict, + database: database, + databaseEvents: databaseEvents, + fileProcessing: fileProcessing, + replication: replication, + replicator: replicator, + remote: remote, + setting: setting, + tweakValue: tweakValue, + vault: vault, + test: test, + ui: ui, + path: path, + API: API, + config: config, + } satisfies Required>; + + super(context, serviceInstancesToInit); + } +} diff --git a/src/modules/services/ObsidianServices.ts b/src/modules/services/ObsidianServices.ts index 7acd888..2f79c67 100644 --- a/src/modules/services/ObsidianServices.ts +++ b/src/modules/services/ObsidianServices.ts @@ -1,48 +1,56 @@ -import { ServiceContext, type ServiceInstances } from "@/lib/src/services/ServiceHub.ts"; -import { - InjectableAPIService, - InjectableAppLifecycleService, - InjectableConflictService, - InjectableDatabaseEventService, - InjectableDatabaseService, - InjectableFileProcessingService, - InjectablePathService, - InjectableRemoteService, - InjectableReplicationService, - InjectableReplicatorService, - InjectableSettingService, - InjectableTestService, - InjectableTweakValueService, - InjectableVaultService, -} from "../../lib/src/services/InjectableServices.ts"; -import { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts"; -import { ConfigServiceBrowserCompat } from "../../lib/src/services/Services.ts"; -import type ObsidianLiveSyncPlugin from "../../main.ts"; -import { ObsidianUIService } from "./ObsidianUIService.ts"; -import type { App, Plugin } from "@/deps"; - -export class ObsidianServiceContext extends ServiceContext { - app: App; - plugin: Plugin; - liveSyncPlugin: ObsidianLiveSyncPlugin; - constructor(app: App, plugin: Plugin, liveSyncPlugin: ObsidianLiveSyncPlugin) { - super(); - this.app = app; - this.plugin = plugin; - this.liveSyncPlugin = liveSyncPlugin; - } -} +import { InjectableAPIService } from "@lib/services/implements/injectable/InjectableAPIService"; +import { InjectableAppLifecycleService } from "@lib/services/implements/injectable/InjectableAppLifecycleService"; +import { InjectableConflictService } from "@lib/services/implements/injectable/InjectableConflictService"; +import { InjectableDatabaseEventService } from "@lib/services/implements/injectable/InjectableDatabaseEventService"; +import { InjectableDatabaseService } from "@lib/services/implements/injectable/InjectableDatabaseService"; +import { InjectableFileProcessingService } from "@lib/services/implements/injectable/InjectableFileProcessingService"; +import { InjectablePathService } from "@lib/services/implements/injectable/InjectablePathService"; +import { InjectableRemoteService } from "@lib/services/implements/injectable/InjectableRemoteService"; +import { InjectableReplicationService } from "@lib/services/implements/injectable/InjectableReplicationService"; +import { InjectableReplicatorService } from "@lib/services/implements/injectable/InjectableReplicatorService"; +import { InjectableSettingService } from "@lib/services/implements/injectable/InjectableSettingService"; +import { InjectableTestService } from "@lib/services/implements/injectable/InjectableTestService"; +import { InjectableTweakValueService } from "@lib/services/implements/injectable/InjectableTweakValueService"; +import { InjectableVaultService } from "@lib/services/implements/injectable/InjectableVaultService"; +import { ConfigServiceBrowserCompat } from "@lib/services/implements/browser/ConfigServiceBrowserCompat"; +import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext.ts"; +import { Platform } from "@/deps"; +import type { SimpleStore } from "@/lib/src/common/utils"; +import type { IDatabaseService } from "@/lib/src/services/base/IService"; +import { handlers } from "@/lib/src/services/lib/HandlerUtils"; // All Services will be migrated to be based on Plain Services, not Injectable Services. // This is a migration step. export class ObsidianAPIService extends InjectableAPIService { getPlatform(): string { - return "obsidian"; + if (Platform.isAndroidApp) { + return "android-app"; + } else if (Platform.isIosApp) { + return "ios"; + } else if (Platform.isMacOS) { + return "macos"; + } else if (Platform.isMobileApp) { + return "mobile-app"; + } else if (Platform.isMobile) { + return "mobile"; + } else if (Platform.isSafari) { + return "safari"; + } else if (Platform.isDesktop) { + return "desktop"; + } else if (Platform.isDesktopApp) { + return "desktop-app"; + } else { + return "unknown-obsidian"; + } } } export class ObsidianPathService extends InjectablePathService {} -export class ObsidianDatabaseService extends InjectableDatabaseService {} +export class ObsidianDatabaseService extends InjectableDatabaseService { + openSimpleStore = handlers().binder("openSimpleStore") as (( + kind: string + ) => SimpleStore) & { setHandler: (handler: IDatabaseService["openSimpleStore"], override?: boolean) => void }; +} export class ObsidianDatabaseEventService extends InjectableDatabaseEventService {} // InjectableReplicatorService @@ -66,49 +74,3 @@ export class ObsidianVaultService extends InjectableVaultService {} export class ObsidianConfigService extends ConfigServiceBrowserCompat {} - -// InjectableServiceHub - -export class ObsidianServiceHub extends InjectableServiceHub { - constructor(plugin: ObsidianLiveSyncPlugin) { - const context = new ObsidianServiceContext(plugin.app, plugin, plugin); - - const API = new ObsidianAPIService(context); - const appLifecycle = new ObsidianAppLifecycleService(context); - const conflict = new ObsidianConflictService(context); - const database = new ObsidianDatabaseService(context); - const fileProcessing = new ObsidianFileProcessingService(context); - const replication = new ObsidianReplicationService(context); - const replicator = new ObsidianReplicatorService(context); - const remote = new ObsidianRemoteService(context); - const setting = new ObsidianSettingService(context); - const tweakValue = new ObsidianTweakValueService(context); - const vault = new ObsidianVaultService(context); - const test = new ObsidianTestService(context); - const databaseEvents = new ObsidianDatabaseEventService(context); - const path = new ObsidianPathService(context); - const ui = new ObsidianUIService(context); - const config = new ObsidianConfigService(context, vault); - // Using 'satisfies' to ensure all services are provided - const serviceInstancesToInit = { - appLifecycle: appLifecycle, - conflict: conflict, - database: database, - databaseEvents: databaseEvents, - fileProcessing: fileProcessing, - replication: replication, - replicator: replicator, - remote: remote, - setting: setting, - tweakValue: tweakValue, - vault: vault, - test: test, - ui: ui, - path: path, - API: API, - config: config, - } satisfies Required>; - - super(context, serviceInstancesToInit); - } -} diff --git a/src/modules/services/ObsidianUIService.ts b/src/modules/services/ObsidianUIService.ts index e4df15e..7c0bd43 100644 --- a/src/modules/services/ObsidianUIService.ts +++ b/src/modules/services/ObsidianUIService.ts @@ -1,156 +1,30 @@ -import { UIService } from "../../lib/src/services/Services"; -import { Notice, type App, type Plugin } from "@/deps"; -import { SvelteDialogManager } from "../features/SetupWizard/ObsidianSvelteDialog"; -import DialogueToCopy from "../../lib/src/UI/dialogues/DialogueToCopy.svelte"; -import type { ObsidianServiceContext } from "./ObsidianServices"; -import type ObsidianLiveSyncPlugin from "@/main"; -import type { Confirm } from "@/lib/src/interfaces/Confirm"; -import { - askSelectString, - askString, - askYesNo, - confirmWithMessage, - confirmWithMessageWithWideButton, -} from "../coreObsidian/UILib/dialogs"; -import { $msg } from "@/lib/src/common/i18n"; -import { disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "@/common/utils"; -export class ObsidianConfirm implements Confirm { - private _app: App; - private _plugin: Plugin; - constructor(app: App, plugin: Plugin) { - this._app = app; - this._plugin = plugin; - } - askYesNo(message: string): Promise<"yes" | "no"> { - return askYesNo(this._app, message); - } - askString(title: string, key: string, placeholder: string, isPassword: boolean = false): Promise { - return askString(this._app, title, key, placeholder, isPassword); - } +import type { ConfigService } from "@lib/services/base/ConfigService"; +import type { AppLifecycleService } from "@lib/services/base/AppLifecycleService"; +import type { ReplicatorService } from "@lib/services/base/ReplicatorService"; +import { UIService } from "@lib/services//implements/base/UIService"; +import { ObsidianServiceContext } from "@/lib/src/services/implements/obsidian/ObsidianServiceContext"; +import { ObsidianSvelteDialogManager } from "./SvelteDialogObsidian"; +import { ObsidianConfirm } from "./ObsidianConfirm"; - async askYesNoDialog( - message: string, - opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number } = { title: "Confirmation" } - ): Promise<"yes" | "no"> { - const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleConfirmation"); - const yesLabel = $msg("moduleInputUIObsidian.optionYes"); - const noLabel = $msg("moduleInputUIObsidian.optionNo"); - const defaultOption = opt.defaultOption === "Yes" ? yesLabel : noLabel; - const ret = await confirmWithMessageWithWideButton( - this._plugin, - opt.title || defaultTitle, - message, - [yesLabel, noLabel], - defaultOption, - opt.timeout - ); - return ret === yesLabel ? "yes" : "no"; - } +export type ObsidianUIServiceDependencies = { + appLifecycle: AppLifecycleService; + config: ConfigService; + replicator: ReplicatorService; +}; - askSelectString(message: string, items: string[]): Promise { - return askSelectString(this._app, message, items); - } - - askSelectStringDialogue( - message: string, - buttons: T, - opt: { title?: string; defaultAction: T[number]; timeout?: number } - ): Promise { - const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleSelect"); - return confirmWithMessageWithWideButton( - this._plugin, - opt.title || defaultTitle, - message, - buttons, - opt.defaultAction, - opt.timeout - ); - } - - askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void) { - const fragment = createFragment((doc) => { - const [beforeText, afterText] = dialogText.split("{HERE}", 2); - doc.createEl("span", undefined, (a) => { - a.appendText(beforeText); - a.appendChild( - a.createEl("a", undefined, (anchor) => { - anchorCallback(anchor); - }) - ); - a.appendText(afterText); - }); - }); - const popupKey = "popup-" + key; - scheduleTask(popupKey, 1000, async () => { - const popup = await memoIfNotExist(popupKey, () => new Notice(fragment, 0)); - const isShown = popup?.noticeEl?.isShown(); - if (!isShown) { - memoObject(popupKey, new Notice(fragment, 0)); - } - scheduleTask(popupKey + "-close", 20000, () => { - const popup = retrieveMemoObject(popupKey); - if (!popup) return; - if (popup?.noticeEl?.isShown()) { - popup.hide(); - } - disposeMemoObject(popupKey); - }); - }); - } - - confirmWithMessage( - title: string, - contentMd: string, - buttons: string[], - defaultAction: (typeof buttons)[number], - timeout?: number - ): Promise<(typeof buttons)[number] | false> { - return confirmWithMessage(this._plugin, title, contentMd, buttons, defaultAction, timeout); - } -} export class ObsidianUIService extends UIService { - private _dialogManager: SvelteDialogManager; - private _plugin: Plugin; - private _liveSyncPlugin: ObsidianLiveSyncPlugin; - private _confirmInstance: ObsidianConfirm; - get dialogManager() { - return this._dialogManager; - } - constructor(context: ObsidianServiceContext) { - super(context); - this._liveSyncPlugin = context.liveSyncPlugin; - this._dialogManager = new SvelteDialogManager(this._liveSyncPlugin); - this._plugin = context.plugin; - this._confirmInstance = new ObsidianConfirm(this._plugin.app, this._plugin); - } - - async promptCopyToClipboard(title: string, value: string): Promise { - const param = { - title: title, - dataToCopy: value, - }; - const result = await this._dialogManager.open(DialogueToCopy, param); - if (result !== "ok") { - return false; - } - return true; - } - - showMarkdownDialog( - title: string, - contentMD: string, - buttons: T, - defaultAction?: (typeof buttons)[number] - ): Promise<(typeof buttons)[number] | false> { - // TODO: implement `confirm` to this service - return this._liveSyncPlugin.confirm.askSelectStringDialogue(contentMD, buttons, { - title, - defaultAction: defaultAction ?? buttons[0], - timeout: 0, + constructor(context: ObsidianServiceContext, dependents: ObsidianUIServiceDependencies) { + const obsidianConfirm = new ObsidianConfirm(context); + const obsidianSvelteDialogManager = new ObsidianSvelteDialogManager(context, { + appLifecycle: dependents.appLifecycle, + config: dependents.config, + replicator: dependents.replicator, + confirm: obsidianConfirm, + }); + super(context, { + appLifecycle: dependents.appLifecycle, + dialogManager: obsidianSvelteDialogManager, + confirm: obsidianConfirm, }); } - - get confirm(): Confirm { - return this._confirmInstance; - } } diff --git a/src/modules/services/SvelteDialogObsidian.ts b/src/modules/services/SvelteDialogObsidian.ts new file mode 100644 index 0000000..c57d8f1 --- /dev/null +++ b/src/modules/services/SvelteDialogObsidian.ts @@ -0,0 +1,37 @@ +import { Modal } from "@/deps"; + +import { + SvelteDialogManagerBase, + SvelteDialogMixIn, + type ComponentHasResult, + type SvelteDialogManagerDependencies, +} from "@lib/services/implements/base/SvelteDialog"; +import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; + +export const SvelteDialogBase = SvelteDialogMixIn(Modal); +export class SvelteDialogObsidian< + T, + U, + C extends ObsidianServiceContext = ObsidianServiceContext, +> extends SvelteDialogBase { + constructor( + context: C, + dependents: SvelteDialogManagerDependencies, + component: ComponentHasResult, + initialData?: U + ) { + super(context.app); + this.initDialog(context, dependents, component, initialData); + } +} + +export class ObsidianSvelteDialogManager extends SvelteDialogManagerBase { + override async openSvelteDialog( + component: ComponentHasResult, + initialData?: TU + ): Promise { + const dialog = new SvelteDialogObsidian(this.context, this.dependents, component, initialData); + dialog.open(); + return await dialog.waitForClose(); + } +} diff --git a/test/harness/harness.ts b/test/harness/harness.ts index 329e532..390934f 100644 --- a/test/harness/harness.ts +++ b/test/harness/harness.ts @@ -3,9 +3,10 @@ import ObsidianLiveSyncPlugin from "@/main"; import { DEFAULT_SETTINGS, type ObsidianLiveSyncSettings } from "@/lib/src/common/types"; import { LOG_LEVEL_VERBOSE, setGlobalLogFunction } from "@lib/common/logger"; import { SettingCache } from "./obsidian-mock"; -import { delay, promiseWithResolvers } from "octagonal-wheels/promises"; +import { delay, fireAndForget, promiseWithResolvers } from "octagonal-wheels/promises"; +import { EVENT_PLATFORM_UNLOADED } from "@lib/events/coreEvents"; import { EVENT_LAYOUT_READY, eventHub } from "@/common/events"; -import { EVENT_PLATFORM_UNLOADED } from "@/lib/src/PlatformAPIs/base/APIBase"; + import { env } from "../suite/variables"; export type LiveSyncHarness = { @@ -79,12 +80,14 @@ export async function generateHarness( await plugin.onload(); let isDisposed = false; const waitPromise = promiseWithResolvers(); - eventHub.once(EVENT_PLATFORM_UNLOADED, async () => { - console.log(`Harness for vault '${vaultName}' disposed.`); - await delay(100); - eventHub.offAll(); - isDisposed = true; - waitPromise.resolve(); + eventHub.once(EVENT_PLATFORM_UNLOADED, () => { + fireAndForget(async () => { + console.log(`Harness for vault '${vaultName}' disposed.`); + await delay(100); + eventHub.offAll(); + isDisposed = true; + waitPromise.resolve(); + }); }); eventHub.once(EVENT_LAYOUT_READY, () => { plugin.app.vault.trigger("layout-ready");