mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-01-25 22:47:11 +00:00
- Rewrite the service's binding/handler assignment systems - Removed loopholes that allowed traversal between services to clarify dependencies. - Consolidated the hidden state-related state, the handler, and the addition of bindings to the handler into a single object. - Currently, functions that can have handlers added implement either addHandler or setHandler directly on the function itself. I understand there are differing opinions on this, but for now, this is how it stands. - Services now possess a Context. Please ensure each platform has a class that inherits from ServiceContext. - To permit services to be dynamically bound, the services themselves are now defined by interfaces.
254 lines
10 KiB
TypeScript
254 lines
10 KiB
TypeScript
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
|
import { EVENT_FILE_RENAMED, EVENT_LEAF_ACTIVE_CHANGED, eventHub } from "../../common/events.js";
|
|
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
|
import { scheduleTask } from "octagonal-wheels/concurrency/task";
|
|
import { type TFile } from "../../deps.ts";
|
|
import { fireAndForget } from "octagonal-wheels/promises";
|
|
import { type FilePathWithPrefix } from "../../lib/src/common/types.ts";
|
|
import { reactive, reactiveSource } from "octagonal-wheels/dataobject/reactive";
|
|
import {
|
|
collectingChunks,
|
|
pluginScanningCount,
|
|
hiddenFilesEventCount,
|
|
hiddenFilesProcessingCount,
|
|
} from "../../lib/src/mock_and_interop/stores.ts";
|
|
import type { LiveSyncCore } from "../../main.ts";
|
|
|
|
export class ModuleObsidianEvents extends AbstractObsidianModule {
|
|
_everyOnloadStart(): Promise<boolean> {
|
|
// this.registerEvent(this.app.workspace.on("editor-change", ));
|
|
this.plugin.registerEvent(
|
|
this.app.vault.on("rename", (file, oldPath) => {
|
|
eventHub.emitEvent(EVENT_FILE_RENAMED, {
|
|
newPath: file.path as FilePathWithPrefix,
|
|
old: oldPath as FilePathWithPrefix,
|
|
});
|
|
})
|
|
);
|
|
this.plugin.registerEvent(
|
|
this.app.workspace.on("active-leaf-change", () => eventHub.emitEvent(EVENT_LEAF_ACTIVE_CHANGED))
|
|
);
|
|
return Promise.resolve(true);
|
|
}
|
|
|
|
private _performRestart(): void {
|
|
this.__performAppReload();
|
|
}
|
|
|
|
__performAppReload() {
|
|
//@ts-ignore
|
|
this.app.commands.executeCommandById("app:reload");
|
|
}
|
|
|
|
initialCallback: any;
|
|
|
|
swapSaveCommand() {
|
|
this._log("Modifying callback of the save command", LOG_LEVEL_VERBOSE);
|
|
const saveCommandDefinition = (this.app as any).commands?.commands?.["editor:save-file"];
|
|
const save = saveCommandDefinition?.callback;
|
|
if (typeof save === "function") {
|
|
this.initialCallback = save;
|
|
saveCommandDefinition.callback = () => {
|
|
scheduleTask("syncOnEditorSave", 250, () => {
|
|
if (this.services.appLifecycle.hasUnloaded()) {
|
|
this._log("Unload and remove the handler.", LOG_LEVEL_VERBOSE);
|
|
saveCommandDefinition.callback = this.initialCallback;
|
|
this.initialCallback = undefined;
|
|
} else {
|
|
if (this.settings.syncOnEditorSave) {
|
|
this._log("Sync on Editor Save.", LOG_LEVEL_VERBOSE);
|
|
fireAndForget(() => this.services.replication.replicateByEvent());
|
|
}
|
|
}
|
|
});
|
|
save();
|
|
};
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
const _this = this;
|
|
//@ts-ignore
|
|
if (!window.CodeMirrorAdapter) {
|
|
this._log("CodeMirrorAdapter is not available");
|
|
return;
|
|
}
|
|
//@ts-ignore
|
|
window.CodeMirrorAdapter.commands.save = () => {
|
|
//@ts-ignore
|
|
_this.app.commands.executeCommandById("editor:save-file");
|
|
// _this.app.performCommand('editor:save-file');
|
|
};
|
|
}
|
|
|
|
registerWatchEvents() {
|
|
this.setHasFocus = this.setHasFocus.bind(this);
|
|
this.watchWindowVisibility = this.watchWindowVisibility.bind(this);
|
|
this.watchWorkspaceOpen = this.watchWorkspaceOpen.bind(this);
|
|
this.watchOnline = this.watchOnline.bind(this);
|
|
this.plugin.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
|
|
this.plugin.registerDomEvent(document, "visibilitychange", this.watchWindowVisibility);
|
|
this.plugin.registerDomEvent(window, "focus", () => this.setHasFocus(true));
|
|
this.plugin.registerDomEvent(window, "blur", () => this.setHasFocus(false));
|
|
this.plugin.registerDomEvent(window, "online", this.watchOnline);
|
|
this.plugin.registerDomEvent(window, "offline", this.watchOnline);
|
|
}
|
|
|
|
hasFocus = true;
|
|
isLastHidden = false;
|
|
|
|
setHasFocus(hasFocus: boolean) {
|
|
this.hasFocus = hasFocus;
|
|
this.watchWindowVisibility();
|
|
}
|
|
|
|
watchWindowVisibility() {
|
|
scheduleTask("watch-window-visibility", 100, () => fireAndForget(() => this.watchWindowVisibilityAsync()));
|
|
}
|
|
|
|
watchOnline() {
|
|
scheduleTask("watch-online", 500, () => fireAndForget(() => this.watchOnlineAsync()));
|
|
}
|
|
async watchOnlineAsync() {
|
|
// If some files were failed to retrieve, scan files again.
|
|
// TODO:FIXME AT V0.17.31, this logic has been disabled.
|
|
if (navigator.onLine && this.localDatabase.needScanning) {
|
|
this.localDatabase.needScanning = false;
|
|
await this.services.vault.scanVault();
|
|
}
|
|
}
|
|
|
|
async watchWindowVisibilityAsync() {
|
|
if (this.settings.suspendFileWatching) return;
|
|
if (!this.settings.isConfigured) return;
|
|
if (!this.services.appLifecycle.isReady()) return;
|
|
|
|
if (this.isLastHidden && !this.hasFocus) {
|
|
// NO OP while non-focused after made hidden;
|
|
return;
|
|
}
|
|
|
|
const isHidden = document.hidden;
|
|
if (this.isLastHidden === isHidden) {
|
|
return;
|
|
}
|
|
this.isLastHidden = isHidden;
|
|
|
|
await this.services.fileProcessing.commitPendingFileEvents();
|
|
|
|
if (isHidden) {
|
|
await this.services.appLifecycle.onSuspending();
|
|
} else {
|
|
// suspend all temporary.
|
|
if (this.services.appLifecycle.isSuspended()) return;
|
|
if (!this.hasFocus) return;
|
|
await this.services.appLifecycle.onResuming();
|
|
await this.services.appLifecycle.onResumed();
|
|
}
|
|
}
|
|
watchWorkspaceOpen(file: TFile | null) {
|
|
if (this.settings.suspendFileWatching) return;
|
|
if (!this.settings.isConfigured) return;
|
|
if (!this.services.appLifecycle.isReady()) return;
|
|
if (!file) return;
|
|
scheduleTask("watch-workspace-open", 500, () => fireAndForget(() => this.watchWorkspaceOpenAsync(file)));
|
|
}
|
|
|
|
async watchWorkspaceOpenAsync(file: TFile) {
|
|
if (this.settings.suspendFileWatching) return;
|
|
if (!this.settings.isConfigured) return;
|
|
if (!this.services.appLifecycle.isReady()) return;
|
|
await this.services.fileProcessing.commitPendingFileEvents();
|
|
if (file == null) {
|
|
return;
|
|
}
|
|
if (this.settings.syncOnFileOpen && !this.services.appLifecycle.isSuspended()) {
|
|
await this.services.replication.replicateByEvent();
|
|
}
|
|
await this.services.conflict.queueCheckForIfOpen(file.path as FilePathWithPrefix);
|
|
}
|
|
|
|
_everyOnLayoutReady(): Promise<boolean> {
|
|
this.swapSaveCommand();
|
|
this.registerWatchEvents();
|
|
return Promise.resolve(true);
|
|
}
|
|
|
|
private _askReload(message?: string) {
|
|
if (this.services.appLifecycle.isReloadingScheduled()) {
|
|
this._log(`Reloading is already scheduled`, LOG_LEVEL_VERBOSE);
|
|
return;
|
|
}
|
|
scheduleTask("configReload", 250, async () => {
|
|
const RESTART_NOW = "Yes, restart immediately";
|
|
const RESTART_AFTER_STABLE = "Yes, schedule a restart after stabilisation";
|
|
const RETRY_LATER = "No, Leave it to me";
|
|
const ret = await this.core.confirm.askSelectStringDialogue(
|
|
message || "Do you want to restart and reload Obsidian now?",
|
|
[RESTART_AFTER_STABLE, RESTART_NOW, RETRY_LATER],
|
|
{ defaultAction: RETRY_LATER }
|
|
);
|
|
if (ret == RESTART_NOW) {
|
|
this.__performAppReload();
|
|
} else if (ret == RESTART_AFTER_STABLE) {
|
|
this.services.appLifecycle.scheduleRestart();
|
|
}
|
|
});
|
|
}
|
|
private _scheduleAppReload() {
|
|
if (!this.core._totalProcessingCount) {
|
|
const __tick = reactiveSource(0);
|
|
this.core._totalProcessingCount = reactive(() => {
|
|
const dbCount = this.core.databaseQueueCount.value;
|
|
const replicationCount = this.core.replicationResultCount.value;
|
|
const storageApplyingCount = this.core.storageApplyingCount.value;
|
|
const chunkCount = collectingChunks.value;
|
|
const pluginScanCount = pluginScanningCount.value;
|
|
const hiddenFilesCount = hiddenFilesEventCount.value + hiddenFilesProcessingCount.value;
|
|
const conflictProcessCount = this.core.conflictProcessQueueCount.value;
|
|
const e = this.core.pendingFileEventCount.value;
|
|
const proc = this.core.processingFileEventCount.value;
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const __ = __tick.value;
|
|
return (
|
|
dbCount +
|
|
replicationCount +
|
|
storageApplyingCount +
|
|
chunkCount +
|
|
pluginScanCount +
|
|
hiddenFilesCount +
|
|
conflictProcessCount +
|
|
e +
|
|
proc
|
|
);
|
|
});
|
|
this.plugin.registerInterval(
|
|
setInterval(() => {
|
|
__tick.value++;
|
|
}, 1000) as unknown as number
|
|
);
|
|
|
|
let stableCheck = 3;
|
|
this.core._totalProcessingCount.onChanged((e) => {
|
|
if (e.value == 0) {
|
|
if (stableCheck-- <= 0) {
|
|
this.__performAppReload();
|
|
}
|
|
this._log(
|
|
`Obsidian will be restarted soon! (Within ${stableCheck} seconds)`,
|
|
LOG_LEVEL_NOTICE,
|
|
"restart-notice"
|
|
);
|
|
} else {
|
|
stableCheck = 3;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
|
services.appLifecycle.onLayoutReady.addHandler(this._everyOnLayoutReady.bind(this));
|
|
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
|
|
services.appLifecycle.performRestart.setHandler(this._performRestart.bind(this));
|
|
services.appLifecycle.askRestart.setHandler(this._askReload.bind(this));
|
|
services.appLifecycle.scheduleRestart.setHandler(this._scheduleAppReload.bind(this));
|
|
}
|
|
}
|