refactor: align platform APIs with community review

This commit is contained in:
vorotamoroz
2026-07-17 14:47:27 +00:00
parent 298738fc67
commit 326bf77183
39 changed files with 444 additions and 294 deletions
+2 -6
View File
@@ -4,20 +4,16 @@ import { EVENT_PLUGIN_UNLOADED, eventHub } from "@/common/events.ts";
import { compatGlobal, type CompatIntervalHandle } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
class AutoClosableModal extends Modal {
_closeByUnload() {
// eslint-disable-next-line @typescript-eslint/unbound-method
_closeByUnload = () => {
eventHub.off(EVENT_PLUGIN_UNLOADED, this._closeByUnload);
this.close();
}
};
constructor(app: App) {
super(app);
this._closeByUnload = this._closeByUnload.bind(this);
// eslint-disable-next-line @typescript-eslint/unbound-method
eventHub.once(EVENT_PLUGIN_UNLOADED, this._closeByUnload);
}
override onClose() {
// eslint-disable-next-line @typescript-eslint/unbound-method
eventHub.off(EVENT_PLUGIN_UNLOADED, this._closeByUnload);
}
}
@@ -15,6 +15,23 @@ import {
import type { LiveSyncCore } from "@/main.ts";
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
type MutableCommandDefinition = {
callback?: () => void;
};
type InternalCommandRegistry = {
commands?: Record<string, MutableCommandDefinition | undefined>;
executeCommandById(commandId: string): unknown;
};
type AppWithInternalCommands = {
commands?: InternalCommandRegistry;
};
type CodeMirrorAdapter = {
commands: { save: () => void };
};
export class ModuleObsidianEvents extends AbstractObsidianModule {
_everyOnloadStart(): Promise<boolean> {
// this.registerEvent(this.app.workspace.on("editor-change", ));
@@ -40,10 +57,10 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
swapSaveCommand() {
this._log("Modifying callback of the save command", LOG_LEVEL_VERBOSE);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Editor Tweaking
const saveCommandDefinition = (this.app as any).commands?.commands?.["editor:save-file"];
const commandRegistry = (this.app as unknown as AppWithInternalCommands).commands;
const saveCommandDefinition = commandRegistry?.commands?.["editor:save-file"];
const save = saveCommandDefinition?.callback;
if (typeof save === "function") {
if (saveCommandDefinition && typeof save === "function") {
this.initialCallback = save;
saveCommandDefinition.callback = () => {
scheduleTask("syncOnEditorSave", 250, () => {
@@ -61,17 +78,14 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
save();
};
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
const _this = this;
//@ts-ignore
if (!compatGlobal.CodeMirrorAdapter) {
const codeMirrorAdapter = (compatGlobal as typeof compatGlobal & { CodeMirrorAdapter?: CodeMirrorAdapter })
.CodeMirrorAdapter;
if (!codeMirrorAdapter) {
this._log("CodeMirrorAdapter is not available");
return;
}
//@ts-ignore
compatGlobal.CodeMirrorAdapter.commands.save = () => {
//@ts-ignore
void _this.app.commands.executeCommandById("editor:save-file");
codeMirrorAdapter.commands.save = () => {
void commandRegistry?.executeCommandById("editor:save-file");
// _this.app.performCommand('editor:save-file');
};
}
@@ -82,18 +96,18 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
this.watchWorkspaceOpen = this.watchWorkspaceOpen.bind(this);
this.watchOnline = this.watchOnline.bind(this);
// Already bound
// eslint-disable-next-line @typescript-eslint/unbound-method
// eslint-disable-next-line @typescript-eslint/unbound-method -- The handler is bound above before registration.
this.plugin.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
// Already bound
// eslint-disable-next-line @typescript-eslint/unbound-method
// eslint-disable-next-line @typescript-eslint/unbound-method -- The handler is bound above before registration.
this.plugin.registerDomEvent(activeDocument, "visibilitychange", this.watchWindowVisibility);
this.plugin.registerDomEvent(compatGlobal, "focus", () => this.setHasFocus(true));
this.plugin.registerDomEvent(compatGlobal, "blur", () => this.setHasFocus(false));
// Already bound
// eslint-disable-next-line @typescript-eslint/unbound-method
// eslint-disable-next-line @typescript-eslint/unbound-method -- The handler is bound above before registration.
this.plugin.registerDomEvent(compatGlobal, "online", this.watchOnline);
// Already bound
// eslint-disable-next-line @typescript-eslint/unbound-method
// eslint-disable-next-line @typescript-eslint/unbound-method -- The handler is bound above before registration.
this.plugin.registerDomEvent(compatGlobal, "offline", this.watchOnline);
}
@@ -317,7 +331,7 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
// const proc = this.core.processingFileEventCount.value;
const e = 0;
const proc = 0;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Reading the tick establishes the reactive polling dependency.
const __ = __tick.value;
return (
dbCount +
+1 -2
View File
@@ -34,8 +34,7 @@ async function measure(
return [name, measures.get(name) as MeasureResult];
}
// eslint-disable-next-line require-await, @typescript-eslint/require-await
async function formatPerfResults(items: NamedMeasureResult[]) {
function formatPerfResults(items: NamedMeasureResult[]) {
return (
`| Name | Runs | Each | Total |\n| --- | --- | --- | --- | \n` +
items
@@ -17,6 +17,11 @@ import { isPlainText, stripPrefix } from "@vrtmrz/livesync-commonlib/compat/stri
import { scheduleOnceIfDuplicated } from "octagonal-wheels/concurrency/lock";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts";
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
import {
DOCUMENT_HISTORY_PREFERENCE_KEYS,
loadDocumentHistoryPreference,
saveDocumentHistoryPreference,
} from "./documentHistoryPreferences.ts";
function isImage(path: string) {
const ext = path.split(".").splice(-1)[0].toLowerCase();
@@ -99,12 +104,10 @@ export class DocumentHistoryModal extends Modal {
if (!file && id) {
this.file = this.services.path.id2path(id);
}
// eslint-disable-next-line obsidianmd/no-unsupported-api -- loadLocalStorage is supported in Obsidian 1.7.2+
if (this.app.loadLocalStorage("ols-history-highlightdiff") == "1") {
if (loadDocumentHistoryPreference(this.app, DOCUMENT_HISTORY_PREFERENCE_KEYS.highlightDiff)) {
this.showDiff = true;
}
// eslint-disable-next-line obsidianmd/no-unsupported-api -- loadLocalStorage is supported in Obsidian 1.7.2+
if (this.app.loadLocalStorage("ols-history-diffonly") == "1") {
if (loadDocumentHistoryPreference(this.app, DOCUMENT_HISTORY_PREFERENCE_KEYS.diffOnly)) {
this.diffOnly = true;
}
}
@@ -520,10 +523,10 @@ export class DocumentHistoryModal extends Modal {
e.addEventListener("click", () => this.navigateSearch("next"));
});
this.searchResultIndicator = searchRow.createEl("span", { text: "" });
this.searchResultIndicator = searchRow.createSpan({ text: "" });
this.searchResultIndicator.addClass("history-search-result-indicator");
this.searchProgressIndicator = searchRow.createEl("span", { text: "" });
this.searchProgressIndicator = searchRow.createSpan({ text: "" });
this.searchProgressIndicator.addClass("history-search-progress-indicator");
const divView = contentEl.createDiv("");
@@ -554,8 +557,11 @@ export class DocumentHistoryModal extends Modal {
}
checkbox.addEventListener("input", (evt: Event) => {
this.showDiff = checkbox.checked;
// eslint-disable-next-line obsidianmd/no-unsupported-api -- saveLocalStorage is supported in Obsidian 1.7.2+
this.app.saveLocalStorage("ols-history-highlightdiff", this.showDiff == true ? "1" : null);
saveDocumentHistoryPreference(
this.app,
DOCUMENT_HISTORY_PREFERENCE_KEYS.highlightDiff,
this.showDiff
);
this.updateDiffNavVisibility();
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
});
@@ -570,8 +576,7 @@ export class DocumentHistoryModal extends Modal {
}
checkbox.addEventListener("input", (evt: Event) => {
this.diffOnly = checkbox.checked;
// eslint-disable-next-line obsidianmd/no-unsupported-api -- saveLocalStorage is supported in Obsidian 1.7.2+
this.app.saveLocalStorage("ols-history-diffonly", this.diffOnly == true ? "1" : null);
saveDocumentHistoryPreference(this.app, DOCUMENT_HISTORY_PREFERENCE_KEYS.diffOnly, this.diffOnly);
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
});
});
@@ -597,7 +602,7 @@ export class DocumentHistoryModal extends Modal {
this.navigateDiff("next");
});
});
this.diffNavIndicator = this.diffNavContainer.createEl("span", { text: "\u2014" });
this.diffNavIndicator = this.diffNavContainer.createSpan({ text: "\u2014" });
this.diffNavIndicator.addClass("diff-nav-indicator");
this.info = contentEl.createDiv("");
@@ -0,0 +1,22 @@
import { requireApiVersion, type App } from "@/deps.ts";
export const DOCUMENT_HISTORY_PREFERENCE_KEYS = {
diffOnly: "ols-history-diffonly",
highlightDiff: "ols-history-highlightdiff",
} as const;
export type DocumentHistoryPreferenceKey =
(typeof DOCUMENT_HISTORY_PREFERENCE_KEYS)[keyof typeof DOCUMENT_HISTORY_PREFERENCE_KEYS];
export function loadDocumentHistoryPreference(app: App, key: DocumentHistoryPreferenceKey): boolean {
if (requireApiVersion("1.8.7")) {
return app.loadLocalStorage(key) === "1";
}
return false;
}
export function saveDocumentHistoryPreference(app: App, key: DocumentHistoryPreferenceKey, enabled: boolean): void {
if (requireApiVersion("1.8.7")) {
app.saveLocalStorage(key, enabled ? "1" : null);
}
}
@@ -0,0 +1,52 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const requireApiVersionMock = vi.hoisted(() => vi.fn<(version: string) => boolean>());
vi.mock("@/deps.ts", () => ({
requireApiVersion: requireApiVersionMock,
}));
import {
DOCUMENT_HISTORY_PREFERENCE_KEYS,
loadDocumentHistoryPreference,
saveDocumentHistoryPreference,
} from "./documentHistoryPreferences.ts";
function createAppStorage() {
return {
loadLocalStorage: vi.fn<(key: string) => unknown>(),
saveLocalStorage: vi.fn<(key: string, value: unknown | null) => void>(),
};
}
describe("document history preferences", () => {
beforeEach(() => {
requireApiVersionMock.mockReset();
});
it("falls back without accessing Vault local storage on older Obsidian versions", () => {
requireApiVersionMock.mockReturnValue(false);
const app = createAppStorage();
expect(loadDocumentHistoryPreference(app as never, DOCUMENT_HISTORY_PREFERENCE_KEYS.highlightDiff)).toBe(false);
saveDocumentHistoryPreference(app as never, DOCUMENT_HISTORY_PREFERENCE_KEYS.diffOnly, true);
expect(requireApiVersionMock).toHaveBeenCalledWith("1.8.7");
expect(app.loadLocalStorage).not.toHaveBeenCalled();
expect(app.saveLocalStorage).not.toHaveBeenCalled();
});
it("loads and saves Vault-scoped preferences when the API is available", () => {
requireApiVersionMock.mockReturnValue(true);
const app = createAppStorage();
app.loadLocalStorage.mockReturnValue("1");
expect(loadDocumentHistoryPreference(app as never, DOCUMENT_HISTORY_PREFERENCE_KEYS.diffOnly)).toBe(true);
saveDocumentHistoryPreference(app as never, DOCUMENT_HISTORY_PREFERENCE_KEYS.highlightDiff, true);
saveDocumentHistoryPreference(app as never, DOCUMENT_HISTORY_PREFERENCE_KEYS.highlightDiff, false);
expect(app.loadLocalStorage).toHaveBeenCalledWith("ols-history-diffonly");
expect(app.saveLocalStorage).toHaveBeenNthCalledWith(1, "ols-history-highlightdiff", "1");
expect(app.saveLocalStorage).toHaveBeenNthCalledWith(2, "ols-history-highlightdiff", null);
});
});
@@ -110,7 +110,7 @@ export class ConflictResolveModal extends Modal {
contentEl.empty();
const diffOptionsRow = contentEl.createDiv("");
diffOptionsRow.addClass("diff-options-row");
diffOptionsRow.createEl("span", { text: this.filename });
diffOptionsRow.createSpan({ text: this.filename });
const diffNavContainer = diffOptionsRow.createDiv("");
diffNavContainer.addClass("diff-nav");
@@ -122,7 +122,7 @@ export class ConflictResolveModal extends Modal {
e.addClass("diff-nav-btn");
e.addEventListener("click", () => this.navigateDiff("next"));
});
this.diffNavIndicator = diffNavContainer.createEl("span", { text: "\u2014" });
this.diffNavIndicator = diffNavContainer.createSpan({ text: "\u2014" });
this.diffNavIndicator.addClass("diff-nav-indicator");
this.diffView = contentEl.createDiv("");
@@ -736,7 +736,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
value: `${order}`,
cls: "sls-setting-tab",
} as DomElementInfo);
el.createEl("div", {
el.createDiv({
cls: "sls-setting-menu-btn",
text: icon,
title: title,
@@ -188,7 +188,7 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
}
this.requestUpdate();
};
text.inputEl.before((dateEl = activeDocument.createElement("span")));
text.inputEl.before((dateEl = activeDocument.createSpan()));
text.inputEl.type = "datetime-local";
if (this.editingSettings.maxMTimeForReflectEvents > 0) {
const date = new Date(this.editingSettings.maxMTimeForReflectEvents);
@@ -16,13 +16,7 @@ import type { PageFunctions } from "./SettingPane.ts";
import InfoPanel from "./InfoPanel.svelte";
import { writable } from "svelte/store";
import { SveltePanel } from "./SveltePanel.ts";
import {
getBucketConfigSummary,
getP2PConfigSummary,
getCouchDBConfigSummary,
getE2EEConfigSummary,
} from "./settingUtils.ts";
import { SETTING_KEY_P2P_DEVICE_NAME } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { getE2EEConfigSummary } from "./settingUtils.ts";
import { SetupManager, UserMode } from "@/modules/features/SetupManager.ts";
import { OnDialogSettingsDefault, type AllSettings } from "./settingConstants.ts";
import { activateRemoteConfiguration } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/remoteConfig";
@@ -43,15 +37,6 @@ function getSettingsFromEditingSettings(editingSettings: AllSettings): ObsidianL
}
return workObj;
}
const toggleActiveSyncClass = (el: HTMLElement, isActive: () => boolean) => {
if (isActive()) {
el.addClass("active-pane");
} else {
el.removeClass("active-pane");
}
return {};
};
function createRemoteConfigurationId(): string {
return `remote-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}
@@ -517,123 +502,6 @@ export function paneRemoteConfig(
refreshList();
});
}
// eslint-disable-next-line no-constant-condition
if (false) {
const initialProps = {
info: getCouchDBConfigSummary(this.editingSettings),
};
const summaryWritable = writable(initialProps);
const updateSummary = () => {
summaryWritable.set({
info: getCouchDBConfigSummary(this.editingSettings),
});
};
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleCouchDB"), () => {}).then((paneEl) => {
new SveltePanel(InfoPanel, paneEl, summaryWritable);
const setupButton = new Setting(paneEl).setName("Configure Remote");
setupButton
.addButton((button) =>
button
.setButtonText("Configure")
.setCta()
.onClick(async () => {
const setupManager = this.core.getModule(SetupManager);
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
await setupManager.onCouchDBManualSetup(
UserMode.Update,
originalSettings,
this.editingSettings.remoteType === REMOTE_COUCHDB
);
updateSummary();
})
)
.addOnUpdate(() =>
toggleActiveSyncClass(paneEl, () => this.editingSettings.remoteType === REMOTE_COUCHDB)
);
});
}
// eslint-disable-next-line no-constant-condition
if (false) {
const initialProps = {
info: getBucketConfigSummary(this.editingSettings),
};
const summaryWritable = writable(initialProps);
const updateSummary = () => {
summaryWritable.set({
info: getBucketConfigSummary(this.editingSettings),
});
};
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleMinioS3R2"), () => {}).then((paneEl) => {
new SveltePanel(InfoPanel, paneEl, summaryWritable);
const setupButton = new Setting(paneEl).setName("Configure Remote");
setupButton
.addButton((button) =>
button
.setButtonText("Configure")
.setCta()
.onClick(async () => {
const setupManager = this.core.getModule(SetupManager);
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
await setupManager.onBucketManualSetup(
UserMode.Update,
originalSettings,
this.editingSettings.remoteType === REMOTE_MINIO
);
//TODO
updateSummary();
})
)
.addOnUpdate(() =>
toggleActiveSyncClass(paneEl, () => this.editingSettings.remoteType === REMOTE_MINIO)
);
});
}
// eslint-disable-next-line no-constant-condition
if (false) {
const getDevicePeerId = () => this.services.config.getSmallConfig(SETTING_KEY_P2P_DEVICE_NAME) || "";
const initialProps = {
info: getP2PConfigSummary(this.editingSettings, {
"Device Peer ID": getDevicePeerId(),
}),
};
const summaryWritable = writable(initialProps);
const updateSummary = () => {
summaryWritable.set({
info: getP2PConfigSummary(this.editingSettings, {
"Device Peer ID": getDevicePeerId(),
}),
});
};
void addPanel(paneEl, "Peer-to-Peer Synchronisation", () => {}).then((paneEl) => {
new SveltePanel(InfoPanel, paneEl, summaryWritable);
const setupButton = new Setting(paneEl).setName("Configure Remote");
setupButton
.addButton((button) =>
button
.setButtonText("Configure")
.setCta()
.onClick(async () => {
const setupManager = this.core.getModule(SetupManager);
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
await setupManager.onP2PManualSetup(
UserMode.Update,
originalSettings,
this.editingSettings.remoteType === REMOTE_P2P
);
//TODO
updateSummary();
})
)
.addOnUpdate(() =>
toggleActiveSyncClass(
paneEl,
() => this.editingSettings.remoteType === REMOTE_P2P || this.editingSettings.P2P_Enabled
)
);
});
}
// new Setting(paneEl)
// .setDesc("Generate ES256 Keypair for testing")
// .addButton((button) =>
+1 -1
View File
@@ -60,7 +60,7 @@ export const enum UserMode {
/**
* Update User Mode - for users who are updating configuration. May be `existing-user` as well, but possibly they want to treat it differently.
*/
// eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
// eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values -- Update is a semantic alias for the unknown setup mode.
Update = "unknown", // Alias for Unknown for better readability
}
+1 -1
View File
@@ -71,7 +71,7 @@ export class ObsidianConfirm<T extends ObsidianServiceContext = ObsidianServiceC
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) => {
doc.createSpan(undefined, (a) => {
a.appendText(beforeText);
a.appendChild(
a.createEl("a", undefined, (anchor) => {