mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2025-02-07 19:30:08 +02:00
New feature:
- The metadata of the deleted files will be kept on the database by default. - We can see the history of deleted files. - Update information became to be shown on the major upgrade. Fixed: - `Pick file to show` was renamed to `Pick a file to show. - Files in the `Pick a file to show` are now ordered by their modified date descent.
This commit is contained in:
parent
c610284cab
commit
ac9be937b4
@ -13,6 +13,7 @@ if you want to view the source, please visit the github repository of this plugi
|
||||
const prod = process.argv[2] === "production";
|
||||
const manifestJson = JSON.parse(fs.readFileSync("./manifest.json"));
|
||||
const packageJson = JSON.parse(fs.readFileSync("./package.json"));
|
||||
const updateInfo = JSON.stringify(fs.readFileSync("./updates.md") + "");
|
||||
esbuild
|
||||
.build({
|
||||
banner: {
|
||||
@ -23,6 +24,7 @@ esbuild
|
||||
define: {
|
||||
"MANIFEST_VERSION": `"${manifestJson.version}"`,
|
||||
"PACKAGE_VERSION": `"${packageJson.version}"`,
|
||||
"UPDATE_INFO": `${updateInfo}`,
|
||||
},
|
||||
external: ["obsidian", "electron", ...builtins],
|
||||
format: "cjs",
|
||||
|
@ -19,6 +19,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
revs_info: PouchDB.Core.RevisionInfo[] = [];
|
||||
currentDoc: LoadedEntry;
|
||||
currentText = "";
|
||||
currentDeleted = false;
|
||||
|
||||
constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile | string) {
|
||||
super(app);
|
||||
@ -41,10 +42,11 @@ export class DocumentHistoryModal extends Modal {
|
||||
const db = this.plugin.localDatabase;
|
||||
const index = this.revs_info.length - 1 - (this.range.value as any) / 1;
|
||||
const rev = this.revs_info[index];
|
||||
const w = await db.getDBEntry(path2id(this.file), { rev: rev.rev }, false, false);
|
||||
const w = await db.getDBEntry(path2id(this.file), { rev: rev.rev }, false, false, true);
|
||||
this.currentText = "";
|
||||
|
||||
this.currentDeleted = false;
|
||||
if (w === false) {
|
||||
this.currentDeleted = true;
|
||||
this.info.innerHTML = "";
|
||||
this.contentView.innerHTML = `Could not read this revision<br>(${rev.rev})`;
|
||||
} else {
|
||||
@ -52,13 +54,13 @@ export class DocumentHistoryModal extends Modal {
|
||||
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
|
||||
let result = "";
|
||||
const w1data = w.datatype == "plain" ? w.data : base64ToString(w.data);
|
||||
|
||||
this.currentDeleted = w.deleted;
|
||||
this.currentText = w1data;
|
||||
if (this.showDiff) {
|
||||
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
|
||||
if (prevRevIdx >= 0 && prevRevIdx < this.revs_info.length) {
|
||||
const oldRev = this.revs_info[prevRevIdx].rev;
|
||||
const w2 = await db.getDBEntry(path2id(this.file), { rev: oldRev }, false, false);
|
||||
const w2 = await db.getDBEntry(path2id(this.file), { rev: oldRev }, false, false, true);
|
||||
if (w2 != false) {
|
||||
const dmp = new diff_match_patch();
|
||||
const w2data = w2.datatype == "plain" ? w2.data : base64ToString(w2.data);
|
||||
@ -86,7 +88,8 @@ export class DocumentHistoryModal extends Modal {
|
||||
} else {
|
||||
result = escapeStringToHTML(w1data);
|
||||
}
|
||||
this.contentView.innerHTML = result;
|
||||
this.contentView.innerHTML = (this.currentDeleted ? "(At this revision, the file has been deleted)\n" : "") + result;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,8 +35,8 @@ import { LRUCache } from "./lib/src/LRUCache";
|
||||
|
||||
const currentVersionRange: ChunkVersionRange = {
|
||||
min: 0,
|
||||
max: 1,
|
||||
current: 1,
|
||||
max: 2,
|
||||
current: 2,
|
||||
}
|
||||
|
||||
type ReplicationCallback = (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>;
|
||||
@ -69,7 +69,9 @@ export class LocalPouchDB {
|
||||
|
||||
isMobile = false;
|
||||
|
||||
chunkVersion = 0;
|
||||
chunkVersion = -1;
|
||||
maxChunkVersion = -1;
|
||||
minChunkVersion = -1;
|
||||
|
||||
cancelHandler<T extends PouchDB.Core.Changes<EntryDoc> | PouchDB.Replication.Sync<EntryDoc> | PouchDB.Replication.Replication<EntryDoc>>(handler: T): T {
|
||||
if (handler != null) {
|
||||
@ -296,7 +298,7 @@ export class LocalPouchDB {
|
||||
}
|
||||
}
|
||||
|
||||
async getDBEntryMeta(path: string, opt?: PouchDB.Core.GetOptions): Promise<false | LoadedEntry> {
|
||||
async getDBEntryMeta(path: string, opt?: PouchDB.Core.GetOptions, includeDeleted = false): Promise<false | LoadedEntry> {
|
||||
const id = path2id(path);
|
||||
try {
|
||||
let obj: EntryDocResponse = null;
|
||||
@ -306,6 +308,7 @@ export class LocalPouchDB {
|
||||
obj = await this.localDatabase.get(id);
|
||||
}
|
||||
const deleted = "deleted" in obj ? obj.deleted : undefined;
|
||||
if (!includeDeleted && deleted) return false;
|
||||
if (obj.type && obj.type == "leaf") {
|
||||
//do nothing for leaf;
|
||||
return false;
|
||||
@ -326,7 +329,7 @@ export class LocalPouchDB {
|
||||
ctime: note.ctime,
|
||||
mtime: note.mtime,
|
||||
size: note.size,
|
||||
_deleted: obj._deleted,
|
||||
// _deleted: obj._deleted,
|
||||
_rev: obj._rev,
|
||||
_conflicts: obj._conflicts,
|
||||
children: children,
|
||||
@ -344,7 +347,7 @@ export class LocalPouchDB {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
async getDBEntry(path: string, opt?: PouchDB.Core.GetOptions, dump = false, waitForReady = true): Promise<false | LoadedEntry> {
|
||||
async getDBEntry(path: string, opt?: PouchDB.Core.GetOptions, dump = false, waitForReady = true, includeDeleted = false): Promise<false | LoadedEntry> {
|
||||
const id = path2id(path);
|
||||
try {
|
||||
let obj: EntryDocResponse = null;
|
||||
@ -354,7 +357,7 @@ export class LocalPouchDB {
|
||||
obj = await this.localDatabase.get(id);
|
||||
}
|
||||
const deleted = "deleted" in obj ? obj.deleted : undefined;
|
||||
|
||||
if (!includeDeleted && deleted) return false;
|
||||
if (obj.type && obj.type == "leaf") {
|
||||
//do nothing for leaf;
|
||||
return false;
|
||||
@ -369,7 +372,7 @@ export class LocalPouchDB {
|
||||
ctime: note.ctime,
|
||||
mtime: note.mtime,
|
||||
size: note.size,
|
||||
_deleted: obj._deleted,
|
||||
// _deleted: obj._deleted,
|
||||
_rev: obj._rev,
|
||||
_conflicts: obj._conflicts,
|
||||
children: [],
|
||||
@ -415,7 +418,7 @@ export class LocalPouchDB {
|
||||
ctime: obj.ctime,
|
||||
mtime: obj.mtime,
|
||||
size: obj.size,
|
||||
_deleted: obj._deleted,
|
||||
// _deleted: obj._deleted,
|
||||
_rev: obj._rev,
|
||||
children: obj.children,
|
||||
datatype: obj.type,
|
||||
@ -476,7 +479,11 @@ export class LocalPouchDB {
|
||||
// simple note
|
||||
}
|
||||
if (obj.type == "newnote" || obj.type == "plain") {
|
||||
obj._deleted = true;
|
||||
obj.deleted = true;
|
||||
if (this.settings.deleteMetadataOfDeletedFiles) {
|
||||
obj._deleted = true;
|
||||
}
|
||||
obj.mtime = Date.now();
|
||||
const r = await this.localDatabase.put(obj);
|
||||
Logger(`entry removed:${obj._id}-${r.rev}`);
|
||||
if (typeof this.corruptedEntries[obj._id] != "undefined") {
|
||||
@ -528,7 +535,15 @@ export class LocalPouchDB {
|
||||
try {
|
||||
await runWithLock("file:" + v, false, async () => {
|
||||
const item = await this.localDatabase.get(v);
|
||||
item._deleted = true;
|
||||
if (item.type == "newnote" || item.type == "plain") {
|
||||
item.deleted = true;
|
||||
if (this.settings.deleteMetadataOfDeletedFiles) {
|
||||
item._deleted = true;
|
||||
}
|
||||
item.mtime = Date.now();
|
||||
} else {
|
||||
item._deleted = true;
|
||||
}
|
||||
await this.localDatabase.put(item);
|
||||
});
|
||||
|
||||
@ -777,16 +792,31 @@ export class LocalPouchDB {
|
||||
remoteMilestone.node_chunk_info = { ...defMilestonePoint.node_chunk_info, ...remoteMilestone.node_chunk_info };
|
||||
this.remoteLocked = remoteMilestone.locked;
|
||||
this.remoteLockedAndDeviceNotAccepted = remoteMilestone.locked && remoteMilestone.accepted_nodes.indexOf(this.nodeid) == -1;
|
||||
const writeMilestone = ((remoteMilestone.node_chunk_info[this.nodeid].min != currentVersionRange.min || remoteMilestone.node_chunk_info[this.nodeid].max != currentVersionRange.max)
|
||||
const writeMilestone = (
|
||||
(
|
||||
remoteMilestone.node_chunk_info[this.nodeid].min != currentVersionRange.min
|
||||
|| remoteMilestone.node_chunk_info[this.nodeid].max != currentVersionRange.max
|
||||
)
|
||||
|| typeof remoteMilestone._rev == "undefined");
|
||||
|
||||
if (writeMilestone) {
|
||||
remoteMilestone.node_chunk_info[this.nodeid].min = currentVersionRange.min;
|
||||
remoteMilestone.node_chunk_info[this.nodeid].max = currentVersionRange.max;
|
||||
await dbret.db.put(remoteMilestone);
|
||||
}
|
||||
|
||||
// Check compatibility and make sure available version
|
||||
//
|
||||
// v min of A v max of A
|
||||
// | v min of B | v max of B
|
||||
// | | | |
|
||||
// | |<--- We can use --->| |
|
||||
// | | | |
|
||||
//If globalMin and globalMax is suitable, we can upgrade.
|
||||
let globalMin = currentVersionRange.min;
|
||||
let globalMax = currentVersionRange.max;
|
||||
for (const nodeid of remoteMilestone.accepted_nodes) {
|
||||
if (nodeid == this.nodeid) continue;
|
||||
if (nodeid in remoteMilestone.node_chunk_info) {
|
||||
const nodeinfo = remoteMilestone.node_chunk_info[nodeid];
|
||||
globalMin = Math.max(nodeinfo.min, globalMin);
|
||||
@ -796,7 +826,15 @@ export class LocalPouchDB {
|
||||
globalMax = 0;
|
||||
}
|
||||
}
|
||||
//If globalMin and globalMax is suitable, we can upgrade.
|
||||
this.maxChunkVersion = globalMax;
|
||||
this.minChunkVersion = globalMin;
|
||||
|
||||
if (this.chunkVersion >= 0 && (globalMin > this.chunkVersion || globalMax < this.chunkVersion)) {
|
||||
if (!setting.ignoreVersionCheck) {
|
||||
Logger("The remote database has no compatibility with the running version. Please upgrade the plugin.", LOG_LEVEL.NOTICE);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (remoteMilestone.locked && remoteMilestone.accepted_nodes.indexOf(this.nodeid) == -1) {
|
||||
Logger("The remote database has been rebuilt or corrupted since we have synchronized last time. Fetch rebuilt DB or explicit unlocking is required. See the settings dialog.", LOG_LEVEL.NOTICE);
|
||||
@ -1242,4 +1280,12 @@ export class LocalPouchDB {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
isVersionUpgradable(ver: number) {
|
||||
if (this.maxChunkVersion < 0) return false;
|
||||
if (this.minChunkVersion < 0) return false;
|
||||
if (this.maxChunkVersion > 0 && this.maxChunkVersion < ver) return false;
|
||||
if (this.minChunkVersion > 0 && this.minChunkVersion > ver) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, RequestUrlParam, requestUrl, TextAreaComponent } from "obsidian";
|
||||
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, RequestUrlParam, requestUrl, TextAreaComponent, MarkdownRenderer } from "obsidian";
|
||||
import { EntryDoc, LOG_LEVEL, RemoteDBSettings } from "./lib/src/types";
|
||||
import { path2id, id2path } from "./utils";
|
||||
import { delay, runWithLock } from "./lib/src/utils";
|
||||
import { delay, runWithLock, versionNumberString2Number } from "./lib/src/utils";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { checkSyncInfo, connectRemoteCouchDBWithSetting } from "./utils_couchdb";
|
||||
import { testCrypt } from "./lib/src/e2ee_v2";
|
||||
@ -39,7 +39,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
};
|
||||
w.addClass("sls-setting-menu");
|
||||
w.innerHTML = `
|
||||
<label class='sls-setting-label selected'><input type='radio' name='disp' value='0' class='sls-setting-tab' checked><div class='sls-setting-menu-btn'>🛰️</div></label>
|
||||
<label class='sls-setting-label selected'><input type='radio' name='disp' value='100' class='sls-setting-tab' checked><div class='sls-setting-menu-btn'>💬</div></label>
|
||||
<label class='sls-setting-label'><input type='radio' name='disp' value='0' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🛰️</div></label>
|
||||
<label class='sls-setting-label'><input type='radio' name='disp' value='10' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>📦</div></label>
|
||||
<label class='sls-setting-label'><input type='radio' name='disp' value='20' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>⚙️</div></label>
|
||||
<label class='sls-setting-label'><input type='radio' name='disp' value='30' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🔁</div></label>
|
||||
@ -68,6 +69,34 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
});
|
||||
});
|
||||
|
||||
const containerInformationEl = containerEl.createDiv();
|
||||
const h3El = containerInformationEl.createEl("h3", { text: "Updates" });
|
||||
const informationDivEl = containerInformationEl.createEl("div", { text: "" });
|
||||
|
||||
//@ts-ignore
|
||||
const manifestVersion: string = MANIFEST_VERSION || "-";
|
||||
//@ts-ignore
|
||||
const updateInformation: string = UPDATE_INFO || "";
|
||||
|
||||
const lastVersion = ~~(versionNumberString2Number(manifestVersion) / 1000);
|
||||
|
||||
const tmpDiv = createSpan();
|
||||
tmpDiv.addClass("sls-header-button");
|
||||
tmpDiv.innerHTML = `<button> OK, I read all. </button>`;
|
||||
if (lastVersion > this.plugin.settings.lastReadUpdates) {
|
||||
const informationButtonDiv = h3El.appendChild(tmpDiv);
|
||||
informationButtonDiv.querySelector("button").addEventListener("click", async () => {
|
||||
this.plugin.settings.lastReadUpdates = lastVersion;
|
||||
await this.plugin.saveSettings();
|
||||
informationButtonDiv.remove();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
MarkdownRenderer.renderMarkdown(updateInformation, informationDivEl, "/", null);
|
||||
|
||||
|
||||
addScreenElement("100", containerInformationEl);
|
||||
const containerRemoteDatabaseEl = containerEl.createDiv();
|
||||
containerRemoteDatabaseEl.createEl("h3", { text: "Remote Database configuration" });
|
||||
const syncWarn = containerRemoteDatabaseEl.createEl("div", { text: `These settings are kept locked while automatic synchronization options are enabled. Disable these options in the "Sync Settings" tab to unlock.` });
|
||||
@ -639,6 +668,15 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new Setting(containerGeneralSettingsEl)
|
||||
.setName("Delete metadata of deleted files.")
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(this.plugin.settings.deleteMetadataOfDeletedFiles).onChange(async (value) => {
|
||||
this.plugin.settings.deleteMetadataOfDeletedFiles = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
addScreenElement("20", containerGeneralSettingsEl);
|
||||
const containerSyncSettingEl = containerEl.createDiv();
|
||||
@ -1295,6 +1333,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
applyDisplayEnabled();
|
||||
addScreenElement("70", containerCorruptedDataEl);
|
||||
changeDisplay("0");
|
||||
if (lastVersion != this.plugin.settings.lastReadUpdates) {
|
||||
changeDisplay("100");
|
||||
} else {
|
||||
changeDisplay("0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
126
src/dialogs.ts
Normal file
126
src/dialogs.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import { App, FuzzySuggestModal, Modal, Setting } from "obsidian";
|
||||
import ObsidianLiveSyncPlugin from "./main";
|
||||
|
||||
//@ts-ignore
|
||||
import PluginPane from "./PluginPane.svelte";
|
||||
|
||||
export class PluginDialogModal extends Modal {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
logEl: HTMLDivElement;
|
||||
component: PluginPane = null;
|
||||
|
||||
constructor(app: App, plugin: ObsidianLiveSyncPlugin) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
if (this.component == null) {
|
||||
this.component = new PluginPane({
|
||||
target: contentEl,
|
||||
props: { plugin: this.plugin },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onClose() {
|
||||
if (this.component != null) {
|
||||
this.component.$destroy();
|
||||
this.component = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class InputStringDialog extends Modal {
|
||||
result: string | false = false;
|
||||
onSubmit: (result: string | boolean) => void;
|
||||
title: string;
|
||||
key: string;
|
||||
placeholder: string;
|
||||
isManuallyClosed = false;
|
||||
|
||||
constructor(app: App, title: string, key: string, placeholder: string, onSubmit: (result: string | false) => void) {
|
||||
super(app);
|
||||
this.onSubmit = onSubmit;
|
||||
this.title = title;
|
||||
this.placeholder = placeholder;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
|
||||
contentEl.createEl("h1", { text: this.title });
|
||||
|
||||
new Setting(contentEl).setName(this.key).addText((text) =>
|
||||
text.onChange((value) => {
|
||||
this.result = value;
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(contentEl).addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("Ok")
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.isManuallyClosed = true;
|
||||
this.close();
|
||||
})
|
||||
).addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("Cancel")
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.close();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
if (this.isManuallyClosed) {
|
||||
this.onSubmit(this.result);
|
||||
} else {
|
||||
this.onSubmit(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
export class PopoverSelectString extends FuzzySuggestModal<string> {
|
||||
app: App;
|
||||
callback: (e: string) => void = () => { };
|
||||
getItemsFun: () => string[] = () => {
|
||||
return ["yes", "no"];
|
||||
|
||||
}
|
||||
|
||||
constructor(app: App, note: string, placeholder: string | null, getItemsFun: () => string[], callback: (e: string) => void) {
|
||||
super(app);
|
||||
this.app = app;
|
||||
this.setPlaceholder(placeholder ?? "y/n) " + note);
|
||||
if (getItemsFun) this.getItemsFun = getItemsFun;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
getItems(): string[] {
|
||||
return this.getItemsFun();
|
||||
}
|
||||
|
||||
getItemText(item: string): string {
|
||||
return item;
|
||||
}
|
||||
|
||||
onChooseItem(item: string, evt: MouseEvent | KeyboardEvent): void {
|
||||
// debugger;
|
||||
this.callback(item);
|
||||
this.callback = null;
|
||||
}
|
||||
onClose(): void {
|
||||
setTimeout(() => {
|
||||
if (this.callback != null) {
|
||||
this.callback("");
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
2
src/lib
2
src/lib
@ -1 +1 @@
|
||||
Subproject commit 1133f82732cd69721f6bc1dc8435b2e41af23d8d
|
||||
Subproject commit a49a096a6a6d93185bb0a590b3e84e6d7c5431d0
|
238
src/main.ts
238
src/main.ts
@ -1,4 +1,4 @@
|
||||
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, Modal, App, FuzzySuggestModal, Setting } from "obsidian";
|
||||
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, App, } from "obsidian";
|
||||
import { diff_match_patch } from "diff-match-patch";
|
||||
|
||||
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, InternalFileEntry } from "./lib/src/types";
|
||||
@ -27,137 +27,51 @@ import { ConflictResolveModal } from "./ConflictResolveModal";
|
||||
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
||||
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
||||
|
||||
//@ts-ignore
|
||||
import PluginPane from "./PluginPane.svelte";
|
||||
|
||||
|
||||
import { clearAllPeriodic, clearAllTriggers, disposeMemoObject, id2path, memoIfNotExist, memoObject, path2id, retriveMemoObject, setTrigger } from "./utils";
|
||||
import { decrypt, encrypt } from "./lib/src/e2ee_v2";
|
||||
|
||||
const isDebug = false;
|
||||
|
||||
import { InputStringDialog, PluginDialogModal, PopoverSelectString } from "./dialogs";
|
||||
|
||||
setNoticeClass(Notice);
|
||||
|
||||
class PluginDialogModal extends Modal {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
logEl: HTMLDivElement;
|
||||
component: PluginPane = null;
|
||||
const ICHeader = "i:";
|
||||
const ICHeaderEnd = "i;";
|
||||
const ICHeaderLength = ICHeader.length;
|
||||
|
||||
constructor(app: App, plugin: ObsidianLiveSyncPlugin) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
if (this.component == null) {
|
||||
this.component = new PluginPane({
|
||||
target: contentEl,
|
||||
props: { plugin: this.plugin },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onClose() {
|
||||
if (this.component != null) {
|
||||
this.component.$destroy();
|
||||
this.component = null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* returns is internal chunk of file
|
||||
* @param str ID
|
||||
* @returns
|
||||
*/
|
||||
function isInteralChunk(str: string): boolean {
|
||||
return str.startsWith(ICHeader);
|
||||
}
|
||||
function id2filenameInternalChunk(str: string): string {
|
||||
return str.substring(ICHeaderLength);
|
||||
}
|
||||
function filename2idInternalChunk(str: string): string {
|
||||
return ICHeader + str;
|
||||
}
|
||||
|
||||
class InputStringDialog extends Modal {
|
||||
result: string | false = false;
|
||||
onSubmit: (result: string | boolean) => void;
|
||||
title: string;
|
||||
key: string;
|
||||
placeholder: string;
|
||||
isManuallyClosed = false;
|
||||
|
||||
constructor(app: App, title: string, key: string, placeholder: string, onSubmit: (result: string | false) => void) {
|
||||
super(app);
|
||||
this.onSubmit = onSubmit;
|
||||
this.title = title;
|
||||
this.placeholder = placeholder;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
|
||||
contentEl.createEl("h1", { text: this.title });
|
||||
|
||||
new Setting(contentEl).setName(this.key).addText((text) =>
|
||||
text.onChange((value) => {
|
||||
this.result = value;
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(contentEl).addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("Ok")
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.isManuallyClosed = true;
|
||||
this.close();
|
||||
})
|
||||
).addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("Cancel")
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.close();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
if (this.isManuallyClosed) {
|
||||
this.onSubmit(this.result);
|
||||
} else {
|
||||
this.onSubmit(false);
|
||||
}
|
||||
}
|
||||
const CHeader = "h:";
|
||||
const CHeaderEnd = "h;";
|
||||
// const CHeaderLength = CHeader.length;
|
||||
function isChunk(str: string): boolean {
|
||||
return str.startsWith(CHeader);
|
||||
}
|
||||
class PopoverSelectString extends FuzzySuggestModal<string> {
|
||||
app: App;
|
||||
callback: (e: string) => void = () => { };
|
||||
getItemsFun: () => string[] = () => {
|
||||
return ["yes", "no"];
|
||||
|
||||
}
|
||||
|
||||
constructor(app: App, note: string, placeholder: string | null, getItemsFun: () => string[], callback: (e: string) => void) {
|
||||
super(app);
|
||||
this.app = app;
|
||||
this.setPlaceholder(placeholder ?? "y/n) " + note);
|
||||
if (getItemsFun) this.getItemsFun = getItemsFun;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
getItems(): string[] {
|
||||
return this.getItemsFun();
|
||||
}
|
||||
|
||||
getItemText(item: string): string {
|
||||
return item;
|
||||
}
|
||||
|
||||
onChooseItem(item: string, evt: MouseEvent | KeyboardEvent): void {
|
||||
// debugger;
|
||||
this.callback(item);
|
||||
this.callback = null;
|
||||
}
|
||||
onClose(): void {
|
||||
setTimeout(() => {
|
||||
if (this.callback != null) {
|
||||
this.callback("");
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
const PSCHeader = "ps:";
|
||||
const PSCHeaderEnd = "ps;";
|
||||
function isPluginChunk(str: string): boolean {
|
||||
return str.startsWith(PSCHeader);
|
||||
}
|
||||
|
||||
|
||||
const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => {
|
||||
return new Promise((res) => {
|
||||
const popover = new PopoverSelectString(app, message, null, null, (result) => res(result as "yes" | "no"));
|
||||
@ -232,9 +146,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
|
||||
async fileHistory() {
|
||||
const pageLimit = 2500;
|
||||
const pageLimit = 1000;
|
||||
let nextKey = "";
|
||||
const notes = [];
|
||||
const notes: { path: string, mtime: number }[] = [];
|
||||
do {
|
||||
const docs = await this.localDatabase.localDatabase.allDocs({ limit: pageLimit, startkey: nextKey, include_docs: true });
|
||||
nextKey = "";
|
||||
@ -244,11 +158,18 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
if (!("type" in doc)) continue;
|
||||
if (doc.type == "newnote" || doc.type == "plain") {
|
||||
// const docId = doc._id.startsWith("i:") ? doc._id.substring("i:".length) : doc._id;
|
||||
notes.push(id2path(doc._id))
|
||||
notes.push({ path: id2path(doc._id), mtime: doc.mtime });
|
||||
}
|
||||
if (isChunk(nextKey)) {
|
||||
// skip the chunk zone.
|
||||
nextKey = CHeaderEnd;
|
||||
}
|
||||
}
|
||||
} while (nextKey != "");
|
||||
const target = await askSelectString(this.app, "File to view History", notes);
|
||||
|
||||
notes.sort((a, b) => b.mtime - a.mtime);
|
||||
const notesList = notes.map(e => e.path);
|
||||
const target = await askSelectString(this.app, "File to view History", notesList);
|
||||
if (target) {
|
||||
this.showHistory(target);
|
||||
}
|
||||
@ -258,13 +179,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
setLogger(this.addLog.bind(this)); // Logger moved to global.
|
||||
Logger("loading plugin");
|
||||
//@ts-ignore
|
||||
const manifestVersion = MANIFEST_VERSION || "-";
|
||||
const manifestVersion: string = MANIFEST_VERSION || "0.0.0";
|
||||
//@ts-ignore
|
||||
const packageVersion = PACKAGE_VERSION || "-";
|
||||
const packageVersion: string = PACKAGE_VERSION || "0.0.0";
|
||||
|
||||
|
||||
Logger(`Self-hosted LiveSync v${manifestVersion} ${packageVersion} `);
|
||||
const lsname = "obsidian-live-sync-ver" + this.getVaultName();
|
||||
const last_version = localStorage.getItem(lsname);
|
||||
await this.loadSettings();
|
||||
const lastVersion = ~~(versionNumberString2Number(manifestVersion) / 1000);
|
||||
if (lastVersion > this.settings.lastReadUpdates) {
|
||||
Logger("Self-hosted LiveSync has undergone a major upgrade. Please open the setting dialog, and check the information pane.", LOG_LEVEL.NOTICE);
|
||||
}
|
||||
//@ts-ignore
|
||||
if (this.app.isMobile) {
|
||||
this.isMobile = true;
|
||||
@ -276,7 +203,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
this.settings.syncOnStart = false;
|
||||
this.settings.syncOnFileOpen = false;
|
||||
this.settings.periodicReplication = false;
|
||||
this.settings.versionUpFlash = "I changed specifications incompatiblly, so when you enable sync again, be sure to made version up all nother devides.";
|
||||
this.settings.versionUpFlash = "Self-hosted LiveSync has been upgraded and some behaviors have changed incompatibly. All automatic synchronization is now disabled temporary. Ensure that other devices are also upgraded, and enable synchronization again.";
|
||||
this.saveSettings();
|
||||
}
|
||||
localStorage.setItem(lsname, `${VER}`);
|
||||
@ -561,7 +488,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
});
|
||||
this.addCommand({
|
||||
id: "livesync-filehistory",
|
||||
name: "Pick file to show history",
|
||||
name: "Pick a file to show history",
|
||||
callback: () => {
|
||||
this.fileHistory();
|
||||
},
|
||||
@ -1069,7 +996,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
if (shouldBeIgnored(pathSrc)) {
|
||||
return;
|
||||
}
|
||||
if (docEntry._deleted) {
|
||||
if (docEntry._deleted || docEntry.deleted) {
|
||||
//basically pass.
|
||||
//but if there are no docs left, delete file.
|
||||
const lastDocs = await this.localDatabase.getDBEntry(pathSrc);
|
||||
@ -1140,7 +1067,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
async handleDBChanged(change: EntryBody) {
|
||||
const targetFile = this.app.vault.getAbstractFileByPath(id2path(change._id));
|
||||
if (targetFile == null) {
|
||||
if (change._deleted) {
|
||||
if (change._deleted || change.deleted) {
|
||||
return;
|
||||
}
|
||||
const doc = change;
|
||||
@ -1194,9 +1121,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
const now = new Date().getTime();
|
||||
if (queue.missingChildren.length == 0) {
|
||||
queue.done = true;
|
||||
if (queue.entry._id.startsWith("i:")) {
|
||||
if (isInteralChunk(queue.entry._id)) {
|
||||
//system file
|
||||
const filename = id2path(queue.entry._id.substring("i:".length));
|
||||
const filename = id2path(id2filenameInternalChunk(queue.entry._id));
|
||||
Logger(`Applying hidden file, ${queue.entry._id} (${queue.entry._rev}) change...`);
|
||||
await this.syncInternalFilesAndDatabase("pull", false, false, [filename])
|
||||
Logger(`Applied hidden file, ${queue.entry._id} (${queue.entry._rev}) change...`);
|
||||
@ -1239,7 +1166,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
async parseIncomingDoc(doc: PouchDB.Core.ExistingDocument<EntryBody>) {
|
||||
const skipOldFile = this.settings.skipOlderFilesOnSync && false; //patched temporary.
|
||||
if ((!doc._id.startsWith("i:")) && skipOldFile) {
|
||||
if ((!isInteralChunk(doc._id)) && skipOldFile) {
|
||||
const info = this.app.vault.getAbstractFileByPath(id2path(doc._id));
|
||||
|
||||
if (info && info instanceof TFile) {
|
||||
@ -1276,13 +1203,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
async parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): Promise<void> {
|
||||
this.refreshStatusText();
|
||||
for (const change of docs) {
|
||||
if (change._id.startsWith("ps:")) {
|
||||
if (isPluginChunk(change._id)) {
|
||||
if (this.settings.notifyPluginOrSettingUpdated) {
|
||||
this.triggerCheckPluginUpdate();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (change._id.startsWith("h:")) {
|
||||
if (isChunk(change._id)) {
|
||||
await this.parseIncomingChunk(change);
|
||||
continue;
|
||||
}
|
||||
@ -1542,7 +1469,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
const filesStorage = this.app.vault.getFiles();
|
||||
const filesStorageName = filesStorage.map((e) => e.path);
|
||||
const wf = await this.localDatabase.localDatabase.allDocs();
|
||||
const filesDatabase = wf.rows.filter((e) => !e.id.startsWith("h:") && !e.id.startsWith("ps:") && e.id != "obsydian_livesync_version").filter(e => isValidPath(e.id)).map((e) => id2path(e.id));
|
||||
const filesDatabase = wf.rows.filter((e) => !isChunk(e.id) && !isPluginChunk(e.id) && e.id != "obsydian_livesync_version").filter(e => isValidPath(e.id)).map((e) => id2path(e.id));
|
||||
const isInitialized = await (this.localDatabase.kvDB.get<boolean>("initialized")) || false;
|
||||
// Make chunk bigger if it is the initial scan. There must be non-active docs.
|
||||
if (filesDatabase.length == 0 && !isInitialized) {
|
||||
@ -1603,7 +1530,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
});
|
||||
if (!initialScan) {
|
||||
await runAll("UPDATE STORAGE", onlyInDatabase, async (e) => {
|
||||
Logger(`Pull from db:${e}`);
|
||||
Logger(`Check or pull from db:${e}`);
|
||||
await this.pullFile(e, filesStorage, false, null, false);
|
||||
});
|
||||
}
|
||||
@ -1877,13 +1804,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
if (targetFile == null) {
|
||||
//have to create;
|
||||
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
|
||||
if (doc === false) return;
|
||||
if (doc === false) {
|
||||
Logger(`${filename} Skipped`);
|
||||
return;
|
||||
}
|
||||
await this.doc2storage_create(doc, force);
|
||||
} else if (targetFile instanceof TFile) {
|
||||
//normal case
|
||||
const file = targetFile;
|
||||
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
|
||||
if (doc === false) return;
|
||||
if (doc === false) {
|
||||
Logger(`${filename} Skipped`);
|
||||
return;
|
||||
}
|
||||
await this.doc2storage_modify(doc, file, force);
|
||||
} else {
|
||||
Logger(`target files:${filename} is exists as the folder`);
|
||||
@ -1927,6 +1860,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
const docx = await this.localDatabase.getDBEntry(file.path, null, false, false);
|
||||
if (docx != false) {
|
||||
await this.doc2storage_modify(docx, file);
|
||||
} else {
|
||||
Logger("STORAGE <- DB :" + file.path + " Skipped");
|
||||
}
|
||||
caches[dK] = { storageMtime, docMtime };
|
||||
return caches;
|
||||
@ -1973,10 +1908,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
const old = await this.localDatabase.getDBEntry(fullpath, null, false, false);
|
||||
if (old !== false) {
|
||||
const oldData = { data: old.data, deleted: old._deleted };
|
||||
const newData = { data: d.data, deleted: d._deleted };
|
||||
const oldData = { data: old.data, deleted: old._deleted || old.deleted, };
|
||||
const newData = { data: d.data, deleted: d._deleted || d.deleted };
|
||||
if (JSON.stringify(oldData) == JSON.stringify(newData)) {
|
||||
Logger(msg + "Skipped (not changed) " + fullpath + (d._deleted ? " (deleted)" : ""), LOG_LEVEL.VERBOSE);
|
||||
Logger(msg + "Skipped (not changed) " + fullpath + ((d._deleted || d.deleted) ? " (deleted)" : ""), LOG_LEVEL.VERBOSE);
|
||||
return true;
|
||||
}
|
||||
// d._rev = old._rev;
|
||||
@ -2029,7 +1964,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
|
||||
async getPluginList(): Promise<{ plugins: PluginList; allPlugins: DevicePluginList; thisDevicePlugins: DevicePluginList }> {
|
||||
const db = this.localDatabase.localDatabase;
|
||||
const docList = await db.allDocs<PluginDataEntry>({ startkey: `ps:`, endkey: `ps;`, include_docs: false });
|
||||
const docList = await db.allDocs<PluginDataEntry>({ startkey: PSCHeader, endkey: PSCHeaderEnd, include_docs: false });
|
||||
const oldDocs: PluginDataEntry[] = ((await Promise.all(docList.rows.map(async (e) => await this.localDatabase.getDBEntry(e.id)))).filter((e) => e !== false) as LoadedEntry[]).map((e) => JSON.parse(e.data));
|
||||
const plugins: { [key: string]: PluginDataEntry[] } = {};
|
||||
const allPlugins: { [key: string]: PluginDataEntry } = {};
|
||||
@ -2125,7 +2060,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
Logger(`Deleting old plugins`, LOG_LEVEL.VERBOSE);
|
||||
const delDocs = oldDocs.rows.map((e) => {
|
||||
e.doc._deleted = true;
|
||||
// e.doc._deleted = true;
|
||||
if (e.doc.type == "newnote" || e.doc.type == "plain") {
|
||||
e.doc.deleted = true;
|
||||
if (this.settings.deleteMetadataOfDeletedFiles) {
|
||||
e.doc._deleted = true;
|
||||
}
|
||||
} else {
|
||||
e.doc._deleted = true;
|
||||
}
|
||||
return e.doc;
|
||||
});
|
||||
await db.bulkDocs(delDocs);
|
||||
@ -2257,7 +2200,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
|
||||
async storeInternaFileToDatabase(file: InternalFileInfo, forceWrite = false) {
|
||||
const id = "i:" + path2id(file.path);
|
||||
const id = filename2idInternalChunk(path2id(file.path));
|
||||
const contentBin = await this.app.vault.adapter.readBinary(file.path);
|
||||
const content = await arrayBufferToBase64(contentBin);
|
||||
const mtime = file.mtime;
|
||||
@ -2299,7 +2242,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
|
||||
async deleteInternaFileOnDatabase(filename: string, forceWrite = false) {
|
||||
const id = "i:" + path2id(filename);
|
||||
const id = filename2idInternalChunk(path2id(filename));
|
||||
const mtime = new Date().getTime();
|
||||
await runWithLock("file-" + id, false, async () => {
|
||||
const old = await this.localDatabase.getDBEntry(id, null, false, false) as InternalFileEntry | false;
|
||||
@ -2356,7 +2299,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
async extractInternaFileFromDatabase(filename: string, force = false) {
|
||||
const isExists = await this.app.vault.adapter.exists(filename);
|
||||
const id = "i:" + path2id(filename);
|
||||
const id = filename2idInternalChunk(path2id(filename));
|
||||
|
||||
return await runWithLock("file-" + id, false, async () => {
|
||||
const fileOnDB = await this.localDatabase.getDBEntry(id, null, false, false) as false | LoadedEntry;
|
||||
@ -2384,7 +2327,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
const contentBin = await this.app.vault.adapter.readBinary(filename);
|
||||
const content = await arrayBufferToBase64(contentBin);
|
||||
if (content == fileOnDB.data && !force) {
|
||||
Logger(`STORAGE <-- DB:${filename}: skipped (hidden) Not changed`);
|
||||
// Logger(`STORAGE <-- DB:${filename}: skipped (hidden) Not changed`, LOG_LEVEL.VERBOSE);
|
||||
return false;
|
||||
}
|
||||
await this.app.vault.adapter.writeBinary(filename, base64ToArrayBuffer(fileOnDB.data), { mtime: fileOnDB.mtime, ctime: fileOnDB.ctime });
|
||||
@ -2418,8 +2361,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
.replace(/\n| /g, "")
|
||||
.split(",").filter(e => e).map(e => new RegExp(e));
|
||||
if (!files) files = await this.scanInternalFiles();
|
||||
const filesOnDB = (await this.localDatabase.localDatabase.allDocs({ startkey: "i:", endkey: "i;", include_docs: true })).rows.map(e => e.doc) as InternalFileEntry[];
|
||||
const allFileNamesSrc = [...new Set([...files.map(e => normalizePath(e.path)), ...filesOnDB.map(e => normalizePath(id2path(e._id.substring("i:".length))))])];
|
||||
const filesOnDB = ((await this.localDatabase.localDatabase.allDocs({ startkey: ICHeader, endkey: ICHeaderEnd, include_docs: true })).rows.map(e => e.doc) as InternalFileEntry[]).filter(e => !e.deleted);
|
||||
|
||||
const allFileNamesSrc = [...new Set([...files.map(e => normalizePath(e.path)), ...filesOnDB.map(e => normalizePath(id2path(id2filenameInternalChunk(e._id))))])];
|
||||
const allFileNames = allFileNamesSrc.filter(filename => !targetFiles || (targetFiles && targetFiles.indexOf(filename) !== -1))
|
||||
function compareMTime(a: number, b: number) {
|
||||
const wa = ~~(a / 1000);
|
||||
@ -2464,7 +2408,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
if (ignorePatterns.some(e => filename.match(e))) continue;
|
||||
|
||||
const fileOnStorage = files.find(e => e.path == filename);
|
||||
const fileOnDatabase = filesOnDB.find(e => e._id == "i:" + id2path(filename));
|
||||
const fileOnDatabase = filesOnDB.find(e => e._id == filename2idInternalChunk(id2path(filename)));
|
||||
// TODO: Fix this somehow smart.
|
||||
let proc: Promise<void> | null;
|
||||
|
||||
|
@ -93,6 +93,10 @@
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.sls-header-button {
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
.sls-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
8
updates.md
Normal file
8
updates.md
Normal file
@ -0,0 +1,8 @@
|
||||
### 0.13.0
|
||||
|
||||
- The metadata of the deleted files will be kept on the database by default. If you want to delete this as the previous version, please turn on `Delete metadata of deleted files.`. And, if you have upgraded from the older version, please ensure every device has been upgraded.
|
||||
- Please turn on `Delete metadata of deleted files.` if you are using livesync-classroom or filesystem-livesync.
|
||||
- We can see the history of deleted files.
|
||||
- `Pick file to show` was renamed to `Pick a file to show.
|
||||
- Files in the `Pick a file to show` are now ordered by their modified date descent.
|
||||
- Update information became to be shown on the major upgrade.
|
Loading…
x
Reference in New Issue
Block a user