Refactor: Migrate the outdated, unstable platform abstraction layer to Services

This commit is contained in:
vorotamoroz
2026-01-26 09:13:40 +00:00
parent 3cd9b9e06d
commit 28146eec2c
19 changed files with 342 additions and 419 deletions

14
package-lock.json generated
View File

@@ -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"
}

View File

@@ -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"

View File

@@ -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();

Submodule src/lib updated: cd32d3d326...7c275d50ae

View File

@@ -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

View File

@@ -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<ServiceContext> {
return this.core.services.UI.dialogManager;
}
/**
* Adjust setting to remote if needed.

View File

@@ -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<T>;
}
_everyOnInitializeDatabase(db: LiveSyncLocalDB): Promise<boolean> {
@@ -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));
}
}

View File

@@ -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

View File

@@ -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<T = any, U = any> = DialogControlBase<T, U> & {
plugin: ObsidianLiveSyncPlugin;
services: ObsidianLiveSyncPlugin["services"];
};
export type DialogMessageProps = Record<string, any>;
// type DialogSvelteComponent<T extends DialogSvelteComponentProps = DialogSvelteComponentProps> = Component<SvelteComponent<T>,any>;
export class SvelteDialog<T, U> extends Modal {
plugin: ObsidianLiveSyncPlugin;
mountedComponent?: ReturnType<typeof mount>;
component: ComponentHasResult<T, U>;
result?: T;
initialData?: U;
title: string = "Obsidian LiveSync - Setup Wizard";
constructor(plugin: ObsidianLiveSyncPlugin, component: ComponentHasResult<T, U>, 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<T | undefined>;
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<any>();
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<T | undefined> {
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<T, U>(
plugin: ObsidianLiveSyncPlugin,
component: ComponentHasResult<T, U>,
initialData?: U
): Promise<T | undefined> {
const dialog = new SvelteDialog<T, U>(plugin, component, initialData);
dialog.open();
return await dialog.waitForClose();
}
export class SvelteDialogManager implements SvelteDialogManagerBase {
plugin: ObsidianLiveSyncPlugin;
constructor(plugin: ObsidianLiveSyncPlugin) {
this.plugin = plugin;
}
async open<T, U>(component: ComponentHasResult<T, U>, initialData?: U): Promise<T | undefined> {
return await openSvelteDialog<T, U>(this.plugin, component, initialData);
}
async openWithExplicitCancel<T, U>(component: ComponentHasResult<T, U>, initialData?: U): Promise<T> {
for (let i = 0; i < 10; i++) {
const ret = await openSvelteDialog<T, U>(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<T = any>(): DialogControls<T> {
return getDialogContext<T>() as DialogControls<T>;
}

View File

@@ -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://");
});

View File

@@ -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 = {

View File

@@ -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<P2PConnectionInfo>({ ...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",

View File

@@ -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";

View File

@@ -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<T extends ObsidianServiceContext = ObsidianServiceContext> 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<string | false> {
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<string> {
return askSelectString(this._app, message, items);
}
askSelectStringDialogue<T extends readonly string[]>(
message: string,
buttons: T,
opt: { title?: string; defaultAction: T[number]; timeout?: number }
): Promise<T[number] | false> {
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<Notice>(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);
}
}

View File

@@ -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<ObsidianServiceContext> {
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<ServiceInstances<ObsidianServiceContext>>;
super(context, serviceInstancesToInit);
}
}

View File

@@ -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<ObsidianServiceContext> {
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<ObsidianServiceContext> {}
export class ObsidianDatabaseService extends InjectableDatabaseService<ObsidianServiceContext> {}
export class ObsidianDatabaseService extends InjectableDatabaseService<ObsidianServiceContext> {
openSimpleStore = handlers<IDatabaseService>().binder("openSimpleStore") as (<T>(
kind: string
) => SimpleStore<T>) & { setHandler: (handler: IDatabaseService["openSimpleStore"], override?: boolean) => void };
}
export class ObsidianDatabaseEventService extends InjectableDatabaseEventService<ObsidianServiceContext> {}
// InjectableReplicatorService
@@ -66,49 +74,3 @@ export class ObsidianVaultService extends InjectableVaultService<ObsidianService
// InjectableTestService
export class ObsidianTestService extends InjectableTestService<ObsidianServiceContext> {}
export class ObsidianConfigService extends ConfigServiceBrowserCompat<ObsidianServiceContext> {}
// InjectableServiceHub
export class ObsidianServiceHub extends InjectableServiceHub<ObsidianServiceContext> {
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<ServiceInstances<ObsidianServiceContext>>;
super(context, serviceInstancesToInit);
}
}

View File

@@ -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<string | false> {
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<T extends ObsidianServiceContext = ObsidianServiceContext> = {
appLifecycle: AppLifecycleService<T>;
config: ConfigService<T>;
replicator: ReplicatorService<T>;
};
askSelectString(message: string, items: string[]): Promise<string> {
return askSelectString(this._app, message, items);
}
askSelectStringDialogue<T extends readonly string[]>(
message: string,
buttons: T,
opt: { title?: string; defaultAction: T[number]; timeout?: number }
): Promise<T[number] | false> {
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<Notice>(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<ObsidianServiceContext> {
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<boolean> {
const param = {
title: title,
dataToCopy: value,
};
const result = await this._dialogManager.open(DialogueToCopy, param);
if (result !== "ok") {
return false;
}
return true;
}
showMarkdownDialog<T extends string[]>(
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<ObsidianServiceContext>) {
const obsidianConfirm = new ObsidianConfirm(context);
const obsidianSvelteDialogManager = new ObsidianSvelteDialogManager<ObsidianServiceContext>(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;
}
}

View File

@@ -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<T, U, C> {
constructor(
context: C,
dependents: SvelteDialogManagerDependencies<C>,
component: ComponentHasResult<T, U>,
initialData?: U
) {
super(context.app);
this.initDialog(context, dependents, component, initialData);
}
}
export class ObsidianSvelteDialogManager<T extends ObsidianServiceContext> extends SvelteDialogManagerBase<T> {
override async openSvelteDialog<TT, TU>(
component: ComponentHasResult<TT, TU>,
initialData?: TU
): Promise<TT | undefined> {
const dialog = new SvelteDialogObsidian<TT, TU, T>(this.context, this.dependents, component, initialData);
dialog.open();
return await dialog.waitForClose();
}
}

View File

@@ -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<void>();
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");