You've already forked obsidian-livesync
mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2025-07-16 00:14:19 +02:00
Implemented:
- New feature `Customization sync` has replaced `Plugin and their settings`
This commit is contained in:
651
src/CmdConfigSync.ts
Normal file
651
src/CmdConfigSync.ts
Normal file
@ -0,0 +1,651 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import { Notice, PluginManifest, stringifyYaml, parseYaml } from "./deps";
|
||||||
|
|
||||||
|
import { EntryDoc, LoadedEntry, LOG_LEVEL, InternalFileEntry, FilePathWithPrefix, FilePath, DocumentID } from "./lib/src/types";
|
||||||
|
import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "./types";
|
||||||
|
import { delay, getDocData } from "./lib/src/utils";
|
||||||
|
import { Logger } from "./lib/src/logger";
|
||||||
|
import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
||||||
|
import { WrappedNotice } from "./lib/src/wrapper";
|
||||||
|
import { base64ToArrayBuffer, arrayBufferToBase64, readString, writeString, uint8ArrayToHexString } from "./lib/src/strbin";
|
||||||
|
import { runWithLock } from "./lib/src/lock";
|
||||||
|
import { LiveSyncCommands } from "./LiveSyncCommands";
|
||||||
|
import { addPrefix, stripAllPrefixes } from "./lib/src/path";
|
||||||
|
import { PeriodicProcessor, askYesNo, disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "./utils";
|
||||||
|
import { Semaphore } from "./lib/src/semaphore";
|
||||||
|
import { PluginDialogModal } from "./dialogs";
|
||||||
|
import { JsonResolveModal } from "./JsonResolveModal";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const pluginList = writable([] as PluginDataExDisplay[]);
|
||||||
|
export const pluginIsEnumerating = writable(false);
|
||||||
|
|
||||||
|
const hashString = (async (key: string) => {
|
||||||
|
const buff = writeString(key);
|
||||||
|
const digest = await crypto.subtle.digest('SHA-256', buff);
|
||||||
|
return uint8ArrayToHexString(new Uint8Array(digest));
|
||||||
|
})
|
||||||
|
|
||||||
|
export type PluginDataExFile = {
|
||||||
|
filename: string,
|
||||||
|
data?: string[],
|
||||||
|
mtime: number,
|
||||||
|
size: number,
|
||||||
|
version?: string,
|
||||||
|
displayName?: string,
|
||||||
|
}
|
||||||
|
export type PluginDataExDisplay = {
|
||||||
|
documentPath: FilePathWithPrefix,
|
||||||
|
category: string,
|
||||||
|
name: string,
|
||||||
|
term: string,
|
||||||
|
displayName?: string,
|
||||||
|
files: PluginDataExFile[],
|
||||||
|
version?: string,
|
||||||
|
mtime: number,
|
||||||
|
}
|
||||||
|
export type PluginDataEx = {
|
||||||
|
documentPath?: FilePathWithPrefix,
|
||||||
|
category: string,
|
||||||
|
name: string,
|
||||||
|
displayName?: string,
|
||||||
|
term: string,
|
||||||
|
files: PluginDataExFile[],
|
||||||
|
version?: string,
|
||||||
|
mtime: number,
|
||||||
|
};
|
||||||
|
export class ConfigSync extends LiveSyncCommands {
|
||||||
|
confirmPopup: WrappedNotice = null;
|
||||||
|
get kvDB() {
|
||||||
|
return this.plugin.kvDB;
|
||||||
|
}
|
||||||
|
ensureDirectoryEx(fullPath: string) {
|
||||||
|
return this.plugin.ensureDirectoryEx(fullPath);
|
||||||
|
}
|
||||||
|
pluginDialog: PluginDialogModal = null;
|
||||||
|
periodicPluginSweepProcessor = new PeriodicProcessor(this.plugin, async () => await this.scanAllConfigFiles(false));
|
||||||
|
|
||||||
|
showPluginSyncModal() {
|
||||||
|
if (!this.settings.usePluginSync) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.pluginDialog != null) {
|
||||||
|
this.pluginDialog.open();
|
||||||
|
} else {
|
||||||
|
this.pluginDialog = new PluginDialogModal(this.app, this.plugin);
|
||||||
|
this.pluginDialog.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hidePluginSyncModal() {
|
||||||
|
if (this.pluginDialog != null) {
|
||||||
|
this.pluginDialog.close();
|
||||||
|
this.pluginDialog = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onunload() {
|
||||||
|
this.hidePluginSyncModal();
|
||||||
|
this.periodicPluginSweepProcessor?.disable();
|
||||||
|
}
|
||||||
|
onload() {
|
||||||
|
this.plugin.addCommand({
|
||||||
|
id: "livesync-plugin-dialog-ex",
|
||||||
|
name: "Show customization sync dialog",
|
||||||
|
callback: () => {
|
||||||
|
this.showPluginSyncModal();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getFileCategory(filePath: string): "CONFIG" | "THEME" | "SNIPPET" | "PLUGIN_MAIN" | "PLUGIN_ETC" | "PLUGIN_DATA" | "" {
|
||||||
|
if (filePath.split("/").length == 2 && filePath.endsWith(".json")) return "CONFIG";
|
||||||
|
if (filePath.split("/").length == 4 && filePath.startsWith(`${this.app.vault.configDir}/themes/`)) return "THEME";
|
||||||
|
if (filePath.startsWith(`${this.app.vault.configDir}/snippets/`) && filePath.endsWith(".css")) return "SNIPPET";
|
||||||
|
if (filePath.startsWith(`${this.app.vault.configDir}/plugins/`)) {
|
||||||
|
if (filePath.endsWith("/styles.css") || filePath.endsWith("/manifest.json") || filePath.endsWith("/main.js")) {
|
||||||
|
return "PLUGIN_MAIN";
|
||||||
|
} else if (filePath.endsWith("/data.json")) {
|
||||||
|
return "PLUGIN_DATA";
|
||||||
|
} else {
|
||||||
|
//TODO: to be configurable.
|
||||||
|
// With algorithm which implemented at v0.19.0, is too heavy.
|
||||||
|
return "";
|
||||||
|
// return "PLUGIN_ETC";
|
||||||
|
}
|
||||||
|
// return "PLUGIN";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
isTargetPath(filePath: string): boolean {
|
||||||
|
if (!filePath.startsWith(this.app.vault.configDir)) return false;
|
||||||
|
// Idea non-filter option?
|
||||||
|
return this.getFileCategory(filePath) != "";
|
||||||
|
}
|
||||||
|
async onInitializeDatabase(showNotice: boolean) {
|
||||||
|
if (this.settings.usePluginSync) {
|
||||||
|
try {
|
||||||
|
Logger("Scanning customizations...");
|
||||||
|
await this.scanAllConfigFiles(showNotice);
|
||||||
|
Logger("Scanning customizations : done");
|
||||||
|
} catch (ex) {
|
||||||
|
Logger("Scanning customizations : failed");
|
||||||
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async beforeReplicate(showNotice: boolean) {
|
||||||
|
if (this.settings.autoSweepPlugins && this.settings.usePluginSync) {
|
||||||
|
await this.scanAllConfigFiles(showNotice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async onResume() {
|
||||||
|
if (this.plugin.suspended)
|
||||||
|
return;
|
||||||
|
if (this.settings.autoSweepPlugins) {
|
||||||
|
await this.scanAllConfigFiles(true);
|
||||||
|
}
|
||||||
|
this.periodicPluginSweepProcessor.enable(this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges ? (PERIODIC_PLUGIN_SWEEP * 1000) : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async reloadPluginList() {
|
||||||
|
pluginList.set([])
|
||||||
|
await this.updatePluginList();
|
||||||
|
}
|
||||||
|
async updatePluginList(updatedDocumentPath?: FilePathWithPrefix): Promise<void> {
|
||||||
|
// pluginList.set([]);
|
||||||
|
if (!this.settings.usePluginSync) {
|
||||||
|
pluginList.set([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await runWithLock("update-plugin-list", false, async () => {
|
||||||
|
if (updatedDocumentPath != "") pluginList.update(e => e.filter(ee => ee.documentPath != updatedDocumentPath));
|
||||||
|
// const work: Record<string, Record<string, Record<string, Record<string, PluginDataEntryEx>>>> = {};
|
||||||
|
const entries = [] as PluginDataExDisplay[]
|
||||||
|
const plugins = this.localDatabase.findEntries(ICXHeader + "", `${ICXHeader}\u{10ffff}`, { include_docs: true });
|
||||||
|
const semaphore = Semaphore(4);
|
||||||
|
const processes = [] as Promise<void>[];
|
||||||
|
let count = 0;
|
||||||
|
pluginIsEnumerating.set(true);
|
||||||
|
try {
|
||||||
|
for await (const plugin of plugins) {
|
||||||
|
const path = plugin.path || this.getPath(plugin);
|
||||||
|
if (updatedDocumentPath && updatedDocumentPath != path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
processes.push((async (v) => {
|
||||||
|
const release = await semaphore.acquire(1);
|
||||||
|
try {
|
||||||
|
Logger(`Enumerating files... ${count++}`, LOG_LEVEL.NOTICE, "get-plugins");
|
||||||
|
|
||||||
|
Logger(`plugin-${path}`, LOG_LEVEL.VERBOSE);
|
||||||
|
const wx = await this.localDatabase.getDBEntry(path, null, false, false);
|
||||||
|
if (wx) {
|
||||||
|
const data = parseYaml(getDocData(wx.data)) as PluginDataEx;
|
||||||
|
const xFiles = [] as PluginDataExFile[];
|
||||||
|
for (const file of data.files) {
|
||||||
|
const work = { ...file };
|
||||||
|
const tempStr = getDocData(work.data);
|
||||||
|
work.data = [await hashString(tempStr)];
|
||||||
|
xFiles.push(work);
|
||||||
|
}
|
||||||
|
entries.push({
|
||||||
|
...data,
|
||||||
|
documentPath: this.getPath(wx),
|
||||||
|
files: xFiles
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
//TODO
|
||||||
|
Logger(`Something happened at enumerating customization :${v.path}`);
|
||||||
|
console.warn(ex);
|
||||||
|
} finally {
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)(plugin));
|
||||||
|
}
|
||||||
|
await Promise.all(processes);
|
||||||
|
pluginList.update(e => {
|
||||||
|
let newList = [...e];
|
||||||
|
for (const item of entries) {
|
||||||
|
console.log(item.documentPath);
|
||||||
|
newList = newList.filter(x => x.documentPath != item.documentPath);
|
||||||
|
newList.push(item)
|
||||||
|
}
|
||||||
|
return newList;
|
||||||
|
})
|
||||||
|
Logger(`All files enumerated`, LOG_LEVEL.NOTICE, "get-plugins");
|
||||||
|
} finally {
|
||||||
|
pluginIsEnumerating.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// return entries;
|
||||||
|
}
|
||||||
|
async compareUsingDisplayData(dataA: PluginDataExDisplay, dataB: PluginDataExDisplay) {
|
||||||
|
const docA = await this.localDatabase.getDBEntry(dataA.documentPath);
|
||||||
|
const docB = await this.localDatabase.getDBEntry(dataB.documentPath);
|
||||||
|
|
||||||
|
if (docA && docB) {
|
||||||
|
const pluginDataA = parseYaml(getDocData(docA.data)) as PluginDataEx;
|
||||||
|
pluginDataA.documentPath = dataA.documentPath;
|
||||||
|
const pluginDataB = parseYaml(getDocData(docB.data)) as PluginDataEx;
|
||||||
|
pluginDataB.documentPath = dataB.documentPath;
|
||||||
|
|
||||||
|
// Use outer structure to wrap each data.
|
||||||
|
return await this.showJSONMergeDialogAndMerge(docA, docB, pluginDataA, pluginDataB);
|
||||||
|
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
showJSONMergeDialogAndMerge(docA: LoadedEntry, docB: LoadedEntry, pluginDataA: PluginDataEx, pluginDataB: PluginDataEx): Promise<boolean> {
|
||||||
|
const fileA = { ...pluginDataA.files[0], ctime: pluginDataA.files[0].mtime, _id: `${pluginDataA.documentPath}` as DocumentID };
|
||||||
|
const fileB = pluginDataB.files[0];
|
||||||
|
const docAx = { ...docA, ...fileA } as LoadedEntry, docBx = { ...docB, ...fileB } as LoadedEntry
|
||||||
|
return runWithLock("config:merge-data", false, () => new Promise((res) => {
|
||||||
|
Logger("Opening data-merging dialog", LOG_LEVEL.VERBOSE);
|
||||||
|
// const docs = [docA, docB];
|
||||||
|
const path = stripAllPrefixes(docAx.path.split("/").slice(-1).join("/") as FilePath);
|
||||||
|
const modal = new JsonResolveModal(this.app, path, [docAx, docBx], async (keep, result) => {
|
||||||
|
if (result == null) return res(false);
|
||||||
|
try {
|
||||||
|
res(await this.applyData(pluginDataA, result));
|
||||||
|
} catch (ex) {
|
||||||
|
Logger("Could not apply merged file");
|
||||||
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||||
|
res(false);
|
||||||
|
}
|
||||||
|
}, "📡", "🛰️", "B");
|
||||||
|
modal.open();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
async applyData(data: PluginDataEx, content?: string): Promise<boolean> {
|
||||||
|
Logger(`Applying ${data.displayName || data.name}..`);
|
||||||
|
const baseDir = this.app.vault.configDir;
|
||||||
|
try {
|
||||||
|
if (!data.documentPath) throw "InternalError: Document path not exist";
|
||||||
|
const dx = await this.localDatabase.getDBEntry(data.documentPath);
|
||||||
|
if (dx == false) {
|
||||||
|
throw "Not found on database"
|
||||||
|
}
|
||||||
|
const loadedData = parseYaml(getDocData(dx.data)) as PluginDataEx;
|
||||||
|
for (const f of loadedData.files) {
|
||||||
|
Logger(`Applying ${f.filename} of ${data.displayName || data.name}..`);
|
||||||
|
try {
|
||||||
|
// console.dir(f);
|
||||||
|
const path = `${baseDir}/${f.filename}`;
|
||||||
|
await this.ensureDirectoryEx(path);
|
||||||
|
if (!content) {
|
||||||
|
const dt = base64ToArrayBuffer(f.data);
|
||||||
|
await this.app.vault.adapter.writeBinary(path, dt);
|
||||||
|
} else {
|
||||||
|
await this.app.vault.adapter.write(path, content);
|
||||||
|
}
|
||||||
|
Logger(`Applying ${f.filename} of ${data.displayName || data.name}.. Done`);
|
||||||
|
|
||||||
|
} catch (ex) {
|
||||||
|
Logger(`Applying ${f.filename} of ${data.displayName || data.name}.. Failed`);
|
||||||
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
const uPath = `${baseDir}/${loadedData.files[0].filename}` as FilePath;
|
||||||
|
await this.storeCustomizationFiles(uPath);
|
||||||
|
await this.updatePluginList(uPath);
|
||||||
|
await delay(100);
|
||||||
|
Logger(`Config ${data.displayName || data.name} has been applied`, LOG_LEVEL.NOTICE);
|
||||||
|
if (data.category == "PLUGIN_DATA" || data.category == "PLUGIN_MAIN") {
|
||||||
|
//@ts-ignore
|
||||||
|
const manifests = Object.values(this.app.plugins.manifests) as any as PluginManifest[];
|
||||||
|
//@ts-ignore
|
||||||
|
const enabledPlugins = this.app.plugins.enabledPlugins as Set<string>;
|
||||||
|
const pluginManifest = manifests.find((manifest) => enabledPlugins.has(manifest.id) && manifest.dir == `${baseDir}/plugins/${data.name}`);
|
||||||
|
if (pluginManifest) {
|
||||||
|
Logger(`Unloading plugin: ${pluginManifest.name}`, LOG_LEVEL.NOTICE, "plugin-reload-" + pluginManifest.id);
|
||||||
|
// @ts-ignore
|
||||||
|
await this.app.plugins.unloadPlugin(pluginManifest.id);
|
||||||
|
// @ts-ignore
|
||||||
|
await this.app.plugins.loadPlugin(pluginManifest.id);
|
||||||
|
Logger(`Plugin reloaded: ${pluginManifest.name}`, LOG_LEVEL.NOTICE, "plugin-reload-" + pluginManifest.id);
|
||||||
|
}
|
||||||
|
} else if (data.category == "CONFIG") {
|
||||||
|
scheduleTask("configReload", 250, async () => {
|
||||||
|
if (await askYesNo(this.app, "Do you want to restart and reload Obsidian now?") == "yes") {
|
||||||
|
// @ts-ignore
|
||||||
|
this.app.commands.executeCommandById("app:reload")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (ex) {
|
||||||
|
Logger(`Applying ${data.displayName || data.name}.. Failed`);
|
||||||
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async deleteData(data: PluginDataEx): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
if (data.documentPath) {
|
||||||
|
await this.deleteConfigOnDatabase(data.documentPath);
|
||||||
|
Logger(`Delete: ${data.documentPath}`, LOG_LEVEL.NOTICE);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (ex) {
|
||||||
|
Logger(`Failed to delete: ${data.documentPath}`, LOG_LEVEL.NOTICE);
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parseReplicationResultItem(docs: PouchDB.Core.ExistingDocument<EntryDoc>) {
|
||||||
|
if (docs._id.startsWith(ICXHeader)) {
|
||||||
|
if (this.plugin.settings.usePluginSync && this.plugin.settings.notifyPluginOrSettingUpdated) {
|
||||||
|
if (!this.pluginDialog || (this.pluginDialog && !this.pluginDialog.isOpened())) {
|
||||||
|
const fragment = createFragment((doc) => {
|
||||||
|
doc.createEl("span", null, (a) => {
|
||||||
|
a.appendText(`Some configuration has been arrived, Press `);
|
||||||
|
a.appendChild(a.createEl("a", null, (anchor) => {
|
||||||
|
anchor.text = "HERE";
|
||||||
|
anchor.addEventListener("click", async () => {
|
||||||
|
this.showPluginSyncModal();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
a.appendText(` to open the config sync dialog , or press elsewhere to dismiss this message.`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedPluginKey = "popupUpdated-plugins";
|
||||||
|
scheduleTask(updatedPluginKey, 1000, async () => {
|
||||||
|
const popup = await memoIfNotExist(updatedPluginKey, () => new Notice(fragment, 0));
|
||||||
|
//@ts-ignore
|
||||||
|
const isShown = popup?.noticeEl?.isShown();
|
||||||
|
if (!isShown) {
|
||||||
|
memoObject(updatedPluginKey, new Notice(fragment, 0));
|
||||||
|
}
|
||||||
|
scheduleTask(updatedPluginKey + "-close", 20000, () => {
|
||||||
|
const popup = retrieveMemoObject<Notice>(updatedPluginKey);
|
||||||
|
if (!popup)
|
||||||
|
return;
|
||||||
|
//@ts-ignore
|
||||||
|
if (popup?.noticeEl?.isShown()) {
|
||||||
|
popup.hide();
|
||||||
|
}
|
||||||
|
disposeMemoObject(updatedPluginKey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.updatePluginList(docs.path ? docs.path : this.getPath(docs));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
async realizeSettingSyncMode(): Promise<void> {
|
||||||
|
this.periodicPluginSweepProcessor?.disable();
|
||||||
|
if (this.plugin.suspended)
|
||||||
|
return;
|
||||||
|
if (!this.settings.usePluginSync) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.settings.autoSweepPlugins) {
|
||||||
|
await this.scanAllConfigFiles(false);
|
||||||
|
}
|
||||||
|
this.periodicPluginSweepProcessor.enable(this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges ? (PERIODIC_PLUGIN_SWEEP * 1000) : 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
recentProcessedInternalFiles = [] as string[];
|
||||||
|
async makeEntryFromFile(path: FilePath): Promise<false | PluginDataExFile> {
|
||||||
|
const stat = await this.app.vault.adapter.stat(path);
|
||||||
|
let version: string | undefined;
|
||||||
|
let displayName: string | undefined;
|
||||||
|
if (!stat) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const contentBin = await this.app.vault.adapter.readBinary(path);
|
||||||
|
let content: string[];
|
||||||
|
try {
|
||||||
|
content = await arrayBufferToBase64(contentBin);
|
||||||
|
if (path.toLowerCase().endsWith("/manifest.json")) {
|
||||||
|
const v = readString(new Uint8Array(contentBin));
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(v);
|
||||||
|
if ("version" in json) {
|
||||||
|
version = `${json.version}`;
|
||||||
|
}
|
||||||
|
if ("name" in json) {
|
||||||
|
displayName = `${json.name}`;
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
Logger(`Configuration sync data: ${path} looks like manifest, but could not read the version`, LOG_LEVEL.INFO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
Logger(`The file ${path} could not be encoded`);
|
||||||
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const mtime = stat.mtime;
|
||||||
|
return {
|
||||||
|
filename: path.substring(this.app.vault.configDir.length + 1),
|
||||||
|
data: content,
|
||||||
|
mtime,
|
||||||
|
size: stat.size,
|
||||||
|
version,
|
||||||
|
displayName: displayName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filenameToUnifiedKey(path: string, termOverRide?: string) {
|
||||||
|
const term = termOverRide || this.plugin.deviceAndVaultName;
|
||||||
|
const category = this.getFileCategory(path);
|
||||||
|
const name = (category == "CONFIG" || category == "SNIPPET") ?
|
||||||
|
(path.split("/").slice(-1)[0]) :
|
||||||
|
(category == "PLUGIN_ETC" ?
|
||||||
|
path.split("/").slice(-2).join("/") :
|
||||||
|
path.split("/").slice(-2)[0]);
|
||||||
|
return `${ICXHeader}${term}/${category}/${name}.md` as FilePathWithPrefix
|
||||||
|
}
|
||||||
|
async storeCustomizationFiles(path: FilePath, termOverRide?: string) {
|
||||||
|
const term = termOverRide || this.plugin.deviceAndVaultName;
|
||||||
|
const vf = this.filenameToUnifiedKey(path, term);
|
||||||
|
return await runWithLock(`plugin-${vf}`, false, async () => {
|
||||||
|
const category = this.getFileCategory(path);
|
||||||
|
let mtime = 0;
|
||||||
|
let fileTargets = [] as FilePath[];
|
||||||
|
// let savePath = "";
|
||||||
|
const name = (category == "CONFIG" || category == "SNIPPET") ?
|
||||||
|
(path.split("/").reverse()[0]) :
|
||||||
|
(path.split("/").reverse()[1]);
|
||||||
|
const parentPath = path.split("/").slice(0, -1).join("/");
|
||||||
|
const prefixedFileName = this.filenameToUnifiedKey(path, term);
|
||||||
|
const id = await this.path2id(prefixedFileName);
|
||||||
|
const dt: PluginDataEx = {
|
||||||
|
category: category,
|
||||||
|
files: [],
|
||||||
|
name: name,
|
||||||
|
mtime: 0,
|
||||||
|
term: term
|
||||||
|
}
|
||||||
|
// let scheduleKey = "";
|
||||||
|
if (category == "CONFIG" || category == "SNIPPET" || category == "PLUGIN_ETC" || category == "PLUGIN_DATA") {
|
||||||
|
fileTargets = [path];
|
||||||
|
if (category == "PLUGIN_ETC") {
|
||||||
|
dt.displayName = path.split("/").slice(-1).join("/");
|
||||||
|
}
|
||||||
|
} else if (category == "PLUGIN_MAIN") {
|
||||||
|
fileTargets = ["manifest.json", "main.js", "styles.css"].map(e => `${parentPath}/${e}` as FilePath);
|
||||||
|
} else if (category == "THEME") {
|
||||||
|
fileTargets = ["manifest.json", "theme.css"].map(e => `${parentPath}/${e}` as FilePath);
|
||||||
|
}
|
||||||
|
for (const target of fileTargets) {
|
||||||
|
const data = await this.makeEntryFromFile(target);
|
||||||
|
if (data == false) {
|
||||||
|
Logger(`Config: skipped: ${target} `, LOG_LEVEL.VERBOSE);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (data.version) {
|
||||||
|
dt.version = data.version;
|
||||||
|
}
|
||||||
|
if (data.displayName) {
|
||||||
|
dt.displayName = data.displayName;
|
||||||
|
}
|
||||||
|
// Use average for total modified time.
|
||||||
|
mtime = mtime == 0 ? data.mtime : ((data.mtime + mtime) / 2);
|
||||||
|
dt.files.push(data);
|
||||||
|
}
|
||||||
|
dt.mtime = mtime;
|
||||||
|
|
||||||
|
// Logger(`Configuration saving: ${prefixedFileName}`);
|
||||||
|
if (dt.files.length == 0) {
|
||||||
|
Logger(`Nothing left: deleting.. ${path}`);
|
||||||
|
return await this.deleteConfigOnDatabase(prefixedFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = stringifyYaml(dt);
|
||||||
|
try {
|
||||||
|
const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, null, false);
|
||||||
|
let saveData: LoadedEntry;
|
||||||
|
if (old === false) {
|
||||||
|
saveData = {
|
||||||
|
_id: id,
|
||||||
|
path: prefixedFileName,
|
||||||
|
data: content,
|
||||||
|
mtime,
|
||||||
|
ctime: mtime,
|
||||||
|
datatype: "newnote",
|
||||||
|
size: content.length,
|
||||||
|
children: [],
|
||||||
|
deleted: false,
|
||||||
|
type: "newnote",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (old.mtime == mtime) {
|
||||||
|
// Logger(`STORAGE --> DB:${file.path}: (hidden) Not changed`, LOG_LEVEL.VERBOSE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
saveData =
|
||||||
|
{
|
||||||
|
...old,
|
||||||
|
data: content,
|
||||||
|
mtime,
|
||||||
|
size: content.length,
|
||||||
|
datatype: "newnote",
|
||||||
|
children: [],
|
||||||
|
deleted: false,
|
||||||
|
type: "newnote",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const ret = await this.localDatabase.putDBEntry(saveData);
|
||||||
|
Logger(`STORAGE --> DB:${prefixedFileName}: (config) Done`);
|
||||||
|
return ret;
|
||||||
|
} catch (ex) {
|
||||||
|
Logger(`STORAGE --> DB:${prefixedFileName}: (config) Failed`);
|
||||||
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// })
|
||||||
|
|
||||||
|
}
|
||||||
|
async watchVaultRawEventsAsync(path: FilePath) {
|
||||||
|
if (!this.isTargetPath(path)) return false;
|
||||||
|
const stat = await this.app.vault.adapter.stat(path);
|
||||||
|
// Make sure that target is a file.
|
||||||
|
if (stat && stat.type != "file")
|
||||||
|
return false;
|
||||||
|
const storageMTime = ~~((stat && stat.mtime || 0) / 1000);
|
||||||
|
const key = `${path}-${storageMTime}`;
|
||||||
|
if (this.recentProcessedInternalFiles.contains(key)) {
|
||||||
|
// If recently processed, it may caused by self.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
this.recentProcessedInternalFiles = [key, ...this.recentProcessedInternalFiles].slice(0, 100);
|
||||||
|
|
||||||
|
this.storeCustomizationFiles(path).then(() => {/* Fire and forget */ });
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async scanAllConfigFiles(showMessage: boolean) {
|
||||||
|
const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO;
|
||||||
|
Logger("Scanning customizing files.", logLevel, "scan-all-config");
|
||||||
|
const term = this.plugin.deviceAndVaultName;
|
||||||
|
if (term == "") {
|
||||||
|
Logger("We have to configure the device name", LOG_LEVEL.NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const filesAll = await this.scanInternalFiles();
|
||||||
|
const files = filesAll.filter(e => this.isTargetPath(e)).map(e => ({ key: this.filenameToUnifiedKey(e), file: e }));
|
||||||
|
const virtualPathsOfLocalFiles = [...new Set(files.map(e => e.key))];
|
||||||
|
const filesOnDB = ((await this.localDatabase.allDocsRaw({ startkey: ICXHeader + "", endkey: `${ICXHeader}\u{10ffff}`, include_docs: true })).rows.map(e => e.doc) as InternalFileEntry[]).filter(e => !e.deleted);
|
||||||
|
let deleteCandidate = filesOnDB.map(e => this.getPath(e)).filter(e => e.startsWith(`${ICXHeader}${term}/`));
|
||||||
|
for (const vp of virtualPathsOfLocalFiles) {
|
||||||
|
const p = files.find(e => e.key == vp).file;
|
||||||
|
await this.storeCustomizationFiles(p);
|
||||||
|
deleteCandidate = deleteCandidate.filter(e => e != vp);
|
||||||
|
}
|
||||||
|
for (const vp of deleteCandidate) {
|
||||||
|
await this.deleteConfigOnDatabase(vp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async deleteConfigOnDatabase(prefixedFileName: FilePathWithPrefix, forceWrite = false) {
|
||||||
|
|
||||||
|
// const id = await this.path2id(prefixedFileName);
|
||||||
|
const mtime = new Date().getTime();
|
||||||
|
await runWithLock("file-x-" + prefixedFileName, false, async () => {
|
||||||
|
try {
|
||||||
|
const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, null, false) as InternalFileEntry | false;
|
||||||
|
let saveData: InternalFileEntry;
|
||||||
|
if (old === false) {
|
||||||
|
Logger(`STORAGE -x> DB:${prefixedFileName}: (config) already deleted (Not found on database)`);
|
||||||
|
} else {
|
||||||
|
if (old.deleted) {
|
||||||
|
Logger(`STORAGE -x> DB:${prefixedFileName}: (config) already deleted`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saveData =
|
||||||
|
{
|
||||||
|
...old,
|
||||||
|
mtime,
|
||||||
|
size: 0,
|
||||||
|
children: [],
|
||||||
|
deleted: true,
|
||||||
|
type: "newnote",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await this.localDatabase.putRaw(saveData);
|
||||||
|
Logger(`STORAGE -x> DB:${prefixedFileName}: (config) Done`);
|
||||||
|
} catch (ex) {
|
||||||
|
Logger(`STORAGE -x> DB:${prefixedFileName}: (config) Failed`);
|
||||||
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async scanInternalFiles(): Promise<FilePath[]> {
|
||||||
|
const filenames = (await this.getFiles(this.app.vault.configDir, 2)).filter(e => e.startsWith(".")).filter(e => !e.startsWith(".trash"));
|
||||||
|
return filenames as FilePath[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async getFiles(
|
||||||
|
path: string,
|
||||||
|
lastDepth: number
|
||||||
|
) {
|
||||||
|
if (lastDepth == -1) return [];
|
||||||
|
const w = await this.app.vault.adapter.list(path);
|
||||||
|
let files = [
|
||||||
|
...w.files
|
||||||
|
];
|
||||||
|
for (const v of w.folders) {
|
||||||
|
files = files.concat(await this.getFiles(v, lastDepth - 1));
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,7 @@ import { InternalFileInfo, ICHeader, ICHeaderEnd } from "./types";
|
|||||||
import { delay, isDocContentSame } from "./lib/src/utils";
|
import { delay, isDocContentSame } from "./lib/src/utils";
|
||||||
import { Logger } from "./lib/src/logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
||||||
import { disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask, trimPrefix, isIdOfInternalMetadata, PeriodicProcessor } from "./utils";
|
import { disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask, isInternalMetadata, PeriodicProcessor } from "./utils";
|
||||||
import { WrappedNotice } from "./lib/src/wrapper";
|
import { WrappedNotice } from "./lib/src/wrapper";
|
||||||
import { base64ToArrayBuffer, arrayBufferToBase64 } from "./lib/src/strbin";
|
import { base64ToArrayBuffer, arrayBufferToBase64 } from "./lib/src/strbin";
|
||||||
import { runWithLock } from "./lib/src/lock";
|
import { runWithLock } from "./lib/src/lock";
|
||||||
@ -28,7 +28,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
onunload() {
|
onunload() {
|
||||||
this.periodicInternalFileScanProcessor?.disable();
|
this.periodicInternalFileScanProcessor?.disable();
|
||||||
}
|
}
|
||||||
onload(): void | Promise<void> {
|
onload() {
|
||||||
this.plugin.addCommand({
|
this.plugin.addCommand({
|
||||||
id: "livesync-scaninternal",
|
id: "livesync-scaninternal",
|
||||||
name: "Sync hidden files",
|
name: "Sync hidden files",
|
||||||
@ -78,7 +78,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
|
|
||||||
procInternalFiles: string[] = [];
|
procInternalFiles: string[] = [];
|
||||||
async execInternalFile() {
|
async execInternalFile() {
|
||||||
await runWithLock("execinternal", false, async () => {
|
await runWithLock("execInternal", false, async () => {
|
||||||
const w = [...this.procInternalFiles];
|
const w = [...this.procInternalFiles];
|
||||||
this.procInternalFiles = [];
|
this.procInternalFiles = [];
|
||||||
Logger(`Applying hidden ${w.length} files change...`);
|
Logger(`Applying hidden ${w.length} files change...`);
|
||||||
@ -95,6 +95,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
|
|
||||||
recentProcessedInternalFiles = [] as string[];
|
recentProcessedInternalFiles = [] as string[];
|
||||||
async watchVaultRawEventsAsync(path: FilePath) {
|
async watchVaultRawEventsAsync(path: FilePath) {
|
||||||
|
if (!this.settings.syncInternalFiles) return;
|
||||||
const stat = await this.app.vault.adapter.stat(path);
|
const stat = await this.app.vault.adapter.stat(path);
|
||||||
// sometimes folder is coming.
|
// sometimes folder is coming.
|
||||||
if (stat && stat.type != "file")
|
if (stat && stat.type != "file")
|
||||||
@ -122,12 +123,6 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
await this.deleteInternalFileOnDatabase(path);
|
await this.deleteInternalFileOnDatabase(path);
|
||||||
} else {
|
} else {
|
||||||
await this.storeInternalFileToDatabase({ path: path, ...stat });
|
await this.storeInternalFileToDatabase({ path: path, ...stat });
|
||||||
const pluginDir = this.app.vault.configDir + "/plugins/";
|
|
||||||
const pluginFiles = ["manifest.json", "data.json", "style.css", "main.js"];
|
|
||||||
if (path.startsWith(pluginDir) && pluginFiles.some(e => path.endsWith(e)) && this.settings.usePluginSync) {
|
|
||||||
const pluginName = trimPrefix(path, pluginDir).split("/")[0];
|
|
||||||
await this.plugin.addOnPluginAndTheirSettings.sweepPlugin(false, pluginName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -138,7 +133,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
for await (const doc of conflicted) {
|
for await (const doc of conflicted) {
|
||||||
if (!("_conflicts" in doc))
|
if (!("_conflicts" in doc))
|
||||||
continue;
|
continue;
|
||||||
if (isIdOfInternalMetadata(doc._id)) {
|
if (isInternalMetadata(doc._id)) {
|
||||||
await this.resolveConflictOnInternalFile(doc.path);
|
await this.resolveConflictOnInternalFile(doc.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
|||||||
this.showPluginSyncModal();
|
this.showPluginSyncModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
this.showPluginSyncModal();
|
||||||
}
|
}
|
||||||
onunload() {
|
onunload() {
|
||||||
this.hidePluginSyncModal();
|
this.hidePluginSyncModal();
|
||||||
@ -165,7 +166,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
|||||||
await runWithLock("sweepplugin", true, async () => {
|
await runWithLock("sweepplugin", true, async () => {
|
||||||
const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO;
|
const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO;
|
||||||
if (!this.deviceAndVaultName) {
|
if (!this.deviceAndVaultName) {
|
||||||
Logger("You have to set your device and vault name.", LOG_LEVEL.NOTICE);
|
Logger("You have to set your device name.", LOG_LEVEL.NOTICE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Logger("Scanning plugins", logLevel);
|
Logger("Scanning plugins", logLevel);
|
||||||
|
@ -7,6 +7,7 @@ import { decrypt, encrypt } from "./lib/src/e2ee_v2";
|
|||||||
import { LiveSyncCommands } from "./LiveSyncCommands";
|
import { LiveSyncCommands } from "./LiveSyncCommands";
|
||||||
import { delay } from "./lib/src/utils";
|
import { delay } from "./lib/src/utils";
|
||||||
import { confirmWithMessage } from "./dialogs";
|
import { confirmWithMessage } from "./dialogs";
|
||||||
|
import { Platform } from "./deps";
|
||||||
|
|
||||||
export class SetupLiveSync extends LiveSyncCommands {
|
export class SetupLiveSync extends LiveSyncCommands {
|
||||||
onunload() { }
|
onunload() { }
|
||||||
@ -191,13 +192,14 @@ export class SetupLiveSync extends LiveSyncCommands {
|
|||||||
}
|
}
|
||||||
async askHiddenFileConfiguration(opt: { enableFetch?: boolean, enableOverwrite?: boolean }) {
|
async askHiddenFileConfiguration(opt: { enableFetch?: boolean, enableOverwrite?: boolean }) {
|
||||||
this.plugin.addOnSetup.suspendExtraSync();
|
this.plugin.addOnSetup.suspendExtraSync();
|
||||||
const message = `Would you like to enable \`Hidden File Synchronization\`?
|
const message = `Would you like to enable \`Hidden File Synchronization\` or \`Customization sync\`?
|
||||||
${opt.enableFetch ? " - Fetch: Use files stored from other devices. \n" : ""}${opt.enableOverwrite ? "- Overwrite: Use files from this device. \n" : ""}- Keep it disabled: Do not use hidden file synchronization.
|
${opt.enableFetch ? " - Fetch: Use files stored from other devices. \n" : ""}${opt.enableOverwrite ? "- Overwrite: Use files from this device. \n" : ""}- Custom: Synchronize only customization files with a dedicated interface.
|
||||||
|
- Keep them disabled: Do not use hidden file synchronization.
|
||||||
Of course, we are able to disable this feature.`
|
Of course, we are able to disable these features.`
|
||||||
const CHOICE_FETCH = "Fetch";
|
const CHOICE_FETCH = "Fetch";
|
||||||
const CHOICE_OVERWRITE = "Overwrite";
|
const CHOICE_OVERWRITE = "Overwrite";
|
||||||
const CHOICE_DISMISS = "keep it disabled";
|
const CHOICE_CUSTOMIZE = "Custom";
|
||||||
|
const CHOICE_DISMISS = "keep them disabled";
|
||||||
const choices = [];
|
const choices = [];
|
||||||
if (opt?.enableFetch) {
|
if (opt?.enableFetch) {
|
||||||
choices.push(CHOICE_FETCH);
|
choices.push(CHOICE_FETCH);
|
||||||
@ -205,6 +207,7 @@ Of course, we are able to disable this feature.`
|
|||||||
if (opt?.enableOverwrite) {
|
if (opt?.enableOverwrite) {
|
||||||
choices.push(CHOICE_OVERWRITE);
|
choices.push(CHOICE_OVERWRITE);
|
||||||
}
|
}
|
||||||
|
choices.push(CHOICE_CUSTOMIZE);
|
||||||
choices.push(CHOICE_DISMISS);
|
choices.push(CHOICE_DISMISS);
|
||||||
|
|
||||||
const ret = await confirmWithMessage(this.plugin, "Hidden file sync", message, choices, CHOICE_DISMISS, 40);
|
const ret = await confirmWithMessage(this.plugin, "Hidden file sync", message, choices, CHOICE_DISMISS, 40);
|
||||||
@ -214,15 +217,19 @@ Of course, we are able to disable this feature.`
|
|||||||
await this.configureHiddenFileSync("OVERWRITE");
|
await this.configureHiddenFileSync("OVERWRITE");
|
||||||
} else if (ret == CHOICE_DISMISS) {
|
} else if (ret == CHOICE_DISMISS) {
|
||||||
await this.configureHiddenFileSync("DISABLE");
|
await this.configureHiddenFileSync("DISABLE");
|
||||||
|
} else if (ret == CHOICE_CUSTOMIZE) {
|
||||||
|
await this.configureHiddenFileSync("CUSTOMIZE");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async configureHiddenFileSync(mode: "FETCH" | "OVERWRITE" | "MERGE" | "DISABLE") {
|
async configureHiddenFileSync(mode: "FETCH" | "OVERWRITE" | "MERGE" | "DISABLE" | "CUSTOMIZE") {
|
||||||
this.plugin.addOnSetup.suspendExtraSync();
|
this.plugin.addOnSetup.suspendExtraSync();
|
||||||
if (mode == "DISABLE") {
|
if (mode == "DISABLE") {
|
||||||
this.plugin.settings.syncInternalFiles = false;
|
this.plugin.settings.syncInternalFiles = false;
|
||||||
|
this.plugin.settings.usePluginSync = false;
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (mode != "CUSTOMIZE") {
|
||||||
Logger("Gathering files for enabling Hidden File Sync", LOG_LEVEL.NOTICE);
|
Logger("Gathering files for enabling Hidden File Sync", LOG_LEVEL.NOTICE);
|
||||||
if (mode == "FETCH") {
|
if (mode == "FETCH") {
|
||||||
await this.plugin.addOnHiddenFileSync.syncInternalFilesAndDatabase("pullForce", true);
|
await this.plugin.addOnHiddenFileSync.syncInternalFilesAndDatabase("pullForce", true);
|
||||||
@ -234,6 +241,37 @@ Of course, we are able to disable this feature.`
|
|||||||
this.plugin.settings.syncInternalFiles = true;
|
this.plugin.settings.syncInternalFiles = true;
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
Logger(`Done! Restarting the app is strongly recommended!`, LOG_LEVEL.NOTICE);
|
Logger(`Done! Restarting the app is strongly recommended!`, LOG_LEVEL.NOTICE);
|
||||||
|
} else if (mode == "CUSTOMIZE") {
|
||||||
|
if (!this.plugin.deviceAndVaultName) {
|
||||||
|
let name = await askString(this.app, "Device name", "Please set this device name", `desktop`);
|
||||||
|
if (!name) {
|
||||||
|
if (Platform.isAndroidApp) {
|
||||||
|
name = "android-app"
|
||||||
|
} else if (Platform.isIosApp) {
|
||||||
|
name = "ios"
|
||||||
|
} else if (Platform.isMacOS) {
|
||||||
|
name = "macos"
|
||||||
|
} else if (Platform.isMobileApp) {
|
||||||
|
name = "mobile-app"
|
||||||
|
} else if (Platform.isMobile) {
|
||||||
|
name = "mobile"
|
||||||
|
} else if (Platform.isSafari) {
|
||||||
|
name = "safari"
|
||||||
|
} else if (Platform.isDesktop) {
|
||||||
|
name = "desktop"
|
||||||
|
} else if (Platform.isDesktopApp) {
|
||||||
|
name = "desktop-app"
|
||||||
|
} else {
|
||||||
|
name = "unknown"
|
||||||
|
}
|
||||||
|
name = name + Math.random().toString(36).slice(-4);
|
||||||
|
}
|
||||||
|
this.plugin.deviceAndVaultName = name;
|
||||||
|
}
|
||||||
|
this.plugin.settings.usePluginSync = true;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
await this.plugin.addOnConfigSync.scanAllConfigFiles(true);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,12 +8,18 @@ export class JsonResolveModal extends Modal {
|
|||||||
callback: (keepRev: string, mergedStr?: string) => Promise<void>;
|
callback: (keepRev: string, mergedStr?: string) => Promise<void>;
|
||||||
docs: LoadedEntry[];
|
docs: LoadedEntry[];
|
||||||
component: JsonResolvePane;
|
component: JsonResolvePane;
|
||||||
|
nameA: string;
|
||||||
|
nameB: string;
|
||||||
|
defaultSelect: string;
|
||||||
|
|
||||||
constructor(app: App, filename: FilePath, docs: LoadedEntry[], callback: (keepRev: string, mergedStr?: string) => Promise<void>) {
|
constructor(app: App, filename: FilePath, docs: LoadedEntry[], callback: (keepRev: string, mergedStr?: string) => Promise<void>, nameA?: string, nameB?: string, defaultSelect?: string) {
|
||||||
super(app);
|
super(app);
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
this.filename = filename;
|
this.filename = filename;
|
||||||
this.docs = docs;
|
this.docs = docs;
|
||||||
|
this.nameA = nameA;
|
||||||
|
this.nameB = nameB;
|
||||||
|
this.defaultSelect = defaultSelect;
|
||||||
}
|
}
|
||||||
async UICallback(keepRev: string, mergedStr?: string) {
|
async UICallback(keepRev: string, mergedStr?: string) {
|
||||||
this.close();
|
this.close();
|
||||||
@ -32,6 +38,9 @@ export class JsonResolveModal extends Modal {
|
|||||||
props: {
|
props: {
|
||||||
docs: this.docs,
|
docs: this.docs,
|
||||||
filename: this.filename,
|
filename: this.filename,
|
||||||
|
nameA: this.nameA,
|
||||||
|
nameB: this.nameB,
|
||||||
|
defaultSelect: this.defaultSelect,
|
||||||
callback: (keepRev, mergedStr) => this.UICallback(keepRev, mergedStr),
|
callback: (keepRev, mergedStr) => this.UICallback(keepRev, mergedStr),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -10,7 +10,9 @@
|
|||||||
Promise.resolve();
|
Promise.resolve();
|
||||||
};
|
};
|
||||||
export let filename: FilePath = "" as FilePath;
|
export let filename: FilePath = "" as FilePath;
|
||||||
|
export let nameA: string = "A";
|
||||||
|
export let nameB: string = "B";
|
||||||
|
export let defaultSelect: string = "";
|
||||||
let docA: LoadedEntry = undefined;
|
let docA: LoadedEntry = undefined;
|
||||||
let docB: LoadedEntry = undefined;
|
let docB: LoadedEntry = undefined;
|
||||||
let docAContent = "";
|
let docAContent = "";
|
||||||
@ -20,14 +22,8 @@
|
|||||||
let objAB: any = {};
|
let objAB: any = {};
|
||||||
let objBA: any = {};
|
let objBA: any = {};
|
||||||
let diffs: Diff[];
|
let diffs: Diff[];
|
||||||
const modes = [
|
type SelectModes = "" | "A" | "B" | "AB" | "BA";
|
||||||
["", "Not now"],
|
let mode: SelectModes = defaultSelect as SelectModes;
|
||||||
["A", "A"],
|
|
||||||
["B", "B"],
|
|
||||||
["AB", "A + B"],
|
|
||||||
["BA", "B + A"],
|
|
||||||
] as ["" | "A" | "B" | "AB" | "BA", string][];
|
|
||||||
let mode: "" | "A" | "B" | "AB" | "BA" = "";
|
|
||||||
|
|
||||||
function docToString(doc: LoadedEntry) {
|
function docToString(doc: LoadedEntry) {
|
||||||
return doc.datatype == "plain" ? getDocData(doc.data) : base64ToString(doc.data);
|
return doc.datatype == "plain" ? getDocData(doc.data) : base64ToString(doc.data);
|
||||||
@ -47,8 +43,13 @@
|
|||||||
return getDiff(JSON.stringify(a, null, 2), JSON.stringify(b, null, 2));
|
return getDiff(JSON.stringify(a, null, 2), JSON.stringify(b, null, 2));
|
||||||
}
|
}
|
||||||
function apply() {
|
function apply() {
|
||||||
|
if (docA._id == docB._id) {
|
||||||
if (mode == "A") return callback(docA._rev, null);
|
if (mode == "A") return callback(docA._rev, null);
|
||||||
if (mode == "B") return callback(docB._rev, null);
|
if (mode == "B") return callback(docB._rev, null);
|
||||||
|
} else {
|
||||||
|
if (mode == "A") return callback(null, docToString(docA));
|
||||||
|
if (mode == "B") return callback(null, docToString(docB));
|
||||||
|
}
|
||||||
if (mode == "BA") return callback(null, JSON.stringify(objBA, null, 2));
|
if (mode == "BA") return callback(null, JSON.stringify(objBA, null, 2));
|
||||||
if (mode == "AB") return callback(null, JSON.stringify(objAB, null, 2));
|
if (mode == "AB") return callback(null, JSON.stringify(objAB, null, 2));
|
||||||
callback(null, null);
|
callback(null, null);
|
||||||
@ -92,12 +93,19 @@
|
|||||||
$: selectedObj = mode in mergedObjs ? mergedObjs[mode] : {};
|
$: selectedObj = mode in mergedObjs ? mergedObjs[mode] : {};
|
||||||
$: {
|
$: {
|
||||||
diffs = getJsonDiff(objA, selectedObj);
|
diffs = getJsonDiff(objA, selectedObj);
|
||||||
console.dir(selectedObj);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: modes = [
|
||||||
|
["", "Not now"],
|
||||||
|
["A", nameA || "A"],
|
||||||
|
["B", nameB || "B"],
|
||||||
|
["AB", `${nameA || "A"} + ${nameB || "B"}`],
|
||||||
|
["BA", `${nameB || "B"} + ${nameA || "A"}`],
|
||||||
|
] as ["" | "A" | "B" | "AB" | "BA", string][];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1>Conflicted settings</h1>
|
<h1>Conflicted settings</h1>
|
||||||
<div><span>{filename}</span></div>
|
<h2>{filename}</h2>
|
||||||
{#if !docA || !docB}
|
{#if !docA || !docB}
|
||||||
<div class="message">Just for a minute, please!</div>
|
<div class="message">Just for a minute, please!</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
@ -125,12 +133,14 @@
|
|||||||
NO PREVIEW
|
NO PREVIEW
|
||||||
{/if}
|
{/if}
|
||||||
<div>
|
<div>
|
||||||
A Rev:{revStringToRevNumber(docA._rev)} ,{new Date(docA.mtime).toLocaleString()}
|
{nameA}
|
||||||
|
{#if docA._id == docB._id} Rev:{revStringToRevNumber(docA._rev)} {/if} ,{new Date(docA.mtime).toLocaleString()}
|
||||||
{docAContent.length} letters
|
{docAContent.length} letters
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
B Rev:{revStringToRevNumber(docB._rev)} ,{new Date(docB.mtime).toLocaleString()}
|
{nameB}
|
||||||
|
{#if docA._id == docB._id} Rev:{revStringToRevNumber(docB._rev)} {/if} ,{new Date(docB.mtime).toLocaleString()}
|
||||||
{docBContent.length} letters
|
{docBContent.length} letters
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1606,13 +1606,13 @@ ${stringifyYaml(pluginConfig)}`;
|
|||||||
// With great respect, thank you TfTHacker!
|
// With great respect, thank you TfTHacker!
|
||||||
// Refer: https://github.com/TfTHacker/obsidian42-brat/blob/main/src/features/BetaPlugins.ts
|
// Refer: https://github.com/TfTHacker/obsidian42-brat/blob/main/src/features/BetaPlugins.ts
|
||||||
const containerPluginSettings = containerEl.createDiv();
|
const containerPluginSettings = containerEl.createDiv();
|
||||||
containerPluginSettings.createEl("h3", { text: "Plugins and settings (beta)" });
|
containerPluginSettings.createEl("h3", { text: "Customization sync (beta)" });
|
||||||
|
|
||||||
const updateDisabledOfDeviceAndVaultName = () => {
|
const updateDisabledOfDeviceAndVaultName = () => {
|
||||||
vaultName.setDisabled(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic);
|
vaultName.setDisabled(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic);
|
||||||
vaultName.setTooltip(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic ? "You could not change when you enabling auto scan." : "");
|
vaultName.setTooltip(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic ? "You could not change when you enabling auto scan." : "");
|
||||||
};
|
};
|
||||||
new Setting(containerPluginSettings).setName("Enable plugin synchronization").addToggle((toggle) =>
|
new Setting(containerPluginSettings).setName("Enable customization sync").addToggle((toggle) =>
|
||||||
toggle.setValue(this.plugin.settings.usePluginSync).onChange(async (value) => {
|
toggle.setValue(this.plugin.settings.usePluginSync).onChange(async (value) => {
|
||||||
this.plugin.settings.usePluginSync = value;
|
this.plugin.settings.usePluginSync = value;
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
@ -1620,8 +1620,8 @@ ${stringifyYaml(pluginConfig)}`;
|
|||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerPluginSettings)
|
new Setting(containerPluginSettings)
|
||||||
.setName("Scan plugins automatically")
|
.setName("Scan customization automatically")
|
||||||
.setDesc("Scan plugins before replicating.")
|
.setDesc("Scan customization before replicating.")
|
||||||
.addToggle((toggle) =>
|
.addToggle((toggle) =>
|
||||||
toggle.setValue(this.plugin.settings.autoSweepPlugins).onChange(async (value) => {
|
toggle.setValue(this.plugin.settings.autoSweepPlugins).onChange(async (value) => {
|
||||||
this.plugin.settings.autoSweepPlugins = value;
|
this.plugin.settings.autoSweepPlugins = value;
|
||||||
@ -1631,8 +1631,8 @@ ${stringifyYaml(pluginConfig)}`;
|
|||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerPluginSettings)
|
new Setting(containerPluginSettings)
|
||||||
.setName("Scan plugins periodically")
|
.setName("Scan customization periodically")
|
||||||
.setDesc("Scan plugins every 1 minute. This configuration will be ignored if monitoring changes of hidden files has been enabled.")
|
.setDesc("Scan customization every 1 minute. This configuration will be ignored if monitoring changes of hidden files has been enabled.")
|
||||||
.addToggle((toggle) =>
|
.addToggle((toggle) =>
|
||||||
toggle.setValue(this.plugin.settings.autoSweepPluginsPeriodic).onChange(async (value) => {
|
toggle.setValue(this.plugin.settings.autoSweepPluginsPeriodic).onChange(async (value) => {
|
||||||
this.plugin.settings.autoSweepPluginsPeriodic = value;
|
this.plugin.settings.autoSweepPluginsPeriodic = value;
|
||||||
@ -1642,8 +1642,8 @@ ${stringifyYaml(pluginConfig)}`;
|
|||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerPluginSettings)
|
new Setting(containerPluginSettings)
|
||||||
.setName("Notify updates")
|
.setName("Notify customized")
|
||||||
.setDesc("Notify when any device has a newer plugin or its setting.")
|
.setDesc("Notify when other device has newly customized.")
|
||||||
.addToggle((toggle) =>
|
.addToggle((toggle) =>
|
||||||
toggle.setValue(this.plugin.settings.notifyPluginOrSettingUpdated).onChange(async (value) => {
|
toggle.setValue(this.plugin.settings.notifyPluginOrSettingUpdated).onChange(async (value) => {
|
||||||
this.plugin.settings.notifyPluginOrSettingUpdated = value;
|
this.plugin.settings.notifyPluginOrSettingUpdated = value;
|
||||||
@ -1651,10 +1651,10 @@ ${stringifyYaml(pluginConfig)}`;
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
const vaultName = new Setting(containerPluginSettings)
|
const vaultName = new Setting(containerPluginSettings)
|
||||||
.setName("Device and Vault name")
|
.setName("Device name")
|
||||||
.setDesc("")
|
.setDesc("")
|
||||||
.addText((text) => {
|
.addText((text) => {
|
||||||
text.setPlaceholder("desktop-main")
|
text.setPlaceholder("desktop")
|
||||||
.setValue(this.plugin.deviceAndVaultName)
|
.setValue(this.plugin.deviceAndVaultName)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.deviceAndVaultName = value;
|
this.plugin.deviceAndVaultName = value;
|
||||||
@ -1664,13 +1664,13 @@ ${stringifyYaml(pluginConfig)}`;
|
|||||||
});
|
});
|
||||||
new Setting(containerPluginSettings)
|
new Setting(containerPluginSettings)
|
||||||
.setName("Open")
|
.setName("Open")
|
||||||
.setDesc("Open the plugin dialog")
|
.setDesc("Open the dialog")
|
||||||
.addButton((button) => {
|
.addButton((button) => {
|
||||||
button
|
button
|
||||||
.setButtonText("Open")
|
.setButtonText("Open")
|
||||||
.setDisabled(false)
|
.setDisabled(false)
|
||||||
.onClick(() => {
|
.onClick(() => {
|
||||||
this.plugin.addOnPluginAndTheirSettings.showPluginSyncModal();
|
this.plugin.addOnConfigSync.showPluginSyncModal();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
314
src/PluginCombo.svelte
Normal file
314
src/PluginCombo.svelte
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { PluginDataExDisplay } from "./CmdConfigSync";
|
||||||
|
import { Logger } from "./lib/src/logger";
|
||||||
|
import { versionNumberString2Number } from "./lib/src/strbin";
|
||||||
|
import { FilePath, LOG_LEVEL } from "./lib/src/types";
|
||||||
|
import { getDocData } from "./lib/src/utils";
|
||||||
|
import type ObsidianLiveSyncPlugin from "./main";
|
||||||
|
import { askString, scheduleTask } from "./utils";
|
||||||
|
|
||||||
|
export let list: PluginDataExDisplay[] = [];
|
||||||
|
export let thisTerm = "";
|
||||||
|
export let hideNotApplicable = false;
|
||||||
|
export let selectNewest = 0;
|
||||||
|
export let applyAllPluse = 0;
|
||||||
|
|
||||||
|
export let applyData: (data: PluginDataExDisplay) => Promise<boolean>;
|
||||||
|
export let compareData: (dataA: PluginDataExDisplay, dataB: PluginDataExDisplay) => Promise<boolean>;
|
||||||
|
export let deleteData: (data: PluginDataExDisplay) => Promise<boolean>;
|
||||||
|
export let hidden: boolean;
|
||||||
|
export let plugin: ObsidianLiveSyncPlugin;
|
||||||
|
export let isMaintenanceMode: boolean = false;
|
||||||
|
const addOn = plugin.addOnConfigSync;
|
||||||
|
|
||||||
|
let selected = "";
|
||||||
|
let freshness = "";
|
||||||
|
let equivalency = "";
|
||||||
|
let version = "";
|
||||||
|
let canApply: boolean = false;
|
||||||
|
let canCompare: boolean = false;
|
||||||
|
let currentSelectNewest = 0;
|
||||||
|
let currentApplyAll = 0;
|
||||||
|
|
||||||
|
// Selectable terminals
|
||||||
|
let terms = [] as string[];
|
||||||
|
|
||||||
|
async function comparePlugin(local: PluginDataExDisplay, remote: PluginDataExDisplay) {
|
||||||
|
let freshness = "";
|
||||||
|
let equivalency = "";
|
||||||
|
let version = "";
|
||||||
|
let contentCheck = false;
|
||||||
|
let canApply: boolean = false;
|
||||||
|
let canCompare = false;
|
||||||
|
if (!local && !remote) {
|
||||||
|
// NO OP. whats happened?
|
||||||
|
freshness = "";
|
||||||
|
} else if (local && !remote) {
|
||||||
|
freshness = "⚠ Local only";
|
||||||
|
} else if (remote && !local) {
|
||||||
|
freshness = "✓ Remote only";
|
||||||
|
canApply = true;
|
||||||
|
} else {
|
||||||
|
const dtDiff = (local?.mtime ?? 0) - (remote?.mtime ?? 0);
|
||||||
|
if (dtDiff / 1000 < -10) {
|
||||||
|
freshness = "✓ Newer";
|
||||||
|
canApply = true;
|
||||||
|
contentCheck = true;
|
||||||
|
} else if (dtDiff / 1000 > 10) {
|
||||||
|
freshness = "⚠ Older";
|
||||||
|
canApply = true;
|
||||||
|
contentCheck = true;
|
||||||
|
} else {
|
||||||
|
freshness = "⚖️ Same old";
|
||||||
|
canApply = false;
|
||||||
|
contentCheck = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const localVersionStr = local?.version || "0.0.0";
|
||||||
|
const remoteVersionStr = remote?.version || "0.0.0";
|
||||||
|
if (local?.version || remote?.version) {
|
||||||
|
const localVersion = versionNumberString2Number(localVersionStr);
|
||||||
|
const remoteVersion = versionNumberString2Number(remoteVersionStr);
|
||||||
|
if (localVersion == remoteVersion) {
|
||||||
|
version = "⚖️ Same ver.";
|
||||||
|
} else if (localVersion > remoteVersion) {
|
||||||
|
version = `⚠ Lower ${localVersionStr} > ${remoteVersionStr}`;
|
||||||
|
} else if (localVersion < remoteVersion) {
|
||||||
|
version = `✓ Higher ${localVersionStr} < ${remoteVersionStr}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentCheck) {
|
||||||
|
const { canApply, equivalency, canCompare } = await checkEquivalency(local, remote);
|
||||||
|
return { canApply, freshness, equivalency, version, canCompare };
|
||||||
|
}
|
||||||
|
return { canApply, freshness, equivalency, version, canCompare };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkEquivalency(local: PluginDataExDisplay, remote: PluginDataExDisplay) {
|
||||||
|
let equivalency = "";
|
||||||
|
let canApply = false;
|
||||||
|
let canCompare = false;
|
||||||
|
const filenames = [...new Set([...local.files.map((e) => e.filename), ...remote.files.map((e) => e.filename)])];
|
||||||
|
const matchingStatus = filenames
|
||||||
|
.map((filename) => {
|
||||||
|
const localFile = local.files.find((e) => e.filename == filename);
|
||||||
|
const remoteFile = remote.files.find((e) => e.filename == filename);
|
||||||
|
if (!localFile && !remoteFile) {
|
||||||
|
return 0b0000000;
|
||||||
|
} else if (localFile && !remoteFile) {
|
||||||
|
return 0b0000010; //"LOCAL_ONLY";
|
||||||
|
} else if (!localFile && remoteFile) {
|
||||||
|
return 0b0001000; //"REMOTE ONLY"
|
||||||
|
} else {
|
||||||
|
if (getDocData(localFile.data) == getDocData(remoteFile.data)) {
|
||||||
|
return 0b0000100; //"EVEN"
|
||||||
|
} else {
|
||||||
|
return 0b0010000; //"DIFFERENT";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.reduce((p, c) => p | c, 0);
|
||||||
|
if (matchingStatus == 0b0000100) {
|
||||||
|
equivalency = "⚖️ Same";
|
||||||
|
canApply = false;
|
||||||
|
} else if (matchingStatus <= 0b0000100) {
|
||||||
|
equivalency = "Same or local only";
|
||||||
|
canApply = false;
|
||||||
|
} else if (matchingStatus == 0b0010000) {
|
||||||
|
canApply = true;
|
||||||
|
canCompare = true;
|
||||||
|
equivalency = "≠ Different";
|
||||||
|
} else {
|
||||||
|
canApply = true;
|
||||||
|
canCompare = true;
|
||||||
|
equivalency = "≠ Different";
|
||||||
|
}
|
||||||
|
return { equivalency, canApply, canCompare };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performCompare(local: PluginDataExDisplay, remote: PluginDataExDisplay) {
|
||||||
|
const result = await comparePlugin(local, remote);
|
||||||
|
canApply = result.canApply;
|
||||||
|
freshness = result.freshness;
|
||||||
|
equivalency = result.equivalency;
|
||||||
|
version = result.version;
|
||||||
|
canCompare = result.canCompare;
|
||||||
|
if (local?.files.length != 1 || !local?.files?.first()?.filename?.endsWith(".json")) {
|
||||||
|
canCompare = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTerms(list: PluginDataExDisplay[], selectNewest: boolean, isMaintenanceMode: boolean) {
|
||||||
|
const local = list.find((e) => e.term == thisTerm);
|
||||||
|
selected = "";
|
||||||
|
if (isMaintenanceMode) {
|
||||||
|
terms = [...new Set(list.map((e) => e.term))];
|
||||||
|
} else if (hideNotApplicable) {
|
||||||
|
const termsTmp = [];
|
||||||
|
const wk = [...new Set(list.map((e) => e.term))];
|
||||||
|
for (const termName of wk) {
|
||||||
|
const remote = list.find((e) => e.term == termName);
|
||||||
|
if ((await comparePlugin(local, remote)).canApply) {
|
||||||
|
termsTmp.push(termName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
terms = [...termsTmp];
|
||||||
|
} else {
|
||||||
|
terms = [...new Set(list.map((e) => e.term))].filter((e) => e != thisTerm);
|
||||||
|
}
|
||||||
|
let newest: PluginDataExDisplay = local;
|
||||||
|
if (selectNewest) {
|
||||||
|
for (const term of terms) {
|
||||||
|
const remote = list.find((e) => e.term == term);
|
||||||
|
if (remote && remote.mtime && (newest?.mtime || 0) < remote.mtime) {
|
||||||
|
newest = remote;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newest && newest.term != thisTerm) {
|
||||||
|
selected = newest.term;
|
||||||
|
}
|
||||||
|
// selectNewest = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$: {
|
||||||
|
// React pulse and select
|
||||||
|
const doSelectNewest = selectNewest != currentSelectNewest;
|
||||||
|
currentSelectNewest = selectNewest;
|
||||||
|
updateTerms(list, doSelectNewest, isMaintenanceMode);
|
||||||
|
}
|
||||||
|
$: {
|
||||||
|
// React pulse and apply
|
||||||
|
const doApply = applyAllPluse != currentApplyAll;
|
||||||
|
currentApplyAll = applyAllPluse;
|
||||||
|
if (doApply && selected) {
|
||||||
|
if (!hidden) {
|
||||||
|
applySelected();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$: {
|
||||||
|
freshness = "";
|
||||||
|
equivalency = "";
|
||||||
|
version = "";
|
||||||
|
canApply = false;
|
||||||
|
if (selected == "") {
|
||||||
|
// NO OP.
|
||||||
|
} else if (selected == thisTerm) {
|
||||||
|
freshness = "This device";
|
||||||
|
canApply = false;
|
||||||
|
} else {
|
||||||
|
const local = list.find((e) => e.term == thisTerm);
|
||||||
|
const remote = list.find((e) => e.term == selected);
|
||||||
|
performCompare(local, remote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function applySelected() {
|
||||||
|
const local = list.find((e) => e.term == thisTerm);
|
||||||
|
const selectedItem = list.find((e) => e.term == selected);
|
||||||
|
if (selectedItem && (await applyData(selectedItem))) {
|
||||||
|
scheduleTask("update-plugin-list", 250, () => addOn.updatePluginList(local.documentPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function compareSelected() {
|
||||||
|
const local = list.find((e) => e.term == thisTerm);
|
||||||
|
const selectedItem = list.find((e) => e.term == selected);
|
||||||
|
if (local && selectedItem && (await compareData(local, selectedItem))) {
|
||||||
|
scheduleTask("update-plugin-list", 250, () => addOn.updatePluginList(local.documentPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function deleteSelected() {
|
||||||
|
const selectedItem = list.find((e) => e.term == selected);
|
||||||
|
const deletedPath = selectedItem.documentPath;
|
||||||
|
if (selectedItem && (await deleteData(selectedItem))) {
|
||||||
|
scheduleTask("update-plugin-list", 250, () => addOn.updatePluginList(deletedPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function duplicateItem() {
|
||||||
|
const local = list.find((e) => e.term == thisTerm);
|
||||||
|
const duplicateTermName = await askString(plugin.app, "Duplicate", "device name", "");
|
||||||
|
if (duplicateTermName) {
|
||||||
|
if (duplicateTermName.contains("/")) {
|
||||||
|
Logger(`We can not use "/" to the device name`, LOG_LEVEL.NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const key = `${plugin.app.vault.configDir}/${local.files[0].filename}`;
|
||||||
|
await addOn.storeCustomizationFiles(key as FilePath, duplicateTermName);
|
||||||
|
await addOn.updatePluginList(addOn.filenameToUnifiedKey(key, duplicateTermName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if terms.length > 0}
|
||||||
|
<span class="spacer" />
|
||||||
|
{#if !hidden}
|
||||||
|
<span class="messages">
|
||||||
|
<span class="message">{freshness}</span>
|
||||||
|
<span class="message">{equivalency}</span>
|
||||||
|
<span class="message">{version}</span>
|
||||||
|
</span>
|
||||||
|
<select bind:value={selected}>
|
||||||
|
<option value={""}>-</option>
|
||||||
|
{#each terms as term}
|
||||||
|
<option value={term}>{term}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{#if canApply || (isMaintenanceMode && selected != "")}
|
||||||
|
{#if canCompare}
|
||||||
|
<button on:click={compareSelected}>🔍</button>
|
||||||
|
{:else}
|
||||||
|
<button disabled />
|
||||||
|
{/if}
|
||||||
|
<button on:click={applySelected}>✓</button>
|
||||||
|
{:else}
|
||||||
|
<button disabled />
|
||||||
|
<button disabled />
|
||||||
|
{/if}
|
||||||
|
{#if isMaintenanceMode}
|
||||||
|
{#if selected != ""}
|
||||||
|
<button on:click={deleteSelected}>🗑️</button>
|
||||||
|
{:else}
|
||||||
|
<button on:click={duplicateItem}>📑</button>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<span class="spacer" />
|
||||||
|
<span class="message even">All devices are even</span>
|
||||||
|
<button disabled />
|
||||||
|
<button disabled />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.spacer {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
margin: 2px 4px;
|
||||||
|
min-width: 3em;
|
||||||
|
max-width: 4em;
|
||||||
|
}
|
||||||
|
button:disabled {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
background-color: transparent;
|
||||||
|
visibility: collapse;
|
||||||
|
}
|
||||||
|
button:disabled:hover {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
background-color: transparent;
|
||||||
|
visibility: collapse;
|
||||||
|
}
|
||||||
|
span.message {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: var(--font-ui-smaller);
|
||||||
|
padding: 0 1em;
|
||||||
|
line-height: var(--line-height-tight);
|
||||||
|
}
|
||||||
|
span.messages {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,309 +1,221 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { DevicePluginList, PluginDataEntry } from "./types";
|
|
||||||
import { versionNumberString2Number } from "./lib/src/strbin";
|
|
||||||
import ObsidianLiveSyncPlugin from "./main";
|
import ObsidianLiveSyncPlugin from "./main";
|
||||||
import { PluginAndTheirSettings } from "./CmdPluginAndTheirSettings";
|
import { PluginDataExDisplay, pluginIsEnumerating, pluginList } from "./CmdConfigSync";
|
||||||
|
import PluginCombo from "./PluginCombo.svelte";
|
||||||
type JudgeResult = "" | "NEWER" | "EVEN" | "EVEN_BUT_DIFFERENT" | "OLDER" | "REMOTE_ONLY";
|
|
||||||
|
|
||||||
interface PluginDataEntryDisp extends PluginDataEntry {
|
|
||||||
versionInfo: string;
|
|
||||||
mtimeInfo: string;
|
|
||||||
mtimeFlag: JudgeResult;
|
|
||||||
versionFlag: JudgeResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
export let plugin: ObsidianLiveSyncPlugin;
|
export let plugin: ObsidianLiveSyncPlugin;
|
||||||
let plugins: PluginDataEntry[] = [];
|
|
||||||
let deviceAndPlugins: { [key: string]: PluginDataEntryDisp[] } = {};
|
|
||||||
let devicePluginList: [string, PluginDataEntryDisp[]][] = null;
|
|
||||||
let ownPlugins: DevicePluginList = null;
|
|
||||||
let showOwnPlugins = false;
|
|
||||||
let targetList: { [key: string]: boolean } = {};
|
|
||||||
|
|
||||||
let addOn: PluginAndTheirSettings;
|
$: hideNotApplicable = true;
|
||||||
$: {
|
$: thisTerm = plugin.deviceAndVaultName;
|
||||||
const f = plugin.addOns.filter((e) => e instanceof PluginAndTheirSettings);
|
|
||||||
if (f && f.length > 0) {
|
|
||||||
addOn = f[0] as PluginAndTheirSettings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function saveTargetList() {
|
|
||||||
window.localStorage.setItem("ols-plugin-targetlist", JSON.stringify(targetList));
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadTargetList() {
|
const addOn = plugin.addOnConfigSync;
|
||||||
let e = window.localStorage.getItem("ols-plugin-targetlist") || "{}";
|
|
||||||
try {
|
|
||||||
targetList = JSON.parse(e);
|
|
||||||
} catch (_) {
|
|
||||||
// NO OP.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearSelection() {
|
let list: PluginDataExDisplay[] = [];
|
||||||
targetList = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateList() {
|
let selectNewestPulse = 0;
|
||||||
let x = await addOn.getPluginList();
|
let hideEven = true;
|
||||||
ownPlugins = x.thisDevicePlugins;
|
let loading = false;
|
||||||
plugins = Object.values(x.allPlugins);
|
let applyAllPluse = 0;
|
||||||
let targetListItems = Array.from(new Set(plugins.map((e) => e.deviceVaultName + "---" + e.manifest.id)));
|
let isMaintenanceMode = false;
|
||||||
let newTargetList: { [key: string]: boolean } = {};
|
async function requestUpdate() {
|
||||||
for (const id of targetListItems) {
|
await addOn.updatePluginList();
|
||||||
for (const tag of ["---plugin", "---setting"]) {
|
|
||||||
newTargetList[id + tag] = id + tag in targetList && targetList[id + tag];
|
|
||||||
}
|
}
|
||||||
|
async function requestReload() {
|
||||||
|
await addOn.reloadPluginList();
|
||||||
}
|
}
|
||||||
targetList = newTargetList;
|
pluginList.subscribe((e) => {
|
||||||
saveTargetList();
|
list = e;
|
||||||
}
|
});
|
||||||
|
pluginIsEnumerating.subscribe((e) => {
|
||||||
$: {
|
loading = e;
|
||||||
deviceAndPlugins = {};
|
});
|
||||||
for (const p of plugins) {
|
|
||||||
if (p.deviceVaultName == plugin.deviceAndVaultName && !showOwnPlugins) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!(p.deviceVaultName in deviceAndPlugins)) {
|
|
||||||
deviceAndPlugins[p.deviceVaultName] = [];
|
|
||||||
}
|
|
||||||
let dispInfo: PluginDataEntryDisp = {
|
|
||||||
...p,
|
|
||||||
versionInfo: "",
|
|
||||||
mtimeInfo: "",
|
|
||||||
versionFlag: "",
|
|
||||||
mtimeFlag: "",
|
|
||||||
};
|
|
||||||
dispInfo.versionInfo = p.manifest.version;
|
|
||||||
let x = new Date().getTime() / 1000;
|
|
||||||
let mtime = p.mtime / 1000;
|
|
||||||
let diff = (x - mtime) / 60;
|
|
||||||
if (p.mtime == 0) {
|
|
||||||
dispInfo.mtimeInfo = `-`;
|
|
||||||
} else if (diff < 60) {
|
|
||||||
dispInfo.mtimeInfo = `${diff | 0} Mins ago`;
|
|
||||||
} else if (diff < 60 * 24) {
|
|
||||||
dispInfo.mtimeInfo = `${(diff / 60) | 0} Hours ago`;
|
|
||||||
} else if (diff < 60 * 24 * 10) {
|
|
||||||
dispInfo.mtimeInfo = `${(diff / (60 * 24)) | 0} Days ago`;
|
|
||||||
} else {
|
|
||||||
dispInfo.mtimeInfo = new Date(dispInfo.mtime).toLocaleString();
|
|
||||||
}
|
|
||||||
// compare with own plugin
|
|
||||||
let id = p.manifest.id;
|
|
||||||
|
|
||||||
if (id in ownPlugins) {
|
|
||||||
// Which we have.
|
|
||||||
const ownPlugin = ownPlugins[id];
|
|
||||||
let localVer = versionNumberString2Number(ownPlugin.manifest.version);
|
|
||||||
let pluginVer = versionNumberString2Number(p.manifest.version);
|
|
||||||
if (localVer > pluginVer) {
|
|
||||||
dispInfo.versionFlag = "OLDER";
|
|
||||||
} else if (localVer == pluginVer) {
|
|
||||||
if (ownPlugin.manifestJson + (ownPlugin.styleCss ?? "") + ownPlugin.mainJs != p.manifestJson + (p.styleCss ?? "") + p.mainJs) {
|
|
||||||
dispInfo.versionFlag = "EVEN_BUT_DIFFERENT";
|
|
||||||
} else {
|
|
||||||
dispInfo.versionFlag = "EVEN";
|
|
||||||
}
|
|
||||||
} else if (localVer < pluginVer) {
|
|
||||||
dispInfo.versionFlag = "NEWER";
|
|
||||||
}
|
|
||||||
if ((ownPlugin.dataJson ?? "") == (p.dataJson ?? "")) {
|
|
||||||
if (ownPlugin.mtime == 0 && p.mtime == 0) {
|
|
||||||
dispInfo.mtimeFlag = "";
|
|
||||||
} else {
|
|
||||||
dispInfo.mtimeFlag = "EVEN";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (((ownPlugin.mtime / 1000) | 0) > ((p.mtime / 1000) | 0)) {
|
|
||||||
dispInfo.mtimeFlag = "OLDER";
|
|
||||||
} else if (((ownPlugin.mtime / 1000) | 0) == ((p.mtime / 1000) | 0)) {
|
|
||||||
dispInfo.mtimeFlag = "EVEN_BUT_DIFFERENT";
|
|
||||||
} else if (((ownPlugin.mtime / 1000) | 0) < ((p.mtime / 1000) | 0)) {
|
|
||||||
dispInfo.mtimeFlag = "NEWER";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dispInfo.versionFlag = "REMOTE_ONLY";
|
|
||||||
dispInfo.mtimeFlag = "REMOTE_ONLY";
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceAndPlugins[p.deviceVaultName].push(dispInfo);
|
|
||||||
}
|
|
||||||
devicePluginList = Object.entries(deviceAndPlugins);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDispString(stat: JudgeResult): string {
|
|
||||||
if (stat == "") return "";
|
|
||||||
if (stat == "NEWER") return " (Newer)";
|
|
||||||
if (stat == "OLDER") return " (Older)";
|
|
||||||
if (stat == "EVEN") return " (Even)";
|
|
||||||
if (stat == "EVEN_BUT_DIFFERENT") return " (Even but different)";
|
|
||||||
if (stat == "REMOTE_ONLY") return " (Remote Only)";
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
loadTargetList();
|
requestUpdate();
|
||||||
await updateList();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function toggleShowOwnPlugins() {
|
function filterList(list: PluginDataExDisplay[], categories: string[]) {
|
||||||
showOwnPlugins = !showOwnPlugins;
|
const w = list.filter((e) => categories.indexOf(e.category) !== -1);
|
||||||
|
return w.sort((a, b) => `${a.category}-${a.name}`.localeCompare(`${b.category}-${b.name}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleTarget(key: string) {
|
function groupBy(items: PluginDataExDisplay[], key: string) {
|
||||||
targetList[key] = !targetList[key];
|
let ret = {} as Record<string, PluginDataExDisplay[]>;
|
||||||
saveTargetList();
|
for (const v of items) {
|
||||||
}
|
|
||||||
|
|
||||||
function toggleAll(devicename: string) {
|
|
||||||
for (const c in targetList) {
|
|
||||||
if (c.startsWith(devicename)) {
|
|
||||||
targetList[c] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sweepPlugins() {
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
await plugin.app.plugins.loadManifests();
|
const k = (key in v ? v[key] : "") as string;
|
||||||
await addOn.sweepPlugin(true);
|
ret[k] = ret[k] || [];
|
||||||
updateList();
|
ret[k].push(v);
|
||||||
|
}
|
||||||
|
for (const k in ret) {
|
||||||
|
ret[k] = ret[k].sort((a, b) => `${a.category}-${a.name}`.localeCompare(`${b.category}-${b.name}`));
|
||||||
|
}
|
||||||
|
const w = Object.entries(ret);
|
||||||
|
return w.sort(([a], [b]) => `${a}`.localeCompare(`${b}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyPlugins() {
|
const displays = {
|
||||||
for (const c in targetList) {
|
CONFIG: "Configuration",
|
||||||
if (targetList[c] == true) {
|
THEME: "Themes",
|
||||||
const [deviceAndVault, id, opt] = c.split("---");
|
SNIPPET: "Snippets",
|
||||||
if (deviceAndVault in deviceAndPlugins) {
|
};
|
||||||
const entry = deviceAndPlugins[deviceAndVault].find((e) => e.manifest.id == id);
|
async function scanAgain() {
|
||||||
if (entry) {
|
await addOn.scanAllConfigFiles(true);
|
||||||
if (opt == "plugin") {
|
await requestUpdate();
|
||||||
if (entry.versionFlag != "EVEN") await addOn.applyPlugin(entry);
|
|
||||||
} else if (opt == "setting") {
|
|
||||||
if (entry.mtimeFlag != "EVEN") await addOn.applyPluginData(entry);
|
|
||||||
}
|
}
|
||||||
}
|
async function replicate() {
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//@ts-ignore
|
|
||||||
await plugin.app.plugins.loadManifests();
|
|
||||||
await addOn.sweepPlugin(true);
|
|
||||||
updateList();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkUpdates() {
|
|
||||||
await addOn.checkPluginUpdate();
|
|
||||||
}
|
|
||||||
async function replicateAndRefresh() {
|
|
||||||
await plugin.replicate(true);
|
await plugin.replicate(true);
|
||||||
updateList();
|
|
||||||
}
|
}
|
||||||
|
function selectAllNewest() {
|
||||||
|
selectNewestPulse++;
|
||||||
|
}
|
||||||
|
function applyAll() {
|
||||||
|
applyAllPluse++;
|
||||||
|
}
|
||||||
|
async function applyData(data: PluginDataExDisplay): Promise<boolean> {
|
||||||
|
return await addOn.applyData(data);
|
||||||
|
}
|
||||||
|
async function compareData(docA: PluginDataExDisplay, docB: PluginDataExDisplay): Promise<boolean> {
|
||||||
|
return await addOn.compareUsingDisplayData(docA, docB);
|
||||||
|
}
|
||||||
|
async function deleteData(data: PluginDataExDisplay): Promise<boolean> {
|
||||||
|
return await addOn.deleteData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: options = {
|
||||||
|
thisTerm,
|
||||||
|
hideNotApplicable,
|
||||||
|
selectNewest: selectNewestPulse,
|
||||||
|
applyAllPluse,
|
||||||
|
applyData,
|
||||||
|
compareData,
|
||||||
|
deleteData,
|
||||||
|
plugin,
|
||||||
|
isMaintenanceMode,
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h1>Plugins and their settings</h1>
|
<div>
|
||||||
<div class="ols-plugins-div-buttons">
|
<h1>Customization sync</h1>
|
||||||
Show own items
|
<div class="buttons">
|
||||||
<div class="checkbox-container" class:is-enabled={showOwnPlugins} on:click={toggleShowOwnPlugins} />
|
<button on:click={() => scanAgain()}>Scan changes</button>
|
||||||
|
<button on:click={() => replicate()}>Sync once</button>
|
||||||
|
<button on:click={() => requestUpdate()}>Refresh</button>
|
||||||
|
{#if isMaintenanceMode}
|
||||||
|
<button on:click={() => requestReload()}>Reload</button>
|
||||||
|
{/if}
|
||||||
|
<button on:click={() => selectAllNewest()}>Select All Shiny</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sls-plugins-wrap">
|
<div class="buttons">
|
||||||
<table class="sls-plugins-tbl">
|
<button on:click={() => applyAll()}>Apply All</button>
|
||||||
<tr style="position:sticky">
|
</div>
|
||||||
<th class="sls-plugins-tbl-device-head">Name</th>
|
</div>
|
||||||
<th class="sls-plugins-tbl-device-head">Info</th>
|
{#if loading}
|
||||||
<th class="sls-plugins-tbl-device-head">Target</th>
|
<div>
|
||||||
</tr>
|
<span>Updating list...</span>
|
||||||
{#if !devicePluginList}
|
|
||||||
<tr>
|
|
||||||
<td colspan="3" class="sls-table-tail tcenter"> Retrieving... </td>
|
|
||||||
</tr>
|
|
||||||
{:else if devicePluginList.length == 0}
|
|
||||||
<tr>
|
|
||||||
<td colspan="3" class="sls-table-tail tcenter"> No plugins found. </td>
|
|
||||||
</tr>
|
|
||||||
{:else}
|
|
||||||
{#each devicePluginList as [deviceName, devicePlugins]}
|
|
||||||
<tr>
|
|
||||||
<th colspan="2" class="sls-plugins-tbl-device-head">{deviceName}</th>
|
|
||||||
<th class="sls-plugins-tbl-device-head">
|
|
||||||
<button class="mod-cta" on:click={() => toggleAll(deviceName)}>✔</button>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
{#each devicePlugins as plugin}
|
|
||||||
<tr>
|
|
||||||
<td class="sls-table-head">{plugin.manifest.name}</td>
|
|
||||||
<td class="sls-table-tail tcenter">{plugin.versionInfo}{getDispString(plugin.versionFlag)}</td>
|
|
||||||
<td class="sls-table-tail tcenter">
|
|
||||||
{#if plugin.versionFlag === "EVEN" || plugin.versionFlag === ""}
|
|
||||||
-
|
|
||||||
{:else}
|
|
||||||
<div class="wrapToggle">
|
|
||||||
<div
|
|
||||||
class="checkbox-container"
|
|
||||||
class:is-enabled={targetList[plugin.deviceVaultName + "---" + plugin.manifest.id + "---plugin"]}
|
|
||||||
on:click={() => toggleTarget(plugin.deviceVaultName + "---" + plugin.manifest.id + "---plugin")}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
<div class="list">
|
||||||
</tr>
|
{#if list.length == 0}
|
||||||
<tr>
|
<div class="center">No Items.</div>
|
||||||
<td class="sls-table-head">Settings</td>
|
|
||||||
<td class="sls-table-tail tcenter">{plugin.mtimeInfo}{getDispString(plugin.mtimeFlag)}</td>
|
|
||||||
<td class="sls-table-tail tcenter">
|
|
||||||
{#if plugin.mtimeFlag === "EVEN" || plugin.mtimeFlag === ""}
|
|
||||||
-
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="wrapToggle">
|
{#each Object.entries(displays) as [key, label]}
|
||||||
<div
|
<div>
|
||||||
class="checkbox-container"
|
<h3>{label}</h3>
|
||||||
class:is-enabled={targetList[plugin.deviceVaultName + "---" + plugin.manifest.id + "---setting"]}
|
{#each groupBy(filterList(list, [key]), "name") as [name, listX]}
|
||||||
on:click={() => toggleTarget(plugin.deviceVaultName + "---" + plugin.manifest.id + "---setting")}
|
<div class="labelrow {hideEven ? 'hideeven' : ''}">
|
||||||
/>
|
<div class="title">
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
<PluginCombo {...options} list={listX} hidden={false} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="divider">
|
|
||||||
<th colspan="3" />
|
|
||||||
</tr>
|
|
||||||
{/each}
|
{/each}
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
<div>
|
||||||
|
<h3>Plugins</h3>
|
||||||
|
{#each groupBy(filterList(list, ["PLUGIN_MAIN", "PLUGIN_DATA", "PLUGIN_ETC"]), "name") as [name, listX]}
|
||||||
|
<div class="labelrow {hideEven ? 'hideeven' : ''}">
|
||||||
|
<div class="title">
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
<PluginCombo {...options} list={listX} hidden={true} />
|
||||||
|
</div>
|
||||||
|
<div class="filerow {hideEven ? 'hideeven' : ''}">
|
||||||
|
<div class="filetitle">Main</div>
|
||||||
|
<PluginCombo {...options} list={filterList(listX, ["PLUGIN_MAIN"])} hidden={false} />
|
||||||
|
</div>
|
||||||
|
<div class="filerow {hideEven ? 'hideeven' : ''}">
|
||||||
|
<div class="filetitle">Data</div>
|
||||||
|
<PluginCombo {...options} list={filterList(listX, ["PLUGIN_DATA"])} hidden={false} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="ols-plugins-div-buttons">
|
<div class="buttons">
|
||||||
<button class="" on:click={replicateAndRefresh}>Replicate and refresh</button>
|
<label><span>Hide not applicable items</span><input type="checkbox" bind:checked={hideEven} /></label>
|
||||||
<button class="" on:click={clearSelection}>Clear Selection</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
<div class="ols-plugins-div-buttons">
|
<label><span>Maintenance mode</span><input type="checkbox" bind:checked={isMaintenanceMode} /></label>
|
||||||
<button class="mod-cta" on:click={checkUpdates}>Check Updates</button>
|
|
||||||
<button class="mod-cta" on:click={sweepPlugins}>Scan installed</button>
|
|
||||||
<button class="mod-cta" on:click={applyPlugins}>Apply all</button>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="ols-plugins-div-buttons">-->
|
|
||||||
<!-- <button class="mod-warning" on:click={applyPlugins}>Delete all selected</button>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.ols-plugins-div-buttons {
|
.labelrow {
|
||||||
|
margin-left: 0.4em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
border-top: 1px solid var(--background-modifier-border);
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
.filerow {
|
||||||
|
margin-left: 1.25em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
.filerow.hideeven:has(.even),
|
||||||
|
.labelrow.hideeven:has(.even) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: var(--text-normal);
|
||||||
|
font-size: var(--font-ui-medium);
|
||||||
|
line-height: var(--line-height-tight);
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.filetitle {
|
||||||
|
color: var(--text-normal);
|
||||||
|
font-size: var(--font-ui-medium);
|
||||||
|
line-height: var(--line-height-tight);
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
.buttons > button {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.wrapToggle {
|
label {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-content: center;
|
align-items: center;
|
||||||
|
}
|
||||||
|
label > span {
|
||||||
|
margin-right: 0.25em;
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 3em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -64,7 +64,7 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
|||||||
}
|
}
|
||||||
// Watch raw events (Internal API)
|
// Watch raw events (Internal API)
|
||||||
watchVaultRawEvents(path: FilePath) {
|
watchVaultRawEvents(path: FilePath) {
|
||||||
if (!this.plugin.settings.syncInternalFiles) return;
|
if (!this.plugin.settings.syncInternalFiles && !this.plugin.settings.usePluginSync) return;
|
||||||
if (!this.plugin.settings.watchInternalFileChanges) return;
|
if (!this.plugin.settings.watchInternalFileChanges) return;
|
||||||
if (!path.startsWith(app.vault.configDir)) return;
|
if (!path.startsWith(app.vault.configDir)) return;
|
||||||
const ignorePatterns = this.plugin.settings.syncInternalFilesIgnorePatterns
|
const ignorePatterns = this.plugin.settings.syncInternalFilesIgnorePatterns
|
||||||
|
@ -3,6 +3,7 @@ import { FilePath } from "./lib/src/types";
|
|||||||
export {
|
export {
|
||||||
addIcon, App, DataWriteOptions, debounce, Editor, FuzzySuggestModal, MarkdownRenderer, MarkdownView, Modal, Notice, Platform, Plugin, PluginManifest,
|
addIcon, App, DataWriteOptions, debounce, Editor, FuzzySuggestModal, MarkdownRenderer, MarkdownView, Modal, Notice, Platform, Plugin, PluginManifest,
|
||||||
PluginSettingTab, Plugin_2, requestUrl, RequestUrlParam, RequestUrlResponse, sanitizeHTMLToDom, Setting, stringifyYaml, TAbstractFile, TextAreaComponent, TFile, TFolder,
|
PluginSettingTab, Plugin_2, requestUrl, RequestUrlParam, RequestUrlResponse, sanitizeHTMLToDom, Setting, stringifyYaml, TAbstractFile, TextAreaComponent, TFile, TFolder,
|
||||||
|
parseYaml
|
||||||
} from "obsidian";
|
} from "obsidian";
|
||||||
import {
|
import {
|
||||||
normalizePath as normalizePath_
|
normalizePath as normalizePath_
|
||||||
|
@ -9,6 +9,9 @@ export class PluginDialogModal extends Modal {
|
|||||||
plugin: ObsidianLiveSyncPlugin;
|
plugin: ObsidianLiveSyncPlugin;
|
||||||
logEl: HTMLDivElement;
|
logEl: HTMLDivElement;
|
||||||
component: PluginPane = null;
|
component: PluginPane = null;
|
||||||
|
isOpened() {
|
||||||
|
return this.component != null;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(app: App, plugin: ObsidianLiveSyncPlugin) {
|
constructor(app: App, plugin: ObsidianLiveSyncPlugin) {
|
||||||
super(app);
|
super(app);
|
||||||
@ -223,4 +226,4 @@ export function confirmWithMessage(plugin: Plugin, title: string, contentMd: str
|
|||||||
const dialog = new MessageBox(plugin, title, contentMd, buttons, defaultAction, timeout, (result) => res(result));
|
const dialog = new MessageBox(plugin, title, contentMd, buttons, defaultAction, timeout, (result) => res(result));
|
||||||
dialog.open();
|
dialog.open();
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
2
src/lib
2
src/lib
Submodule src/lib updated: c14ab28b4d...75f24a27b0
24
src/main.ts
24
src/main.ts
@ -11,7 +11,7 @@ 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, cancelAllPeriodicTask, cancelAllTasks, cancelTask, generatePatchObj, id2path, isObjectMargeApplicable, isSensibleMargeApplicable, flattenObject, path2id, scheduleTask, tryParseJSON, createFile, modifyFile, isValidPath, getAbstractFileByPath, touch, recentlyTouched, isIdOfInternalMetadata, isPluginMetadata, stripInternalMetadataPrefix, isChunk, askSelectString, askYesNo, askString, PeriodicProcessor, clearTouched, getPath, getPathWithoutPrefix, getPathFromTFile, localDatabaseCleanUp, balanceChunks, performRebuildDB } from "./utils";
|
import { applyPatch, cancelAllPeriodicTask, cancelAllTasks, cancelTask, generatePatchObj, id2path, isObjectMargeApplicable, isSensibleMargeApplicable, flattenObject, path2id, scheduleTask, tryParseJSON, createFile, modifyFile, isValidPath, getAbstractFileByPath, touch, recentlyTouched, isInternalMetadata, isPluginMetadata, stripInternalMetadataPrefix, isChunk, askSelectString, askYesNo, askString, PeriodicProcessor, clearTouched, getPath, getPathWithoutPrefix, getPathFromTFile, localDatabaseCleanUp, balanceChunks, performRebuildDB } from "./utils";
|
||||||
import { encrypt, tryDecrypt } from "./lib/src/e2ee_v2";
|
import { encrypt, tryDecrypt } from "./lib/src/e2ee_v2";
|
||||||
import { enableEncryption, isCloudantURI, isErrorOfMissingDoc, isValidRemoteCouchDBURI } from "./lib/src/utils_couchdb";
|
import { enableEncryption, isCloudantURI, isErrorOfMissingDoc, isValidRemoteCouchDBURI } from "./lib/src/utils_couchdb";
|
||||||
import { getGlobalStore, ObservableStore, observeStores } from "./lib/src/store";
|
import { getGlobalStore, ObservableStore, observeStores } from "./lib/src/store";
|
||||||
@ -26,9 +26,9 @@ import { LiveSyncLocalDB, LiveSyncLocalDBEnv } from "./lib/src/LiveSyncLocalDB";
|
|||||||
import { LiveSyncDBReplicator, LiveSyncReplicatorEnv } from "./lib/src/LiveSyncReplicator";
|
import { LiveSyncDBReplicator, LiveSyncReplicatorEnv } from "./lib/src/LiveSyncReplicator";
|
||||||
import { KeyValueDatabase, OpenKeyValueDatabase } from "./KeyValueDB";
|
import { KeyValueDatabase, OpenKeyValueDatabase } from "./KeyValueDB";
|
||||||
import { LiveSyncCommands } from "./LiveSyncCommands";
|
import { LiveSyncCommands } from "./LiveSyncCommands";
|
||||||
import { PluginAndTheirSettings } from "./CmdPluginAndTheirSettings";
|
|
||||||
import { HiddenFileSync } from "./CmdHiddenFileSync";
|
import { HiddenFileSync } from "./CmdHiddenFileSync";
|
||||||
import { SetupLiveSync } from "./CmdSetupLiveSync";
|
import { SetupLiveSync } from "./CmdSetupLiveSync";
|
||||||
|
import { ConfigSync } from "./CmdConfigSync";
|
||||||
import { confirmWithMessage } from "./dialogs";
|
import { confirmWithMessage } from "./dialogs";
|
||||||
|
|
||||||
setNoticeClass(Notice);
|
setNoticeClass(Notice);
|
||||||
@ -48,10 +48,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
packageVersion = "";
|
packageVersion = "";
|
||||||
manifestVersion = "";
|
manifestVersion = "";
|
||||||
|
|
||||||
addOnPluginAndTheirSettings = new PluginAndTheirSettings(this);
|
// addOnPluginAndTheirSettings = new PluginAndTheirSettings(this);
|
||||||
addOnHiddenFileSync = new HiddenFileSync(this);
|
addOnHiddenFileSync = new HiddenFileSync(this);
|
||||||
addOnSetup = new SetupLiveSync(this);
|
addOnSetup = new SetupLiveSync(this);
|
||||||
addOns = [this.addOnPluginAndTheirSettings, this.addOnHiddenFileSync, this.addOnSetup] as LiveSyncCommands[];
|
addOnConfigSync = new ConfigSync(this);
|
||||||
|
addOns = [this.addOnHiddenFileSync, this.addOnSetup, this.addOnConfigSync] as LiveSyncCommands[];
|
||||||
|
|
||||||
periodicSyncProcessor = new PeriodicProcessor(this, async () => await this.replicate());
|
periodicSyncProcessor = new PeriodicProcessor(this, async () => await this.replicate());
|
||||||
|
|
||||||
@ -206,7 +207,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
|
|
||||||
id2path(id: DocumentID, entry: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix {
|
id2path(id: DocumentID, entry: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix {
|
||||||
const tempId = id2path(id, entry);
|
const tempId = id2path(id, entry);
|
||||||
if (stripPrefix && isIdOfInternalMetadata(tempId)) {
|
if (stripPrefix && isInternalMetadata(tempId)) {
|
||||||
const out = stripInternalMetadataPrefix(tempId);
|
const out = stripInternalMetadataPrefix(tempId);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@ -342,7 +343,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
}
|
}
|
||||||
|
|
||||||
async resolveConflicted(target: FilePathWithPrefix) {
|
async resolveConflicted(target: FilePathWithPrefix) {
|
||||||
if (isIdOfInternalMetadata(target)) {
|
if (isInternalMetadata(target)) {
|
||||||
await this.addOnHiddenFileSync.resolveConflictOnInternalFile(target);
|
await this.addOnHiddenFileSync.resolveConflictOnInternalFile(target);
|
||||||
} else if (isPluginMetadata(target)) {
|
} else if (isPluginMetadata(target)) {
|
||||||
await this.resolveConflictByNewerEntry(target);
|
await this.resolveConflictByNewerEntry(target);
|
||||||
@ -906,6 +907,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
await this.kvDB.set(keyD2, mtime);
|
await this.kvDB.set(keyD2, mtime);
|
||||||
} else if (queue.type == "INTERNAL") {
|
} else if (queue.type == "INTERNAL") {
|
||||||
await this.addOnHiddenFileSync.watchVaultRawEventsAsync(file.path);
|
await this.addOnHiddenFileSync.watchVaultRawEventsAsync(file.path);
|
||||||
|
await this.addOnConfigSync.watchVaultRawEventsAsync(file.path);
|
||||||
} else {
|
} else {
|
||||||
const targetFile = this.app.vault.getAbstractFileByPath(file.path);
|
const targetFile = this.app.vault.getAbstractFileByPath(file.path);
|
||||||
if (!(targetFile instanceof TFile)) {
|
if (!(targetFile instanceof TFile)) {
|
||||||
@ -1283,7 +1285,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
if (queue.missingChildren.length == 0) {
|
if (queue.missingChildren.length == 0) {
|
||||||
queue.done = true;
|
queue.done = true;
|
||||||
if (isIdOfInternalMetadata(queue.entry._id)) {
|
if (isInternalMetadata(queue.entry._id)) {
|
||||||
//system file
|
//system file
|
||||||
const filename = this.getPathWithoutPrefix(queue.entry);
|
const filename = this.getPathWithoutPrefix(queue.entry);
|
||||||
this.addOnHiddenFileSync.procInternalFile(filename);
|
this.addOnHiddenFileSync.procInternalFile(filename);
|
||||||
@ -1328,7 +1330,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
if (!this.isTargetFile(path)) return;
|
if (!this.isTargetFile(path)) return;
|
||||||
const skipOldFile = this.settings.skipOlderFilesOnSync && false; //patched temporary.
|
const skipOldFile = this.settings.skipOlderFilesOnSync && false; //patched temporary.
|
||||||
// Do not handle internal files if the feature has not been enabled.
|
// Do not handle internal files if the feature has not been enabled.
|
||||||
if (isIdOfInternalMetadata(doc._id) && !this.settings.syncInternalFiles) return;
|
if (isInternalMetadata(doc._id) && !this.settings.syncInternalFiles) return;
|
||||||
// It is better for your own safety, not to handle the following files
|
// It is better for your own safety, not to handle the following files
|
||||||
const ignoreFiles = [
|
const ignoreFiles = [
|
||||||
"_design/replicate",
|
"_design/replicate",
|
||||||
@ -1336,11 +1338,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
FLAGMD_REDFLAG2,
|
FLAGMD_REDFLAG2,
|
||||||
FLAGMD_REDFLAG3
|
FLAGMD_REDFLAG3
|
||||||
];
|
];
|
||||||
if (!isIdOfInternalMetadata(doc._id) && ignoreFiles.contains(path)) {
|
if (!isInternalMetadata(doc._id) && ignoreFiles.contains(path)) {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
}
|
}
|
||||||
if ((!isIdOfInternalMetadata(doc._id)) && skipOldFile) {
|
if ((!isInternalMetadata(doc._id)) && skipOldFile) {
|
||||||
const info = getAbstractFileByPath(stripAllPrefixes(path));
|
const info = getAbstractFileByPath(stripAllPrefixes(path));
|
||||||
|
|
||||||
if (info && info instanceof TFile) {
|
if (info && info instanceof TFile) {
|
||||||
@ -2227,7 +2229,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
const dK = `${file.path}-diff`;
|
const dK = `${file.path}-diff`;
|
||||||
const isLastDiff = dK in caches ? caches[dK] : { storageMtime: 0, docMtime: 0 };
|
const isLastDiff = dK in caches ? caches[dK] : { storageMtime: 0, docMtime: 0 };
|
||||||
if (isLastDiff.docMtime == docMtime && isLastDiff.storageMtime == storageMtime) {
|
if (isLastDiff.docMtime == docMtime && isLastDiff.storageMtime == storageMtime) {
|
||||||
Logger("STORAGE .. DB :" + file.path, LOG_LEVEL.VERBOSE);
|
// Logger("STORAGE .. DB :" + file.path, LOG_LEVEL.VERBOSE);
|
||||||
caches[dK] = { storageMtime, docMtime };
|
caches[dK] = { storageMtime, docMtime };
|
||||||
return caches;
|
return caches;
|
||||||
}
|
}
|
||||||
|
@ -62,13 +62,21 @@ export type FileEventItem = {
|
|||||||
key: string,
|
key: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hidden items (Now means `chunk`)
|
||||||
export const CHeader = "h:";
|
export const CHeader = "h:";
|
||||||
|
|
||||||
|
// Plug-in Stored Container (Obsolete)
|
||||||
export const PSCHeader = "ps:";
|
export const PSCHeader = "ps:";
|
||||||
export const PSCHeaderEnd = "ps;";
|
export const PSCHeaderEnd = "ps;";
|
||||||
|
|
||||||
|
// Internal data Container
|
||||||
export const ICHeader = "i:";
|
export const ICHeader = "i:";
|
||||||
export const ICHeaderEnd = "i;";
|
export const ICHeaderEnd = "i;";
|
||||||
export const ICHeaderLength = ICHeader.length;
|
export const ICHeaderLength = ICHeader.length;
|
||||||
|
|
||||||
|
// Internal data Container (eXtended)
|
||||||
|
export const ICXHeader = "ix:";
|
||||||
|
|
||||||
export const FileWatchEventQueueMax = 10;
|
export const FileWatchEventQueueMax = 10;
|
||||||
export const configURIBase = "obsidian://setuplivesync?settings=";
|
export const configURIBase = "obsidian://setuplivesync?settings=";
|
||||||
|
|
||||||
|
11
src/utils.ts
11
src/utils.ts
@ -234,8 +234,8 @@ export function applyPatch(from: Record<string | number | symbol, any>, patch: R
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function mergeObject(
|
export function mergeObject(
|
||||||
objA: Record<string | number | symbol, any>,
|
objA: Record<string | number | symbol, any> | [any],
|
||||||
objB: Record<string | number | symbol, any>
|
objB: Record<string | number | symbol, any> | [any]
|
||||||
) {
|
) {
|
||||||
const newEntries = Object.entries(objB);
|
const newEntries = Object.entries(objB);
|
||||||
const ret: any = { ...objA };
|
const ret: any = { ...objA };
|
||||||
@ -278,6 +278,11 @@ export function mergeObject(
|
|||||||
ret[key] = v;
|
ret[key] = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (Array.isArray(objA) && Array.isArray(objB)) {
|
||||||
|
return Object.values(Object.entries(ret)
|
||||||
|
.sort()
|
||||||
|
.reduce((p, [key, value]) => ({ ...p, [key]: value }), {}));
|
||||||
|
}
|
||||||
return Object.entries(ret)
|
return Object.entries(ret)
|
||||||
.sort()
|
.sort()
|
||||||
.reduce((p, [key, value]) => ({ ...p, [key]: value }), {});
|
.reduce((p, [key, value]) => ({ ...p, [key]: value }), {});
|
||||||
@ -362,7 +367,7 @@ export function clearTouched() {
|
|||||||
* @param id ID
|
* @param id ID
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function isIdOfInternalMetadata(id: FilePath | FilePathWithPrefix | DocumentID): boolean {
|
export function isInternalMetadata(id: FilePath | FilePathWithPrefix | DocumentID): boolean {
|
||||||
return id.startsWith(ICHeader);
|
return id.startsWith(ICHeader);
|
||||||
}
|
}
|
||||||
export function stripInternalMetadataPrefix<T extends FilePath | FilePathWithPrefix | DocumentID>(id: T): T {
|
export function stripInternalMetadataPrefix<T extends FilePath | FilePathWithPrefix | DocumentID>(id: T): T {
|
||||||
|
Reference in New Issue
Block a user