import type { App } from "@/deps.ts"; import { addIcon } from "@/deps.ts"; import { EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, eventHub } from "@/common/events.ts"; import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; import type { CustomisationSyncDialogView, CustomisationSyncUIControl, } from "@/features/ConfigSync/customisationSyncView.ts"; import type { HiddenFileSyncInitialisationView } from "@/features/HiddenFileSync/hiddenFileSyncViews.ts"; import { PluginDialogModal } from "@/features/ConfigSync/PluginDialogModal.ts"; import { $msg } from "@/common/translation"; /** Services needed to compose the Obsidian-owned Customisation Sync UI. */ export type CustomisationSyncUIHost = NecessaryServices<"API" | "appLifecycle", never>; /** The small modal surface owned by this composition feature. */ export interface CustomisationSyncDialog { open(): void; close(): void; isOpened?(): boolean; isOpen?(): boolean; } /** Replaceable modal construction seam used by focused interaction tests. */ export type CustomisationSyncDialogFactory = ( app: App, customisationSync: CustomisationSyncDialogView, hiddenFileSync: HiddenFileSyncInitialisationView ) => CustomisationSyncDialog; type RibbonElement = { addClass?: (name: string) => unknown; remove?: () => unknown; }; const CUSTOM_SYNC_ICON = ` `; function defaultDialogFactory( app: App, customisationSync: CustomisationSyncDialogView, hiddenFileSync: HiddenFileSyncInitialisationView ): CustomisationSyncDialog { return new PluginDialogModal(app, customisationSync, hiddenFileSync); } function readDialogOpenState(dialog: CustomisationSyncDialog, fallback: boolean): boolean { const isOpened = dialog.isOpened?.(); if (typeof isOpened === "boolean") return isOpened; const isOpen = dialog.isOpen?.(); if (typeof isOpen === "boolean") return isOpen; return fallback; } /** * Compose the Obsidian-owned Customisation Sync command, ribbon, and modal. * * Registration happens from `onLoaded`: the legacy add-on registered its UI * from `onload`, which is invoked after that lifecycle phase. Settings have * already been loaded by `onLoaded`, while command availability is still * evaluated lazily from the focused view. */ export function useCustomisationSyncUI( host: CustomisationSyncUIHost, app: App, customisationSync: CustomisationSyncDialogView, hiddenFileSync: HiddenFileSyncInitialisationView, createDialog: CustomisationSyncDialogFactory = defaultDialogFactory ): CustomisationSyncUIControl { const { API, appLifecycle } = host.services; let dialog: CustomisationSyncDialog | undefined; let dialogOpened = false; let ribbonElement: RibbonElement | undefined; let requestOpenDisposer: (() => void) | undefined; let loadedDisposer: (() => void) | undefined; let resumedDisposer: (() => void) | undefined; let disposed = false; let registered = false; const open = () => { if (disposed || !customisationSync.isEnabled()) return; if (!dialog) { dialog = createDialog(app, customisationSync, hiddenFileSync); } dialog.open(); dialogOpened = true; }; const close = () => { const currentDialog = dialog; dialog = undefined; dialogOpened = false; currentDialog?.close(); }; const isOpen = () => { if (!dialog) return false; return readDialogOpenState(dialog, dialogOpened); }; const updateRibbonVisibility = () => { if (typeof activeDocument === "undefined") return true; const element = activeDocument.querySelector(".livesync-ribbon-showcustom"); element?.toggleClass("sls-hidden", !customisationSync.isEnabled()); return true; }; const registerUI = () => { if (disposed || registered) return Promise.resolve(true); registered = true; addIcon("custom-sync", CUSTOM_SYNC_ICON); API.addCommand({ id: "livesync-plugin-dialog-ex", name: "Show customization sync dialog", checkCallback: (checking) => { if (!customisationSync.isEnabled()) return false; if (!checking) open(); return true; }, }); ribbonElement = API.addRibbonIcon("custom-sync", $msg("cmdConfigSync.showCustomizationSync"), () => open()); ribbonElement?.addClass?.("livesync-ribbon-showcustom"); requestOpenDisposer = eventHub.onEvent(EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, () => open()); return Promise.resolve(true); }; loadedDisposer = appLifecycle.onLoaded.addHandler(registerUI); resumedDisposer = appLifecycle.onResumed.addHandler(() => Promise.resolve(updateRibbonVisibility())); appLifecycle.onUnload.addHandler(() => { disposed = true; close(); requestOpenDisposer?.(); requestOpenDisposer = undefined; ribbonElement?.remove?.(); ribbonElement = undefined; loadedDisposer?.(); loadedDisposer = undefined; resumedDisposer?.(); resumedDisposer = undefined; return Promise.resolve(true); }); return Object.freeze({ open, close, isOpen }); }