refactor: consume Commonlib as a package

This commit is contained in:
vorotamoroz
2026-07-17 11:13:16 +00:00
parent 6475d32769
commit a721d3b602
665 changed files with 3438 additions and 31592 deletions
@@ -0,0 +1,75 @@
import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
import {
encodeQR,
encodeSettingsToQRCodeData,
OutputFormat,
} from "@vrtmrz/livesync-commonlib/compat/API/processSetting";
import { EVENT_REQUEST_SHOW_SETUP_QR } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
import { fireAndForget } from "@vrtmrz/livesync-commonlib/compat/common/utils";
import type { SetupFeatureHost } from "./types";
export async function encodeSetupSettingsAsQR(host: SetupFeatureHost) {
const settingString = encodeSettingsToQRCodeData(host.services.setting.currentSettings());
const result = encodeQR(settingString, OutputFormat.SVG);
if (result === "") {
return "";
}
if (typeof result === "string") {
const msg = host.services.context.translate("Setup.QRCode", { qr_image: result });
await host.services.UI.confirm.confirmWithMessage("Settings QR Code", msg, ["OK"], "OK");
return result;
} else {
// Multi-page QR code
let currentIndex = 0;
while (currentIndex < result.total) {
const msg = `The setting is too large for a single QR code.
We are using the aggregator to combine multiple QR codes.
Your settings will not be sent to any server; they will be processed only on your device.
Please scan this QR code with your mobile's camera, and open the page in your browser.
After all parts are collected, the page will navigate you back to Obsidian with the aggregated settings.
Progress: ${currentIndex + 1} / ${result.total}
${result.parts[currentIndex]}`;
const buttons = [];
if (currentIndex > 0) buttons.push("Back");
if (currentIndex < result.total - 1) {
buttons.push("Next");
buttons.push("Cancel");
} else {
buttons.push("Done");
}
const choice = await host.services.UI.confirm.confirmWithMessage(
"Settings QR Code (Aggregated)",
msg,
buttons,
buttons[buttons.indexOf("Next") !== -1 ? buttons.indexOf("Next") : buttons.indexOf("Done")]
);
if (choice === "Next") {
currentIndex++;
} else if (choice === "Back") {
currentIndex--;
} else {
break;
}
}
return result.parts[0]; // Return the first one for compatibility
}
}
export function useSetupQRCodeFeature(host: NecessaryServices<"API" | "UI" | "setting" | "appLifecycle", never>) {
host.services.appLifecycle.onLoaded.addHandler(() => {
host.services.API.addCommand({
id: "livesync-setting-qr",
name: "Show settings as a QR code",
callback: () => fireAndForget(encodeSetupSettingsAsQR(host)),
});
host.services.context.events.onEvent(EVENT_REQUEST_SHOW_SETUP_QR, () =>
fireAndForget(() => encodeSetupSettingsAsQR(host))
);
return Promise.resolve(true);
});
}
@@ -0,0 +1,117 @@
import { describe, expect, it, vi, afterEach } from "vitest";
import { EVENT_REQUEST_SHOW_SETUP_QR } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
import { createServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import { encodeSetupSettingsAsQR, useSetupQRCodeFeature } from "./qrCode";
import { encodeQR, encodeSettingsToQRCodeData } from "@vrtmrz/livesync-commonlib/compat/API/processSetting";
vi.mock("@vrtmrz/livesync-commonlib/compat/API/processSetting", () => {
return {
encodeQR: vi.fn(),
encodeSettingsToQRCodeData: vi.fn(),
OutputFormat: {
SVG: "svg",
},
};
});
describe("setupObsidian/qrCode", () => {
afterEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
});
it("encodeSetupSettingsAsQR should return empty string when QR generation fails", async () => {
const confirmWithMessage = vi.fn();
const host = {
services: {
context: createServiceContext(),
setting: {
currentSettings: vi.fn(() => ({ any: "settings" })),
},
UI: {
confirm: {
confirmWithMessage,
},
},
},
} as any;
vi.mocked(encodeSettingsToQRCodeData).mockReturnValue("encoded-settings");
vi.mocked(encodeQR).mockReturnValue("");
const result = await encodeSetupSettingsAsQR(host);
expect(result).toBe("");
expect(confirmWithMessage).not.toHaveBeenCalled();
});
it("encodeSetupSettingsAsQR should show confirm dialog when QR is generated", async () => {
const confirmWithMessage = vi.fn(() => true);
const translate = vi.fn(() => "qr-message");
const host = {
services: {
context: createServiceContext({ translate }),
setting: {
currentSettings: vi.fn(() => ({ any: "settings" })),
},
UI: {
confirm: {
confirmWithMessage,
},
},
},
} as any;
vi.mocked(encodeSettingsToQRCodeData).mockReturnValue("encoded-settings");
vi.mocked(encodeQR).mockReturnValue("<svg/>");
const result = await encodeSetupSettingsAsQR(host);
expect(result).toBe("<svg/>");
expect(translate).toHaveBeenCalledWith("Setup.QRCode", { qr_image: "<svg/>" });
expect(confirmWithMessage).toHaveBeenCalledWith("Settings QR Code", "qr-message", ["OK"], "OK");
});
it("useSetupQRCodeFeature should register onLoaded handler that wires command and event", async () => {
const addHandler = vi.fn();
const addCommand = vi.fn();
const context = createServiceContext();
const onEventSpy = vi.spyOn(context.events, "onEvent");
const host = {
services: {
context,
API: {
addCommand,
},
appLifecycle: {
onLoaded: {
addHandler,
},
},
setting: {
currentSettings: vi.fn(() => ({ any: "settings" })),
},
UI: {
confirm: {
confirmWithMessage: vi.fn(),
},
},
},
} as any;
useSetupQRCodeFeature(host);
expect(addHandler).toHaveBeenCalledTimes(1);
const loadedHandler = addHandler.mock.calls[0][0] as () => Promise<boolean>;
await loadedHandler();
expect(addCommand).toHaveBeenCalledWith(
expect.objectContaining({
id: "livesync-setting-qr",
name: "Show settings as a QR code",
})
);
expect(onEventSpy).toHaveBeenCalledWith(EVENT_REQUEST_SHOW_SETUP_QR, expect.any(Function));
});
});
@@ -1,9 +1,8 @@
import { type SetupManager, UserMode } from "@/modules/features/SetupManager";
import type { SetupFeatureHost } from "@lib/serviceFeatures/setupObsidian/types";
import { EVENT_REQUEST_OPEN_P2P_SETTINGS, EVENT_REQUEST_OPEN_SETUP_URI } from "@lib/events/coreEvents";
import { eventHub } from "@lib/hub/hub";
import { fireAndForget } from "@lib/common/utils";
import type { NecessaryServices } from "@lib/interfaces/ServiceModule";
import type { SetupFeatureHost } from "@/serviceFeatures/setupObsidian/types";
import { EVENT_REQUEST_OPEN_P2P_SETTINGS, EVENT_REQUEST_OPEN_SETUP_URI } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
import { fireAndForget } from "@vrtmrz/livesync-commonlib/compat/common/utils";
import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
export async function openSetupURI(setupManager: SetupManager) {
await setupManager.onUseSetupURI(UserMode.Unknown);
@@ -24,8 +23,10 @@ export function useSetupManagerHandlersFeature(
callback: () => fireAndForget(openSetupURI(setupManager)),
});
eventHub.onEvent(EVENT_REQUEST_OPEN_SETUP_URI, () => fireAndForget(() => openSetupURI(setupManager)));
eventHub.onEvent(EVENT_REQUEST_OPEN_P2P_SETTINGS, () =>
host.services.context.events.onEvent(EVENT_REQUEST_OPEN_SETUP_URI, () =>
fireAndForget(() => openSetupURI(setupManager))
);
host.services.context.events.onEvent(EVENT_REQUEST_OPEN_P2P_SETTINGS, () =>
fireAndForget(() => openP2PSettings(host, setupManager))
);
@@ -1,6 +1,5 @@
import { describe, expect, it, vi, afterEach } from "vitest";
import { eventHub } from "@lib/hub/hub";
import { EVENT_REQUEST_OPEN_P2P_SETTINGS, EVENT_REQUEST_OPEN_SETUP_URI } from "@lib/events/coreEvents";
import { EVENT_REQUEST_OPEN_P2P_SETTINGS, EVENT_REQUEST_OPEN_SETUP_URI } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
import { openP2PSettings, openSetupURI, useSetupManagerHandlersFeature } from "./setupManagerHandlers";
vi.mock("@/modules/features/SetupManager", () => {
@@ -47,10 +46,11 @@ describe("setupObsidian/setupManagerHandlers", () => {
it("useSetupManagerHandlersFeature should register onLoaded handler that wires command and events", async () => {
const addHandler = vi.fn();
const addCommand = vi.fn();
const onEventSpy = vi.spyOn(eventHub, "onEvent");
const events = { onEvent: vi.fn() };
const host = {
services: {
context: { events },
API: {
addCommand,
},
@@ -81,7 +81,7 @@ describe("setupObsidian/setupManagerHandlers", () => {
name: "Use the copied setup URI (Formerly Open setup URI)",
})
);
expect(onEventSpy).toHaveBeenCalledWith(EVENT_REQUEST_OPEN_SETUP_URI, expect.any(Function));
expect(onEventSpy).toHaveBeenCalledWith(EVENT_REQUEST_OPEN_P2P_SETTINGS, expect.any(Function));
expect(events.onEvent).toHaveBeenCalledWith(EVENT_REQUEST_OPEN_SETUP_URI, expect.any(Function));
expect(events.onEvent).toHaveBeenCalledWith(EVENT_REQUEST_OPEN_P2P_SETTINGS, expect.any(Function));
});
});
@@ -1,9 +1,9 @@
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "@lib/common/types";
import type { LogFunction } from "@lib/services/lib/logUtils";
import { createInstanceLogFunction } from "@lib/services/lib/logUtils";
import type { SetupFeatureHost } from "@lib/serviceFeatures/setupObsidian/types";
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { LogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils";
import { createInstanceLogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils";
import type { SetupFeatureHost } from "@/serviceFeatures/setupObsidian/types";
import { configURIBase } from "@/common/types";
import type { NecessaryServices } from "@lib/interfaces/ServiceModule";
import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
import { type SetupManager, UserMode } from "@/modules/features/SetupManager";
async function handleSetupProtocol(setupManager: SetupManager, conf: Record<string, string>) {
@@ -0,0 +1,73 @@
import { LOG_LEVEL_NOTICE, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { LogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils";
import { createInstanceLogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils";
import { encodeSettingsToSetupURI } from "@vrtmrz/livesync-commonlib/compat/API/processSetting";
import { EVENT_REQUEST_COPY_SETUP_URI } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
import { fireAndForget } from "@vrtmrz/livesync-commonlib/compat/common/utils";
import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
import type { SetupFeatureHost } from "./types";
export async function askEncryptingPassphrase(host: SetupFeatureHost): Promise<string | false> {
return await host.services.UI.confirm.askString(
"Encrypt your settings",
"The passphrase to encrypt the setup URI",
"",
true
);
}
export async function copySetupURI(host: SetupFeatureHost, log: LogFunction, stripExtra = true) {
const encryptingPassphrase = await askEncryptingPassphrase(host);
if (encryptingPassphrase === false) return;
const encryptedURI = await encodeSettingsToSetupURI(
host.services.setting.currentSettings(),
encryptingPassphrase,
[...((stripExtra ? ["pluginSyncExtendedSetting"] : []) as (keyof ObsidianLiveSyncSettings)[])],
true
);
if (await host.services.UI.promptCopyToClipboard("Setup URI", encryptedURI)) {
log("Setup URI copied to clipboard", LOG_LEVEL_NOTICE);
}
}
export async function copySetupURIFull(host: SetupFeatureHost, log: LogFunction) {
const encryptingPassphrase = await askEncryptingPassphrase(host);
if (encryptingPassphrase === false) return;
const encryptedURI = await encodeSettingsToSetupURI(
host.services.setting.currentSettings(),
encryptingPassphrase,
[],
false
);
if (await host.services.UI.promptCopyToClipboard("Setup URI", encryptedURI)) {
log("Setup URI copied to clipboard", LOG_LEVEL_NOTICE);
}
}
export function useSetupURIFeature(host: NecessaryServices<"API" | "UI" | "setting" | "appLifecycle", never>) {
const log = createInstanceLogFunction("SF:SetupURI", host.services.API);
host.services.appLifecycle.onLoaded.addHandler(() => {
host.services.API.addCommand({
id: "livesync-copysetupuri",
name: "Copy settings as a new setup URI",
callback: () => fireAndForget(copySetupURI(host, log)),
});
host.services.API.addCommand({
id: "livesync-copysetupuri-short",
name: "Copy settings as a new setup URI (With customization sync)",
callback: () => fireAndForget(copySetupURI(host, log, false)),
});
host.services.API.addCommand({
id: "livesync-copysetupurifull",
name: "Copy settings as a new setup URI (Full)",
callback: () => fireAndForget(copySetupURIFull(host, log)),
});
host.services.context.events.onEvent(EVENT_REQUEST_COPY_SETUP_URI, () =>
fireAndForget(() => copySetupURI(host, log))
);
return Promise.resolve(true);
});
}
@@ -0,0 +1,159 @@
import { describe, expect, it, vi, afterEach } from "vitest";
import { EVENT_REQUEST_COPY_SETUP_URI } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
import { createServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import { askEncryptingPassphrase, copySetupURI, copySetupURIFull, useSetupURIFeature } from "./setupUri";
import { encodeSettingsToSetupURI } from "@vrtmrz/livesync-commonlib/compat/API/processSetting";
vi.mock("@vrtmrz/livesync-commonlib/compat/API/processSetting", () => {
return {
encodeSettingsToSetupURI: vi.fn(),
};
});
describe("setupObsidian/setupUri", () => {
afterEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
});
it("askEncryptingPassphrase should delegate to confirm.askString", async () => {
const askString = vi.fn(() => "secret");
const host = {
services: {
UI: {
confirm: {
askString,
},
},
},
} as any;
const result = await askEncryptingPassphrase(host);
expect(result).toBe("secret");
expect(askString).toHaveBeenCalled();
});
it("copySetupURI should return early when user cancels passphrase", async () => {
const promptCopyToClipboard = vi.fn();
const host = {
services: {
setting: {
currentSettings: vi.fn(() => ({ foo: "bar" })),
},
UI: {
confirm: {
askString: vi.fn(() => false),
},
promptCopyToClipboard,
},
},
} as any;
const log = vi.fn();
await copySetupURI(host, log);
expect(encodeSettingsToSetupURI).not.toHaveBeenCalled();
expect(promptCopyToClipboard).not.toHaveBeenCalled();
expect(log).not.toHaveBeenCalled();
});
it("copySetupURI should encode with short mode by default", async () => {
const promptCopyToClipboard = vi.fn(() => true);
const currentSettings = { pluginSyncExtendedSetting: true, x: 1 };
const host = {
services: {
setting: {
currentSettings: vi.fn(() => currentSettings),
},
UI: {
confirm: {
askString: vi.fn(() => "pass"),
},
promptCopyToClipboard,
},
},
} as any;
const log = vi.fn();
vi.mocked(encodeSettingsToSetupURI).mockResolvedValue("uri://value" as any);
await copySetupURI(host, log);
expect(encodeSettingsToSetupURI).toHaveBeenCalledWith(
currentSettings,
"pass",
["pluginSyncExtendedSetting"],
true
);
expect(promptCopyToClipboard).toHaveBeenCalledWith("Setup URI", "uri://value");
expect(log).toHaveBeenCalled();
});
it("copySetupURIFull should encode with full mode", async () => {
const promptCopyToClipboard = vi.fn(() => true);
const currentSettings = { pluginSyncExtendedSetting: true, x: 1 };
const host = {
services: {
setting: {
currentSettings: vi.fn(() => currentSettings),
},
UI: {
confirm: {
askString: vi.fn(() => "pass-full"),
},
promptCopyToClipboard,
},
},
} as any;
const log = vi.fn();
vi.mocked(encodeSettingsToSetupURI).mockResolvedValue("uri://full" as any);
await copySetupURIFull(host, log);
expect(encodeSettingsToSetupURI).toHaveBeenCalledWith(currentSettings, "pass-full", [], false);
expect(promptCopyToClipboard).toHaveBeenCalledWith("Setup URI", "uri://full");
expect(log).toHaveBeenCalled();
});
it("useSetupURIFeature should register onLoaded handler that wires commands and event", async () => {
const addHandler = vi.fn();
const addCommand = vi.fn();
const context = createServiceContext();
const onEventSpy = vi.spyOn(context.events, "onEvent");
const host = {
services: {
context,
API: {
addCommand,
addLog: vi.fn(),
},
appLifecycle: {
onLoaded: {
addHandler,
},
},
setting: {
currentSettings: vi.fn(() => ({ x: 1 })),
},
UI: {
confirm: {
askString: vi.fn(() => "pass"),
},
promptCopyToClipboard: vi.fn(() => true),
},
},
} as any;
useSetupURIFeature(host);
expect(addHandler).toHaveBeenCalledTimes(1);
const loadedHandler = addHandler.mock.calls[0][0] as () => Promise<boolean>;
await loadedHandler();
expect(addCommand).toHaveBeenCalledTimes(3);
expect(addCommand).toHaveBeenCalledWith(expect.objectContaining({ id: "livesync-copysetupuri" }));
expect(addCommand).toHaveBeenCalledWith(expect.objectContaining({ id: "livesync-copysetupuri-short" }));
expect(addCommand).toHaveBeenCalledWith(expect.objectContaining({ id: "livesync-copysetupurifull" }));
expect(onEventSpy).toHaveBeenCalledWith(EVENT_REQUEST_COPY_SETUP_URI, expect.any(Function));
});
});
@@ -0,0 +1,3 @@
import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
export type SetupFeatureHost = NecessaryServices<"API" | "UI" | "setting", never>;