mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2025-01-17 18:26:11 +02:00
Fixed:
- Now periodic internal file scanning works well. - The handler of Window-visibility-changed has been fixed. - And minor fixes possibly included. Refactored: - Unused logic has been removed. - Some utility functions have been moved into suitable files. - Function names have been renamed.
This commit is contained in:
parent
6b7956ab67
commit
9efb6ed0c1
@ -1,4 +1,4 @@
|
|||||||
import { App, Modal } from "obsidian";
|
import { App, Modal } from "./deps";
|
||||||
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
|
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
|
||||||
import { diff_result } from "./lib/src/types";
|
import { diff_result } from "./lib/src/types";
|
||||||
import { escapeStringToHTML } from "./lib/src/strbin";
|
import { escapeStringToHTML } from "./lib/src/strbin";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { TFile, Modal, App } from "obsidian";
|
import { TFile, Modal, App } from "./deps";
|
||||||
import { isValidPath, path2id } from "./utils";
|
import { isValidPath, path2id } from "./utils";
|
||||||
import { base64ToArrayBuffer, base64ToString, escapeStringToHTML } from "./lib/src/strbin";
|
import { base64ToArrayBuffer, base64ToString, escapeStringToHTML } from "./lib/src/strbin";
|
||||||
import ObsidianLiveSyncPlugin from "./main";
|
import ObsidianLiveSyncPlugin from "./main";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { App, Modal } from "obsidian";
|
import { App, Modal } from "./deps";
|
||||||
import { LoadedEntry } from "./lib/src/types";
|
import { LoadedEntry } from "./lib/src/types";
|
||||||
import JsonResolvePane from "./JsonResolvePane.svelte";
|
import JsonResolvePane from "./JsonResolvePane.svelte";
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { requestUrl, RequestUrlParam, RequestUrlResponse } from "obsidian";
|
import { requestUrl, RequestUrlParam, RequestUrlResponse } from "./deps";
|
||||||
import { KeyValueDatabase, OpenKeyValueDatabase } from "./KeyValueDB.js";
|
import { KeyValueDatabase, OpenKeyValueDatabase } from "./KeyValueDB.js";
|
||||||
import { LocalPouchDBBase } from "./lib/src/LocalPouchDBBase.js";
|
import { LocalPouchDBBase } from "./lib/src/LocalPouchDBBase.js";
|
||||||
import { Logger } from "./lib/src/logger.js";
|
import { Logger } from "./lib/src/logger.js";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { App, Modal } from "obsidian";
|
import { App, Modal } from "./deps";
|
||||||
import { logMessageStore } from "./lib/src/stores";
|
import { logMessageStore } from "./lib/src/stores";
|
||||||
import { escapeStringToHTML } from "./lib/src/strbin";
|
import { escapeStringToHTML } from "./lib/src/strbin";
|
||||||
import ObsidianLiveSyncPlugin from "./main";
|
import ObsidianLiveSyncPlugin from "./main";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, RequestUrlParam, requestUrl, TextAreaComponent, MarkdownRenderer, stringifyYaml } from "obsidian";
|
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, RequestUrlParam, requestUrl, TextAreaComponent, MarkdownRenderer, stringifyYaml } from "./deps";
|
||||||
import { DEFAULT_SETTINGS, LOG_LEVEL, ObsidianLiveSyncSettings, ConfigPassphraseStore, RemoteDBSettings } from "./lib/src/types";
|
import { DEFAULT_SETTINGS, LOG_LEVEL, ObsidianLiveSyncSettings, ConfigPassphraseStore, RemoteDBSettings } from "./lib/src/types";
|
||||||
import { path2id, id2path } from "./utils";
|
import { path2id, id2path } from "./utils";
|
||||||
import { delay } from "./lib/src/utils";
|
import { delay } from "./lib/src/utils";
|
||||||
@ -28,6 +28,7 @@ const requestToCouchDB = async (baseUri: string, username: string, password: str
|
|||||||
};
|
};
|
||||||
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||||
plugin: ObsidianLiveSyncPlugin;
|
plugin: ObsidianLiveSyncPlugin;
|
||||||
|
selectedScreen = "";
|
||||||
|
|
||||||
constructor(app: App, plugin: ObsidianLiveSyncPlugin) {
|
constructor(app: App, plugin: ObsidianLiveSyncPlugin) {
|
||||||
super(app, plugin);
|
super(app, plugin);
|
||||||
@ -93,6 +94,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
element.addClass("selected");
|
element.addClass("selected");
|
||||||
(element.querySelector("input[type=radio]") as HTMLInputElement).checked = true;
|
(element.querySelector("input[type=radio]") as HTMLInputElement).checked = true;
|
||||||
});
|
});
|
||||||
|
this.selectedScreen = screen;
|
||||||
};
|
};
|
||||||
menuTabs.forEach((element) => {
|
menuTabs.forEach((element) => {
|
||||||
const e = element.querySelector(".sls-setting-tab");
|
const e = element.querySelector(".sls-setting-tab");
|
||||||
@ -1392,29 +1394,51 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
Logger("Select any preset.", LOG_LEVEL.NOTICE);
|
Logger("Select any preset.", LOG_LEVEL.NOTICE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.plugin.settings.batchSave = false;
|
const presetAllDisabled = {
|
||||||
this.plugin.settings.liveSync = false;
|
batchSave: false,
|
||||||
this.plugin.settings.periodicReplication = false;
|
liveSync: false,
|
||||||
this.plugin.settings.syncOnSave = false;
|
periodicReplication: false,
|
||||||
this.plugin.settings.syncOnStart = false;
|
syncOnSave: false,
|
||||||
this.plugin.settings.syncOnFileOpen = false;
|
syncOnStart: false,
|
||||||
this.plugin.settings.syncAfterMerge = false;
|
syncOnFileOpen: false,
|
||||||
|
syncAfterMerge: false,
|
||||||
|
} as Partial<ObsidianLiveSyncSettings>;
|
||||||
|
const presetLiveSync = {
|
||||||
|
...presetAllDisabled,
|
||||||
|
liveSync: true
|
||||||
|
} as Partial<ObsidianLiveSyncSettings>;
|
||||||
|
const presetPeriodic = {
|
||||||
|
...presetAllDisabled,
|
||||||
|
batchSave: true,
|
||||||
|
periodicReplication: true,
|
||||||
|
syncOnSave: false,
|
||||||
|
syncOnStart: true,
|
||||||
|
syncOnFileOpen: true,
|
||||||
|
syncAfterMerge: true,
|
||||||
|
} as Partial<ObsidianLiveSyncSettings>;
|
||||||
|
|
||||||
if (currentPreset == "LIVESYNC") {
|
if (currentPreset == "LIVESYNC") {
|
||||||
this.plugin.settings.liveSync = true;
|
this.plugin.settings = {
|
||||||
|
...this.plugin.settings,
|
||||||
|
...presetLiveSync
|
||||||
|
}
|
||||||
Logger("Synchronization setting configured as LiveSync.", LOG_LEVEL.NOTICE);
|
Logger("Synchronization setting configured as LiveSync.", LOG_LEVEL.NOTICE);
|
||||||
} else if (currentPreset == "PERIODIC") {
|
} else if (currentPreset == "PERIODIC") {
|
||||||
this.plugin.settings.batchSave = true;
|
this.plugin.settings = {
|
||||||
this.plugin.settings.periodicReplication = true;
|
...this.plugin.settings,
|
||||||
this.plugin.settings.syncOnSave = false;
|
...presetPeriodic
|
||||||
this.plugin.settings.syncOnStart = true;
|
}
|
||||||
this.plugin.settings.syncOnFileOpen = true;
|
|
||||||
this.plugin.settings.syncAfterMerge = true;
|
|
||||||
Logger("Synchronization setting configured as Periodic sync with batch database update.", LOG_LEVEL.NOTICE);
|
Logger("Synchronization setting configured as Periodic sync with batch database update.", LOG_LEVEL.NOTICE);
|
||||||
} else {
|
} else {
|
||||||
Logger("All synchronization disabled.", LOG_LEVEL.NOTICE);
|
Logger("All synchronization disabled.", LOG_LEVEL.NOTICE);
|
||||||
|
this.plugin.settings = {
|
||||||
|
...this.plugin.settings,
|
||||||
|
...presetAllDisabled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.plugin.saveSettings();
|
this.plugin.saveSettings();
|
||||||
await this.plugin.realizeSettingSyncMode();
|
await this.plugin.realizeSettingSyncMode();
|
||||||
|
this.display();
|
||||||
if (inWizard) {
|
if (inWizard) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.plugin.app.setting.close()
|
this.plugin.app.setting.close()
|
||||||
@ -1432,8 +1456,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.plugin.app.commands.executeCommandById("obsidian-livesync:livesync-copysetupuri")
|
this.plugin.app.commands.executeCommandById("obsidian-livesync:livesync-copysetupuri")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1769,18 +1791,22 @@ ${stringifyYaml(pluginConfig)}`;
|
|||||||
}
|
}
|
||||||
applyDisplayEnabled();
|
applyDisplayEnabled();
|
||||||
addScreenElement("70", containerCorruptedDataEl);
|
addScreenElement("70", containerCorruptedDataEl);
|
||||||
if (lastVersion != this.plugin.settings.lastReadUpdates) {
|
if (this.selectedScreen == "") {
|
||||||
if (JSON.stringify(this.plugin.settings) != JSON.stringify(DEFAULT_SETTINGS)) {
|
if (lastVersion != this.plugin.settings.lastReadUpdates) {
|
||||||
changeDisplay("100");
|
if (JSON.stringify(this.plugin.settings) != JSON.stringify(DEFAULT_SETTINGS)) {
|
||||||
|
changeDisplay("100");
|
||||||
|
} else {
|
||||||
|
changeDisplay("110")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
changeDisplay("110")
|
if (isAnySyncEnabled()) {
|
||||||
|
changeDisplay("0");
|
||||||
|
} else {
|
||||||
|
changeDisplay("110")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isAnySyncEnabled()) {
|
changeDisplay(this.selectedScreen);
|
||||||
changeDisplay("0");
|
|
||||||
} else {
|
|
||||||
changeDisplay("110")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
172
src/StorageEventManager.ts
Normal file
172
src/StorageEventManager.ts
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import { Plugin_2, TAbstractFile, TFile, TFolder } from "./deps";
|
||||||
|
import { isPlainText, shouldBeIgnored } from "./lib/src/path";
|
||||||
|
import { getGlobalStore } from "./lib/src/store";
|
||||||
|
import { ObsidianLiveSyncSettings } from "./lib/src/types";
|
||||||
|
import { FileEventItem, FileEventType, FileInfo, InternalFileInfo, queueItem } from "./types";
|
||||||
|
import { recentlyTouched } from "./utils";
|
||||||
|
|
||||||
|
|
||||||
|
export abstract class StorageEventManager {
|
||||||
|
abstract fetchEvent(): FileEventItem | false;
|
||||||
|
abstract cancelRelativeEvent(item: FileEventItem): void;
|
||||||
|
abstract getQueueLength(): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type LiveSyncForStorageEventManager = Plugin_2 &
|
||||||
|
{
|
||||||
|
settings: ObsidianLiveSyncSettings
|
||||||
|
} & {
|
||||||
|
isTargetFile: (file: string | TAbstractFile) => boolean,
|
||||||
|
procFileEvent: (applyBatch?: boolean) => Promise<boolean>
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export class StorageEventManagerObsidian extends StorageEventManager {
|
||||||
|
plugin: LiveSyncForStorageEventManager;
|
||||||
|
queuedFilesStore = getGlobalStore("queuedFiles", { queuedItems: [] as queueItem[], fileEventItems: [] as FileEventItem[] });
|
||||||
|
|
||||||
|
watchedFileEventQueue = [] as FileEventItem[];
|
||||||
|
|
||||||
|
constructor(plugin: LiveSyncForStorageEventManager) {
|
||||||
|
super();
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.watchVaultChange = this.watchVaultChange.bind(this);
|
||||||
|
this.watchVaultCreate = this.watchVaultCreate.bind(this);
|
||||||
|
this.watchVaultDelete = this.watchVaultDelete.bind(this);
|
||||||
|
this.watchVaultRename = this.watchVaultRename.bind(this);
|
||||||
|
this.watchVaultRawEvents = this.watchVaultRawEvents.bind(this);
|
||||||
|
plugin.registerEvent(app.vault.on("modify", this.watchVaultChange));
|
||||||
|
plugin.registerEvent(app.vault.on("delete", this.watchVaultDelete));
|
||||||
|
plugin.registerEvent(app.vault.on("rename", this.watchVaultRename));
|
||||||
|
plugin.registerEvent(app.vault.on("create", this.watchVaultCreate));
|
||||||
|
//@ts-ignore : Internal API
|
||||||
|
plugin.registerEvent(app.vault.on("raw", this.watchVaultRawEvents));
|
||||||
|
}
|
||||||
|
|
||||||
|
watchVaultCreate(file: TAbstractFile, ctx?: any) {
|
||||||
|
this.appendWatchEvent([{ type: "CREATE", file }], ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
watchVaultChange(file: TAbstractFile, ctx?: any) {
|
||||||
|
this.appendWatchEvent([{ type: "CHANGED", file }], ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
watchVaultDelete(file: TAbstractFile, ctx?: any) {
|
||||||
|
this.appendWatchEvent([{ type: "DELETE", file }], ctx);
|
||||||
|
}
|
||||||
|
watchVaultRename(file: TAbstractFile, oldFile: string, ctx?: any) {
|
||||||
|
if (file instanceof TFile) {
|
||||||
|
this.appendWatchEvent([
|
||||||
|
{ type: "CREATE", file },
|
||||||
|
{ type: "DELETE", file: { path: oldFile, mtime: file.stat.mtime, ctime: file.stat.ctime, size: file.stat.size, deleted: true } }
|
||||||
|
], ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Watch raw events (Internal API)
|
||||||
|
watchVaultRawEvents(path: string) {
|
||||||
|
if (!this.plugin.settings.syncInternalFiles) return;
|
||||||
|
if (!this.plugin.settings.watchInternalFileChanges) return;
|
||||||
|
if (!path.startsWith(app.vault.configDir)) return;
|
||||||
|
const ignorePatterns = this.plugin.settings.syncInternalFilesIgnorePatterns
|
||||||
|
.replace(/\n| /g, "")
|
||||||
|
.split(",").filter(e => e).map(e => new RegExp(e, "i"));
|
||||||
|
if (ignorePatterns.some(e => path.match(e))) return;
|
||||||
|
this.appendWatchEvent(
|
||||||
|
[{
|
||||||
|
type: "INTERNAL",
|
||||||
|
file: { path, mtime: 0, ctime: 0, size: 0 }
|
||||||
|
}], null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache file and waiting to can be proceed.
|
||||||
|
async appendWatchEvent(params: { type: FileEventType, file: TAbstractFile | InternalFileInfo, oldPath?: string }[], ctx?: any) {
|
||||||
|
let forcePerform = false;
|
||||||
|
for (const param of params) {
|
||||||
|
if (shouldBeIgnored(param.file.path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const atomicKey = [0, 0, 0, 0, 0, 0].map(e => `${Math.floor(Math.random() * 100000)}`).join("-");
|
||||||
|
const type = param.type;
|
||||||
|
const file = param.file;
|
||||||
|
const oldPath = param.oldPath;
|
||||||
|
if (file instanceof TFolder) continue;
|
||||||
|
if (!this.plugin.isTargetFile(file.path)) continue;
|
||||||
|
if (this.plugin.settings.suspendFileWatching) continue;
|
||||||
|
|
||||||
|
let cache: null | string | ArrayBuffer;
|
||||||
|
// new file or something changed, cache the changes.
|
||||||
|
if (file instanceof TFile && (type == "CREATE" || type == "CHANGED")) {
|
||||||
|
if (recentlyTouched(file)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!isPlainText(file.name)) {
|
||||||
|
cache = await app.vault.readBinary(file);
|
||||||
|
} else {
|
||||||
|
// cache = await this.app.vault.read(file);
|
||||||
|
cache = await app.vault.cachedRead(file);
|
||||||
|
if (!cache) cache = await app.vault.read(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type == "DELETE" || type == "RENAME") {
|
||||||
|
forcePerform = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (this.plugin.settings.batchSave) {
|
||||||
|
// if the latest event is the same type, omit that
|
||||||
|
// a.md MODIFY <- this should be cancelled when a.md MODIFIED
|
||||||
|
// b.md MODIFY <- this should be cancelled when b.md MODIFIED
|
||||||
|
// a.md MODIFY
|
||||||
|
// a.md CREATE
|
||||||
|
// :
|
||||||
|
let i = this.watchedFileEventQueue.length;
|
||||||
|
L1:
|
||||||
|
while (i >= 0) {
|
||||||
|
i--;
|
||||||
|
if (i < 0) break L1;
|
||||||
|
if (this.watchedFileEventQueue[i].args.file.path != file.path) {
|
||||||
|
continue L1;
|
||||||
|
}
|
||||||
|
if (this.watchedFileEventQueue[i].type != type) break L1;
|
||||||
|
this.watchedFileEventQueue.remove(this.watchedFileEventQueue[i]);
|
||||||
|
//this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
|
||||||
|
this.queuedFilesStore.apply((value) => ({ ...value, fileEventItems: this.watchedFileEventQueue }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileInfo = file instanceof TFile ? {
|
||||||
|
ctime: file.stat.ctime,
|
||||||
|
mtime: file.stat.mtime,
|
||||||
|
file: file,
|
||||||
|
path: file.path,
|
||||||
|
size: file.stat.size
|
||||||
|
} as FileInfo : file as InternalFileInfo;
|
||||||
|
this.watchedFileEventQueue.push({
|
||||||
|
type,
|
||||||
|
args: {
|
||||||
|
file: fileInfo,
|
||||||
|
oldPath,
|
||||||
|
cache,
|
||||||
|
ctx
|
||||||
|
},
|
||||||
|
key: atomicKey
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
|
||||||
|
this.queuedFilesStore.apply((value) => ({ ...value, fileEventItems: this.watchedFileEventQueue }));
|
||||||
|
this.plugin.procFileEvent(forcePerform);
|
||||||
|
}
|
||||||
|
fetchEvent(): FileEventItem | false {
|
||||||
|
if (this.watchedFileEventQueue.length == 0) return false;
|
||||||
|
const item = this.watchedFileEventQueue.shift();
|
||||||
|
this.queuedFilesStore.apply((value) => ({ ...value, fileEventItems: this.watchedFileEventQueue }));
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
cancelRelativeEvent(item: FileEventItem) {
|
||||||
|
this.watchedFileEventQueue = [...this.watchedFileEventQueue].filter(e => e.key != item.key);
|
||||||
|
this.queuedFilesStore.apply((value) => ({ ...value, fileEventItems: this.watchedFileEventQueue }));
|
||||||
|
}
|
||||||
|
getQueueLength() {
|
||||||
|
return this.watchedFileEventQueue.length;
|
||||||
|
}
|
||||||
|
}
|
4
src/deps.ts
Normal file
4
src/deps.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export {
|
||||||
|
addIcon, App, DataWriteOptions, debounce, Editor, FuzzySuggestModal, MarkdownRenderer, MarkdownView, Modal, normalizePath, Notice, Platform, Plugin, PluginManifest,
|
||||||
|
PluginSettingTab, Plugin_2, requestUrl, RequestUrlParam, RequestUrlResponse, sanitizeHTMLToDom, Setting, stringifyYaml, TAbstractFile, TextAreaComponent, TFile, TFolder
|
||||||
|
} from "obsidian";
|
@ -1,4 +1,4 @@
|
|||||||
import { App, FuzzySuggestModal, Modal, Setting } from "obsidian";
|
import { App, FuzzySuggestModal, Modal, Setting } from "./deps";
|
||||||
import ObsidianLiveSyncPlugin from "./main";
|
import ObsidianLiveSyncPlugin from "./main";
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
|
448
src/main.ts
448
src/main.ts
@ -1,7 +1,8 @@
|
|||||||
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, App } from "obsidian";
|
const isDebug = false;
|
||||||
import { Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
|
import { Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
|
||||||
|
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest } from "./deps";
|
||||||
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, InternalFileEntry, SALT_OF_PASSPHRASE, ConfigPassphraseStore, CouchDBConnection, FLAGMD_REDFLAG2, FLAGMD_REDFLAG3, PREFIXMD_LOGFILE } from "./lib/src/types";
|
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, InternalFileEntry, SALT_OF_PASSPHRASE, ConfigPassphraseStore, CouchDBConnection, FLAGMD_REDFLAG2, FLAGMD_REDFLAG3, PREFIXMD_LOGFILE } from "./lib/src/types";
|
||||||
import { PluginDataEntry, PERIODIC_PLUGIN_SWEEP, PluginList, DevicePluginList, InternalFileInfo, queueItem, FileInfo } from "./types";
|
import { PluginDataEntry, PERIODIC_PLUGIN_SWEEP, PluginList, DevicePluginList, InternalFileInfo, queueItem, CacheData, FileEventItem, configURIBase, FileWatchEventQueueMax, PSCHeader, PSCHeaderEnd, ICHeader, ICHeaderEnd } from "./types";
|
||||||
import { delay, getDocData, isDocContentSame } from "./lib/src/utils";
|
import { delay, getDocData, isDocContentSame } from "./lib/src/utils";
|
||||||
import { Logger } from "./lib/src/logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { LocalPouchDB } from "./LocalPouchDB";
|
import { LocalPouchDB } from "./LocalPouchDB";
|
||||||
@ -9,12 +10,9 @@ import { LogDisplayModal } from "./LogDisplayModal";
|
|||||||
import { ConflictResolveModal } from "./ConflictResolveModal";
|
import { ConflictResolveModal } from "./ConflictResolveModal";
|
||||||
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
||||||
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
||||||
import { applyPatch, clearAllPeriodic, clearAllTriggers, clearTrigger, disposeMemoObject, generatePatchObj, id2path, isObjectMargeApplicable, isSensibleMargeApplicable, memoIfNotExist, memoObject, flattenObject, path2id, retrieveMemoObject, setTrigger, tryParseJSON, createFile, modifyFile, isValidPath } from "./utils";
|
import { applyPatch, cancelAllPeriodicTask, cancelAllTasks, cancelTask, disposeMemoObject, generatePatchObj, id2path, isObjectMargeApplicable, isSensibleMargeApplicable, memoIfNotExist, memoObject, flattenObject, path2id, retrieveMemoObject, scheduleTask, tryParseJSON, createFile, modifyFile, isValidPath, getAbstractFileByPath, trimPrefix, touch, recentlyTouched, isInternalMetadata, isPluginMetadata, filename2idInternalMetadata, id2filenameInternalMetadata, isChunk, askSelectString, askYesNo, askString, PeriodicProcessor, clearTouched } from "./utils";
|
||||||
import { decrypt, encrypt, tryDecrypt } from "./lib/src/e2ee_v2";
|
import { decrypt, encrypt, tryDecrypt } from "./lib/src/e2ee_v2";
|
||||||
|
import { PluginDialogModal } from "./dialogs";
|
||||||
const isDebug = false;
|
|
||||||
|
|
||||||
import { InputStringDialog, PluginDialogModal, PopoverSelectString } from "./dialogs";
|
|
||||||
import { isCloudantURI, isErrorOfMissingDoc } from "./lib/src/utils_couchdb";
|
import { isCloudantURI, isErrorOfMissingDoc } from "./lib/src/utils_couchdb";
|
||||||
import { getGlobalStore, observeStores } from "./lib/src/store";
|
import { getGlobalStore, observeStores } from "./lib/src/store";
|
||||||
import { lockStore, logMessageStore, logStore } from "./lib/src/stores";
|
import { lockStore, logMessageStore, logStore } from "./lib/src/stores";
|
||||||
@ -24,110 +22,10 @@ import { isPlainText, shouldBeIgnored } from "./lib/src/path";
|
|||||||
import { runWithLock } from "./lib/src/lock";
|
import { runWithLock } from "./lib/src/lock";
|
||||||
import { Semaphore } from "./lib/src/semaphore";
|
import { Semaphore } from "./lib/src/semaphore";
|
||||||
import { JsonResolveModal } from "./JsonResolveModal";
|
import { JsonResolveModal } from "./JsonResolveModal";
|
||||||
|
import { StorageEventManager, StorageEventManagerObsidian } from "./StorageEventManager";
|
||||||
|
|
||||||
setNoticeClass(Notice);
|
setNoticeClass(Notice);
|
||||||
|
|
||||||
const ICHeader = "i:";
|
|
||||||
const ICHeaderEnd = "i;";
|
|
||||||
const ICHeaderLength = ICHeader.length;
|
|
||||||
const FileWatchEventQueueMax = 10;
|
|
||||||
|
|
||||||
const configURIBase = "obsidian://setuplivesync?settings=";
|
|
||||||
|
|
||||||
function getAbstractFileByPath(path: string): TAbstractFile | null {
|
|
||||||
// Hidden API but so useful.
|
|
||||||
// @ts-ignore
|
|
||||||
if ("getAbstractFileByPathInsensitive" in app.vault && (app.vault.adapter?.insensitive ?? false)) {
|
|
||||||
// @ts-ignore
|
|
||||||
return app.vault.getAbstractFileByPathInsensitive(path);
|
|
||||||
} else {
|
|
||||||
return app.vault.getAbstractFileByPath(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function trimPrefix(target: string, prefix: string) {
|
|
||||||
return target.startsWith(prefix) ? target.substring(prefix.length) : target;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* returns is internal chunk of file
|
|
||||||
* @param str ID
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function isInternalMetadata(str: string): boolean {
|
|
||||||
return str.startsWith(ICHeader);
|
|
||||||
}
|
|
||||||
function id2filenameInternalMetadata(str: string): string {
|
|
||||||
return str.substring(ICHeaderLength);
|
|
||||||
}
|
|
||||||
function filename2idInternalMetadata(str: string): string {
|
|
||||||
return ICHeader + str;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CHeader = "h:";
|
|
||||||
// const CHeaderLength = CHeader.length;
|
|
||||||
function isChunk(str: string): boolean {
|
|
||||||
return str.startsWith(CHeader);
|
|
||||||
}
|
|
||||||
|
|
||||||
const PSCHeader = "ps:";
|
|
||||||
const PSCHeaderEnd = "ps;";
|
|
||||||
function isPluginMetadata(str: string): boolean {
|
|
||||||
return str.startsWith(PSCHeader);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => {
|
|
||||||
return new Promise((res) => {
|
|
||||||
const popover = new PopoverSelectString(app, message, null, null, (result) => res(result as "yes" | "no"));
|
|
||||||
popover.open();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const askSelectString = (app: App, message: string, items: string[]): Promise<string> => {
|
|
||||||
const getItemsFun = () => items;
|
|
||||||
return new Promise((res) => {
|
|
||||||
const popover = new PopoverSelectString(app, message, "", getItemsFun, (result) => res(result));
|
|
||||||
popover.open();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const askString = (app: App, title: string, key: string, placeholder: string): Promise<string | false> => {
|
|
||||||
return new Promise((res) => {
|
|
||||||
const dialog = new InputStringDialog(app, title, key, placeholder, (result) => res(result));
|
|
||||||
dialog.open();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
let touchedFiles: string[] = [];
|
|
||||||
function touch(file: TFile | string) {
|
|
||||||
const f = file instanceof TFile ? file : getAbstractFileByPath(file) as TFile;
|
|
||||||
const key = `${f.path}-${f.stat.mtime}-${f.stat.size}`;
|
|
||||||
touchedFiles.unshift(key);
|
|
||||||
touchedFiles = touchedFiles.slice(0, 100);
|
|
||||||
}
|
|
||||||
function recentlyTouched(file: TFile) {
|
|
||||||
const key = `${file.path}-${file.stat.mtime}-${file.stat.size}`;
|
|
||||||
if (touchedFiles.indexOf(key) == -1) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
function clearTouched() {
|
|
||||||
touchedFiles = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
type CacheData = string | ArrayBuffer;
|
|
||||||
type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "RENAME" | "INTERNAL";
|
|
||||||
type FileEventArgs = {
|
|
||||||
file: FileInfo | InternalFileInfo;
|
|
||||||
cache?: CacheData;
|
|
||||||
oldPath?: string;
|
|
||||||
ctx?: any;
|
|
||||||
}
|
|
||||||
type FileEventItem = {
|
|
||||||
type: FileEventType,
|
|
||||||
args: FileEventArgs,
|
|
||||||
key: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ObsidianLiveSyncPlugin extends Plugin {
|
export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||||
settings: ObsidianLiveSyncSettings;
|
settings: ObsidianLiveSyncSettings;
|
||||||
localDatabase: LocalPouchDB;
|
localDatabase: LocalPouchDB;
|
||||||
@ -140,7 +38,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
packageVersion = "";
|
packageVersion = "";
|
||||||
manifestVersion = "";
|
manifestVersion = "";
|
||||||
|
|
||||||
watchedFileEventQueue = [] as FileEventItem[];
|
periodicSyncProcessor = new PeriodicProcessor(this, async () => await this.replicate());
|
||||||
|
periodicPluginSweepProcessor = new PeriodicProcessor(this, async () => await this.sweepPlugin(false));
|
||||||
|
periodicInternalFileScanProcessor = new PeriodicProcessor(this, async () => await this.syncInternalFilesAndDatabase("push", false));
|
||||||
|
|
||||||
getVaultName(): string {
|
getVaultName(): string {
|
||||||
return this.app.vault.getName() + (this.settings?.additionalSuffixOfDatabaseName ? ("-" + this.settings.additionalSuffixOfDatabaseName) : "");
|
return this.app.vault.getName() + (this.settings?.additionalSuffixOfDatabaseName ? ("-" + this.settings.additionalSuffixOfDatabaseName) : "");
|
||||||
@ -544,21 +444,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
localStorage.setItem(lsKey, `${VER}`);
|
localStorage.setItem(lsKey, `${VER}`);
|
||||||
await this.openDatabase();
|
await this.openDatabase();
|
||||||
|
|
||||||
this.watchVaultChange = this.watchVaultChange.bind(this);
|
|
||||||
this.watchVaultCreate = this.watchVaultCreate.bind(this);
|
|
||||||
this.watchVaultDelete = this.watchVaultDelete.bind(this);
|
|
||||||
this.watchVaultRename = this.watchVaultRename.bind(this);
|
|
||||||
this.watchVaultRawEvents = this.watchVaultRawEvents.bind(this);
|
|
||||||
this.watchWorkspaceOpen = debounce(this.watchWorkspaceOpen.bind(this), 1000, false);
|
this.watchWorkspaceOpen = debounce(this.watchWorkspaceOpen.bind(this), 1000, false);
|
||||||
this.watchWindowVisibility = debounce(this.watchWindowVisibility.bind(this), 1000, false);
|
this.watchWindowVisibility = debounce(this.watchWindowVisibility.bind(this), 1000, false);
|
||||||
this.watchOnline = debounce(this.watchOnline.bind(this), 500, false);
|
this.watchOnline = debounce(this.watchOnline.bind(this), 500, false);
|
||||||
|
|
||||||
this.parseReplicationResult = this.parseReplicationResult.bind(this);
|
this.parseReplicationResult = this.parseReplicationResult.bind(this);
|
||||||
|
|
||||||
this.setPeriodicSync = this.setPeriodicSync.bind(this);
|
|
||||||
this.clearPeriodicSync = this.clearPeriodicSync.bind(this);
|
|
||||||
this.periodicSync = this.periodicSync.bind(this);
|
|
||||||
this.loadQueuedFiles = this.loadQueuedFiles.bind(this);
|
this.loadQueuedFiles = this.loadQueuedFiles.bind(this);
|
||||||
|
|
||||||
this.getPluginList = this.getPluginList.bind(this);
|
this.getPluginList = this.getPluginList.bind(this);
|
||||||
@ -764,21 +655,16 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
if (this.localDatabase != null) {
|
if (this.localDatabase != null) {
|
||||||
this.localDatabase.onunload();
|
this.localDatabase.onunload();
|
||||||
}
|
}
|
||||||
if (this.gcTimerHandler != null) {
|
|
||||||
clearTimeout(this.gcTimerHandler);
|
this.periodicSyncProcessor?.disable();
|
||||||
this.gcTimerHandler = null;
|
this.periodicPluginSweepProcessor?.disable();
|
||||||
}
|
this.periodicInternalFileScanProcessor?.disable();
|
||||||
this.clearPeriodicSync();
|
|
||||||
this.clearPluginSweep();
|
|
||||||
this.clearInternalFileScan();
|
|
||||||
if (this.localDatabase != null) {
|
if (this.localDatabase != null) {
|
||||||
this.localDatabase.closeReplication();
|
this.localDatabase.closeReplication();
|
||||||
this.localDatabase.close();
|
this.localDatabase.close();
|
||||||
}
|
}
|
||||||
clearAllPeriodic();
|
cancelAllPeriodicTask();
|
||||||
clearAllTriggers();
|
cancelAllTasks();
|
||||||
window.removeEventListener("visibilitychange", this.watchWindowVisibility);
|
|
||||||
window.removeEventListener("online", this.watchOnline);
|
|
||||||
Logger("unloading plugin");
|
Logger("unloading plugin");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -940,21 +826,16 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
this.triggerRealizeSettingSyncMode();
|
this.triggerRealizeSettingSyncMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
gcTimerHandler: any = null;
|
vaultManager: StorageEventManager;
|
||||||
|
|
||||||
registerFileWatchEvents() {
|
registerFileWatchEvents() {
|
||||||
this.registerEvent(this.app.vault.on("modify", this.watchVaultChange));
|
this.vaultManager = new StorageEventManagerObsidian(this)
|
||||||
this.registerEvent(this.app.vault.on("delete", this.watchVaultDelete));
|
|
||||||
this.registerEvent(this.app.vault.on("rename", this.watchVaultRename));
|
|
||||||
this.registerEvent(this.app.vault.on("create", this.watchVaultCreate));
|
|
||||||
//@ts-ignore : Internal API
|
|
||||||
this.registerEvent(this.app.vault.on("raw", this.watchVaultRawEvents));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerWatchEvents() {
|
registerWatchEvents() {
|
||||||
this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
|
this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
|
||||||
window.addEventListener("visibilitychange", this.watchWindowVisibility);
|
this.registerDomEvent(document, "visibilitychange", this.watchWindowVisibility);
|
||||||
window.addEventListener("online", this.watchOnline);
|
this.registerDomEvent(window, "online", this.watchOnline);
|
||||||
|
this.registerDomEvent(window, "offline", this.watchOnline);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -963,6 +844,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
async watchOnlineAsync() {
|
async watchOnlineAsync() {
|
||||||
// If some files were failed to retrieve, scan files again.
|
// 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) {
|
if (navigator.onLine && this.localDatabase.needScanning) {
|
||||||
this.localDatabase.needScanning = false;
|
this.localDatabase.needScanning = false;
|
||||||
await this.syncAllFiles();
|
await this.syncAllFiles();
|
||||||
@ -980,7 +862,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
await this.applyBatchChange();
|
await this.applyBatchChange();
|
||||||
if (isHidden) {
|
if (isHidden) {
|
||||||
this.localDatabase.closeReplication();
|
this.localDatabase.closeReplication();
|
||||||
this.clearPeriodicSync();
|
this.periodicSyncProcessor?.disable();
|
||||||
} else {
|
} else {
|
||||||
// suspend all temporary.
|
// suspend all temporary.
|
||||||
if (this.suspended) return;
|
if (this.suspended) return;
|
||||||
@ -993,175 +875,68 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
if (this.settings.syncOnStart) {
|
if (this.settings.syncOnStart) {
|
||||||
this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult);
|
this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult);
|
||||||
}
|
}
|
||||||
if (this.settings.periodicReplication) {
|
this.periodicSyncProcessor.enable(this.settings.periodicReplication ? this.settings.periodicReplicationInterval * 1000 : 0);
|
||||||
this.setPeriodicSync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache file and waiting to can be proceed.
|
|
||||||
async appendWatchEvent(params: { type: FileEventType, file: TAbstractFile | InternalFileInfo, oldPath?: string }[], ctx?: any) {
|
|
||||||
let forcePerform = false;
|
|
||||||
for (const param of params) {
|
|
||||||
if (shouldBeIgnored(param.file.path)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const atomicKey = [0, 0, 0, 0, 0, 0].map(e => `${Math.floor(Math.random() * 100000)}`).join("-");
|
|
||||||
const type = param.type;
|
|
||||||
const file = param.file;
|
|
||||||
const oldPath = param.oldPath;
|
|
||||||
if (file instanceof TFolder) continue;
|
|
||||||
if (!this.isTargetFile(file.path)) continue;
|
|
||||||
if (this.settings.suspendFileWatching) continue;
|
|
||||||
|
|
||||||
let cache: null | string | ArrayBuffer;
|
|
||||||
// new file or something changed, cache the changes.
|
|
||||||
if (file instanceof TFile && (type == "CREATE" || type == "CHANGED")) {
|
|
||||||
if (recentlyTouched(file)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!isPlainText(file.name)) {
|
|
||||||
cache = await this.app.vault.readBinary(file);
|
|
||||||
} else {
|
|
||||||
// cache = await this.app.vault.read(file);
|
|
||||||
cache = await this.app.vault.cachedRead(file);
|
|
||||||
if (!cache) cache = await this.app.vault.read(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (type == "DELETE" || type == "RENAME") {
|
|
||||||
forcePerform = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (this.settings.batchSave) {
|
|
||||||
// if the latest event is the same type, omit that
|
|
||||||
// a.md MODIFY <- this should be cancelled when a.md MODIFIED
|
|
||||||
// b.md MODIFY <- this should be cancelled when b.md MODIFIED
|
|
||||||
// a.md MODIFY
|
|
||||||
// a.md CREATE
|
|
||||||
// :
|
|
||||||
let i = this.watchedFileEventQueue.length;
|
|
||||||
L1:
|
|
||||||
while (i >= 0) {
|
|
||||||
i--;
|
|
||||||
if (i < 0) break L1;
|
|
||||||
if (this.watchedFileEventQueue[i].args.file.path != file.path) {
|
|
||||||
continue L1;
|
|
||||||
}
|
|
||||||
if (this.watchedFileEventQueue[i].type != type) break L1;
|
|
||||||
this.watchedFileEventQueue.remove(this.watchedFileEventQueue[i]);
|
|
||||||
this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileInfo = file instanceof TFile ? {
|
|
||||||
ctime: file.stat.ctime,
|
|
||||||
mtime: file.stat.mtime,
|
|
||||||
file: file,
|
|
||||||
path: file.path,
|
|
||||||
size: file.stat.size
|
|
||||||
} as FileInfo : file as InternalFileInfo;
|
|
||||||
this.watchedFileEventQueue.push({
|
|
||||||
type,
|
|
||||||
args: {
|
|
||||||
file: fileInfo,
|
|
||||||
oldPath,
|
|
||||||
cache,
|
|
||||||
ctx
|
|
||||||
},
|
|
||||||
key: atomicKey
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
|
|
||||||
if (this.isReady) {
|
|
||||||
await this.procFileEvent(forcePerform);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
async procFileEvent(applyBatch?: boolean) {
|
async procFileEvent(applyBatch?: boolean) {
|
||||||
if (!this.isReady) return;
|
if (!this.isReady) return;
|
||||||
if (this.settings.batchSave) {
|
if (this.settings.batchSave) {
|
||||||
if (!applyBatch && this.watchedFileEventQueue.length < FileWatchEventQueueMax) {
|
if (!applyBatch && this.vaultManager.getQueueLength() < FileWatchEventQueueMax) {
|
||||||
// Defer till applying batch save or queue has been grown enough.
|
// Defer till applying batch save or queue has been grown enough.
|
||||||
// or 120 seconds after.
|
// or 30 seconds after.
|
||||||
setTrigger("applyBatchAuto", 30000, () => {
|
scheduleTask("applyBatchAuto", 30000, () => {
|
||||||
this.procFileEvent(true);
|
this.procFileEvent(true);
|
||||||
})
|
})
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
clearTrigger("applyBatchAuto");
|
cancelTask("applyBatchAuto");
|
||||||
const ret = await runWithLock("procFiles", true, async () => {
|
const ret = await runWithLock("procFiles", true, async () => {
|
||||||
L2:
|
|
||||||
do {
|
do {
|
||||||
const procs = [...this.watchedFileEventQueue];
|
const queue = this.vaultManager.fetchEvent();
|
||||||
this.watchedFileEventQueue = [];
|
if (queue === false) break;
|
||||||
|
if (queue === undefined) break;
|
||||||
|
const file = queue.args.file;
|
||||||
|
const key = `file-last-proc-${queue.type}-${file.path}`;
|
||||||
|
const last = Number(await this.localDatabase.kvDB.get(key) || 0);
|
||||||
|
if (queue.type == "DELETE") {
|
||||||
|
await this.deleteFromDBbyPath(file.path);
|
||||||
|
} else if (queue.type == "INTERNAL") {
|
||||||
|
await this.watchVaultRawEventsAsync(file.path);
|
||||||
|
} else {
|
||||||
|
const targetFile = this.app.vault.getAbstractFileByPath(file.path);
|
||||||
|
if (!(targetFile instanceof TFile)) {
|
||||||
|
Logger(`Target file was not found: ${file.path}`, LOG_LEVEL.INFO);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
//TODO: check from cache time.
|
||||||
|
if (file.mtime == last) {
|
||||||
|
Logger(`File has been already scanned on ${queue.type}, skip: ${file.path}`, LOG_LEVEL.VERBOSE);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
L1:
|
const cache = queue.args.cache;
|
||||||
do {
|
if (queue.type == "CREATE" || queue.type == "CHANGED") {
|
||||||
const queue = procs.shift();
|
if (!await this.updateIntoDB(targetFile, false, cache)) {
|
||||||
if (queue === undefined) break L1;
|
Logger(`DB -> STORAGE: failed, cancel the relative operations: ${targetFile.path}`, LOG_LEVEL.INFO);
|
||||||
|
// cancel running queues and remove one of atomic operation
|
||||||
const file = queue.args.file;
|
this.vaultManager.cancelRelativeEvent(queue);
|
||||||
const key = `file-last-proc-${queue.type}-${file.path}`;
|
continue;
|
||||||
const last = Number(await this.localDatabase.kvDB.get(key) || 0);
|
|
||||||
if (queue.type == "DELETE") {
|
|
||||||
await this.deleteFromDBbyPath(file.path);
|
|
||||||
} else if (queue.type == "INTERNAL") {
|
|
||||||
await this.watchVaultRawEventsAsync(file.path);
|
|
||||||
} else {
|
|
||||||
const targetFile = this.app.vault.getAbstractFileByPath(file.path);
|
|
||||||
if (!(targetFile instanceof TFile)) {
|
|
||||||
Logger(`Target file was not found: ${file.path}`, LOG_LEVEL.INFO);
|
|
||||||
continue L1;
|
|
||||||
}
|
|
||||||
//TODO: check from cache time.
|
|
||||||
if (file.mtime == last) {
|
|
||||||
Logger(`File has been already scanned on ${queue.type}, skip: ${file.path}`, LOG_LEVEL.VERBOSE);
|
|
||||||
continue L1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cache = queue.args.cache;
|
|
||||||
if (queue.type == "CREATE" || queue.type == "CHANGED") {
|
|
||||||
if (!await this.updateIntoDB(targetFile, false, cache)) {
|
|
||||||
Logger(`DB -> STORAGE: failed, cancel the relative operations: ${targetFile.path}`, LOG_LEVEL.INFO);
|
|
||||||
// cancel running queues and remove one of atomic operation
|
|
||||||
this.watchedFileEventQueue = [...procs, ...this.watchedFileEventQueue].filter(e => e.key != queue.key);
|
|
||||||
continue L2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (queue.type == "RENAME") {
|
|
||||||
// Obsolete
|
|
||||||
await this.watchVaultRenameAsync(targetFile, queue.args.oldPath);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.localDatabase.kvDB.set(key, file.mtime);
|
if (queue.type == "RENAME") {
|
||||||
} while (procs.length > 0);
|
// Obsolete
|
||||||
} while (this.watchedFileEventQueue.length != 0);
|
await this.watchVaultRenameAsync(targetFile, queue.args.oldPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.localDatabase.kvDB.set(key, file.mtime);
|
||||||
|
} while (this.vaultManager.getQueueLength() > 0);
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
watchVaultCreate(file: TAbstractFile, ctx?: any) {
|
|
||||||
this.appendWatchEvent([{ type: "CREATE", file }], ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
watchVaultChange(file: TAbstractFile, ctx?: any) {
|
|
||||||
this.appendWatchEvent([{ type: "CHANGED", file }], ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
watchVaultDelete(file: TAbstractFile, ctx?: any) {
|
|
||||||
this.appendWatchEvent([{ type: "DELETE", file }], ctx);
|
|
||||||
}
|
|
||||||
watchVaultRename(file: TAbstractFile, oldFile: string, ctx?: any) {
|
|
||||||
if (file instanceof TFile) {
|
|
||||||
this.appendWatchEvent([
|
|
||||||
{ type: "CREATE", file },
|
|
||||||
{ type: "DELETE", file: { path: oldFile, mtime: file.stat.mtime, ctime: file.stat.ctime, size: file.stat.size, deleted: true } }
|
|
||||||
], ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watchWorkspaceOpen(file: TFile) {
|
watchWorkspaceOpen(file: TFile) {
|
||||||
if (this.settings.suspendFileWatching) return;
|
if (this.settings.suspendFileWatching) return;
|
||||||
@ -1188,21 +963,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch raw events (Internal API)
|
|
||||||
watchVaultRawEvents(path: string) {
|
|
||||||
if (!this.settings.syncInternalFiles) return;
|
|
||||||
if (!this.settings.watchInternalFileChanges) return;
|
|
||||||
if (!path.startsWith(this.app.vault.configDir)) return;
|
|
||||||
const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns
|
|
||||||
.replace(/\n| /g, "")
|
|
||||||
.split(",").filter(e => e).map(e => new RegExp(e, "i"));
|
|
||||||
if (ignorePatterns.some(e => path.match(e))) return;
|
|
||||||
this.appendWatchEvent(
|
|
||||||
[{
|
|
||||||
type: "INTERNAL",
|
|
||||||
file: { path, mtime: 0, ctime: 0, size: 0 }
|
|
||||||
}], null);
|
|
||||||
}
|
|
||||||
recentProcessedInternalFiles = [] as string[];
|
recentProcessedInternalFiles = [] as string[];
|
||||||
async watchVaultRawEventsAsync(path: string) {
|
async watchVaultRawEventsAsync(path: string) {
|
||||||
const stat = await this.app.vault.adapter.stat(path);
|
const stat = await this.app.vault.adapter.stat(path);
|
||||||
@ -1571,7 +1331,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
procInternalFile(filename: string) {
|
procInternalFile(filename: string) {
|
||||||
this.procInternalFiles.push(filename);
|
this.procInternalFiles.push(filename);
|
||||||
setTrigger("procInternal", 500, async () => {
|
scheduleTask("procInternal", 500, async () => {
|
||||||
await this.execInternalFile();
|
await this.execInternalFile();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1600,7 +1360,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.queuedFiles = this.queuedFiles.filter((e) => !e.done);
|
this.queuedFiles = this.queuedFiles.filter((e) => !e.done);
|
||||||
this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
|
this.queuedFilesStore.apply((value) => ({ ...value, queuedItems: this.queuedFiles }));
|
||||||
this.saveQueuedFiles();
|
this.saveQueuedFiles();
|
||||||
}
|
}
|
||||||
parseIncomingChunk(chunk: PouchDB.Core.ExistingDocument<EntryDoc>) {
|
parseIncomingChunk(chunk: PouchDB.Core.ExistingDocument<EntryDoc>) {
|
||||||
@ -1673,7 +1433,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
this.saveQueuedFiles();
|
this.saveQueuedFiles();
|
||||||
this.procQueuedFiles();
|
this.procQueuedFiles();
|
||||||
}
|
}
|
||||||
periodicSyncHandler: number = null;
|
|
||||||
|
|
||||||
//---> Sync
|
//---> Sync
|
||||||
async parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): Promise<void> {
|
async parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): Promise<void> {
|
||||||
@ -1739,50 +1498,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
Logger("Everything is up to date.", LOG_LEVEL.NOTICE);
|
Logger("Everything is up to date.", LOG_LEVEL.NOTICE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearPeriodicSync() {
|
|
||||||
if (this.periodicSyncHandler != null) {
|
|
||||||
clearInterval(this.periodicSyncHandler);
|
|
||||||
this.periodicSyncHandler = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setPeriodicSync() {
|
|
||||||
this.clearPeriodicSync();
|
|
||||||
if (this.settings.periodicReplication && this.settings.periodicReplicationInterval > 0) {
|
|
||||||
this.periodicSyncHandler = this.setInterval(async () => await this.periodicSync(), Math.max(this.settings.periodicReplicationInterval, 30) * 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async periodicSync() {
|
|
||||||
await this.replicate();
|
|
||||||
}
|
|
||||||
|
|
||||||
periodicPluginSweepHandler: number = null;
|
|
||||||
|
|
||||||
clearPluginSweep() {
|
|
||||||
if (this.periodicPluginSweepHandler != null) {
|
|
||||||
clearInterval(this.periodicPluginSweepHandler);
|
|
||||||
this.periodicPluginSweepHandler = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setPluginSweep() {
|
|
||||||
if (this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges) {
|
|
||||||
this.clearPluginSweep();
|
|
||||||
this.periodicPluginSweepHandler = this.setInterval(async () => await this.periodicPluginSweep(), PERIODIC_PLUGIN_SWEEP * 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async periodicPluginSweep() {
|
async periodicPluginSweep() {
|
||||||
await this.sweepPlugin(false);
|
await this.sweepPlugin(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async realizeSettingSyncMode() {
|
async realizeSettingSyncMode() {
|
||||||
this.localDatabase.closeReplication();
|
this.localDatabase.closeReplication();
|
||||||
this.clearPeriodicSync();
|
this.periodicSyncProcessor?.disable();
|
||||||
this.clearPluginSweep();
|
this.periodicPluginSweepProcessor?.disable();
|
||||||
this.clearInternalFileScan();
|
this.periodicInternalFileScanProcessor?.disable();
|
||||||
await this.applyBatchChange();
|
await this.applyBatchChange();
|
||||||
// disable all sync temporary.
|
// disable all sync temporary.
|
||||||
if (this.suspended) return;
|
if (this.suspended) return;
|
||||||
@ -1795,9 +1519,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
if (this.settings.syncInternalFiles) {
|
if (this.settings.syncInternalFiles) {
|
||||||
await this.syncInternalFilesAndDatabase("safe", false);
|
await this.syncInternalFilesAndDatabase("safe", false);
|
||||||
}
|
}
|
||||||
this.setPeriodicSync();
|
this.periodicSyncProcessor.enable(this.settings.periodicReplication ? this.settings.periodicReplicationInterval * 1000 : 0);
|
||||||
this.setPluginSweep();
|
this.periodicPluginSweepProcessor.enable(this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges ? (PERIODIC_PLUGIN_SWEEP * 1000) : 0)
|
||||||
this.setPeriodicInternalFileScan();
|
this.periodicInternalFileScanProcessor.enable(this.settings.syncInternalFiles && this.settings.syncInternalFilesInterval ? (this.settings.syncInternalFilesInterval * 1000) : 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
lastMessage = "";
|
lastMessage = "";
|
||||||
@ -1842,7 +1566,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
this.statusBar.title = e.syncStatus;
|
this.statusBar.title = e.syncStatus;
|
||||||
let waiting = "";
|
let waiting = "";
|
||||||
if (this.settings.batchSave) {
|
if (this.settings.batchSave) {
|
||||||
waiting = " " + this.watchedFileEventQueue.map((e) => "🛫").join("");
|
waiting = " " + "🛫".repeat(this.vaultManager.getQueueLength());
|
||||||
waiting = waiting.replace(/(🛫){10}/g, "🚀");
|
waiting = waiting.replace(/(🛫){10}/g, "🚀");
|
||||||
}
|
}
|
||||||
let queued = "";
|
let queued = "";
|
||||||
@ -3011,27 +2735,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
periodicInternalFileScanHandler: number = null;
|
|
||||||
|
|
||||||
clearInternalFileScan() {
|
|
||||||
if (this.periodicInternalFileScanHandler != null) {
|
|
||||||
clearInterval(this.periodicInternalFileScanHandler);
|
|
||||||
this.periodicInternalFileScanHandler = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setPeriodicInternalFileScan() {
|
|
||||||
if (this.periodicInternalFileScanHandler != null) {
|
|
||||||
this.clearInternalFileScan();
|
|
||||||
}
|
|
||||||
if (this.settings.syncInternalFiles && this.settings.syncInternalFilesInterval > 0 && !this.settings.watchInternalFileChanges) {
|
|
||||||
this.periodicPluginSweepHandler = this.setInterval(async () => await this.periodicInternalFileScan(), this.settings.syncInternalFilesInterval * 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async periodicInternalFileScan() {
|
|
||||||
await this.syncInternalFilesAndDatabase("push", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getFiles(
|
async getFiles(
|
||||||
path: string,
|
path: string,
|
||||||
@ -3539,14 +3242,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const updatedPluginKey = "popupUpdated-" + updatePluginId;
|
const updatedPluginKey = "popupUpdated-" + updatePluginId;
|
||||||
setTrigger(updatedPluginKey, 1000, async () => {
|
scheduleTask(updatedPluginKey, 1000, async () => {
|
||||||
const popup = await memoIfNotExist(updatedPluginKey, () => new Notice(fragment, 0));
|
const popup = await memoIfNotExist(updatedPluginKey, () => new Notice(fragment, 0));
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const isShown = popup?.noticeEl?.isShown();
|
const isShown = popup?.noticeEl?.isShown();
|
||||||
if (!isShown) {
|
if (!isShown) {
|
||||||
memoObject(updatedPluginKey, new Notice(fragment, 0))
|
memoObject(updatedPluginKey, new Notice(fragment, 0))
|
||||||
}
|
}
|
||||||
setTrigger(updatedPluginKey + "-close", 20000, () => {
|
scheduleTask(updatedPluginKey + "-close", 20000, () => {
|
||||||
const popup = retrieveMemoObject<Notice>(updatedPluginKey)
|
const popup = retrieveMemoObject<Notice>(updatedPluginKey)
|
||||||
if (!popup) return;
|
if (!popup) return;
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
@ -3581,13 +3284,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
setTrigger("popupUpdated-" + configDir, 1000, () => {
|
scheduleTask("popupUpdated-" + configDir, 1000, () => {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const isShown = this.confirmPopup?.noticeEl?.isShown();
|
const isShown = this.confirmPopup?.noticeEl?.isShown();
|
||||||
if (!isShown) {
|
if (!isShown) {
|
||||||
this.confirmPopup = new Notice(fragment, 0);
|
this.confirmPopup = new Notice(fragment, 0);
|
||||||
}
|
}
|
||||||
setTrigger("popupClose" + configDir, 20000, () => {
|
scheduleTask("popupClose" + configDir, 20000, () => {
|
||||||
this.confirmPopup?.hide();
|
this.confirmPopup?.hide();
|
||||||
this.confirmPopup = null;
|
this.confirmPopup = null;
|
||||||
})
|
})
|
||||||
@ -3608,3 +3311,4 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
28
src/types.ts
28
src/types.ts
@ -1,4 +1,4 @@
|
|||||||
import { PluginManifest, TFile } from "obsidian";
|
import { PluginManifest, TFile } from "./deps";
|
||||||
import { DatabaseEntry, EntryBody } from "./lib/src/types";
|
import { DatabaseEntry, EntryBody } from "./lib/src/types";
|
||||||
|
|
||||||
export interface PluginDataEntry extends DatabaseEntry {
|
export interface PluginDataEntry extends DatabaseEntry {
|
||||||
@ -46,4 +46,28 @@ export type queueItem = {
|
|||||||
timeout?: number;
|
timeout?: number;
|
||||||
done?: boolean;
|
done?: boolean;
|
||||||
warned?: boolean;
|
warned?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CacheData = string | ArrayBuffer;
|
||||||
|
export type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "RENAME" | "INTERNAL";
|
||||||
|
export type FileEventArgs = {
|
||||||
|
file: FileInfo | InternalFileInfo;
|
||||||
|
cache?: CacheData;
|
||||||
|
oldPath?: string;
|
||||||
|
ctx?: any;
|
||||||
|
}
|
||||||
|
export type FileEventItem = {
|
||||||
|
type: FileEventType,
|
||||||
|
args: FileEventArgs,
|
||||||
|
key: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CHeader = "h:";
|
||||||
|
export const PSCHeader = "ps:";
|
||||||
|
export const PSCHeaderEnd = "ps;";
|
||||||
|
export const ICHeader = "i:";
|
||||||
|
export const ICHeaderEnd = "i;";
|
||||||
|
export const ICHeaderLength = ICHeader.length;
|
||||||
|
|
||||||
|
export const FileWatchEventQueueMax = 10;
|
||||||
|
export const configURIBase = "obsidian://setuplivesync?settings=";
|
145
src/utils.ts
145
src/utils.ts
@ -1,8 +1,10 @@
|
|||||||
import { DataWriteOptions, normalizePath, TFile, Platform } from "obsidian";
|
import { DataWriteOptions, normalizePath, TFile, Platform, TAbstractFile, App, Plugin_2 } from "./deps";
|
||||||
import { path2id_base, id2path_base, isValidFilenameInLinux, isValidFilenameInDarwin, isValidFilenameInWidows, isValidFilenameInAndroid } from "./lib/src/path";
|
import { path2id_base, id2path_base, isValidFilenameInLinux, isValidFilenameInDarwin, isValidFilenameInWidows, isValidFilenameInAndroid } from "./lib/src/path";
|
||||||
|
|
||||||
import { Logger } from "./lib/src/logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { LOG_LEVEL } from "./lib/src/types";
|
import { LOG_LEVEL } from "./lib/src/types";
|
||||||
|
import { CHeader, ICHeader, ICHeaderLength, PSCHeader } from "./types";
|
||||||
|
import { InputStringDialog, PopoverSelectString } from "./dialogs";
|
||||||
|
|
||||||
// For backward compatibility, using the path for determining id.
|
// For backward compatibility, using the path for determining id.
|
||||||
// Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/".
|
// Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/".
|
||||||
@ -15,40 +17,44 @@ export function id2path(filename: string): string {
|
|||||||
return id2path_base(normalizePath(filename));
|
return id2path_base(normalizePath(filename));
|
||||||
}
|
}
|
||||||
|
|
||||||
const triggers: { [key: string]: ReturnType<typeof setTimeout> } = {};
|
const tasks: { [key: string]: ReturnType<typeof setTimeout> } = {};
|
||||||
export function setTrigger(key: string, timeout: number, proc: (() => Promise<any> | void)) {
|
export function scheduleTask(key: string, timeout: number, proc: (() => Promise<any> | void)) {
|
||||||
clearTrigger(key);
|
cancelTask(key);
|
||||||
triggers[key] = setTimeout(async () => {
|
tasks[key] = setTimeout(async () => {
|
||||||
delete triggers[key];
|
delete tasks[key];
|
||||||
await proc();
|
await proc();
|
||||||
}, timeout);
|
}, timeout);
|
||||||
}
|
}
|
||||||
export function clearTrigger(key: string) {
|
export function cancelTask(key: string) {
|
||||||
if (key in triggers) {
|
if (key in tasks) {
|
||||||
clearTimeout(triggers[key]);
|
clearTimeout(tasks[key]);
|
||||||
|
delete tasks[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function clearAllTriggers() {
|
export function cancelAllTasks() {
|
||||||
for (const v in triggers) {
|
for (const v in tasks) {
|
||||||
clearTimeout(triggers[v]);
|
clearTimeout(tasks[v]);
|
||||||
|
delete tasks[v];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const intervals: { [key: string]: ReturnType<typeof setInterval> } = {};
|
const intervals: { [key: string]: ReturnType<typeof setInterval> } = {};
|
||||||
export function setPeriodic(key: string, timeout: number, proc: (() => Promise<any> | void)) {
|
export function setPeriodicTask(key: string, timeout: number, proc: (() => Promise<any> | void)) {
|
||||||
clearPeriodic(key);
|
cancelPeriodicTask(key);
|
||||||
intervals[key] = setInterval(async () => {
|
intervals[key] = setInterval(async () => {
|
||||||
delete intervals[key];
|
delete intervals[key];
|
||||||
await proc();
|
await proc();
|
||||||
}, timeout);
|
}, timeout);
|
||||||
}
|
}
|
||||||
export function clearPeriodic(key: string) {
|
export function cancelPeriodicTask(key: string) {
|
||||||
if (key in intervals) {
|
if (key in intervals) {
|
||||||
clearInterval(intervals[key]);
|
clearInterval(intervals[key]);
|
||||||
|
delete intervals[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function clearAllPeriodic() {
|
export function cancelAllPeriodicTask() {
|
||||||
for (const v in intervals) {
|
for (const v in intervals) {
|
||||||
clearInterval(intervals[v]);
|
clearInterval(intervals[v]);
|
||||||
|
delete intervals[v];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,4 +296,109 @@ export function isValidPath(filename: string) {
|
|||||||
//Fallback
|
//Fallback
|
||||||
Logger("Could not determine platform for checking filename", LOG_LEVEL.VERBOSE);
|
Logger("Could not determine platform for checking filename", LOG_LEVEL.VERBOSE);
|
||||||
return isValidFilenameInWidows(filename);
|
return isValidFilenameInWidows(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let touchedFiles: string[] = [];
|
||||||
|
|
||||||
|
export function getAbstractFileByPath(path: string): TAbstractFile | null {
|
||||||
|
// Hidden API but so useful.
|
||||||
|
// @ts-ignore
|
||||||
|
if ("getAbstractFileByPathInsensitive" in app.vault && (app.vault.adapter?.insensitive ?? false)) {
|
||||||
|
// @ts-ignore
|
||||||
|
return app.vault.getAbstractFileByPathInsensitive(path);
|
||||||
|
} else {
|
||||||
|
return app.vault.getAbstractFileByPath(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function trimPrefix(target: string, prefix: string) {
|
||||||
|
return target.startsWith(prefix) ? target.substring(prefix.length) : target;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function touch(file: TFile | string) {
|
||||||
|
const f = file instanceof TFile ? file : getAbstractFileByPath(file) as TFile;
|
||||||
|
const key = `${f.path}-${f.stat.mtime}-${f.stat.size}`;
|
||||||
|
touchedFiles.unshift(key);
|
||||||
|
touchedFiles = touchedFiles.slice(0, 100);
|
||||||
|
}
|
||||||
|
export function recentlyTouched(file: TFile) {
|
||||||
|
const key = `${file.path}-${file.stat.mtime}-${file.stat.size}`;
|
||||||
|
if (touchedFiles.indexOf(key) == -1) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
export function clearTouched() {
|
||||||
|
touchedFiles = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns is internal chunk of file
|
||||||
|
* @param str ID
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function isInternalMetadata(str: string): boolean {
|
||||||
|
return str.startsWith(ICHeader);
|
||||||
|
}
|
||||||
|
export function id2filenameInternalMetadata(str: string): string {
|
||||||
|
return str.substring(ICHeaderLength);
|
||||||
|
}
|
||||||
|
export function filename2idInternalMetadata(str: string): string {
|
||||||
|
return ICHeader + str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// const CHeaderLength = CHeader.length;
|
||||||
|
export function isChunk(str: string): boolean {
|
||||||
|
return str.startsWith(CHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPluginMetadata(str: string): boolean {
|
||||||
|
return str.startsWith(PSCHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => {
|
||||||
|
return new Promise((res) => {
|
||||||
|
const popover = new PopoverSelectString(app, message, null, null, (result) => res(result as "yes" | "no"));
|
||||||
|
popover.open();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const askSelectString = (app: App, message: string, items: string[]): Promise<string> => {
|
||||||
|
const getItemsFun = () => items;
|
||||||
|
return new Promise((res) => {
|
||||||
|
const popover = new PopoverSelectString(app, message, "", getItemsFun, (result) => res(result));
|
||||||
|
popover.open();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const askString = (app: App, title: string, key: string, placeholder: string): Promise<string | false> => {
|
||||||
|
return new Promise((res) => {
|
||||||
|
const dialog = new InputStringDialog(app, title, key, placeholder, (result) => res(result));
|
||||||
|
dialog.open();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export class PeriodicProcessor {
|
||||||
|
_process: () => Promise<any>;
|
||||||
|
_timer?: number;
|
||||||
|
_plugin: Plugin_2;
|
||||||
|
constructor(plugin: Plugin_2, process: () => Promise<any>) {
|
||||||
|
this._plugin = plugin;
|
||||||
|
this._process = process;
|
||||||
|
}
|
||||||
|
async process() {
|
||||||
|
try {
|
||||||
|
await this._process();
|
||||||
|
} catch (ex) {
|
||||||
|
Logger(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enable(interval: number) {
|
||||||
|
this.disable();
|
||||||
|
if (interval == 0) return;
|
||||||
|
this._timer = window.setInterval(() => this._process().then(() => { }), interval);
|
||||||
|
this._plugin.registerInterval(this._timer);
|
||||||
|
}
|
||||||
|
disable() {
|
||||||
|
if (this._timer) clearInterval(this._timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user