Preparing v0.24.0

This commit is contained in:
vorotamoroz
2024-10-16 12:44:07 +01:00
parent 48315d657d
commit 89e23b1bf4
85 changed files with 9211 additions and 6033 deletions

View File

@@ -0,0 +1,134 @@
// This file is based on a file that was published by the @remotely-save, under the Apache 2 License.
// I would love to express my deepest gratitude to the original authors for their hard work and dedication. Without their contributions, this project would not have been possible.
//
// Original Implementation is here: https://github.com/remotely-save/remotely-save/blob/28b99557a864ef59c19d2ad96101196e401718f0/src/remoteForS3.ts
import {
FetchHttpHandler,
type FetchHttpHandlerOptions,
} from "@smithy/fetch-http-handler";
import { HttpRequest, HttpResponse, type HttpHandlerOptions } from "@smithy/protocol-http";
//@ts-ignore
import { requestTimeout } from "@smithy/fetch-http-handler/dist-es/request-timeout";
import { buildQueryString } from "@smithy/querystring-builder";
import { requestUrl, type RequestUrlParam } from "../../../deps.ts";
////////////////////////////////////////////////////////////////////////////////
// special handler using Obsidian requestUrl
////////////////////////////////////////////////////////////////////////////////
/**
* This is close to origin implementation of FetchHttpHandler
* https://github.com/aws/aws-sdk-js-v3/blob/main/packages/fetch-http-handler/src/fetch-http-handler.ts
* that is released under Apache 2 License.
* But this uses Obsidian requestUrl instead.
*/
export class ObsHttpHandler extends FetchHttpHandler {
requestTimeoutInMs: number | undefined;
reverseProxyNoSignUrl: string | undefined;
constructor(
options?: FetchHttpHandlerOptions,
reverseProxyNoSignUrl?: string
) {
super(options);
this.requestTimeoutInMs =
options === undefined ? undefined : options.requestTimeout;
this.reverseProxyNoSignUrl = reverseProxyNoSignUrl;
}
// eslint-disable-next-line require-await
async handle(
request: HttpRequest,
{ abortSignal }: HttpHandlerOptions = {}
): Promise<{ response: HttpResponse }> {
if (abortSignal?.aborted) {
const abortError = new Error("Request aborted");
abortError.name = "AbortError";
return Promise.reject(abortError);
}
let path = request.path;
if (request.query) {
const queryString = buildQueryString(request.query);
if (queryString) {
path += `?${queryString}`;
}
}
const { port, method } = request;
let url = `${request.protocol}//${request.hostname}${port ? `:${port}` : ""
}${path}`;
if (
this.reverseProxyNoSignUrl !== undefined &&
this.reverseProxyNoSignUrl !== ""
) {
const urlObj = new URL(url);
urlObj.host = this.reverseProxyNoSignUrl;
url = urlObj.href;
}
const body =
method === "GET" || method === "HEAD" ? undefined : request.body;
const transformedHeaders: Record<string, string> = {};
for (const key of Object.keys(request.headers)) {
const keyLower = key.toLowerCase();
if (keyLower === "host" || keyLower === "content-length") {
continue;
}
transformedHeaders[keyLower] = request.headers[key];
}
let contentType: string | undefined = undefined;
if (transformedHeaders["content-type"] !== undefined) {
contentType = transformedHeaders["content-type"];
}
let transformedBody: any = body;
if (ArrayBuffer.isView(body)) {
transformedBody = new Uint8Array(body.buffer).buffer;
}
const param: RequestUrlParam = {
body: transformedBody,
headers: transformedHeaders,
method: method,
url: url,
contentType: contentType,
};
const raceOfPromises = [
requestUrl(param).then((rsp) => {
const headers = rsp.headers;
const headersLower: Record<string, string> = {};
for (const key of Object.keys(headers)) {
headersLower[key.toLowerCase()] = headers[key];
}
const stream = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new Uint8Array(rsp.arrayBuffer));
controller.close();
},
});
return {
response: new HttpResponse({
headers: headersLower,
statusCode: rsp.status,
body: stream,
}),
};
}),
requestTimeout(this.requestTimeoutInMs),
];
if (abortSignal) {
raceOfPromises.push(
new Promise<never>((resolve, reject) => {
abortSignal.onabort = () => {
const abortError = new Error("Request aborted");
abortError.name = "AbortError";
reject(abortError);
};
})
);
}
return Promise.race(raceOfPromises);
}
}

View File

@@ -0,0 +1,218 @@
import { AbstractObsidianModule, type IObsidianModule } from '../AbstractObsidianModule.ts';
import { LOG_LEVEL_DEBUG, LOG_LEVEL_VERBOSE } from 'octagonal-wheels/common/logger';
import { Notice, requestUrl, type RequestUrlParam, type RequestUrlResponse } from '../../deps.ts';
import { type EntryDoc, type FilePathWithPrefix } from '../../lib/src/common/types.ts';
import { getPathFromTFile } from '../../common/utils.ts';
import { disableEncryption, enableEncryption, isCloudantURI, isValidRemoteCouchDBURI, replicationFilter } from '../../lib/src/pouchdb/utils_couchdb.ts';
import { setNoticeClass } from '../../lib/src/mock_and_interop/wrapper.ts';
import { ObsHttpHandler } from './APILib/ObsHttpHandler.ts';
import { PouchDB } from '../../lib/src/pouchdb/pouchdb-browser.ts';
import { reactive, reactiveSource } from 'octagonal-wheels/dataobject/reactive';
setNoticeClass(Notice);
async function fetchByAPI(request: RequestUrlParam): Promise<RequestUrlResponse> {
const ret = await requestUrl(request);
if (ret.status - (ret.status % 100) !== 200) {
const er: Error & { status?: number } = new Error(`Request Error:${ret.status}`);
if (ret.json) {
er.message = ret.json.reason;
er.name = `${ret.json.error ?? ""}:${ret.json.message ?? ""}`;
}
er.status = ret.status;
throw er;
}
return ret;
}
export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidianModule {
_customHandler!: ObsHttpHandler;
authHeaderSource = reactiveSource<string>("");
authHeader = reactive(() =>
this.authHeaderSource.value == "" ? "" : "Basic " + window.btoa(this.authHeaderSource.value));
$$customFetchHandler(): ObsHttpHandler {
if (!this._customHandler) this._customHandler = new ObsHttpHandler(undefined, undefined);
return this._customHandler;
}
async $$connectRemoteCouchDB(uri: string, auth: { username: string; password: string }, disableRequestURI: boolean, passphrase: string | false, useDynamicIterationCount: boolean, performSetup: boolean, skipInfo: boolean, compression: boolean): Promise<string | { db: PouchDB.Database<EntryDoc>; info: PouchDB.Core.DatabaseInfo }> {
if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid";
if (uri.toLowerCase() != uri) return "Remote URI and database name could not contain capital letters.";
if (uri.indexOf(" ") !== -1) return "Remote URI and database name could not contain spaces.";
const userNameAndPassword = (auth.username && auth.password) ? `${auth.username}:${auth.password}` : "";
if (this.authHeaderSource.value != userNameAndPassword) {
this.authHeaderSource.value = userNameAndPassword;
}
const authHeader = this.authHeader.value;
// const _this = this;
const conf: PouchDB.HttpAdapter.HttpAdapterConfiguration = {
adapter: "http",
auth,
skip_setup: !performSetup,
fetch: async (url: string | Request, opts?: RequestInit) => {
let size = "";
const localURL = url.toString().substring(uri.length);
const method = opts?.method ?? "GET";
if (opts?.body) {
const opts_length = opts.body.toString().length;
if (opts_length > 1000 * 1000 * 10) {
// over 10MB
if (isCloudantURI(uri)) {
this.plugin.last_successful_post = false;
this._log("This request should fail on IBM Cloudant.", LOG_LEVEL_VERBOSE);
throw new Error("This request should fail on IBM Cloudant.");
}
}
size = ` (${opts_length})`;
}
if (!disableRequestURI && typeof url == "string" && typeof (opts?.body ?? "") == "string") {
const body = opts?.body as string;
const transformedHeaders = { ...(opts?.headers as Record<string, string>) };
if (authHeader != "") transformedHeaders["authorization"] = authHeader;
delete transformedHeaders["host"];
delete transformedHeaders["Host"];
delete transformedHeaders["content-length"];
delete transformedHeaders["Content-Length"];
const requestParam: RequestUrlParam = {
url,
method: opts?.method,
body: body,
headers: transformedHeaders,
contentType: "application/json",
// contentType: opts.headers,
};
try {
this.plugin.requestCount.value = this.plugin.requestCount.value + 1;
const r = await fetchByAPI(requestParam);
if (method == "POST" || method == "PUT") {
this.plugin.last_successful_post = r.status - (r.status % 100) == 200;
} else {
this.plugin.last_successful_post = true;
}
this._log(`HTTP:${method}${size} to:${localURL} -> ${r.status}`, LOG_LEVEL_DEBUG);
return new Response(r.arrayBuffer, {
headers: r.headers,
status: r.status,
statusText: `${r.status}`,
});
} catch (ex) {
this._log(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL_VERBOSE);
// limit only in bulk_docs.
if (url.toString().indexOf("_bulk_docs") !== -1) {
this.plugin.last_successful_post = false;
}
this._log(ex);
throw ex;
} finally {
this.plugin.responseCount.value = this.plugin.responseCount.value + 1;
}
}
// -old implementation
try {
// const orgHeaders = opts?.headers;
// const newHeaders = new Headers();
// newHeaders.append("authorization", authHeader);
// if (orgHeaders && typeof orgHeaders.forEach !== "string") {
// const items = Object.entries(orgHeaders);
// items.forEach(([key, value]) => newHeaders.append(key, value));
// }
// newHeaders.append("ngrok-skip-browser-warning", "123");
// opts!.headers = newHeaders;
DEV: {
// Issue #407
(opts!.headers as Headers).append("ngrok-skip-browser-warning", "123");
}
this.plugin.requestCount.value = this.plugin.requestCount.value + 1;
const response: Response = await fetch(url, opts);
if (method == "POST" || method == "PUT") {
this.plugin.last_successful_post = response.ok;
} else {
this.plugin.last_successful_post = true;
}
this._log(`HTTP:${method}${size} to:${localURL} -> ${response.status}`, LOG_LEVEL_DEBUG);
if (Math.floor(response.status / 100) !== 2) {
if (method != "GET" && localURL.indexOf("/_local/") === -1 && !localURL.endsWith("/")) {
const r = response.clone();
this._log(`The request may have failed. The reason sent by the server: ${r.status}: ${r.statusText}`);
try {
this._log(await (await r.blob()).text(), LOG_LEVEL_VERBOSE);
} catch (_) {
this._log("Cloud not parse response", LOG_LEVEL_VERBOSE);
this._log(_, LOG_LEVEL_VERBOSE);
}
} else {
this._log(`Just checkpoint or some server information has been missing. The 404 error shown above is not an error.`, LOG_LEVEL_VERBOSE)
}
}
return response;
} catch (ex) {
this._log(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL_VERBOSE);
// limit only in bulk_docs.
if (url.toString().indexOf("_bulk_docs") !== -1) {
this.plugin.last_successful_post = false;
}
this._log(ex);
throw ex;
} finally {
this.plugin.responseCount.value = this.plugin.responseCount.value + 1;
}
// return await fetch(url, opts);
},
};
const db: PouchDB.Database<EntryDoc> = new PouchDB<EntryDoc>(uri, conf);
replicationFilter(db, compression);
disableEncryption();
if (passphrase !== "false" && typeof passphrase === "string") {
enableEncryption(db, passphrase, useDynamicIterationCount, false);
}
if (skipInfo) {
return { db: db, info: { db_name: "", doc_count: 0, update_seq: "" } };
}
try {
const info = await db.info();
return { db: db, info: info };
} catch (ex: any) {
let msg = `${ex?.name}:${ex?.message}`;
if (ex?.name == "TypeError" && ex?.message == "Failed to fetch") {
msg += "\n**Note** This error caused by many reasons. The only sure thing is you didn't touch the server.\nTo check details, open inspector.";
}
this._log(ex, LOG_LEVEL_VERBOSE);
return msg;
}
}
$$isMobile(): boolean {
//@ts-ignore : internal API
return this.app.isMobile;
}
$$vaultName(): string {
return this.app.vault.getName();
}
$$getVaultName(): string {
return this.core.$$vaultName() + (this.settings?.additionalSuffixOfDatabaseName ? ("-" + this.settings.additionalSuffixOfDatabaseName) : "");
}
$$getActiveFilePath(): FilePathWithPrefix | undefined {
const file = this.app.workspace.getActiveFile();
if (file) {
return getPathFromTFile(file);
}
return undefined;
}
$anyGetAppId(): Promise<string | undefined> {
return Promise.resolve(`${("appId" in this.app ? this.app.appId : "")}`);
}
}

View File

@@ -0,0 +1,217 @@
import { AbstractObsidianModule, type IObsidianModule } 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';
export class ModuleObsidianEvents extends AbstractObsidianModule implements IObsidianModule {
$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, old: oldPath });
}));
this.plugin.registerEvent(this.app.workspace.on("active-leaf-change", () => eventHub.emitEvent(EVENT_LEAF_ACTIVE_CHANGED)));
return Promise.resolve(true);
}
$$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.plugin._unloaded) {
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.core.$$replicate());
}
}
});
save();
};
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
const _this = this;
//@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.plugin.$$performFullScan();
}
}
async watchWindowVisibilityAsync() {
if (this.settings.suspendFileWatching) return;
if (!this.settings.isConfigured) return;
if (!this.plugin.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.core.$everyCommitPendingFileEvent();
if (isHidden) {
await this.core.$everyBeforeSuspendProcess();
} else {
// suspend all temporary.
if (this.plugin.suspended) return;
if (!this.hasFocus) return;
await this.core.$everyOnResumeProcess();
await this.core.$everyAfterResumeProcess();
}
}
watchWorkspaceOpen(file: TFile | null) {
if (this.settings.suspendFileWatching) return;
if (!this.settings.isConfigured) return;
if (!this.plugin.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.plugin.isReady) return;
await this.core.$everyCommitPendingFileEvent();
if (file == null) {
return;
}
if (this.settings.syncOnFileOpen && !this.plugin.suspended) {
await this.core.$$replicate();
}
await this.core.$$queueConflictCheckIfOpen(file.path as FilePathWithPrefix);
}
$everyOnLayoutReady(): Promise<boolean> {
this.swapSaveCommand();
this.registerWatchEvents();
return Promise.resolve(true);
}
$$askReload(message?: string) {
if (this.core.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.core.$$scheduleAppReload();
}
})
}
$$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;
}
})
}
}
}

View File

@@ -0,0 +1,136 @@
import { fireAndForget } from "octagonal-wheels/promises";
import { addIcon, type Editor, type MarkdownFileInfo, type MarkdownView } from "../../deps.ts";
import { LOG_LEVEL_NOTICE, type FilePathWithPrefix } from "../../lib/src/common/types.ts";
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsidianModule {
$everyOnloadStart(): Promise<boolean> {
// UI
addIcon(
"replicate",
`<g transform="matrix(1.15 0 0 1.15 -8.31 -9.52)" fill="currentColor" fill-rule="evenodd">
<path d="m85 22.2c-0.799-4.74-4.99-8.37-9.88-8.37-0.499 0-1.1 0.101-1.6 0.101-2.4-3.03-6.09-4.94-10.3-4.94-6.09 0-11.2 4.14-12.8 9.79-5.59 1.11-9.78 6.05-9.78 12 0 6.76 5.39 12.2 12 12.2h29.9c5.79 0 10.1-4.74 10.1-10.6 0-4.84-3.29-8.88-7.68-10.2zm-2.99 14.7h-29.5c-2.3-0.202-4.29-1.51-5.29-3.53-0.899-2.12-0.699-4.54 0.698-6.46 1.2-1.61 2.99-2.52 4.89-2.52 0.299 0 0.698 0 0.998 0.101l1.8 0.303v-2.02c0-3.63 2.4-6.76 5.89-7.57 0.599-0.101 1.2-0.202 1.8-0.202 2.89 0 5.49 1.62 6.79 4.24l0.598 1.21 1.3-0.504c0.599-0.202 1.3-0.303 2-0.303 1.3 0 2.5 0.404 3.59 1.11 1.6 1.21 2.6 3.13 2.6 5.15v1.61h2c2.6 0 4.69 2.12 4.69 4.74-0.099 2.52-2.2 4.64-4.79 4.64z"/>
<path d="m53.2 49.2h-41.6c-1.8 0-3.2 1.4-3.2 3.2v28.6c0 1.8 1.4 3.2 3.2 3.2h15.8v4h-7v6h24v-6h-7v-4h15.8c1.8 0 3.2-1.4 3.2-3.2v-28.6c0-1.8-1.4-3.2-3.2-3.2zm-2.8 29h-36v-23h36z"/>
<path d="m73 49.2c1.02 1.29 1.53 2.97 1.53 4.56 0 2.97-1.74 5.65-4.39 7.04v-4.06l-7.46 7.33 7.46 7.14v-4.06c7.66-1.98 12.2-9.61 10-17-0.102-0.297-0.205-0.595-0.307-0.892z"/>
<path d="m24.1 43c-0.817-0.991-1.53-2.97-1.53-4.56 0-2.97 1.74-5.65 4.39-7.04v4.06l7.46-7.33-7.46-7.14v4.06c-7.66 1.98-12.2 9.61-10 17 0.102 0.297 0.205 0.595 0.307 0.892z"/>
</g>`
);
this.addRibbonIcon("replicate", "Replicate", async () => {
await this.plugin.$$replicate(true);
}).addClass("livesync-ribbon-replicate");
this.addCommand({
id: "livesync-replicate",
name: "Replicate now",
callback: async () => {
await this.plugin.$$replicate();
},
});
this.addCommand({
id: "livesync-dump",
name: "Dump information of this doc ",
callback: () => {
const file = this.plugin.$$getActiveFilePath();
if (!file) return;
fireAndForget(() => this.localDatabase.getDBEntry(file, {}, true, false));
},
});
this.addCommand({
id: "livesync-checkdoc-conflicted",
name: "Resolve if conflicted.",
editorCallback: (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
const file = view.file;
if (!file) return;
void this.plugin.$$queueConflictCheckIfOpen(file.path as FilePathWithPrefix);
},
});
this.addCommand({
id: "livesync-toggle",
name: "Toggle LiveSync",
callback: async () => {
if (this.settings.liveSync) {
this.settings.liveSync = false;
this._log("LiveSync Disabled.", LOG_LEVEL_NOTICE);
} else {
this.settings.liveSync = true;
this._log("LiveSync Enabled.", LOG_LEVEL_NOTICE);
}
await this.plugin.realizeSettingSyncMode();
await this.plugin.saveSettings();
},
});
this.addCommand({
id: "livesync-suspendall",
name: "Toggle All Sync.",
callback: async () => {
if (this.plugin.suspended) {
this.plugin.suspended = false;
this._log("Self-hosted LiveSync resumed", LOG_LEVEL_NOTICE);
} else {
this.plugin.suspended = true;
this._log("Self-hosted LiveSync suspended", LOG_LEVEL_NOTICE);
}
await this.plugin.realizeSettingSyncMode();
await this.plugin.saveSettings();
},
});
this.addCommand({
id: "livesync-scan-files",
name: "Scan storage and database again",
callback: async () => {
await this.plugin.$$performFullScan(true)
}
})
this.addCommand({
id: "livesync-runbatch",
name: "Run pended batch processes",
callback: async () => {
await this.core.$everyCommitPendingFileEvent();
},
})
// TODO, Replicator is possibly one of features. It should be moved to features.
this.addCommand({
id: "livesync-abortsync",
name: "Abort synchronization immediately",
callback: () => {
this.plugin.replicator.terminateSync();
},
})
return Promise.resolve(true);
}
$everyOnload(): Promise<boolean> {
this.app.workspace.onLayoutReady(this.plugin.onLiveSyncReady.bind(this.plugin));
// eslint-disable-next-line no-unused-labels
return Promise.resolve(true);
}
async $$showView(viewType: string) {
const leaves = this.app.workspace.getLeavesOfType(viewType);
if (leaves.length == 0) {
await this.app.workspace.getLeaf(true).setViewState({
type: viewType,
active: true,
});
} else {
await leaves[0].setViewState({
type: viewType,
active: true,
})
}
if (leaves.length > 0) {
this.app.workspace.revealLeaf(
leaves[0]
);
}
}
}