mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-01-27 15:37:11 +00:00
- 0.23.21:
- New Features:
- Case-insensitive file handling
- Files can now be handled case-insensitively.
- This behaviour can be modified in the settings under `Handle files as Case-Sensitive` (Default: Prompt, Enabled for previous behaviour).
- Improved chunk revision fixing
- Revisions for chunks can now be fixed for faster chunk creation.
- This can be adjusted in the settings under `Compute revisions for chunks` (Default: Prompt, Enabled for previous behaviour).
- Bulk chunk transfer
- Chunks can now be transferred in bulk during uploads.
- This feature is enabled by default through `Send chunks in bulk`.
- Creation of missing chunks without
- Missing chunks can be created without storing notes, enhancing efficiency for first synchronisation or after prolonged periods without synchronisation.
- Improvements:
- File status scanning on the startup
- Quite significant performance improvements.
- No more missing scans of some files.
- Status in editor enhancements
- Significant performance improvements in the status display within the editor.
- Notifications for files that will not be synchronised will now be properly communicated.
- Encryption and Decryption
- These processes are now performed in background threads to ensure fast and stable transfers.
- Verify and repair all files
- Got faster through parallel checking.
- Migration on update
- Migration messages and wizards have become more helpful.
- Behavioural changes:
- Chunk size adjustments
- Large chunks will no longer be created for older, stable files, addressing storage consumption issues.
- Flag file automation
- Confirmation will be shown and we can cancel it.
- Fixed:
- Database File Scanning
- All files in the database will now be enumerated correctly.
- Miscellaneous
- Dependency updated.
- Now, tree shaking is left to terser, from esbuild.
This commit is contained in:
@@ -23,8 +23,8 @@ const keepTest = !prod || dev;
|
||||
const terserOpt = {
|
||||
sourceMap: !prod
|
||||
? {
|
||||
url: "inline",
|
||||
}
|
||||
url: "inline",
|
||||
}
|
||||
: {},
|
||||
format: {
|
||||
indent_level: 2,
|
||||
@@ -41,6 +41,7 @@ const terserOpt = {
|
||||
// compress options
|
||||
defaults: false,
|
||||
evaluate: true,
|
||||
dead_code: true,
|
||||
inline: 3,
|
||||
join_vars: true,
|
||||
loops: true,
|
||||
@@ -57,6 +58,7 @@ const terserOpt = {
|
||||
ecma: 2018,
|
||||
unused: true,
|
||||
},
|
||||
// mangle: false,
|
||||
|
||||
ecma: 2018, // specify one of: 5, 2015, 2016, etc.
|
||||
enclose: false, // or specify true, or "args:values"
|
||||
@@ -122,7 +124,7 @@ const context = await esbuild.context({
|
||||
logLevel: "info",
|
||||
platform: "browser",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
treeShaking: false,
|
||||
outfile: "main_org.js",
|
||||
mainFields: ["browser", "module", "main"],
|
||||
minifyWhitespace: false,
|
||||
|
||||
2796
package-lock.json
generated
2796
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
@@ -14,28 +14,29 @@
|
||||
"author": "vorotamoroz",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@chialab/esbuild-plugin-worker": "^0.18.1",
|
||||
"@tsconfig/svelte": "^5.0.4",
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/pouchdb": "^6.4.2",
|
||||
"@types/pouchdb-adapter-http": "^6.1.6",
|
||||
"@types/pouchdb-adapter-idb": "^6.1.7",
|
||||
"@types/pouchdb-browser": "^6.1.5",
|
||||
"@types/pouchdb-core": "^7.0.14",
|
||||
"@types/pouchdb-core": "^7.0.15",
|
||||
"@types/pouchdb-mapreduce": "^6.1.10",
|
||||
"@types/pouchdb-replication": "^6.4.7",
|
||||
"@types/transform-pouch": "^1.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.0",
|
||||
"@typescript-eslint/parser": "^7.16.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.4.0",
|
||||
"@typescript-eslint/parser": "^8.4.0",
|
||||
"builtin-modules": "^4.0.0",
|
||||
"esbuild": "0.23.0",
|
||||
"esbuild": "0.23.1",
|
||||
"esbuild-svelte": "^0.8.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"events": "^3.3.0",
|
||||
"obsidian": "^1.5.7",
|
||||
"postcss": "^8.4.39",
|
||||
"obsidian": "^1.6.6",
|
||||
"postcss": "^8.4.45",
|
||||
"postcss-load-config": "^6.0.1",
|
||||
"pouchdb-adapter-http": "^9.0.0",
|
||||
"pouchdb-adapter-idb": "^9.0.0",
|
||||
@@ -47,25 +48,25 @@
|
||||
"pouchdb-merge": "^9.0.0",
|
||||
"pouchdb-replication": "^9.0.0",
|
||||
"pouchdb-utils": "^9.0.0",
|
||||
"svelte": "^4.2.18",
|
||||
"svelte": "^4.2.19",
|
||||
"svelte-preprocess": "^6.0.2",
|
||||
"terser": "^5.31.2",
|
||||
"terser": "^5.31.6",
|
||||
"transform-pouch": "^2.0.0",
|
||||
"tslib": "^2.6.3",
|
||||
"typescript": "^5.5.3"
|
||||
"tslib": "^2.7.0",
|
||||
"typescript": "^5.5.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.614.0",
|
||||
"@smithy/fetch-http-handler": "^3.2.1",
|
||||
"@smithy/protocol-http": "^4.0.3",
|
||||
"@aws-sdk/client-s3": "^3.645.0",
|
||||
"@smithy/fetch-http-handler": "^3.2.4",
|
||||
"@smithy/protocol-http": "^4.1.0",
|
||||
"@smithy/querystring-builder": "^3.0.3",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"esbuild-plugin-inline-worker": "^0.1.1",
|
||||
"fflate": "^0.8.2",
|
||||
"idb": "^8.0.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"octagonal-wheels": "^0.1.13",
|
||||
"octagonal-wheels": "^0.1.14",
|
||||
"xxhash-wasm": "0.4.2",
|
||||
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/common/events.ts
Normal file
16
src/common/events.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const EVENT_LAYOUT_READY = "layout-ready";
|
||||
export const EVENT_PLUGIN_LOADED = "plugin-loaded";
|
||||
export const EVENT_PLUGIN_UNLOADED = "plugin-unloaded";
|
||||
export const EVENT_SETTING_SAVED = "setting-saved";
|
||||
export const EVENT_FILE_RENAMED = "file-renamed";
|
||||
|
||||
export const EVENT_LEAF_ACTIVE_CHANGED = "leaf-active-changed";
|
||||
|
||||
|
||||
// export const EVENT_FILE_CHANGED = "file-changed";
|
||||
|
||||
import { eventHub } from "../lib/src/hub/hub";
|
||||
// TODO: Add overloads for the emit method to allow for type checking
|
||||
|
||||
export { eventHub };
|
||||
|
||||
@@ -15,14 +15,14 @@ export { scheduleTask, setPeriodicTask, cancelTask, cancelAllTasks, cancelPeriod
|
||||
// For backward compatibility, using the path for determining id.
|
||||
// Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/".
|
||||
// The first slash will be deleted when the path is normalized.
|
||||
export async function path2id(filename: FilePathWithPrefix | FilePath, obfuscatePassphrase: string | false): Promise<DocumentID> {
|
||||
export async function path2id(filename: FilePathWithPrefix | FilePath, obfuscatePassphrase: string | false, caseInsensitive: boolean): Promise<DocumentID> {
|
||||
const temp = filename.split(":");
|
||||
const path = temp.pop();
|
||||
const normalizedPath = normalizePath(path as FilePath);
|
||||
temp.push(normalizedPath);
|
||||
const fixedPath = temp.join(":") as FilePathWithPrefix;
|
||||
|
||||
const out = await path2id_base(fixedPath, obfuscatePassphrase);
|
||||
const out = await path2id_base(fixedPath, obfuscatePassphrase, caseInsensitive);
|
||||
return out;
|
||||
}
|
||||
export function id2path(id: DocumentID, entry?: EntryHasPath): FilePathWithPrefix {
|
||||
@@ -465,3 +465,34 @@ export function compareFileFreshness(baseFile: TFile | AnyEntry | undefined, che
|
||||
return compareMTime(modifiedBase, modifiedTarget);
|
||||
}
|
||||
|
||||
const _cached = new Map<string, {
|
||||
value: any;
|
||||
context: Map<string, any>;
|
||||
}>();
|
||||
|
||||
export type MemoOption = {
|
||||
key: string;
|
||||
forceUpdate?: boolean;
|
||||
validator?: () => boolean;
|
||||
}
|
||||
|
||||
export function useMemo<T>({ key, forceUpdate, validator }: MemoOption, updateFunc: (context: Map<string, any>, prev: T) => T): T {
|
||||
const cached = _cached.get(key);
|
||||
if (cached && !forceUpdate && (!validator || validator && !validator())) {
|
||||
return cached.value;
|
||||
}
|
||||
const context = cached?.context || new Map<string, any>();
|
||||
const value = updateFunc(context, cached?.value);
|
||||
if (value !== cached?.value) {
|
||||
_cached.set(key, { value, context });
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function disposeMemo(key: string) {
|
||||
_cached.delete(key);
|
||||
}
|
||||
|
||||
export function disposeAllMemo() {
|
||||
_cached.clear();
|
||||
}
|
||||
@@ -374,7 +374,7 @@ Of course, we are able to disable these features.`
|
||||
await this.plugin.openDatabase();
|
||||
this.plugin.isReady = true;
|
||||
if (makeLocalChunkBeforeSync) {
|
||||
await this.plugin.initializeDatabase(true);
|
||||
await this.plugin.createAllChunks(true);
|
||||
}
|
||||
await this.plugin.markRemoteResolved();
|
||||
await delay(500);
|
||||
@@ -390,6 +390,7 @@ Of course, we are able to disable these features.`
|
||||
async rebuildRemote() {
|
||||
this.suspendExtraSync();
|
||||
this.plugin.settings.isConfigured = true;
|
||||
|
||||
await this.plugin.realizeSettingSyncMode();
|
||||
await this.plugin.markRemoteLocked();
|
||||
await this.plugin.tryResetRemoteDatabase();
|
||||
|
||||
234
src/features/CmdStatusInsideEditor.ts
Normal file
234
src/features/CmdStatusInsideEditor.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import { computed, reactive, reactiveSource, type ReactiveValue } from "octagonal-wheels/dataobject/reactive";
|
||||
import type { DatabaseConnectingStatus, EntryDoc } from "../lib/src/common/types";
|
||||
import { LiveSyncCommands } from "./LiveSyncCommands";
|
||||
import { scheduleTask } from "octagonal-wheels/concurrency/task";
|
||||
import { isDirty, throttle } from "../lib/src/common/utils";
|
||||
import { collectingChunks, pluginScanningCount, hiddenFilesEventCount, hiddenFilesProcessingCount } from "../lib/src/mock_and_interop/stores";
|
||||
import { eventHub } from "../lib/src/hub/hub";
|
||||
import { EVENT_FILE_RENAMED, EVENT_LAYOUT_READY, EVENT_LEAF_ACTIVE_CHANGED } from "../common/events";
|
||||
|
||||
export class LogAddOn extends LiveSyncCommands {
|
||||
|
||||
statusBar?: HTMLElement;
|
||||
|
||||
statusDiv?: HTMLElement;
|
||||
statusLine?: HTMLDivElement;
|
||||
logMessage?: HTMLDivElement;
|
||||
logHistory?: HTMLDivElement;
|
||||
messageArea?: HTMLDivElement;
|
||||
|
||||
statusBarLabels!: ReactiveValue<{ message: string, status: string }>;
|
||||
|
||||
observeForLogs() {
|
||||
const padSpaces = `\u{2007}`.repeat(10);
|
||||
// const emptyMark = `\u{2003}`;
|
||||
function padLeftSpComputed(numI: ReactiveValue<number>, mark: string) {
|
||||
const formatted = reactiveSource("");
|
||||
let timer: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||
let maxLen = 1;
|
||||
numI.onChanged(numX => {
|
||||
const num = numX.value;
|
||||
const numLen = `${Math.abs(num)}`.length + 1;
|
||||
maxLen = maxLen < numLen ? numLen : maxLen;
|
||||
if (timer) clearTimeout(timer);
|
||||
if (num == 0) {
|
||||
timer = setTimeout(() => {
|
||||
formatted.value = "";
|
||||
maxLen = 1;
|
||||
}, 3000);
|
||||
}
|
||||
formatted.value = ` ${mark}${`${padSpaces}${num}`.slice(-(maxLen))}`;
|
||||
})
|
||||
return computed(() => formatted.value);
|
||||
}
|
||||
const labelReplication = padLeftSpComputed(this.plugin.replicationResultCount, `📥`);
|
||||
const labelDBCount = padLeftSpComputed(this.plugin.databaseQueueCount, `📄`);
|
||||
const labelStorageCount = padLeftSpComputed(this.plugin.storageApplyingCount, `💾`);
|
||||
const labelChunkCount = padLeftSpComputed(collectingChunks, `🧩`);
|
||||
const labelPluginScanCount = padLeftSpComputed(pluginScanningCount, `🔌`);
|
||||
const labelConflictProcessCount = padLeftSpComputed(this.plugin.conflictProcessQueueCount, `🔩`);
|
||||
const hiddenFilesCount = reactive(() => hiddenFilesEventCount.value + hiddenFilesProcessingCount.value);
|
||||
const labelHiddenFilesCount = padLeftSpComputed(hiddenFilesCount, `⚙️`)
|
||||
const queueCountLabelX = reactive(() => {
|
||||
return `${labelReplication()}${labelDBCount()}${labelStorageCount()}${labelChunkCount()}${labelPluginScanCount()}${labelHiddenFilesCount()}${labelConflictProcessCount()}`;
|
||||
})
|
||||
const queueCountLabel = () => queueCountLabelX.value;
|
||||
|
||||
const requestingStatLabel = computed(() => {
|
||||
const diff = this.plugin.requestCount.value - this.plugin.responseCount.value;
|
||||
return diff != 0 ? "📲 " : "";
|
||||
})
|
||||
|
||||
const replicationStatLabel = computed(() => {
|
||||
const e = this.plugin.replicationStat.value;
|
||||
const sent = e.sent;
|
||||
const arrived = e.arrived;
|
||||
const maxPullSeq = e.maxPullSeq;
|
||||
const maxPushSeq = e.maxPushSeq;
|
||||
const lastSyncPullSeq = e.lastSyncPullSeq;
|
||||
const lastSyncPushSeq = e.lastSyncPushSeq;
|
||||
let pushLast = "";
|
||||
let pullLast = "";
|
||||
let w = "";
|
||||
const labels: Partial<Record<DatabaseConnectingStatus, string>> = {
|
||||
"CONNECTED": "⚡",
|
||||
"JOURNAL_SEND": "📦↑",
|
||||
"JOURNAL_RECEIVE": "📦↓",
|
||||
}
|
||||
switch (e.syncStatus) {
|
||||
case "CLOSED":
|
||||
case "COMPLETED":
|
||||
case "NOT_CONNECTED":
|
||||
w = "⏹";
|
||||
break;
|
||||
case "STARTED":
|
||||
w = "🌀";
|
||||
break;
|
||||
case "PAUSED":
|
||||
w = "💤";
|
||||
break;
|
||||
case "CONNECTED":
|
||||
case "JOURNAL_SEND":
|
||||
case "JOURNAL_RECEIVE":
|
||||
w = labels[e.syncStatus] || "⚡";
|
||||
pushLast = ((lastSyncPushSeq == 0) ? "" : (lastSyncPushSeq >= maxPushSeq ? " (LIVE)" : ` (${maxPushSeq - lastSyncPushSeq})`));
|
||||
pullLast = ((lastSyncPullSeq == 0) ? "" : (lastSyncPullSeq >= maxPullSeq ? " (LIVE)" : ` (${maxPullSeq - lastSyncPullSeq})`));
|
||||
break;
|
||||
case "ERRORED":
|
||||
w = "⚠";
|
||||
break;
|
||||
default:
|
||||
w = "?";
|
||||
}
|
||||
return { w, sent, pushLast, arrived, pullLast };
|
||||
})
|
||||
const labelProc = padLeftSpComputed(this.plugin.vaultManager.processing, `⏳`);
|
||||
const labelPend = padLeftSpComputed(this.plugin.vaultManager.totalQueued, `🛫`);
|
||||
const labelInBatchDelay = padLeftSpComputed(this.plugin.vaultManager.batched, `📬`);
|
||||
const waitingLabel = computed(() => {
|
||||
return `${labelProc()}${labelPend()}${labelInBatchDelay()}`;
|
||||
})
|
||||
const statusLineLabel = computed(() => {
|
||||
const { w, sent, pushLast, arrived, pullLast } = replicationStatLabel();
|
||||
const queued = queueCountLabel();
|
||||
const waiting = waitingLabel();
|
||||
const networkActivity = requestingStatLabel();
|
||||
return {
|
||||
message: `${networkActivity}Sync: ${w} ↑ ${sent}${pushLast} ↓ ${arrived}${pullLast}${waiting}${queued}`,
|
||||
};
|
||||
})
|
||||
const statusBarLabels = reactive(() => {
|
||||
const scheduleMessage = this.plugin.isReloadingScheduled ? `WARNING! RESTARTING OBSIDIAN IS SCHEDULED!\n` : "";
|
||||
const { message } = statusLineLabel();
|
||||
const status = scheduleMessage + this.plugin.statusLog.value;
|
||||
|
||||
return {
|
||||
message, status
|
||||
}
|
||||
})
|
||||
this.statusBarLabels = statusBarLabels;
|
||||
|
||||
const applyToDisplay = throttle((label: typeof statusBarLabels.value) => {
|
||||
const v = label;
|
||||
this.applyStatusBarText();
|
||||
|
||||
}, 20);
|
||||
statusBarLabels.onChanged(label => applyToDisplay(label.value))
|
||||
}
|
||||
|
||||
adjustStatusDivPosition() {
|
||||
const mdv = this.app.workspace.getMostRecentLeaf();
|
||||
if (mdv && this.statusDiv) {
|
||||
this.statusDiv.remove();
|
||||
// this.statusDiv.pa();
|
||||
const container = mdv.view.containerEl;
|
||||
container.insertBefore(this.statusDiv, container.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
onunload() {
|
||||
if (this.statusDiv) {
|
||||
this.statusDiv.remove();
|
||||
}
|
||||
document.querySelectorAll(`.livesync-status`)?.forEach(e => e.remove());
|
||||
}
|
||||
async setFileStatus() {
|
||||
this.messageArea!.innerText = await this.plugin.getActiveFileStatus();
|
||||
}
|
||||
onActiveLeafChange() {
|
||||
this.adjustStatusDivPosition();
|
||||
this.setFileStatus();
|
||||
|
||||
}
|
||||
onload(): void | Promise<void> {
|
||||
eventHub.on(EVENT_FILE_RENAMED, (evt: CustomEvent<{ oldPath: string, newPath: string }>) => {
|
||||
this.setFileStatus();
|
||||
});
|
||||
eventHub.on(EVENT_LEAF_ACTIVE_CHANGED, () => this.onActiveLeafChange());
|
||||
const w = document.querySelectorAll(`.livesync-status`);
|
||||
w.forEach(e => e.remove());
|
||||
|
||||
this.observeForLogs();
|
||||
this.adjustStatusDivPosition();
|
||||
this.statusDiv = this.app.workspace.containerEl.createDiv({ cls: "livesync-status" });
|
||||
this.statusLine = this.statusDiv.createDiv({ cls: "livesync-status-statusline" });
|
||||
this.messageArea = this.statusDiv.createDiv({ cls: "livesync-status-messagearea" });
|
||||
this.logMessage = this.statusDiv.createDiv({ cls: "livesync-status-logmessage" });
|
||||
this.logHistory = this.statusDiv.createDiv({ cls: "livesync-status-loghistory" });
|
||||
eventHub.on(EVENT_LAYOUT_READY, () => this.adjustStatusDivPosition());
|
||||
if (this.settings.showStatusOnStatusbar) {
|
||||
this.statusBar = this.plugin.addStatusBarItem();
|
||||
this.statusBar.addClass("syncstatusbar");
|
||||
}
|
||||
}
|
||||
nextFrameQueue: ReturnType<typeof requestAnimationFrame> | undefined = undefined;
|
||||
logLines: { ttl: number, message: string }[] = [];
|
||||
|
||||
applyStatusBarText() {
|
||||
if (this.nextFrameQueue) {
|
||||
return;
|
||||
}
|
||||
this.nextFrameQueue = requestAnimationFrame(() => {
|
||||
this.nextFrameQueue = undefined;
|
||||
const { message, status } = this.statusBarLabels.value;
|
||||
// const recent = logMessages.value;
|
||||
const newMsg = message;
|
||||
const newLog = this.settings.showOnlyIconsOnEditor ? "" : status;
|
||||
|
||||
this.statusBar?.setText(newMsg.split("\n")[0]);
|
||||
if (this.settings.showStatusOnEditor && this.statusDiv) {
|
||||
// const root = activeDocument.documentElement;
|
||||
// root.style.setProperty("--sls-log-text", "'" + (newMsg + "\\A " + newLog) + "'");
|
||||
// this.statusDiv.innerText = newMsg + "\\A " + newLog;
|
||||
if (this.settings.showLongerLogInsideEditor) {
|
||||
const now = new Date().getTime();
|
||||
this.logLines = this.logLines.filter(e => e.ttl > now);
|
||||
const minimumNext = this.logLines.reduce((a, b) => a < b.ttl ? a : b.ttl, Number.MAX_SAFE_INTEGER);
|
||||
if (this.logLines.length > 0) setTimeout(() => this.applyStatusBarText(), minimumNext - now);
|
||||
const recent = this.logLines.map(e => e.message);
|
||||
const recentLogs = recent.reverse().join("\n");
|
||||
if (isDirty("recentLogs", recentLogs)) this.logHistory!.innerText = recentLogs;
|
||||
}
|
||||
if (isDirty("newMsg", newMsg)) this.statusLine!.innerText = newMsg;
|
||||
if (isDirty("newLog", newLog)) this.logMessage!.innerText = newLog;
|
||||
} else {
|
||||
// const root = activeDocument.documentElement;
|
||||
// root.style.setProperty("--log-text", "'" + (newMsg + "\\A " + newLog) + "'");
|
||||
}
|
||||
});
|
||||
|
||||
scheduleTask("log-hide", 3000, () => { this.plugin.statusLog.value = "" });
|
||||
}
|
||||
|
||||
|
||||
onInitializeDatabase(showNotice: boolean) { }
|
||||
beforeReplicate(showNotice: boolean) { }
|
||||
onResume() { }
|
||||
parseReplicationResultItem(docs: PouchDB.Core.ExistingDocument<EntryDoc>): boolean | Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
async realizeSettingSyncMode() { }
|
||||
|
||||
|
||||
}
|
||||
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: f0253a8548...c9af24ecd6
735
src/main.ts
735
src/main.ts
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@ import { type App, TFile, type DataWriteOptions, TFolder, TAbstractFile } from "
|
||||
import { serialized } from "../lib/src/concurrency/lock.ts";
|
||||
import { Logger } from "../lib/src/common/logger.ts";
|
||||
import { isPlainText } from "../lib/src/string_and_binary/path.ts";
|
||||
import type { FilePath } from "../lib/src/common/types.ts";
|
||||
import type { FilePath, HasSettings } from "../lib/src/common/types.ts";
|
||||
import { createBinaryBlob, isDocContentSame } from "../lib/src/common/utils.ts";
|
||||
import type { InternalFileInfo } from "../common/types.ts";
|
||||
import { markChangesAreSame } from "../common/utils.ts";
|
||||
@@ -31,8 +31,10 @@ async function processWriteFile<T>(file: TFile | TFolder | string, proc: () => P
|
||||
}
|
||||
export class SerializedFileAccess {
|
||||
app: App
|
||||
constructor(app: App) {
|
||||
plugin: HasSettings<{ handleFilenameCaseSensitive: boolean }>
|
||||
constructor(app: App, plugin: typeof this["plugin"]) {
|
||||
this.app = app;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
async adapterStat(file: TFile | string) {
|
||||
@@ -138,17 +140,23 @@ export class SerializedFileAccess {
|
||||
return await processWriteFile(file, () => this.app.vault.trash(file, force));
|
||||
}
|
||||
|
||||
|
||||
|
||||
isStorageInsensitive(): boolean {
|
||||
//@ts-ignore
|
||||
return this.app.vault.adapter.insensitive ?? true;
|
||||
}
|
||||
|
||||
getAbstractFileByPathInsensitive(path: FilePath | string): TAbstractFile | null {
|
||||
//@ts-ignore
|
||||
return app.vault.getAbstractFileByPathInsensitive(path);
|
||||
}
|
||||
|
||||
getAbstractFileByPath(path: FilePath | string): TAbstractFile | null {
|
||||
// Disabled temporary.
|
||||
if (!this.plugin.settings.handleFilenameCaseSensitive || this.isStorageInsensitive()) {
|
||||
return this.getAbstractFileByPathInsensitive(path);
|
||||
}
|
||||
return this.app.vault.getAbstractFileByPath(path);
|
||||
// // Hidden API but so useful.
|
||||
// // @ts-ignore
|
||||
// if ("getAbstractFileByPathInsensitive" in app.vault && (app.vault.adapter?.insensitive ?? false)) {
|
||||
// // @ts-ignore
|
||||
// return app.vault.getAbstractFileByPathInsensitive(path);
|
||||
// } else {
|
||||
// return app.vault.getAbstractFileByPath(path);
|
||||
// }
|
||||
}
|
||||
|
||||
getFiles() {
|
||||
|
||||
@@ -35,6 +35,7 @@ import { LiveSyncCouchDBReplicator } from "../lib/src/replication/couchdb/LiveSy
|
||||
import { type AllSettingItemKey, type AllStringItemKey, type AllNumericItemKey, type AllBooleanItemKey, type AllSettings, OnDialogSettingsDefault, getConfig, type OnDialogSettings, getConfName } from "./settingConstants.ts";
|
||||
import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "src/lib/src/common/rosetta.ts";
|
||||
import { $t } from "src/lib/src/common/i18n.ts";
|
||||
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
||||
|
||||
type OnUpdateResult = {
|
||||
visibility?: boolean,
|
||||
@@ -272,11 +273,11 @@ class Setting extends SettingOrg {
|
||||
})
|
||||
return this;
|
||||
}
|
||||
addApplyButton(keys: AllSettingItemKey[]) {
|
||||
addApplyButton(keys: AllSettingItemKey[], text?: string) {
|
||||
this.addButton((button) => {
|
||||
this.applyButtonComponent = button;
|
||||
this.watchDirtyKeys = unique([...keys, ...this.watchDirtyKeys]);
|
||||
button.setButtonText("Apply")
|
||||
button.setButtonText(text ?? "Apply")
|
||||
button.onClick(async () => {
|
||||
await Setting.env.saveSettings(keys);
|
||||
Setting.env.reloadAllSettings();
|
||||
@@ -1438,6 +1439,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
|
||||
.addApplyButton(["configPassphrase", "configPassphraseStore"])
|
||||
.setClass("wizardHidden")
|
||||
|
||||
|
||||
addScreenElement("20", containerGeneralSettingsEl);
|
||||
const containerSyncSettingEl = containerEl.createDiv();
|
||||
this.createEl(containerSyncSettingEl, "h3", { text: "Sync Settings" });
|
||||
@@ -1776,6 +1778,13 @@ However, your report is needed to stabilise this. I appreciate you for your grea
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setClass("wizardHidden")
|
||||
.autoWireToggle("readChunksOnline", { onUpdate: onlyOnCouchDB })
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setClass("wizardHidden")
|
||||
.autoWireToggle("sendChunksBulk", { onUpdate: onlyOnCouchDB })
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setClass("wizardHidden")
|
||||
.autoWireNumeric("sendChunksBulkMaxSize", { clampMax: 100, clampMin: 1, onUpdate: onlyOnCouchDB })
|
||||
|
||||
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setClass("wizardHidden")
|
||||
@@ -1916,7 +1925,8 @@ However, your report is needed to stabilise this. I appreciate you for your grea
|
||||
const endpointScheme = pluginConfig.endpoint.startsWith("http:") ? "(HTTP)" : (pluginConfig.endpoint.startsWith("https:")) ? "(HTTPS)" : "";
|
||||
pluginConfig.endpoint = `${endpoint.indexOf(".r2.cloudflarestorage.") !== -1 ? "R2" : "self-hosted?"}(${endpointScheme})`;
|
||||
}
|
||||
const obsidianInfo = navigator.userAgent;
|
||||
const obsidianInfo = `Navigator: ${navigator.userAgent}
|
||||
FileSystem: ${this.plugin.vaultAccess.isStorageInsensitive() ? "insensitive" : "sensitive"}`;
|
||||
const msgConfig = `---- Obsidian info ----
|
||||
${obsidianInfo}
|
||||
---- remote config ----
|
||||
@@ -1998,7 +2008,7 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
if (fileOnDB) {
|
||||
el.appendChild(this.createEl(el, "button", { text: "Database -> Storage" }, buttonEl => {
|
||||
buttonEl.onClickEvent(() => {
|
||||
this.plugin.pullFile(this.plugin.getPath(fileOnDB), [], true, undefined, false);
|
||||
this.plugin.pullFile(this.plugin.getPath(fileOnDB), undefined, true, undefined, false);
|
||||
el.remove();
|
||||
})
|
||||
}))
|
||||
@@ -2017,6 +2027,18 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
addResult(file.path, file, fileOnDB)
|
||||
}
|
||||
}
|
||||
new Setting(containerHatchEl)
|
||||
.setName("Recreate missing chunks for all files")
|
||||
.setDesc("This will recreate chunks for all files. If there were missing chunks, this may fix the errors.")
|
||||
.addButton((button) =>
|
||||
button.
|
||||
setButtonText("Recreate all")
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
await this.plugin.createAllChunks(true);
|
||||
})
|
||||
)
|
||||
|
||||
new Setting(containerHatchEl)
|
||||
.setName("Verify and repair all files")
|
||||
.setDesc("Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.")
|
||||
@@ -2024,8 +2046,9 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
button
|
||||
.setButtonText("Verify all")
|
||||
.setDisabled(false)
|
||||
.setWarning()
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
Logger("Start verifying all files", LOG_LEVEL_NOTICE, "verify");
|
||||
const files = this.app.vault.getFiles();
|
||||
const documents = [] as FilePathWithPrefix[];
|
||||
|
||||
@@ -2033,33 +2056,53 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
for await (const i of adn) documents.push(this.plugin.getPath(i));
|
||||
const allPaths = [...new Set([...documents, ...files.map(e => e.path as FilePathWithPrefix)])];
|
||||
let i = 0;
|
||||
for (const path of allPaths) {
|
||||
const incProc = () => {
|
||||
i++;
|
||||
Logger(`${i}/${files.length}\n${path}`, LOG_LEVEL_NOTICE, "verify");
|
||||
if (shouldBeIgnored(path)) continue;
|
||||
const abstractFile = this.plugin.vaultAccess.getAbstractFileByPath(path);
|
||||
const fileOnStorage = abstractFile instanceof TFile ? abstractFile : false;
|
||||
if (!await this.plugin.isTargetFile(path)) continue;
|
||||
|
||||
if (fileOnStorage && this.plugin.isFileSizeExceeded(fileOnStorage.stat.size)) continue;
|
||||
const fileOnDB = await this.plugin.localDatabase.getDBEntry(path);
|
||||
if (fileOnDB && this.plugin.isFileSizeExceeded(fileOnDB.size)) continue;
|
||||
|
||||
if (!fileOnDB && fileOnStorage) {
|
||||
Logger(`Compare: Not found on the local database: ${path}`, LOG_LEVEL_NOTICE);
|
||||
addResult(path, fileOnStorage, false)
|
||||
continue;
|
||||
}
|
||||
if (fileOnDB && !fileOnStorage) {
|
||||
Logger(`Compare: Not found on the storage: ${path}`, LOG_LEVEL_NOTICE);
|
||||
addResult(path, false, fileOnDB)
|
||||
continue;
|
||||
}
|
||||
if (fileOnStorage && fileOnDB) {
|
||||
await checkBetweenStorageAndDatabase(fileOnStorage, fileOnDB)
|
||||
}
|
||||
if (i % 25 == 0) Logger(`Checking ${i}/${files.length} files \n`, LOG_LEVEL_NOTICE, "verify-processed");
|
||||
}
|
||||
const semaphore = Semaphore(10);
|
||||
const processes = allPaths.map(async path => {
|
||||
try {
|
||||
if (shouldBeIgnored(path)) {
|
||||
return incProc();
|
||||
}
|
||||
const abstractFile = this.plugin.vaultAccess.getAbstractFileByPath(path);
|
||||
const fileOnStorage = abstractFile instanceof TFile ? abstractFile : false;
|
||||
if (!await this.plugin.isTargetFile(path)) return incProc();
|
||||
const releaser = await semaphore.acquire(1)
|
||||
if (fileOnStorage && this.plugin.isFileSizeExceeded(fileOnStorage.stat.size)) return incProc();
|
||||
try {
|
||||
const fileOnDB = await this.plugin.localDatabase.getDBEntry(path);
|
||||
if (fileOnDB && this.plugin.isFileSizeExceeded(fileOnDB.size)) return incProc();
|
||||
|
||||
if (!fileOnDB && fileOnStorage) {
|
||||
Logger(`Compare: Not found on the local database: ${path}`, LOG_LEVEL_NOTICE);
|
||||
addResult(path, fileOnStorage, false)
|
||||
return incProc();
|
||||
}
|
||||
if (fileOnDB && !fileOnStorage) {
|
||||
Logger(`Compare: Not found on the storage: ${path}`, LOG_LEVEL_NOTICE);
|
||||
addResult(path, false, fileOnDB)
|
||||
return incProc();
|
||||
}
|
||||
if (fileOnStorage && fileOnDB) {
|
||||
await checkBetweenStorageAndDatabase(fileOnStorage, fileOnDB)
|
||||
}
|
||||
} catch (ex) {
|
||||
Logger(`Error while processing ${path}`, LOG_LEVEL_NOTICE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
} finally {
|
||||
releaser();
|
||||
incProc();
|
||||
}
|
||||
} catch (ex) {
|
||||
Logger(`Error while processing without semaphore ${path}`, LOG_LEVEL_NOTICE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
});
|
||||
await Promise.all(processes);
|
||||
Logger("done", LOG_LEVEL_NOTICE, "verify");
|
||||
// Logger(`${i}/${files.length}\n`, LOG_LEVEL_NOTICE, "verify-processed");
|
||||
})
|
||||
);
|
||||
const resultArea = containerHatchEl.createDiv({ text: "" });
|
||||
@@ -2181,6 +2224,34 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
new Setting(containerHatchEl)
|
||||
.autoWireToggle("useIndexedDBAdapter", { invert: true })
|
||||
|
||||
new Setting(containerHatchEl)
|
||||
.autoWireToggle("doNotUseFixedRevisionForChunks", { holdValue: true })
|
||||
.setClass("wizardHidden")
|
||||
new Setting(containerHatchEl)
|
||||
.autoWireToggle("handleFilenameCaseSensitive", { holdValue: true })
|
||||
.setClass("wizardHidden")
|
||||
|
||||
new Setting(containerHatchEl)
|
||||
.setName("Apply")
|
||||
.setDesc("These configurations require a database rebuild.")
|
||||
.setClass("wizardHidden")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Apply and rebuild")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
await this.saveAllDirtySettings();
|
||||
// await this.applySetting(["doNotUseFixedRevisionForChunks", "handleFilenameCaseSensitive"]);
|
||||
// await this.saveSettings(["doNotUseFixedRevisionForChunks", "handleFilenameCaseSensitive"]);
|
||||
// debugger;
|
||||
await rebuildDB("rebuildBothByThisDevice");
|
||||
})
|
||||
)
|
||||
.addOnUpdate(() => ({
|
||||
isCta: this.isSomeDirty(["doNotUseFixedRevisionForChunks", "handleFilenameCaseSensitive"]),
|
||||
disabled: !this.isSomeDirty(["doNotUseFixedRevisionForChunks", "handleFilenameCaseSensitive"]),
|
||||
}))
|
||||
this.addOnSaved("useIndexedDBAdapter", async () => {
|
||||
await this.saveAllDirtySettings();
|
||||
await rebuildDB("localOnly");
|
||||
@@ -2328,7 +2399,17 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
await rebuildDB("remoteOnly");
|
||||
})
|
||||
)
|
||||
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Send chunks")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
if (this.plugin.replicator instanceof LiveSyncCouchDBReplicator) {
|
||||
await this.plugin.replicator.sendChunks(this.plugin.settings, undefined, true, 0);
|
||||
}
|
||||
})
|
||||
)
|
||||
new Setting(containerMaintenanceEl)
|
||||
.setName("Reset journal received history")
|
||||
.setDesc("Initialise journal received history. On the next sync, every item except this device sent will be downloaded again.")
|
||||
|
||||
@@ -328,7 +328,19 @@ export const SettingInformation: Partial<Record<keyof AllSettings, Configuration
|
||||
"usePluginSyncV2": {
|
||||
name: "Enable per-file-saved customization sync",
|
||||
desc: "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions."
|
||||
}
|
||||
},
|
||||
"handleFilenameCaseSensitive": {
|
||||
name: "Handle files as Case-Sensitive",
|
||||
desc: "If this enabled, All files are handled as case-Sensitive (Previous behaviour)."
|
||||
},
|
||||
"doNotUseFixedRevisionForChunks": {
|
||||
name: "Compute revisions for chunks (Previous behaviour)",
|
||||
desc: "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)"
|
||||
},
|
||||
"sendChunksBulkMaxSize": {
|
||||
name: "Maximum size of chunks to send in one request",
|
||||
desc: "MB"
|
||||
},
|
||||
}
|
||||
function translateInfo(infoSrc: ConfigurationItem | undefined | false) {
|
||||
if (!infoSrc) return false;
|
||||
|
||||
103
styles.css
103
styles.css
@@ -97,55 +97,6 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.CodeMirror-wrap::before,
|
||||
.markdown-preview-view.cm-s-obsidian::before,
|
||||
.markdown-source-view.cm-s-obsidian::before,
|
||||
.canvas-wrapper::before,
|
||||
.empty-state::before {
|
||||
content: var(--sls-log-text, "");
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-variant-emoji: emoji;
|
||||
tab-size: 4;
|
||||
text-align: right;
|
||||
white-space: pre-wrap;
|
||||
position: absolute;
|
||||
border-radius: 4px;
|
||||
/* border:1px solid --background-modifier-border; */
|
||||
display: inline-block;
|
||||
top: 8px;
|
||||
color: --text-normal;
|
||||
opacity: 0.5;
|
||||
font-size: 80%;
|
||||
-webkit-filter: grayscale(100%);
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.empty-state::before,
|
||||
.markdown-preview-view.cm-s-obsidian::before,
|
||||
.markdown-source-view.cm-s-obsidian::before {
|
||||
top: var(--header-height);
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
.is-mobile .empty-state::before,
|
||||
.is-mobile .markdown-preview-view.cm-s-obsidian::before,
|
||||
.is-mobile .markdown-source-view.cm-s-obsidian::before {
|
||||
top: var(--view-header-height);
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
.canvas-wrapper::before {
|
||||
right: 48px;
|
||||
}
|
||||
|
||||
.CodeMirror-wrap::before {
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.cm-s-obsidian > .cm-editor::before {
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
.sls-setting-tab {
|
||||
display: none;
|
||||
}
|
||||
@@ -171,8 +122,8 @@ div.sls-setting-menu-btn {
|
||||
/* width: 100%; */
|
||||
}
|
||||
|
||||
.sls-setting-tab:hover ~ div.sls-setting-menu-btn,
|
||||
.sls-setting-label.selected .sls-setting-tab:checked ~ div.sls-setting-menu-btn {
|
||||
.sls-setting-tab:hover~div.sls-setting-menu-btn,
|
||||
.sls-setting-label.selected .sls-setting-tab:checked~div.sls-setting-menu-btn {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
@@ -291,7 +242,7 @@ div.sls-setting-menu-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.password-input > .setting-item-control > input {
|
||||
.password-input>.setting-item-control>input {
|
||||
-webkit-text-security: disc;
|
||||
}
|
||||
|
||||
@@ -321,6 +272,7 @@ span.ls-mark-cr::after {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.ls-imgdiff-wrap .overlay .img-overlay {
|
||||
-webkit-filter: invert(100%) opacity(50%);
|
||||
filter: invert(100%) opacity(50%);
|
||||
@@ -329,14 +281,61 @@ span.ls-mark-cr::after {
|
||||
left: 0;
|
||||
animation: ls-blink-diff 0.5s cubic-bezier(0.4, 0, 1, 1) infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes ls-blink-diff {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.livesync-status {
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
height: auto;
|
||||
min-height: 1em;
|
||||
position: absolute;
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
padding-right: 16px;
|
||||
top: var(--header-height);
|
||||
z-index: calc(var(--layer-cover) + 1);
|
||||
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-variant-emoji: emoji;
|
||||
tab-size: 4;
|
||||
text-align: right;
|
||||
white-space: pre-wrap;
|
||||
display: inline-block;
|
||||
color: var(--text-normal);
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.livesync-status div {
|
||||
opacity: 0.6;
|
||||
-webkit-filter: grayscale(100%);
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.livesync-status .livesync-status-loghistory {
|
||||
text-align: left;
|
||||
opacity: 0.4;
|
||||
|
||||
}
|
||||
|
||||
.livesync-status div.livesync-status-messagearea {
|
||||
opacity: 0.6;
|
||||
color: var(--text-on-accent);
|
||||
background: var(--background-modifier-error);
|
||||
-webkit-filter: unset;
|
||||
filter: unset;
|
||||
}
|
||||
Reference in New Issue
Block a user