mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-03-17 23:35:18 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48e4d57278 | ||
|
|
7eae25edd0 | ||
|
|
3285c1694b | ||
|
|
ede126d7d4 | ||
|
|
630889680e | ||
|
|
e46714e0f9 | ||
|
|
86d5582f37 | ||
|
|
697ee1855b | ||
|
|
b8edc85528 | ||
|
|
e2740cbefe |
@@ -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,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.23.17",
|
||||
"version": "0.23.22",
|
||||
"minAppVersion": "0.9.12",
|
||||
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"author": "vorotamoroz",
|
||||
|
||||
2800
package-lock.json
generated
2800
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
37
package.json
37
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.23.17",
|
||||
"version": "0.23.22",
|
||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"main": "main.js",
|
||||
"type": "module",
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,10 @@ export class PluginDialogModal extends Modal {
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
this.titleEl.setText("Customization Sync (Beta2)")
|
||||
this.contentEl.style.overflow = "auto";
|
||||
this.contentEl.style.display = "flex";
|
||||
this.contentEl.style.flexDirection = "column";
|
||||
this.titleEl.setText("Customization Sync (Beta3)")
|
||||
if (!this.component) {
|
||||
this.component = new PluginPane({
|
||||
target: contentEl,
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -1,27 +1,29 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { Notice, type PluginManifest, parseYaml, normalizePath, type ListedFiles } from "../deps.ts";
|
||||
import { Notice, type PluginManifest, parseYaml, normalizePath, type ListedFiles, diff_match_patch } from "../deps.ts";
|
||||
|
||||
import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, FilePath, DocumentID, AnyEntry, SavingEntry } from "../lib/src/common/types.ts";
|
||||
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE } from "../lib/src/common/types.ts";
|
||||
import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, FilePath, AnyEntry, SavingEntry, diff_result } from "../lib/src/common/types.ts";
|
||||
import { CANCELLED, LEAVE_TO_SUBSEQUENT, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_SHINY } from "../lib/src/common/types.ts";
|
||||
import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "../common/types.ts";
|
||||
import { createSavingEntryFromLoadedEntry, createTextBlob, delay, fireAndForget, getDocDataAsArray, isDocContentSame } from "../lib/src/common/utils.ts";
|
||||
import { createBlob, createSavingEntryFromLoadedEntry, createTextBlob, delay, fireAndForget, getDocData, getDocDataAsArray, isDocContentSame, isLoadedEntry, isObjectDifferent } from "../lib/src/common/utils.ts";
|
||||
import { Logger } from "../lib/src/common/logger.ts";
|
||||
import { digestHash } from "../lib/src/string_and_binary/hash.ts";
|
||||
import { arrayBufferToBase64, decodeBinary, readString } from 'src/lib/src/string_and_binary/convert.ts';
|
||||
import { serialized, shareRunningResult } from "../lib/src/concurrency/lock.ts";
|
||||
import { LiveSyncCommands } from "./LiveSyncCommands.ts";
|
||||
import { stripAllPrefixes } from "../lib/src/string_and_binary/path.ts";
|
||||
import { PeriodicProcessor, disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "../common/utils.ts";
|
||||
import { EVEN, PeriodicProcessor, disposeMemoObject, isMarkedAsSameChanges, markChangesAreSame, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "../common/utils.ts";
|
||||
import { PluginDialogModal } from "../common/dialogs.ts";
|
||||
import { JsonResolveModal } from "../ui/JsonResolveModal.ts";
|
||||
import { QueueProcessor } from '../lib/src/concurrency/processor.ts';
|
||||
import { pluginScanningCount } from '../lib/src/mock_and_interop/stores.ts';
|
||||
import type ObsidianLiveSyncPlugin from '../main.ts';
|
||||
import { base64ToArrayBuffer, base64ToString } from 'octagonal-wheels/binary/base64';
|
||||
import { ConflictResolveModal } from '../ui/ConflictResolveModal.ts';
|
||||
import { Semaphore } from 'octagonal-wheels/concurrency/semaphore';
|
||||
|
||||
const d = "\u200b";
|
||||
const d2 = "\n";
|
||||
|
||||
|
||||
function serialize(data: PluginDataEx): string {
|
||||
// For higher performance, create custom plug-in data strings.
|
||||
// Self-hosted LiveSync uses `\n` to split chunks. Therefore, grouping together those with similar entropy would work nicely.
|
||||
@@ -41,7 +43,15 @@ function serialize(data: PluginDataEx): string {
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
const DUMMY_HEAD = serialize({
|
||||
category: "CONFIG",
|
||||
name: "migrated",
|
||||
files: [],
|
||||
mtime: 0,
|
||||
term: "-",
|
||||
displayName: `MIRAGED`
|
||||
});
|
||||
const DUMMY_END = d + d2 + "\u200c";
|
||||
function splitWithDelimiters(sources: string[]): string[] {
|
||||
const result: string[] = [];
|
||||
for (const str of sources) {
|
||||
@@ -186,6 +196,7 @@ function deserialize<T>(str: string[], def: T) {
|
||||
|
||||
export const pluginList = writable([] as PluginDataExDisplay[]);
|
||||
export const pluginIsEnumerating = writable(false);
|
||||
export const pluginV2Progress = writable(0);
|
||||
|
||||
export type PluginDataExFile = {
|
||||
filename: string,
|
||||
@@ -196,6 +207,16 @@ export type PluginDataExFile = {
|
||||
hash?: string,
|
||||
displayName?: string,
|
||||
}
|
||||
export interface IPluginDataExDisplay {
|
||||
documentPath: FilePathWithPrefix;
|
||||
category: string;
|
||||
name: string;
|
||||
term: string;
|
||||
displayName?: string;
|
||||
files: (LoadedEntryPluginDataExFile | PluginDataExFile)[];
|
||||
version?: string;
|
||||
mtime: number;
|
||||
}
|
||||
export type PluginDataExDisplay = {
|
||||
documentPath: FilePathWithPrefix,
|
||||
category: string,
|
||||
@@ -206,6 +227,90 @@ export type PluginDataExDisplay = {
|
||||
version?: string,
|
||||
mtime: number,
|
||||
}
|
||||
type LoadedEntryPluginDataExFile = LoadedEntry & PluginDataExFile;
|
||||
|
||||
function categoryToFolder(category: string, configDir: string = ""): string {
|
||||
switch (category) {
|
||||
case "CONFIG": return `${configDir}/`;
|
||||
case "THEME": return `${configDir}/themes/`;
|
||||
case "SNIPPET": return `${configDir}/snippets/`;
|
||||
case "PLUGIN_MAIN": return `${configDir}/plugins/`;
|
||||
case "PLUGIN_DATA": return `${configDir}/plugins/`;
|
||||
case "PLUGIN_ETC": return `${configDir}/plugins/`;
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
export const pluginManifests = new Map<string, PluginManifest>();
|
||||
export const pluginManifestStore = writable(pluginManifests);
|
||||
|
||||
function setManifest(key: string, manifest: PluginManifest) {
|
||||
const old = pluginManifests.get(key);
|
||||
if (old && !isObjectDifferent(manifest, old)) {
|
||||
return;
|
||||
}
|
||||
pluginManifests.set(key, manifest);
|
||||
pluginManifestStore.set(pluginManifests);
|
||||
}
|
||||
|
||||
export class PluginDataExDisplayV2 {
|
||||
documentPath: FilePathWithPrefix;
|
||||
category: string;
|
||||
|
||||
term: string;
|
||||
|
||||
files = [] as LoadedEntryPluginDataExFile[];
|
||||
|
||||
name: string;
|
||||
confKey: string;
|
||||
constructor(data: IPluginDataExDisplay) {
|
||||
this.documentPath = `${data.documentPath}` as FilePathWithPrefix;
|
||||
this.category = `${data.category}`;
|
||||
this.name = `${data.name}`;
|
||||
this.term = `${data.term}`;
|
||||
this.files = [...data.files as LoadedEntryPluginDataExFile[]];
|
||||
this.confKey = `${categoryToFolder(this.category, this.term)}${this.name}`;
|
||||
this.applyLoadedManifest();
|
||||
}
|
||||
async setFile(file: LoadedEntryPluginDataExFile) {
|
||||
const old = this.files.find(e => e.filename == file.filename);
|
||||
if (old) {
|
||||
if (old.mtime == file.mtime && await isDocContentSame(old.data, file.data)) return;
|
||||
this.files = this.files.filter(e => e.filename != file.filename);
|
||||
}
|
||||
this.files.push(file);
|
||||
if (file.filename == "manifest.json") {
|
||||
this.applyLoadedManifest();
|
||||
}
|
||||
}
|
||||
deleteFile(filename: string) {
|
||||
this.files = this.files.filter(e => e.filename != filename);
|
||||
}
|
||||
|
||||
_displayName: string | undefined;
|
||||
_version: string | undefined;
|
||||
|
||||
applyLoadedManifest() {
|
||||
const manifest = pluginManifests.get(this.confKey);
|
||||
if (manifest) {
|
||||
this._displayName = manifest.name;
|
||||
if (this.category == "PLUGIN_MAIN" || this.category == "THEME") {
|
||||
this._version = manifest?.version;
|
||||
}
|
||||
}
|
||||
}
|
||||
get displayName(): string {
|
||||
// if (this._displayNameBuffer !== symbolUnInitialised) return this._displayNameBuffer;
|
||||
// return this._bufferManifest().displayName;
|
||||
return this._displayName || this.name;
|
||||
}
|
||||
get version(): string | undefined {
|
||||
return this._version;
|
||||
}
|
||||
get mtime(): number {
|
||||
return ~~this.files.reduce((a, b) => a + b.mtime, 0) / this.files.length;
|
||||
}
|
||||
}
|
||||
export type PluginDataEx = {
|
||||
documentPath?: FilePathWithPrefix,
|
||||
category: string,
|
||||
@@ -216,25 +321,30 @@ export type PluginDataEx = {
|
||||
version?: string,
|
||||
mtime: number,
|
||||
};
|
||||
|
||||
export class ConfigSync extends LiveSyncCommands {
|
||||
constructor(plugin: ObsidianLiveSyncPlugin) {
|
||||
super(plugin);
|
||||
pluginScanningCount.onChanged((e) => {
|
||||
const total = e.value;
|
||||
pluginIsEnumerating.set(total != 0);
|
||||
// if (total == 0) {
|
||||
// Logger(`Processing configurations done`, LOG_LEVEL_INFO, "get-plugins");
|
||||
// }
|
||||
})
|
||||
}
|
||||
get kvDB() {
|
||||
return this.plugin.kvDB;
|
||||
}
|
||||
|
||||
get useV2() {
|
||||
return this.plugin.settings.usePluginSyncV2;
|
||||
}
|
||||
get useSyncPluginEtc() {
|
||||
return this.plugin.settings.usePluginEtc;
|
||||
}
|
||||
|
||||
pluginDialog?: PluginDialogModal = undefined;
|
||||
periodicPluginSweepProcessor = new PeriodicProcessor(this.plugin, async () => await this.scanAllConfigFiles(false));
|
||||
|
||||
pluginList: PluginDataExDisplay[] = [];
|
||||
pluginList: IPluginDataExDisplay[] = [];
|
||||
showPluginSyncModal() {
|
||||
if (!this.settings.usePluginSync) {
|
||||
return;
|
||||
@@ -277,10 +387,8 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
} 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";
|
||||
// Planned at v0.19.0, realised v0.23.18!
|
||||
return (this.useV2 && this.useSyncPluginEtc) ? "PLUGIN_ETC" : "";
|
||||
}
|
||||
// return "PLUGIN";
|
||||
}
|
||||
@@ -321,6 +429,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
}
|
||||
async reloadPluginList(showMessage: boolean) {
|
||||
this.pluginList = [];
|
||||
this.loadedManifest_mTime.clear();
|
||||
pluginList.set(this.pluginList)
|
||||
await this.updatePluginList(showMessage);
|
||||
}
|
||||
@@ -355,30 +464,36 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
async createMissingConfigurationEntry() {
|
||||
let saveRequired = false;
|
||||
for (const v of this.pluginList) {
|
||||
const key = `${v.category}/${v.name}`;
|
||||
if (!(key in this.plugin.settings.pluginSyncExtendedSetting)) {
|
||||
this.plugin.settings.pluginSyncExtendedSetting[key] = {
|
||||
key,
|
||||
mode: MODE_SELECTIVE,
|
||||
files: []
|
||||
}
|
||||
}
|
||||
if (this.plugin.settings.pluginSyncExtendedSetting[key].files.sort().join(",").toLowerCase() !=
|
||||
v.files.map(e => e.filename).sort().join(",").toLowerCase()) {
|
||||
this.plugin.settings.pluginSyncExtendedSetting[key].files = v.files.map(e => e.filename).sort();
|
||||
saveRequired = true;
|
||||
}
|
||||
}
|
||||
if (saveRequired) {
|
||||
await this.plugin.saveSettingData();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pluginScanProcessor = new QueueProcessor(async (v: AnyEntry[]) => {
|
||||
const plugin = v[0];
|
||||
if (this.useV2) {
|
||||
await this.migrateV1ToV2(false, plugin);
|
||||
return [];
|
||||
}
|
||||
const path = plugin.path || this.getPath(plugin);
|
||||
const oldEntry = (this.pluginList.find(e => e.documentPath == path));
|
||||
if (oldEntry && oldEntry.mtime == plugin.mtime) return [];
|
||||
try {
|
||||
const pluginData = await this.loadPluginData(path);
|
||||
if (pluginData) {
|
||||
let newList = [...this.pluginList];
|
||||
newList = newList.filter(x => x.documentPath != pluginData.documentPath);
|
||||
newList.push(pluginData);
|
||||
this.pluginList = newList;
|
||||
pluginList.set(newList);
|
||||
}
|
||||
// Failed to load
|
||||
return [];
|
||||
|
||||
} catch (ex) {
|
||||
Logger(`Something happened at enumerating customization :${path}`, LOG_LEVEL_NOTICE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
return [];
|
||||
}, { suspended: false, batchSize: 1, concurrentLimit: 10, delay: 100, yieldThreshold: 10, maintainDelay: false, totalRemainingReactiveSource: pluginScanningCount }).startPipeline();
|
||||
|
||||
pluginScanProcessorV2 = new QueueProcessor(async (v: AnyEntry[]) => {
|
||||
const plugin = v[0];
|
||||
const path = plugin.path || this.getPath(plugin);
|
||||
const oldEntry = (this.pluginList.find(e => e.documentPath == path));
|
||||
@@ -400,17 +515,220 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
return [];
|
||||
}, { suspended: false, batchSize: 1, concurrentLimit: 10, delay: 100, yieldThreshold: 10, maintainDelay: false, totalRemainingReactiveSource: pluginScanningCount }).startPipeline().root.onUpdateProgress(() => {
|
||||
scheduleTask("checkMissingConfigurations", 250, async () => {
|
||||
if (this.pluginScanProcessor.isIdle()) {
|
||||
await this.createMissingConfigurationEntry();
|
||||
}
|
||||
});
|
||||
});
|
||||
}, { suspended: false, batchSize: 1, concurrentLimit: 10, delay: 100, yieldThreshold: 10, maintainDelay: false, totalRemainingReactiveSource: pluginScanningCount }).startPipeline();
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
filenameWithUnifiedKey(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]) : path.split("/").slice(-2)[0];
|
||||
const baseName = category == "CONFIG" || category == "SNIPPET" ? name : path.split("/").slice(3).join("/");
|
||||
return `${ICXHeader}${term}/${category}/${name}%${baseName}` as FilePathWithPrefix;
|
||||
}
|
||||
|
||||
unifiedKeyPrefixOfTerminal(termOverRide?: string) {
|
||||
const term = termOverRide || this.plugin.deviceAndVaultName;
|
||||
return `${ICXHeader}${term}/` as FilePathWithPrefix;
|
||||
}
|
||||
|
||||
parseUnifiedPath(unifiedPath: FilePathWithPrefix): { category: string, device: string, key: string, filename: string, pathV1: FilePathWithPrefix } {
|
||||
const [device, category, ...rest] = stripAllPrefixes(unifiedPath).split("/");
|
||||
const relativePath = rest.join("/");
|
||||
const [key, filename] = relativePath.split("%");
|
||||
const pathV1 = (unifiedPath.split("%")[0] + ".md") as FilePathWithPrefix;
|
||||
return { device, category, key, filename, pathV1 };
|
||||
}
|
||||
|
||||
loadedManifest_mTime = new Map<string, number>();
|
||||
|
||||
async createPluginDataExFileV2(unifiedPathV2: FilePathWithPrefix, loaded?: LoadedEntry): Promise<false | LoadedEntryPluginDataExFile> {
|
||||
const { category, key, filename, device } = this.parseUnifiedPath(unifiedPathV2);
|
||||
if (!loaded) {
|
||||
const d = await this.localDatabase.getDBEntry(unifiedPathV2);
|
||||
if (!d) {
|
||||
Logger(`The file ${unifiedPathV2} is not found`, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
if (!isLoadedEntry(d)) {
|
||||
Logger(`The file ${unifiedPathV2} is not a note`, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
loaded = d;
|
||||
}
|
||||
const confKey = `${categoryToFolder(category, device)}${key}`;
|
||||
const relativeFilename = `${categoryToFolder(category, "")}${(category == "CONFIG" || category == "SNIPPET") ? "" : (key + "/")}${filename}`.substring(1);
|
||||
const dataSrc = getDocData(loaded.data);
|
||||
const dataStart = dataSrc.indexOf(DUMMY_END);
|
||||
const data = dataSrc.substring(dataStart + DUMMY_END.length);
|
||||
const file: LoadedEntryPluginDataExFile = {
|
||||
...loaded,
|
||||
hash: "",
|
||||
data: [base64ToString(data)],
|
||||
filename: relativeFilename,
|
||||
displayName: filename,
|
||||
};
|
||||
if (filename == "manifest.json") {
|
||||
// Same as previously loaded
|
||||
if (this.loadedManifest_mTime.get(confKey) != file.mtime && pluginManifests.get(confKey) == undefined) {
|
||||
try {
|
||||
const parsedManifest = JSON.parse(base64ToString(data)) as PluginManifest;
|
||||
setManifest(confKey, parsedManifest);
|
||||
this.pluginList.filter(e => e instanceof PluginDataExDisplayV2 && e.confKey == confKey).forEach(e => (e as PluginDataExDisplayV2).applyLoadedManifest());
|
||||
pluginList.set(this.pluginList);
|
||||
} catch (ex) {
|
||||
Logger(`The file ${loaded.path} seems to manifest, but could not be decoded as JSON`, LOG_LEVEL_VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
this.loadedManifest_mTime.set(confKey, file.mtime);
|
||||
} else {
|
||||
this.pluginList.filter(e => e instanceof PluginDataExDisplayV2 && e.confKey == confKey).forEach(e => (e as PluginDataExDisplayV2).applyLoadedManifest());
|
||||
pluginList.set(this.pluginList);
|
||||
}
|
||||
// }
|
||||
}
|
||||
return file;
|
||||
|
||||
}
|
||||
createPluginDataFromV2(unifiedPathV2: FilePathWithPrefix) {
|
||||
const { category, device, key, pathV1 } = this.parseUnifiedPath(unifiedPathV2);
|
||||
if (category == "") return;
|
||||
|
||||
const ret: PluginDataExDisplayV2 = new PluginDataExDisplayV2({
|
||||
documentPath: pathV1,
|
||||
category: category,
|
||||
name: key,
|
||||
term: `${device}`,
|
||||
files: [],
|
||||
mtime: 0,
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
updatingV2Count = 0;
|
||||
|
||||
async updatePluginListV2(showMessage: boolean, unifiedFilenameWithKey: FilePathWithPrefix): Promise<void> {
|
||||
try {
|
||||
this.updatingV2Count++;
|
||||
pluginV2Progress.set(this.updatingV2Count);
|
||||
// const unifiedFilenameWithKey = this.filenameWithUnifiedKey(updatedDocumentPath);
|
||||
const { pathV1 } = this.parseUnifiedPath(unifiedFilenameWithKey);
|
||||
|
||||
const oldEntry = this.pluginList.find(e => e.documentPath == pathV1);
|
||||
let entry: PluginDataExDisplayV2 | undefined = undefined;
|
||||
|
||||
if (!oldEntry || !(oldEntry instanceof PluginDataExDisplayV2)) {
|
||||
const newEntry = this.createPluginDataFromV2(unifiedFilenameWithKey);
|
||||
if (newEntry) {
|
||||
entry = newEntry;
|
||||
}
|
||||
} else if (oldEntry instanceof PluginDataExDisplayV2) {
|
||||
entry = oldEntry;
|
||||
}
|
||||
if (!entry) return;
|
||||
const file = await this.createPluginDataExFileV2(unifiedFilenameWithKey);
|
||||
if (file) {
|
||||
await entry.setFile(file);
|
||||
} else {
|
||||
entry.deleteFile(unifiedFilenameWithKey);
|
||||
if (entry.files.length == 0) {
|
||||
this.pluginList = this.pluginList.filter(e => e.documentPath != pathV1);
|
||||
}
|
||||
}
|
||||
const newList = this.pluginList.filter(e => e.documentPath != entry.documentPath);
|
||||
newList.push(entry);
|
||||
this.pluginList = newList;
|
||||
|
||||
scheduleTask("updatePluginListV2", 100, () => {
|
||||
pluginList.set(this.pluginList);
|
||||
});
|
||||
} finally {
|
||||
this.updatingV2Count--;
|
||||
pluginV2Progress.set(this.updatingV2Count);
|
||||
}
|
||||
}
|
||||
|
||||
async migrateV1ToV2(showMessage: boolean, entry: AnyEntry): Promise<void> {
|
||||
const v1Path = entry.path;
|
||||
Logger(`Migrating ${entry.path} to V2`, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
|
||||
if (entry.deleted) {
|
||||
Logger(`The entry ${v1Path} is already deleted`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
if (!v1Path.endsWith(".md") && !v1Path.startsWith(ICXHeader)) {
|
||||
Logger(`The entry ${v1Path} is not a customisation sync binder`, LOG_LEVEL_VERBOSE);
|
||||
return
|
||||
}
|
||||
if (v1Path.indexOf("%") !== -1) {
|
||||
Logger(`The entry ${v1Path} is already migrated`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
const loadedEntry = await this.localDatabase.getDBEntry(v1Path);
|
||||
if (!loadedEntry) {
|
||||
Logger(`The entry ${v1Path} is not found`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
|
||||
const pluginData = deserialize(getDocDataAsArray(loadedEntry.data), {}) as PluginDataEx;
|
||||
const prefixPath = v1Path.slice(0, -(".md".length)) + "%";
|
||||
const category = pluginData.category;
|
||||
|
||||
for (const f of pluginData.files) {
|
||||
const stripTable: Record<string, number> = {
|
||||
"CONFIG": 0,
|
||||
"THEME": 2,
|
||||
"SNIPPET": 1,
|
||||
"PLUGIN_MAIN": 2,
|
||||
"PLUGIN_DATA": 2,
|
||||
"PLUGIN_ETC": 2,
|
||||
}
|
||||
const deletePrefixCount = stripTable?.[category] ?? 1;
|
||||
const relativeFilename = f.filename.split("/").slice(deletePrefixCount).join("/");
|
||||
const v2Path = (prefixPath + relativeFilename) as FilePathWithPrefix;
|
||||
// console.warn(`Migrating ${v1Path} / ${relativeFilename} to ${v2Path}`);
|
||||
Logger(`Migrating ${v1Path} / ${relativeFilename} to ${v2Path}`, LOG_LEVEL_VERBOSE);
|
||||
const newId = await this.plugin.path2id(v2Path);
|
||||
// const buf =
|
||||
|
||||
const data = createBlob([DUMMY_HEAD, DUMMY_END, ...getDocDataAsArray(f.data)]);
|
||||
|
||||
const saving: SavingEntry = {
|
||||
...loadedEntry,
|
||||
_rev: undefined,
|
||||
_id: newId,
|
||||
path: v2Path,
|
||||
data: data,
|
||||
datatype: "plain",
|
||||
type: "plain",
|
||||
children: [],
|
||||
eden: {}
|
||||
}
|
||||
const r = await this.plugin.localDatabase.putDBEntry(saving);
|
||||
if (r && r.ok) {
|
||||
Logger(`Migrated ${v1Path} / ${f.filename} to ${v2Path}`, LOG_LEVEL_INFO);
|
||||
const delR = await this.deleteConfigOnDatabase(v1Path);
|
||||
if (delR) {
|
||||
Logger(`Deleted ${v1Path} successfully`, LOG_LEVEL_INFO);
|
||||
} else {
|
||||
Logger(`Failed to delete ${v1Path}`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async updatePluginList(showMessage: boolean, updatedDocumentPath?: FilePathWithPrefix): Promise<void> {
|
||||
// pluginList.set([]);
|
||||
if (!this.settings.usePluginSync) {
|
||||
this.pluginScanProcessor.clearQueue();
|
||||
this.pluginList = [];
|
||||
@@ -418,60 +736,173 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.updatingV2Count++;
|
||||
pluginV2Progress.set(this.updatingV2Count);
|
||||
const updatedDocumentId = updatedDocumentPath ? await this.path2id(updatedDocumentPath) : "";
|
||||
const plugins = updatedDocumentPath ?
|
||||
this.localDatabase.findEntries(updatedDocumentId, updatedDocumentId + "\u{10ffff}", { include_docs: true, key: updatedDocumentId, limit: 1 }) :
|
||||
this.localDatabase.findEntries(ICXHeader + "", `${ICXHeader}\u{10ffff}`, { include_docs: true });
|
||||
for await (const v of plugins) {
|
||||
if (v.deleted || v._deleted) continue;
|
||||
if (v.path.indexOf("%") !== -1) {
|
||||
fireAndForget(() => this.updatePluginListV2(showMessage, v.path));
|
||||
continue;
|
||||
}
|
||||
|
||||
const path = v.path || this.getPath(v);
|
||||
if (updatedDocumentPath && updatedDocumentPath != path) continue;
|
||||
this.pluginScanProcessor.enqueue(v);
|
||||
|
||||
}
|
||||
} finally {
|
||||
pluginIsEnumerating.set(false);
|
||||
this.updatingV2Count--;
|
||||
pluginV2Progress.set(this.updatingV2Count);
|
||||
}
|
||||
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 = deserialize(getDocDataAsArray(docA.data), {}) as PluginDataEx;
|
||||
pluginDataA.documentPath = dataA.documentPath;
|
||||
const pluginDataB = deserialize(getDocDataAsArray(docB.data), {}) as PluginDataEx;
|
||||
pluginDataB.documentPath = dataB.documentPath;
|
||||
|
||||
// Use outer structure to wrap each data.
|
||||
return await this.showJSONMergeDialogAndMerge(docA, docB, pluginDataA, pluginDataB);
|
||||
|
||||
async compareUsingDisplayData(dataA: IPluginDataExDisplay, dataB: IPluginDataExDisplay, compareEach = false) {
|
||||
const loadFile = async (data: IPluginDataExDisplay) => {
|
||||
if (data instanceof PluginDataExDisplayV2 || compareEach) {
|
||||
return data.files[0] as LoadedEntryPluginDataExFile;
|
||||
}
|
||||
const loadDoc = await this.localDatabase.getDBEntry(data.documentPath);
|
||||
if (!loadDoc) return false;
|
||||
const pluginData = deserialize(getDocDataAsArray(loadDoc.data), {}) as PluginDataEx;
|
||||
pluginData.documentPath = data.documentPath;
|
||||
const file = pluginData.files[0];
|
||||
const doc = { ...loadDoc, ...file, datatype: "newnote" } as LoadedEntryPluginDataExFile;
|
||||
return doc;
|
||||
}
|
||||
const fileA = await loadFile(dataA);
|
||||
const fileB = await loadFile(dataB);
|
||||
Logger(`Comparing: ${dataA.documentPath} <-> ${dataB.documentPath}`, LOG_LEVEL_VERBOSE);
|
||||
if (!fileA || !fileB) {
|
||||
Logger(`Could not load ${dataA.name} for comparison: ${!fileA ? dataA.term : ""}${!fileB ? dataB.term : ""}`, LOG_LEVEL_NOTICE);
|
||||
return false;
|
||||
}
|
||||
let path = stripAllPrefixes(fileA.path.split("/").slice(-1).join("/") as FilePath); // TODO:adjust
|
||||
if (path.indexOf("%") !== -1) {
|
||||
path = path.split("%")[1] as FilePath;
|
||||
}
|
||||
if (fileA.path.endsWith(".json")) {
|
||||
return serialized("config:merge-data", () => new Promise<boolean>((res) => {
|
||||
Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE);
|
||||
// const docs = [docA, docB];
|
||||
const modal = new JsonResolveModal(this.app, path, [fileA, fileB], async (keep, result) => {
|
||||
if (result == null) return res(false);
|
||||
try {
|
||||
res(await this.applyData(dataA, result));
|
||||
} catch (ex) {
|
||||
Logger("Could not apply merged file");
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
res(false);
|
||||
}
|
||||
}, "Local", `${dataB.term}`, "B", true, true, "Difference between local and remote");
|
||||
modal.open();
|
||||
}));
|
||||
} else {
|
||||
const dmp = new diff_match_patch();
|
||||
let docAData = getDocData(fileA.data);
|
||||
let docBData = getDocData(fileB.data);
|
||||
if (fileA?.datatype != "plain") {
|
||||
docAData = base64ToString(docAData);
|
||||
}
|
||||
if (fileB?.datatype != "plain") {
|
||||
docBData = base64ToString(docBData);
|
||||
}
|
||||
const diffMap = dmp.diff_linesToChars_(docAData, docBData);
|
||||
|
||||
const diff = dmp.diff_main(diffMap.chars1, diffMap.chars2, false);
|
||||
dmp.diff_charsToLines_(diff, diffMap.lineArray);
|
||||
dmp.diff_cleanupSemantic(diff);
|
||||
const diffResult: diff_result = {
|
||||
left: { rev: "A", ...fileA, data: docAData },
|
||||
right: { rev: "B", ...fileB, data: docBData },
|
||||
diff: diff
|
||||
}
|
||||
console.dir(diffResult);
|
||||
const d = new ConflictResolveModal(this.app, path, diffResult, true, dataB.term);
|
||||
d.open();
|
||||
const ret = await d.waitForResult();
|
||||
if (ret === CANCELLED) return false;
|
||||
if (ret === LEAVE_TO_SUBSEQUENT) return false;
|
||||
const resultContent = ret == "A" ? docAData : ret == "B" ? docBData : undefined;
|
||||
if (resultContent) {
|
||||
return await this.applyData(dataA, resultContent);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
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, datatype: "newnote" } as LoadedEntry, docBx = { ...docB, ...fileB, datatype: "newnote" } as LoadedEntry
|
||||
return serialized("config:merge-data", () => 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);
|
||||
async applyDataV2(data: PluginDataExDisplayV2, content?: string): Promise<boolean> {
|
||||
const baseDir = this.app.vault.configDir;
|
||||
try {
|
||||
if (content) {
|
||||
// const dt = createBlob(content);
|
||||
const filename = data.files[0].filename;
|
||||
Logger(`Applying ${filename} of ${data.displayName || data.name}..`);
|
||||
const path = `${baseDir}/${filename}` as FilePath;
|
||||
await this.vaultAccess.ensureDirectory(path);
|
||||
// If the content has applied, modified time will be updated to the current time.
|
||||
await this.vaultAccess.adapterWrite(path, content);
|
||||
await this.storeCustomisationFileV2(path, this.plugin.deviceAndVaultName);
|
||||
|
||||
} else {
|
||||
const files = data.files;
|
||||
for (const f of files) {
|
||||
// If files have applied, modified time will be updated to the current time.
|
||||
const stat = { mtime: f.mtime, ctime: f.ctime };
|
||||
const path = `${baseDir}/${f.filename}` as FilePath;
|
||||
Logger(`Applying ${f.filename} of ${data.displayName || data.name}..`);
|
||||
// const contentEach = createBlob(f.data);
|
||||
this.vaultAccess.ensureDirectory(path);
|
||||
|
||||
if (f.datatype == "newnote") {
|
||||
let oldData;
|
||||
try {
|
||||
oldData = await this.vaultAccess.adapterReadBinary(path);
|
||||
} catch (ex) {
|
||||
oldData = new ArrayBuffer(0);
|
||||
}
|
||||
const content = base64ToArrayBuffer(f.data);
|
||||
if (await isDocContentSame(oldData, content)) {
|
||||
Logger(`The file ${f.filename} is already up-to-date`, LOG_LEVEL_VERBOSE);
|
||||
continue;
|
||||
}
|
||||
await this.vaultAccess.adapterWrite(path, content, stat);
|
||||
} else {
|
||||
let oldData;
|
||||
try {
|
||||
oldData = await this.vaultAccess.adapterRead(path);
|
||||
} catch (ex) {
|
||||
oldData = "";
|
||||
}
|
||||
const content = getDocData(f.data);
|
||||
if (await isDocContentSame(oldData, content)) {
|
||||
Logger(`The file ${f.filename} is already up-to-date`, LOG_LEVEL_VERBOSE);
|
||||
continue;
|
||||
}
|
||||
await this.vaultAccess.adapterWrite(path, content, stat);
|
||||
}
|
||||
Logger(`Applied ${f.filename} of ${data.displayName || data.name}..`);
|
||||
await this.storeCustomisationFileV2(path, this.plugin.deviceAndVaultName);
|
||||
}
|
||||
}, "📡", "🛰️", "B");
|
||||
modal.open();
|
||||
}));
|
||||
}
|
||||
} catch (ex) {
|
||||
Logger(`Applying ${data.displayName || data.name}.. Failed`, LOG_LEVEL_NOTICE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
async applyData(data: PluginDataEx, content?: string): Promise<boolean> {
|
||||
Logger(`Applying ${data.displayName || data.name}..`);
|
||||
async applyData(data: IPluginDataExDisplay, content?: string): Promise<boolean> {
|
||||
Logger(`Applying ${data.displayName || data.name
|
||||
}..`);
|
||||
|
||||
if (data instanceof PluginDataExDisplayV2) {
|
||||
return this.applyDataV2(data, content);
|
||||
}
|
||||
const baseDir = this.app.vault.configDir;
|
||||
try {
|
||||
if (!data.documentPath) throw "InternalError: Document path not exist";
|
||||
@@ -532,9 +963,22 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
async deleteData(data: PluginDataEx): Promise<boolean> {
|
||||
try {
|
||||
if (data.documentPath) {
|
||||
await this.deleteConfigOnDatabase(data.documentPath);
|
||||
await this.updatePluginList(false, data.documentPath);
|
||||
Logger(`Delete: ${data.documentPath}`, LOG_LEVEL_NOTICE);
|
||||
const delList = [];
|
||||
if (this.useV2) {
|
||||
const deleteList = this.pluginList.filter(e => e.documentPath == data.documentPath).filter(e => e instanceof PluginDataExDisplayV2).map(e => e.files).flat();
|
||||
for (const e of deleteList) {
|
||||
delList.push(e.path);
|
||||
}
|
||||
}
|
||||
delList.push(data.documentPath);
|
||||
const p = delList.map(async e => {
|
||||
await this.deleteConfigOnDatabase(e);
|
||||
await this.updatePluginList(false, e)
|
||||
});
|
||||
await Promise.allSettled(p);
|
||||
// await this.deleteConfigOnDatabase(data.documentPath);
|
||||
// await this.updatePluginList(false, data.documentPath);
|
||||
Logger(`Deleted: ${data.category}/${data.name} of ${data.category} (${delList.length} items)`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
return true;
|
||||
} catch (ex) {
|
||||
@@ -645,15 +1089,78 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
}
|
||||
}
|
||||
|
||||
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 storeCustomisationFileV2(path: FilePath, term: string, force = false) {
|
||||
const vf = this.filenameWithUnifiedKey(path, term);
|
||||
return await serialized(`plugin-${vf}`, async () => {
|
||||
const prefixedFileName = vf;
|
||||
|
||||
const id = await this.path2id(prefixedFileName);
|
||||
const stat = await this.vaultAccess.adapterStat(path);
|
||||
if (!stat) {
|
||||
return false;
|
||||
}
|
||||
const mtime = stat.mtime;
|
||||
const content = await this.vaultAccess.adapterReadBinary(path);
|
||||
const contentBlob = createBlob([DUMMY_HEAD, DUMMY_END, ...await arrayBufferToBase64(content)]);
|
||||
// const contentBlob = createBlob(content);
|
||||
try {
|
||||
const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, undefined, false);
|
||||
let saveData: SavingEntry;
|
||||
if (old === false) {
|
||||
saveData = {
|
||||
_id: id,
|
||||
path: prefixedFileName,
|
||||
data: contentBlob,
|
||||
mtime,
|
||||
ctime: mtime,
|
||||
datatype: "plain",
|
||||
size: contentBlob.size,
|
||||
children: [],
|
||||
deleted: false,
|
||||
type: "plain",
|
||||
eden: {}
|
||||
};
|
||||
} else {
|
||||
if (isMarkedAsSameChanges(prefixedFileName, [old.mtime, mtime + 1]) == EVEN) {
|
||||
Logger(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (Already checked the same)`, LOG_LEVEL_DEBUG);
|
||||
return;
|
||||
}
|
||||
const docXDoc = await this.localDatabase.getDBEntryFromMeta(old, {}, false, false);
|
||||
if (docXDoc == false) {
|
||||
throw "Could not load the document";
|
||||
}
|
||||
const dataSrc = getDocData(docXDoc.data);
|
||||
const dataStart = dataSrc.indexOf(DUMMY_END);
|
||||
const oldContent = dataSrc.substring(dataStart + DUMMY_END.length);
|
||||
const oldContentArray = base64ToArrayBuffer(oldContent);
|
||||
if (await isDocContentSame(oldContentArray, content)) {
|
||||
Logger(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (the same content)`, LOG_LEVEL_VERBOSE);
|
||||
markChangesAreSame(prefixedFileName, old.mtime, mtime + 1);
|
||||
return true;
|
||||
}
|
||||
saveData =
|
||||
{
|
||||
...old,
|
||||
data: contentBlob,
|
||||
mtime,
|
||||
size: contentBlob.size,
|
||||
datatype: "plain",
|
||||
children: [],
|
||||
deleted: false,
|
||||
type: "plain",
|
||||
};
|
||||
}
|
||||
const ret = await this.localDatabase.putDBEntry(saveData);
|
||||
Logger(`STORAGE --> DB:${prefixedFileName}: (config) Done`);
|
||||
fireAndForget(() => this.updatePluginListV2(false, this.filenameWithUnifiedKey(path)));
|
||||
return ret;
|
||||
} catch (ex) {
|
||||
Logger(`STORAGE --> DB:${prefixedFileName}: (config) Failed`);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
})
|
||||
}
|
||||
async storeCustomizationFiles(path: FilePath, termOverRide?: string) {
|
||||
const term = termOverRide || this.plugin.deviceAndVaultName;
|
||||
@@ -661,7 +1168,13 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
Logger("We have to configure the device name", LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
if (this.useV2) {
|
||||
return await this.storeCustomisationFileV2(path, term);
|
||||
}
|
||||
const vf = this.filenameToUnifiedKey(path, term);
|
||||
// console.warn(`Storing ${path} to ${bareVF} :--> ${keyedVF}`);
|
||||
|
||||
|
||||
return await serialized(`plugin-${vf}`, async () => {
|
||||
const category = this.getFileCategory(path);
|
||||
let mtime = 0;
|
||||
@@ -787,7 +1300,9 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
return false;
|
||||
|
||||
const configDir = normalizePath(this.app.vault.configDir);
|
||||
const synchronisedInConfigSync = Object.values(this.settings.pluginSyncExtendedSetting).filter(e => e.mode != MODE_SELECTIVE).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase());
|
||||
const synchronisedInConfigSync = Object.values(this.settings.pluginSyncExtendedSetting).filter(e =>
|
||||
e.mode != MODE_SELECTIVE && e.mode != MODE_SHINY
|
||||
).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase());
|
||||
if (synchronisedInConfigSync.some(e => e.startsWith(path.toLowerCase()))) {
|
||||
Logger(`Customization file skipped: ${path}`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
@@ -807,6 +1322,8 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async scanAllConfigFiles(showMessage: boolean) {
|
||||
await shareRunningResult("scanAllConfigFiles", async () => {
|
||||
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||
@@ -817,40 +1334,94 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
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;
|
||||
if (!p) {
|
||||
Logger(`scanAllConfigFiles - File not found: ${vp}`, LOG_LEVEL_VERBOSE);
|
||||
continue;
|
||||
if (this.useV2) {
|
||||
const filesAllUnified = filesAll.filter(e => this.isTargetPath(e)).map(e => [this.filenameWithUnifiedKey(e, term), e] as [FilePathWithPrefix, FilePath]);
|
||||
const localFileMap = new Map(filesAllUnified.map(e => [e[0], e[1]]));
|
||||
const prefix = this.unifiedKeyPrefixOfTerminal(term);
|
||||
const entries = this.localDatabase.findEntries(prefix + "", `${prefix}\u{10ffff}`, { include_docs: true });
|
||||
const tasks = [] as (() => Promise<void>)[];
|
||||
const concurrency = 10;
|
||||
const semaphore = Semaphore(concurrency);
|
||||
for await (const item of entries) {
|
||||
if (item.path.indexOf("%") !== -1) {
|
||||
continue;
|
||||
}
|
||||
tasks.push(async () => {
|
||||
const releaser = await semaphore.acquire();
|
||||
try {
|
||||
const unifiedFilenameWithKey = `${item._id}` as FilePathWithPrefix;
|
||||
const localPath = localFileMap.get(unifiedFilenameWithKey);
|
||||
if (localPath) {
|
||||
await this.storeCustomisationFileV2(localPath, term);
|
||||
localFileMap.delete(unifiedFilenameWithKey);
|
||||
} else {
|
||||
await this.deleteConfigOnDatabase(unifiedFilenameWithKey);
|
||||
}
|
||||
} catch (ex) {
|
||||
Logger(`scanAllConfigFiles - Error: ${item._id}`, LOG_LEVEL_VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
} finally {
|
||||
releaser();
|
||||
}
|
||||
})
|
||||
}
|
||||
await this.storeCustomizationFiles(p);
|
||||
deleteCandidate = deleteCandidate.filter(e => e != vp);
|
||||
await Promise.all(tasks.map(e => e()));
|
||||
// Extra files
|
||||
const taskExtra = [] as (() => Promise<void>)[];
|
||||
for (const [, filePath] of localFileMap) {
|
||||
taskExtra.push(async () => {
|
||||
const releaser = await semaphore.acquire();
|
||||
try {
|
||||
await this.storeCustomisationFileV2(filePath, term);
|
||||
} catch (ex) {
|
||||
Logger(`scanAllConfigFiles - Error: ${filePath}`, LOG_LEVEL_VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
finally {
|
||||
releaser();
|
||||
}
|
||||
})
|
||||
}
|
||||
await Promise.all(taskExtra.map(e => e()));
|
||||
|
||||
this.updatePluginList(false).then(/* fire and forget */);
|
||||
} else {
|
||||
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;
|
||||
if (!p) {
|
||||
Logger(`scanAllConfigFiles - File not found: ${vp}`, LOG_LEVEL_VERBOSE);
|
||||
continue;
|
||||
}
|
||||
await this.storeCustomizationFiles(p);
|
||||
deleteCandidate = deleteCandidate.filter(e => e != vp);
|
||||
}
|
||||
for (const vp of deleteCandidate) {
|
||||
await this.deleteConfigOnDatabase(vp);
|
||||
}
|
||||
this.updatePluginList(false).then(/* fire and forget */);
|
||||
}
|
||||
for (const vp of deleteCandidate) {
|
||||
await this.deleteConfigOnDatabase(vp);
|
||||
}
|
||||
this.updatePluginList(false).then(/* fire and forget */);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteConfigOnDatabase(prefixedFileName: FilePathWithPrefix, forceWrite = false) {
|
||||
|
||||
// const id = await this.path2id(prefixedFileName);
|
||||
const mtime = new Date().getTime();
|
||||
await serialized("file-x-" + prefixedFileName, async () => {
|
||||
return await serialized("file-x-" + prefixedFileName, async () => {
|
||||
try {
|
||||
const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, undefined, false) as InternalFileEntry | false;
|
||||
let saveData: InternalFileEntry;
|
||||
if (old === false) {
|
||||
Logger(`STORAGE -x> DB:${prefixedFileName}: (config) already deleted (Not found on database)`);
|
||||
return;
|
||||
return true;
|
||||
} else {
|
||||
if (old.deleted) {
|
||||
Logger(`STORAGE -x> DB:${prefixedFileName}: (config) already deleted`);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
saveData =
|
||||
{
|
||||
@@ -865,6 +1436,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
await this.localDatabase.putRaw(saveData);
|
||||
await this.updatePluginList(false, prefixedFileName);
|
||||
Logger(`STORAGE -x> DB:${prefixedFileName}: (config) Done`);
|
||||
return true;
|
||||
} catch (ex) {
|
||||
Logger(`STORAGE -x> DB:${prefixedFileName}: (config) Failed`);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
|
||||
@@ -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: f05b631841...633af447d2
754
src/main.ts
754
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() {
|
||||
|
||||
@@ -13,10 +13,22 @@ export class ConflictResolveModal extends Modal {
|
||||
isClosed = false;
|
||||
consumed = false;
|
||||
|
||||
constructor(app: App, filename: string, diff: diff_result) {
|
||||
title: string = "Conflicting changes";
|
||||
|
||||
pluginPickMode: boolean = false;
|
||||
localName: string = "Keep A";
|
||||
remoteName: string = "Keep B";
|
||||
|
||||
constructor(app: App, filename: string, diff: diff_result, pluginPickMode?: boolean, remoteName?: string) {
|
||||
super(app);
|
||||
this.result = diff;
|
||||
this.filename = filename;
|
||||
this.pluginPickMode = pluginPickMode || false;
|
||||
if (this.pluginPickMode) {
|
||||
this.title = "Pick a version";
|
||||
this.remoteName = `Use ${remoteName || "Remote"}`;
|
||||
this.localName = "Use Local"
|
||||
}
|
||||
// Send cancel signal for the previous merge dialogue
|
||||
// if not there, simply be ignored.
|
||||
// sendValue("close-resolve-conflict:" + this.filename, false);
|
||||
@@ -36,7 +48,7 @@ export class ConflictResolveModal extends Modal {
|
||||
}
|
||||
}, 10)
|
||||
// sendValue("close-resolve-conflict:" + this.filename, false);
|
||||
this.titleEl.setText("Conflicting changes");
|
||||
this.titleEl.setText(this.title);
|
||||
contentEl.empty();
|
||||
contentEl.createEl("span", { text: this.filename });
|
||||
const div = contentEl.createDiv("");
|
||||
@@ -62,10 +74,12 @@ export class ConflictResolveModal extends Modal {
|
||||
div2.innerHTML = `
|
||||
<span class='deleted'>A:${date1}</span><br /><span class='added'>B:${date2}</span><br>
|
||||
`;
|
||||
contentEl.createEl("button", { text: "Keep A" }, (e) => e.addEventListener("click", () => this.sendResponse(this.result.right.rev)));
|
||||
contentEl.createEl("button", { text: "Keep B" }, (e) => e.addEventListener("click", () => this.sendResponse(this.result.left.rev)));
|
||||
contentEl.createEl("button", { text: "Concat both" }, (e) => e.addEventListener("click", () => this.sendResponse(LEAVE_TO_SUBSEQUENT)));
|
||||
contentEl.createEl("button", { text: "Not now" }, (e) => e.addEventListener("click", () => this.sendResponse(CANCELLED)));
|
||||
contentEl.createEl("button", { text: this.localName }, (e) => e.addEventListener("click", () => this.sendResponse(this.result.right.rev))).style.marginRight = "4px";
|
||||
contentEl.createEl("button", { text: this.remoteName }, (e) => e.addEventListener("click", () => this.sendResponse(this.result.left.rev))).style.marginRight = "4px";
|
||||
if (!this.pluginPickMode) {
|
||||
contentEl.createEl("button", { text: "Concat both" }, (e) => e.addEventListener("click", () => this.sendResponse(LEAVE_TO_SUBSEQUENT))).style.marginRight = "4px";
|
||||
}
|
||||
contentEl.createEl("button", { text: !this.pluginPickMode ? "Not now" : "Cancel" }, (e) => e.addEventListener("click", () => this.sendResponse(CANCELLED))).style.marginRight = "4px";
|
||||
}
|
||||
|
||||
sendResponse(result: MergeDialogResult) {
|
||||
|
||||
@@ -12,15 +12,24 @@ export class JsonResolveModal extends Modal {
|
||||
nameA: string;
|
||||
nameB: string;
|
||||
defaultSelect: string;
|
||||
keepOrder: boolean;
|
||||
hideLocal: boolean;
|
||||
title: string = "Conflicted Setting";
|
||||
|
||||
constructor(app: App, filename: FilePath, docs: LoadedEntry[], callback: (keepRev?: string, mergedStr?: string) => Promise<void>, nameA?: string, nameB?: string, defaultSelect?: string) {
|
||||
constructor(app: App, filename: FilePath,
|
||||
docs: LoadedEntry[], callback: (keepRev?: string, mergedStr?: string) => Promise<void>,
|
||||
nameA?: string, nameB?: string, defaultSelect?: string,
|
||||
keepOrder?: boolean, hideLocal?: boolean, title: string = "Conflicted Setting") {
|
||||
super(app);
|
||||
this.callback = callback;
|
||||
this.filename = filename;
|
||||
this.docs = docs;
|
||||
this.nameA = nameA || "";
|
||||
this.nameB = nameB || "";
|
||||
this.keepOrder = keepOrder || false;
|
||||
this.defaultSelect = defaultSelect || "";
|
||||
this.title = title;
|
||||
this.hideLocal = hideLocal ?? false;
|
||||
waitForSignal(`cancel-internal-conflict:${filename}`).then(() => this.close());
|
||||
}
|
||||
async UICallback(keepRev?: string, mergedStr?: string) {
|
||||
@@ -31,7 +40,7 @@ export class JsonResolveModal extends Modal {
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
this.titleEl.setText("Conflicted Setting");
|
||||
this.titleEl.setText(this.title);
|
||||
contentEl.empty();
|
||||
|
||||
if (this.component == undefined) {
|
||||
@@ -43,6 +52,8 @@ export class JsonResolveModal extends Modal {
|
||||
nameA: this.nameA,
|
||||
nameB: this.nameB,
|
||||
defaultSelect: this.defaultSelect,
|
||||
keepOrder: this.keepOrder,
|
||||
hideLocal: this.hideLocal,
|
||||
callback: (keepRev: string | undefined, mergedStr: string | undefined) => this.UICallback(keepRev, mergedStr),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
export let nameA: string = "A";
|
||||
export let nameB: string = "B";
|
||||
export let defaultSelect: string = "";
|
||||
export let keepOrder = false;
|
||||
export let hideLocal: boolean = false;
|
||||
let docA: LoadedEntry;
|
||||
let docB: LoadedEntry;
|
||||
let docAContent = "";
|
||||
@@ -55,9 +57,12 @@
|
||||
if (mode == "AB") return callback(undefined, JSON.stringify(objAB, null, 2));
|
||||
callback(undefined, undefined);
|
||||
}
|
||||
function cancel() {
|
||||
callback(undefined, undefined);
|
||||
}
|
||||
$: {
|
||||
if (docs && docs.length >= 1) {
|
||||
if (docs[0].mtime < docs[1].mtime) {
|
||||
if (keepOrder || docs[0].mtime < docs[1].mtime) {
|
||||
docA = docs[0];
|
||||
docB = docs[1];
|
||||
} else {
|
||||
@@ -96,13 +101,19 @@
|
||||
diffs = getJsonDiff(objA, 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][];
|
||||
let modes = [] as ["" | "A" | "B" | "AB" | "BA", string][];
|
||||
$: {
|
||||
let newModes = [] as typeof modes;
|
||||
|
||||
if (!hideLocal) {
|
||||
newModes.push(["", "Not now"]);
|
||||
newModes.push(["A", nameA || "A"]);
|
||||
}
|
||||
newModes.push(["B", nameB || "B"]);
|
||||
newModes.push(["AB", `${nameA || "A"} + ${nameB || "B"}`]);
|
||||
newModes.push(["BA", `${nameB || "B"} + ${nameA || "A"}`]);
|
||||
modes = newModes;
|
||||
}
|
||||
</script>
|
||||
|
||||
<h2>{filename}</h2>
|
||||
@@ -132,28 +143,54 @@
|
||||
{:else}
|
||||
NO PREVIEW
|
||||
{/if}
|
||||
<div>
|
||||
{nameA}
|
||||
{#if docA._id == docB._id}
|
||||
Rev:{revStringToRevNumber(docA._rev)}
|
||||
{/if} ,{new Date(docA.mtime).toLocaleString()}
|
||||
{docAContent.length} letters
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{nameB}
|
||||
{#if docA._id == docB._id}
|
||||
Rev:{revStringToRevNumber(docB._rev)}
|
||||
{/if} ,{new Date(docB.mtime).toLocaleString()}
|
||||
{docBContent.length} letters
|
||||
<div class="infos">
|
||||
<table>
|
||||
<tr>
|
||||
<th>{nameA}</th>
|
||||
<td
|
||||
>{#if docA._id == docB._id}
|
||||
Rev:{revStringToRevNumber(docA._rev)}
|
||||
{/if}
|
||||
{new Date(docA.mtime).toLocaleString()}</td
|
||||
>
|
||||
<td>
|
||||
{docAContent.length} letters
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{nameB}</th>
|
||||
<td
|
||||
>{#if docA._id == docB._id}
|
||||
Rev:{revStringToRevNumber(docB._rev)}
|
||||
{/if}
|
||||
{new Date(docB.mtime).toLocaleString()}</td
|
||||
>
|
||||
<td>
|
||||
{docBContent.length} letters
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
{#if hideLocal}
|
||||
<button on:click={cancel}>Cancel</button>
|
||||
{/if}
|
||||
<button on:click={apply}>Apply</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.infos {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 4px 0.5em;
|
||||
}
|
||||
|
||||
.deleted {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
@@ -2231,7 +2302,7 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
// With great respect, thank you TfTHacker!
|
||||
// Refer: https://github.com/TfTHacker/obsidian42-brat/blob/main/src/features/BetaPlugins.ts
|
||||
const containerPluginSettings = containerEl.createDiv();
|
||||
this.createEl(containerPluginSettings, "h3", { text: "Customization sync (beta)" });
|
||||
this.createEl(containerPluginSettings, "h3", { text: "Customization sync (beta 3)" });
|
||||
|
||||
const enableOnlyOnPluginSyncIsNotEnabled = enableOnly(() => this.isConfiguredAs("usePluginSync", false));
|
||||
const visibleOnlyOnPluginSyncEnabled = visibleOnly(() => this.isConfiguredAs("usePluginSync", true));
|
||||
@@ -2242,6 +2313,9 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
onUpdate: enableOnlyOnPluginSyncIsNotEnabled
|
||||
});
|
||||
|
||||
new Setting(containerPluginSettings)
|
||||
.autoWireToggle("usePluginSyncV2")
|
||||
|
||||
new Setting(containerPluginSettings)
|
||||
.autoWireToggle("usePluginSync", {
|
||||
onUpdate: enableOnly(() => !this.isConfiguredAs("deviceAndVaultName", ""))
|
||||
@@ -2325,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.")
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import ObsidianLiveSyncPlugin from "../main";
|
||||
import { type PluginDataExDisplay, pluginIsEnumerating, pluginList } from "../features/CmdConfigSync";
|
||||
import { type IPluginDataExDisplay, pluginIsEnumerating, pluginList, pluginManifestStore, pluginV2Progress } from "../features/CmdConfigSync";
|
||||
import PluginCombo from "./components/PluginCombo.svelte";
|
||||
import { Menu } from "obsidian";
|
||||
import { Menu, type PluginManifest } from "obsidian";
|
||||
import { unique } from "../lib/src/common/utils";
|
||||
import { MODE_SELECTIVE, MODE_AUTOMATIC, MODE_PAUSED, type SYNC_MODE, type PluginSyncSettingEntry } from "../lib/src/common/types";
|
||||
import { MODE_SELECTIVE, MODE_AUTOMATIC, MODE_PAUSED, type SYNC_MODE, type PluginSyncSettingEntry, MODE_SHINY } from "../lib/src/common/types";
|
||||
import { normalizePath } from "../deps";
|
||||
export let plugin: ObsidianLiveSyncPlugin;
|
||||
|
||||
@@ -14,9 +14,10 @@
|
||||
|
||||
const addOn = plugin.addOnConfigSync;
|
||||
|
||||
let list: PluginDataExDisplay[] = [];
|
||||
let list: IPluginDataExDisplay[] = [];
|
||||
|
||||
let selectNewestPulse = 0;
|
||||
let selectNewestStyle = 0;
|
||||
let hideEven = false;
|
||||
let loading = false;
|
||||
let applyAllPluse = 0;
|
||||
@@ -39,13 +40,13 @@
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
function filterList(list: PluginDataExDisplay[], categories: string[]) {
|
||||
function filterList(list: IPluginDataExDisplay[], categories: string[]) {
|
||||
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 groupBy(items: PluginDataExDisplay[], key: string) {
|
||||
let ret = {} as Record<string, PluginDataExDisplay[]>;
|
||||
function groupBy(items: IPluginDataExDisplay[], key: string) {
|
||||
let ret = {} as Record<string, IPluginDataExDisplay[]>;
|
||||
for (const v of items) {
|
||||
//@ts-ignore
|
||||
const k = (key in v ? v[key] : "") as string;
|
||||
@@ -71,19 +72,24 @@
|
||||
async function replicate() {
|
||||
await plugin.replicate(true);
|
||||
}
|
||||
function selectAllNewest() {
|
||||
function selectAllNewest(selectMode: boolean) {
|
||||
selectNewestPulse++;
|
||||
selectNewestStyle = selectMode ? 1 : 2;
|
||||
}
|
||||
function resetSelectNewest() {
|
||||
selectNewestPulse++;
|
||||
selectNewestStyle = 3;
|
||||
}
|
||||
function applyAll() {
|
||||
applyAllPluse++;
|
||||
}
|
||||
async function applyData(data: PluginDataExDisplay): Promise<boolean> {
|
||||
async function applyData(data: IPluginDataExDisplay): Promise<boolean> {
|
||||
return await addOn.applyData(data);
|
||||
}
|
||||
async function compareData(docA: PluginDataExDisplay, docB: PluginDataExDisplay): Promise<boolean> {
|
||||
return await addOn.compareUsingDisplayData(docA, docB);
|
||||
async function compareData(docA: IPluginDataExDisplay, docB: IPluginDataExDisplay, compareEach = false): Promise<boolean> {
|
||||
return await addOn.compareUsingDisplayData(docA, docB, compareEach);
|
||||
}
|
||||
async function deleteData(data: PluginDataExDisplay): Promise<boolean> {
|
||||
async function deleteData(data: IPluginDataExDisplay): Promise<boolean> {
|
||||
return await addOn.deleteData(data);
|
||||
}
|
||||
function askMode(evt: MouseEvent, title: string, key: string) {
|
||||
@@ -91,7 +97,7 @@
|
||||
menu.addItem((item) => item.setTitle(title).setIsLabel(true));
|
||||
menu.addSeparator();
|
||||
const prevMode = automaticList.get(key) ?? MODE_SELECTIVE;
|
||||
for (const mode of [MODE_SELECTIVE, MODE_AUTOMATIC, MODE_PAUSED]) {
|
||||
for (const mode of [MODE_SELECTIVE, MODE_AUTOMATIC, MODE_PAUSED, MODE_SHINY]) {
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(`${getIcon(mode as SYNC_MODE)}:${TITLES[mode]}`)
|
||||
.onClick((e) => {
|
||||
@@ -139,6 +145,7 @@
|
||||
thisTerm,
|
||||
hideNotApplicable,
|
||||
selectNewest: selectNewestPulse,
|
||||
selectNewestStyle,
|
||||
applyAllPluse,
|
||||
applyData,
|
||||
compareData,
|
||||
@@ -150,24 +157,29 @@
|
||||
const ICON_EMOJI_PAUSED = `⛔`;
|
||||
const ICON_EMOJI_AUTOMATIC = `✨`;
|
||||
const ICON_EMOJI_SELECTIVE = `🔀`;
|
||||
const ICON_EMOJI_FLAGGED = `🚩`;
|
||||
|
||||
const ICONS: { [key: number]: string } = {
|
||||
[MODE_SELECTIVE]: ICON_EMOJI_SELECTIVE,
|
||||
[MODE_PAUSED]: ICON_EMOJI_PAUSED,
|
||||
[MODE_AUTOMATIC]: ICON_EMOJI_AUTOMATIC,
|
||||
[MODE_SHINY]: ICON_EMOJI_FLAGGED,
|
||||
};
|
||||
const TITLES: { [key: number]: string } = {
|
||||
[MODE_SELECTIVE]: "Selective",
|
||||
[MODE_PAUSED]: "Ignore",
|
||||
[MODE_AUTOMATIC]: "Automatic",
|
||||
[MODE_SHINY]: "Flagged Selective",
|
||||
};
|
||||
const PREFIX_PLUGIN_ALL = "PLUGIN_ALL";
|
||||
const PREFIX_PLUGIN_DATA = "PLUGIN_DATA";
|
||||
const PREFIX_PLUGIN_MAIN = "PLUGIN_MAIN";
|
||||
const PREFIX_PLUGIN_ETC = "PLUGIN_ETC";
|
||||
function setMode(key: string, mode: SYNC_MODE) {
|
||||
if (key.startsWith(PREFIX_PLUGIN_ALL + "/")) {
|
||||
setMode(PREFIX_PLUGIN_DATA + key.substring(PREFIX_PLUGIN_ALL.length), mode);
|
||||
setMode(PREFIX_PLUGIN_MAIN + key.substring(PREFIX_PLUGIN_ALL.length), mode);
|
||||
return;
|
||||
}
|
||||
const files = unique(
|
||||
list
|
||||
@@ -176,17 +188,23 @@
|
||||
.flat()
|
||||
.map((e) => e.filename),
|
||||
);
|
||||
automaticList.set(key, mode);
|
||||
automaticListDisp = automaticList;
|
||||
if (!(key in plugin.settings.pluginSyncExtendedSetting)) {
|
||||
plugin.settings.pluginSyncExtendedSetting[key] = {
|
||||
key,
|
||||
mode,
|
||||
files: [],
|
||||
};
|
||||
if (mode == MODE_SELECTIVE) {
|
||||
automaticList.delete(key);
|
||||
delete plugin.settings.pluginSyncExtendedSetting[key];
|
||||
automaticListDisp = automaticList;
|
||||
} else {
|
||||
automaticList.set(key, mode);
|
||||
automaticListDisp = automaticList;
|
||||
if (!(key in plugin.settings.pluginSyncExtendedSetting)) {
|
||||
plugin.settings.pluginSyncExtendedSetting[key] = {
|
||||
key,
|
||||
mode,
|
||||
files: [],
|
||||
};
|
||||
}
|
||||
plugin.settings.pluginSyncExtendedSetting[key].files = files;
|
||||
plugin.settings.pluginSyncExtendedSetting[key].mode = mode;
|
||||
}
|
||||
plugin.settings.pluginSyncExtendedSetting[key].files = files;
|
||||
plugin.settings.pluginSyncExtendedSetting[key].mode = mode;
|
||||
plugin.saveSettingData();
|
||||
}
|
||||
function getIcon(mode: SYNC_MODE) {
|
||||
@@ -208,9 +226,9 @@
|
||||
|
||||
let displayKeys: Record<string, string[]> = {};
|
||||
|
||||
$: {
|
||||
function computeDisplayKeys(list: IPluginDataExDisplay[]) {
|
||||
const extraKeys = Object.keys(plugin.settings.pluginSyncExtendedSetting);
|
||||
displayKeys = [
|
||||
return [
|
||||
...list,
|
||||
...extraKeys
|
||||
.map((e) => `${e}///`.split("/"))
|
||||
@@ -220,6 +238,9 @@
|
||||
.sort((a, b) => (a.displayName ?? a.name).localeCompare(b.displayName ?? b.name))
|
||||
.reduce((p, c) => ({ ...p, [c.category]: unique(c.category in p ? [...p[c.category], c.displayName ?? c.name] : [c.displayName ?? c.name]) }), {} as Record<string, string[]>);
|
||||
}
|
||||
$: {
|
||||
displayKeys = computeDisplayKeys(list);
|
||||
}
|
||||
|
||||
let deleteTerm = "";
|
||||
|
||||
@@ -230,146 +251,203 @@
|
||||
}
|
||||
addOn.reloadPluginList(true);
|
||||
}
|
||||
|
||||
let nameMap = new Map<string, string>();
|
||||
function updateNameMap(e: Map<string, PluginManifest>) {
|
||||
const items = [...e.entries()].map(([k, v]) => [k.split("/").slice(-2).join("/"), v.name] as [string, string]);
|
||||
const newMap = new Map(items);
|
||||
if (newMap.size == nameMap.size) {
|
||||
let diff = false;
|
||||
for (const [k, v] of newMap) {
|
||||
if (nameMap.get(k) != v) {
|
||||
diff = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!diff) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
nameMap = newMap;
|
||||
}
|
||||
$: updateNameMap($pluginManifestStore);
|
||||
|
||||
let displayEntries = [] as [string, string][];
|
||||
$: {
|
||||
displayEntries = Object.entries(displays).filter(([key, _]) => key in displayKeys);
|
||||
}
|
||||
|
||||
let pluginEntries = [] as [string, IPluginDataExDisplay[]][];
|
||||
$: {
|
||||
pluginEntries = groupBy(filterList(list, ["PLUGIN_MAIN", "PLUGIN_DATA", "PLUGIN_ETC"]), "name");
|
||||
}
|
||||
let useSyncPluginEtc = plugin.settings.usePluginEtc;
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<div class="buttons">
|
||||
<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 class="buttons">
|
||||
<button on:click={() => applyAll()}>Apply All</button>
|
||||
</div>
|
||||
<div class="buttonsWrap">
|
||||
<div class="buttons">
|
||||
<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}
|
||||
</div>
|
||||
{#if loading}
|
||||
<div>
|
||||
<span>Updating list...</span>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button on:click={() => selectAllNewest(true)}>Select All Shiny</button>
|
||||
<button on:click={() => selectAllNewest(false)}>{ICON_EMOJI_FLAGGED} Select Flagged Shiny</button>
|
||||
<button on:click={() => resetSelectNewest()}>Deselect all</button>
|
||||
<button on:click={() => applyAll()} class="mod-cta">Apply All Selected</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loading">
|
||||
{#if loading || $pluginV2Progress !== 0}
|
||||
<span>Updating list...{$pluginV2Progress == 0 ? "" : ` (${$pluginV2Progress})`}</span>
|
||||
{/if}
|
||||
<div class="list">
|
||||
{#if list.length == 0}
|
||||
<div class="center">No Items.</div>
|
||||
{:else}
|
||||
{#each Object.entries(displays).filter(([key, _]) => key in displayKeys) as [key, label]}
|
||||
<div>
|
||||
<h3>{label}</h3>
|
||||
{#each displayKeys[key] as name}
|
||||
{@const bindKey = `${key}/${name}`}
|
||||
{@const mode = automaticListDisp.get(bindKey) ?? MODE_SELECTIVE}
|
||||
<div class="labelrow {hideEven ? 'hideeven' : ''}">
|
||||
<div class="title">
|
||||
<button class="status" on:click={(evt) => askMode(evt, `${key}/${name}`, bindKey)}>
|
||||
{getIcon(mode)}
|
||||
</button>
|
||||
<span class="name">{name}</span>
|
||||
</div>
|
||||
{#if mode == MODE_SELECTIVE}
|
||||
<PluginCombo {...options} list={list.filter((e) => e.category == key && e.name == name)} hidden={false} />
|
||||
</div>
|
||||
<div class="list">
|
||||
{#if list.length == 0}
|
||||
<div class="center">No Items.</div>
|
||||
{:else}
|
||||
{#each displayEntries as [key, label]}
|
||||
<div>
|
||||
<h3>{label}</h3>
|
||||
{#each displayKeys[key] as name}
|
||||
{@const bindKey = `${key}/${name}`}
|
||||
{@const mode = automaticListDisp.get(bindKey) ?? MODE_SELECTIVE}
|
||||
<div class="labelrow {hideEven ? 'hideeven' : ''}">
|
||||
<div class="title">
|
||||
<button class="status" on:click={(evt) => askMode(evt, `${key}/${name}`, bindKey)}>
|
||||
{getIcon(mode)}
|
||||
</button>
|
||||
<span class="name">{(key == "THEME" && nameMap.get(`themes/${name}`)) || name}</span>
|
||||
</div>
|
||||
<div class="body">
|
||||
{#if mode == MODE_SELECTIVE || mode == MODE_SHINY}
|
||||
<PluginCombo {...options} isFlagged={mode == MODE_SHINY} list={list.filter((e) => e.category == key && e.name == name)} hidden={false} />
|
||||
{:else}
|
||||
<div class="statusnote">{TITLES[mode]}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
<div>
|
||||
<h3>Plugins</h3>
|
||||
{#each groupBy(filterList(list, ["PLUGIN_MAIN", "PLUGIN_DATA", "PLUGIN_ETC"]), "name") as [name, listX]}
|
||||
{@const bindKeyAll = `${PREFIX_PLUGIN_ALL}/${name}`}
|
||||
{@const modeAll = automaticListDisp.get(bindKeyAll) ?? MODE_SELECTIVE}
|
||||
{@const bindKeyMain = `${PREFIX_PLUGIN_MAIN}/${name}`}
|
||||
{@const modeMain = automaticListDisp.get(bindKeyMain) ?? MODE_SELECTIVE}
|
||||
{@const bindKeyData = `${PREFIX_PLUGIN_DATA}/${name}`}
|
||||
{@const modeData = automaticListDisp.get(bindKeyData) ?? MODE_SELECTIVE}
|
||||
<div class="labelrow {hideEven ? 'hideeven' : ''}">
|
||||
<div class="title">
|
||||
<button class="status" on:click={(evt) => askMode(evt, `${PREFIX_PLUGIN_ALL}/${name}`, bindKeyAll)}>
|
||||
{getIcon(modeAll)}
|
||||
</button>
|
||||
<span class="name">{name}</span>
|
||||
</div>
|
||||
{#if modeAll == MODE_SELECTIVE}
|
||||
<PluginCombo {...options} list={listX} hidden={true} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
<div>
|
||||
<h3>Plugins</h3>
|
||||
{#each pluginEntries as [name, listX]}
|
||||
{@const bindKeyAll = `${PREFIX_PLUGIN_ALL}/${name}`}
|
||||
{@const modeAll = automaticListDisp.get(bindKeyAll) ?? MODE_SELECTIVE}
|
||||
{@const bindKeyMain = `${PREFIX_PLUGIN_MAIN}/${name}`}
|
||||
{@const modeMain = automaticListDisp.get(bindKeyMain) ?? MODE_SELECTIVE}
|
||||
{@const bindKeyData = `${PREFIX_PLUGIN_DATA}/${name}`}
|
||||
{@const modeData = automaticListDisp.get(bindKeyData) ?? MODE_SELECTIVE}
|
||||
{@const bindKeyETC = `${PREFIX_PLUGIN_ETC}/${name}`}
|
||||
{@const modeEtc = automaticListDisp.get(bindKeyETC) ?? MODE_SELECTIVE}
|
||||
<div class="labelrow {hideEven ? 'hideeven' : ''}">
|
||||
<div class="title">
|
||||
<button class="status" on:click={(evt) => askMode(evt, `${PREFIX_PLUGIN_ALL}/${name}`, bindKeyAll)}>
|
||||
{getIcon(modeAll)}
|
||||
</button>
|
||||
<span class="name">{nameMap.get(`plugins/${name}`) || name}</span>
|
||||
</div>
|
||||
<div class="body">
|
||||
{#if modeAll == MODE_SELECTIVE || modeAll == MODE_SHINY}
|
||||
<PluginCombo {...options} isFlagged={modeAll == MODE_SHINY} list={listX} hidden={true} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if modeAll == MODE_SELECTIVE}
|
||||
<div class="filerow {hideEven ? 'hideeven' : ''}">
|
||||
<div class="filetitle">
|
||||
<button class="status" on:click={(evt) => askMode(evt, `${PREFIX_PLUGIN_MAIN}/${name}/MAIN`, bindKeyMain)}>
|
||||
{getIcon(modeMain)}
|
||||
</button>
|
||||
<span class="name">MAIN</span>
|
||||
</div>
|
||||
{#if modeMain == MODE_SELECTIVE}
|
||||
<PluginCombo {...options} list={filterList(listX, ["PLUGIN_MAIN"])} hidden={false} />
|
||||
</div>
|
||||
{#if modeAll == MODE_SELECTIVE || modeAll == MODE_SHINY}
|
||||
<div class="filerow {hideEven ? 'hideeven' : ''}">
|
||||
<div class="filetitle">
|
||||
<button class="status" on:click={(evt) => askMode(evt, `${PREFIX_PLUGIN_MAIN}/${name}/MAIN`, bindKeyMain)}>
|
||||
{getIcon(modeMain)}
|
||||
</button>
|
||||
<span class="name">MAIN</span>
|
||||
</div>
|
||||
<div class="body">
|
||||
{#if modeMain == MODE_SELECTIVE || modeMain == MODE_SHINY}
|
||||
<PluginCombo {...options} isFlagged={modeMain == MODE_SHINY} list={filterList(listX, ["PLUGIN_MAIN"])} hidden={false} />
|
||||
{:else}
|
||||
<div class="statusnote">{TITLES[modeMain]}</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="filerow {hideEven ? 'hideeven' : ''}">
|
||||
<div class="filetitle">
|
||||
<button class="status" on:click={(evt) => askMode(evt, `${PREFIX_PLUGIN_DATA}/${name}`, bindKeyData)}>
|
||||
{getIcon(modeData)}
|
||||
</button>
|
||||
<span class="name">DATA</span>
|
||||
</div>
|
||||
{#if modeData == MODE_SELECTIVE}
|
||||
<PluginCombo {...options} list={filterList(listX, ["PLUGIN_DATA"])} hidden={false} />
|
||||
</div>
|
||||
<div class="filerow {hideEven ? 'hideeven' : ''}">
|
||||
<div class="filetitle">
|
||||
<button class="status" on:click={(evt) => askMode(evt, `${PREFIX_PLUGIN_DATA}/${name}`, bindKeyData)}>
|
||||
{getIcon(modeData)}
|
||||
</button>
|
||||
<span class="name">DATA</span>
|
||||
</div>
|
||||
<div class="body">
|
||||
{#if modeData == MODE_SELECTIVE || modeData == MODE_SHINY}
|
||||
<PluginCombo {...options} isFlagged={modeData == MODE_SHINY} list={filterList(listX, ["PLUGIN_DATA"])} hidden={false} />
|
||||
{:else}
|
||||
<div class="statusnote">{TITLES[modeData]}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="noterow">
|
||||
<div class="statusnote">{TITLES[modeAll]}</div>
|
||||
</div>
|
||||
{#if useSyncPluginEtc}
|
||||
<div class="filerow {hideEven ? 'hideeven' : ''}">
|
||||
<div class="filetitle">
|
||||
<button class="status" on:click={(evt) => askMode(evt, `${PREFIX_PLUGIN_ETC}/${name}`, bindKeyETC)}>
|
||||
{getIcon(modeEtc)}
|
||||
</button>
|
||||
<span class="name">Other files</span>
|
||||
</div>
|
||||
<div class="body">
|
||||
{#if modeEtc == MODE_SELECTIVE || modeEtc == MODE_SHINY}
|
||||
<PluginCombo {...options} isFlagged={modeEtc == MODE_SHINY} list={filterList(listX, ["PLUGIN_ETC"])} hidden={false} />
|
||||
{:else}
|
||||
<div class="statusnote">{TITLES[modeEtc]}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if isMaintenanceMode}
|
||||
<div class="list">
|
||||
<div>
|
||||
<h3>Maintenance Commands</h3>
|
||||
<div class="maintenancerow">
|
||||
<label for="">Delete All of </label>
|
||||
<select bind:value={deleteTerm}>
|
||||
{#each allTerms as term}
|
||||
<option value={term}>{term}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<button
|
||||
class="status"
|
||||
on:click={(evt) => {
|
||||
deleteAllItems(deleteTerm);
|
||||
}}
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="noterow">
|
||||
<div class="statusnote">{TITLES[modeAll]}</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if isMaintenanceMode}
|
||||
<div class="buttons">
|
||||
<label><span>Hide not applicable items</span><input type="checkbox" bind:checked={hideEven} /></label>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<label><span>Maintenance mode</span><input type="checkbox" bind:checked={isMaintenanceMode} /></label>
|
||||
<div>
|
||||
<h3>Maintenance Commands</h3>
|
||||
<div class="maintenancerow">
|
||||
<label for="">Delete All of </label>
|
||||
<select bind:value={deleteTerm}>
|
||||
{#each allTerms as term}
|
||||
<option value={term}>{term}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<button
|
||||
class="status"
|
||||
on:click={(evt) => {
|
||||
deleteAllItems(deleteTerm);
|
||||
}}
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="buttons">
|
||||
<label><span>Hide not applicable items</span><input type="checkbox" bind:checked={hideEven} /></label>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<label><span>Maintenance mode</span><input type="checkbox" bind:checked={isMaintenanceMode} /></label>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* span.spacer {
|
||||
min-width: 1px;
|
||||
flex-grow: 1;
|
||||
} */
|
||||
.buttonsWrap {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
h3 {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@@ -414,13 +492,23 @@
|
||||
min-width: 10em;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.list {
|
||||
overflow-y: auto;
|
||||
}
|
||||
.title {
|
||||
color: var(--text-normal);
|
||||
font-size: var(--font-ui-medium);
|
||||
line-height: var(--line-height-tight);
|
||||
margin-right: auto;
|
||||
}
|
||||
.body {
|
||||
/* margin-left: 0.4em; */
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
/* flex-wrap: wrap; */
|
||||
}
|
||||
.filetitle {
|
||||
color: var(--text-normal);
|
||||
font-size: var(--font-ui-medium);
|
||||
@@ -467,4 +555,24 @@
|
||||
margin-right: 0.5em;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.loading {
|
||||
transition: height 0.25s ease-in-out;
|
||||
transition-delay: 4ms;
|
||||
overflow-y: hidden;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
.loading:empty {
|
||||
height: 0px;
|
||||
transition: height 0.25s ease-in-out;
|
||||
transition-delay: 1s;
|
||||
}
|
||||
.loading:not(:empty) {
|
||||
height: 2em;
|
||||
transition: height 0.25s ease-in-out;
|
||||
transition-delay: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,39 +1,42 @@
|
||||
<script lang="ts">
|
||||
import type { PluginDataExDisplay } from "../../features/CmdConfigSync";
|
||||
import { PluginDataExDisplayV2, type IPluginDataExDisplay } from "../../features/CmdConfigSync";
|
||||
import { Logger } from "../../lib/src/common/logger";
|
||||
import { versionNumberString2Number } from "../../lib/src/string_and_binary/convert";
|
||||
import { type FilePath, LOG_LEVEL_NOTICE } from "../../lib/src/common/types";
|
||||
import { getDocData } from "../../lib/src/common/utils";
|
||||
import { type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "../../lib/src/common/types";
|
||||
import { getDocData, timeDeltaToHumanReadable, unique } from "../../lib/src/common/utils";
|
||||
import type ObsidianLiveSyncPlugin from "../../main";
|
||||
import { askString, scheduleTask } from "../../common/utils";
|
||||
import { askString } from "../../common/utils";
|
||||
import { Menu } from "obsidian";
|
||||
|
||||
export let list: PluginDataExDisplay[] = [];
|
||||
export let list: IPluginDataExDisplay[] = [];
|
||||
export let thisTerm = "";
|
||||
export let hideNotApplicable = false;
|
||||
export let selectNewest = 0;
|
||||
export let selectNewestStyle = 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 applyData: (data: IPluginDataExDisplay) => Promise<boolean>;
|
||||
export let compareData: (dataA: IPluginDataExDisplay, dataB: IPluginDataExDisplay, compareEach?: boolean) => Promise<boolean>;
|
||||
export let deleteData: (data: IPluginDataExDisplay) => Promise<boolean>;
|
||||
export let hidden: boolean;
|
||||
export let plugin: ObsidianLiveSyncPlugin;
|
||||
export let isMaintenanceMode: boolean = false;
|
||||
export let isFlagged: boolean = false;
|
||||
const addOn = plugin.addOnConfigSync;
|
||||
|
||||
let selected = "";
|
||||
export let selected = "";
|
||||
let freshness = "";
|
||||
let equivalency = "";
|
||||
let version = "";
|
||||
let canApply: boolean = false;
|
||||
let canCompare: boolean = false;
|
||||
let pickToCompare: boolean = false;
|
||||
let currentSelectNewest = 0;
|
||||
let currentApplyAll = 0;
|
||||
|
||||
// Selectable terminals
|
||||
let terms = [] as string[];
|
||||
|
||||
async function comparePlugin(local: PluginDataExDisplay, remote: PluginDataExDisplay) {
|
||||
async function comparePlugin(local: IPluginDataExDisplay | undefined, remote: IPluginDataExDisplay | undefined) {
|
||||
let freshness = "";
|
||||
let equivalency = "";
|
||||
let version = "";
|
||||
@@ -41,25 +44,28 @@
|
||||
let canApply: boolean = false;
|
||||
let canCompare = false;
|
||||
if (!local && !remote) {
|
||||
// NO OP. whats happened?
|
||||
// NO OP. what's happened?
|
||||
freshness = "";
|
||||
} else if (local && !remote) {
|
||||
freshness = "⚠ Local only";
|
||||
freshness = "Local only";
|
||||
} else if (remote && !local) {
|
||||
freshness = "✓ Remote only";
|
||||
freshness = "Remote only";
|
||||
canApply = true;
|
||||
} else {
|
||||
const dtDiff = (local?.mtime ?? 0) - (remote?.mtime ?? 0);
|
||||
const diff = timeDeltaToHumanReadable(Math.abs(dtDiff));
|
||||
if (dtDiff / 1000 < -10) {
|
||||
freshness = "✓ Newer";
|
||||
// freshness = "✓ Newer";
|
||||
freshness = `Newer (${diff})`;
|
||||
canApply = true;
|
||||
contentCheck = true;
|
||||
} else if (dtDiff / 1000 > 10) {
|
||||
freshness = "⚠ Older";
|
||||
// freshness = "⚠ Older";
|
||||
freshness = `Older (${diff})`;
|
||||
canApply = true;
|
||||
contentCheck = true;
|
||||
} else {
|
||||
freshness = "⚖️ Same old";
|
||||
freshness = "Same";
|
||||
canApply = false;
|
||||
contentCheck = true;
|
||||
}
|
||||
@@ -67,25 +73,26 @@
|
||||
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}`;
|
||||
const compare = `${localVersionStr}`.localeCompare(remoteVersionStr, undefined, { numeric: true });
|
||||
if (compare == 0) {
|
||||
version = "Same";
|
||||
} else if (compare < 0) {
|
||||
version = `Lower (${localVersionStr} < ${remoteVersionStr})`;
|
||||
} else if (compare > 0) {
|
||||
version = `Higher (${localVersionStr} > ${remoteVersionStr})`;
|
||||
}
|
||||
}
|
||||
|
||||
if (contentCheck) {
|
||||
const { canApply, equivalency, canCompare } = await checkEquivalency(local, remote);
|
||||
return { canApply, freshness, equivalency, version, canCompare };
|
||||
if (local && remote) {
|
||||
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) {
|
||||
async function checkEquivalency(local: IPluginDataExDisplay, remote: IPluginDataExDisplay) {
|
||||
let equivalency = "";
|
||||
let canApply = false;
|
||||
let canCompare = false;
|
||||
@@ -100,17 +107,21 @@
|
||||
return 0b0000010; //"LOCAL_ONLY";
|
||||
} else if (!localFile && remoteFile) {
|
||||
return 0b0001000; //"REMOTE ONLY"
|
||||
} else {
|
||||
if (getDocData(localFile.data) == getDocData(remoteFile.data)) {
|
||||
} else if (localFile && remoteFile) {
|
||||
const localDoc = getDocData(localFile.data);
|
||||
const remoteDoc = getDocData(remoteFile.data);
|
||||
if (localDoc == remoteDoc) {
|
||||
return 0b0000100; //"EVEN"
|
||||
} else {
|
||||
return 0b0010000; //"DIFFERENT";
|
||||
}
|
||||
} else {
|
||||
return 0b0010000; //"DIFFERENT";
|
||||
}
|
||||
})
|
||||
.reduce((p, c) => p | (c as number), 0 as number);
|
||||
if (matchingStatus == 0b0000100) {
|
||||
equivalency = "⚖️ Same";
|
||||
equivalency = "Same";
|
||||
canApply = false;
|
||||
} else if (matchingStatus <= 0b0000100) {
|
||||
equivalency = "Same or local only";
|
||||
@@ -118,30 +129,37 @@
|
||||
} else if (matchingStatus == 0b0010000) {
|
||||
canApply = true;
|
||||
canCompare = true;
|
||||
equivalency = "≠ Different";
|
||||
equivalency = "Different";
|
||||
} else {
|
||||
canApply = true;
|
||||
canCompare = true;
|
||||
equivalency = "≠ Different";
|
||||
equivalency = "Mixed";
|
||||
}
|
||||
return { equivalency, canApply, canCompare };
|
||||
}
|
||||
|
||||
async function performCompare(local: PluginDataExDisplay, remote: PluginDataExDisplay) {
|
||||
async function performCompare(local: IPluginDataExDisplay | undefined, remote: IPluginDataExDisplay | undefined) {
|
||||
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;
|
||||
pickToCompare = false;
|
||||
if (canCompare) {
|
||||
if (local?.files.length == remote?.files.length && local?.files.length == 1 && local?.files[0].filename == remote?.files[0].filename) {
|
||||
pickToCompare = false;
|
||||
} else {
|
||||
pickToCompare = true;
|
||||
// pickToCompare = false;
|
||||
// canCompare = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTerms(list: PluginDataExDisplay[], selectNewest: boolean, isMaintenanceMode: boolean) {
|
||||
async function updateTerms(list: IPluginDataExDisplay[], selectNewest: boolean, isMaintenanceMode: boolean) {
|
||||
const local = list.find((e) => e.term == thisTerm);
|
||||
selected = "";
|
||||
// selected = "";
|
||||
if (isMaintenanceMode) {
|
||||
terms = [...new Set(list.map((e) => e.term))];
|
||||
} else if (hideNotApplicable) {
|
||||
@@ -157,7 +175,7 @@
|
||||
} else {
|
||||
terms = [...new Set(list.map((e) => e.term))].filter((e) => e != thisTerm);
|
||||
}
|
||||
let newest: PluginDataExDisplay = local;
|
||||
let newest: IPluginDataExDisplay | undefined = local;
|
||||
if (selectNewest) {
|
||||
for (const term of terms) {
|
||||
const remote = list.find((e) => e.term == term);
|
||||
@@ -170,12 +188,25 @@
|
||||
}
|
||||
// selectNewest = false;
|
||||
}
|
||||
if (terms.indexOf(selected) < 0) {
|
||||
selected = "";
|
||||
}
|
||||
}
|
||||
$: {
|
||||
// React pulse and select
|
||||
const doSelectNewest = selectNewest != currentSelectNewest;
|
||||
currentSelectNewest = selectNewest;
|
||||
let doSelectNewest = false;
|
||||
if (selectNewest != currentSelectNewest) {
|
||||
if (selectNewestStyle == 1) {
|
||||
doSelectNewest = true;
|
||||
} else if (selectNewestStyle == 2) {
|
||||
doSelectNewest = isFlagged;
|
||||
} else if (selectNewestStyle == 3) {
|
||||
selected = "";
|
||||
}
|
||||
// currentSelectNewest = selectNewest;
|
||||
}
|
||||
updateTerms(list, doSelectNewest, isMaintenanceMode);
|
||||
currentSelectNewest = selectNewest;
|
||||
}
|
||||
$: {
|
||||
// React pulse and apply
|
||||
@@ -213,10 +244,52 @@
|
||||
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))) {
|
||||
addOn.updatePluginList(true, local.documentPath);
|
||||
await compareItems(local, selectedItem);
|
||||
}
|
||||
async function compareItems(local: IPluginDataExDisplay | undefined, remote: IPluginDataExDisplay | undefined, filename?: string) {
|
||||
if (local && remote) {
|
||||
if (!filename) {
|
||||
if (await compareData(local, remote)) {
|
||||
addOn.updatePluginList(true, local.documentPath);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
const localCopy = local instanceof PluginDataExDisplayV2 ? new PluginDataExDisplayV2(local) : { ...local };
|
||||
const remoteCopy = remote instanceof PluginDataExDisplayV2 ? new PluginDataExDisplayV2(remote) : { ...remote };
|
||||
localCopy.files = localCopy.files.filter((e) => e.filename == filename);
|
||||
remoteCopy.files = remoteCopy.files.filter((e) => e.filename == filename);
|
||||
if (await compareData(localCopy, remoteCopy, true)) {
|
||||
addOn.updatePluginList(true, local.documentPath);
|
||||
}
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
if (!remote && !local) {
|
||||
Logger(`Could not find both remote and local item`, LOG_LEVEL_INFO);
|
||||
} else if (!remote) {
|
||||
Logger(`Could not find remote item`, LOG_LEVEL_INFO);
|
||||
} else if (!local) {
|
||||
Logger(`Could not locally item`, LOG_LEVEL_INFO);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function pickCompareItem(evt: MouseEvent) {
|
||||
const local = list.find((e) => e.term == thisTerm);
|
||||
const selectedItem = list.find((e) => e.term == selected);
|
||||
if (!local) return;
|
||||
if (!selectedItem) return;
|
||||
const menu = new Menu();
|
||||
menu.addItem((item) => item.setTitle("Compare file").setIsLabel(true));
|
||||
menu.addSeparator();
|
||||
const files = unique(local.files.map((e) => e.filename).concat(selectedItem.files.map((e) => e.filename)));
|
||||
for (const filename of files) {
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(filename).onClick((e) => compareItems(local, selectedItem, filename));
|
||||
});
|
||||
}
|
||||
menu.showAtMouseEvent(evt);
|
||||
}
|
||||
async function deleteSelected() {
|
||||
const selectedItem = list.find((e) => e.term == selected);
|
||||
// const deletedPath = selectedItem.documentPath;
|
||||
@@ -226,6 +299,10 @@
|
||||
}
|
||||
async function duplicateItem() {
|
||||
const local = list.find((e) => e.term == thisTerm);
|
||||
if (!local) {
|
||||
Logger(`Could not find local item`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
const duplicateTermName = await askString(plugin.app, "Duplicate", "device name", "");
|
||||
if (duplicateTermName) {
|
||||
if (duplicateTermName.contains("/")) {
|
||||
@@ -242,10 +319,10 @@
|
||||
{#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 class="chip-wrap">
|
||||
<span class="chip modified">{freshness}</span>
|
||||
<span class="chip content">{equivalency}</span>
|
||||
<span class="chip version">{version}</span>
|
||||
</span>
|
||||
<select bind:value={selected}>
|
||||
<option value={""}>-</option>
|
||||
@@ -255,7 +332,12 @@
|
||||
</select>
|
||||
{#if canApply || (isMaintenanceMode && selected != "")}
|
||||
{#if canCompare}
|
||||
<button on:click={compareSelected}>🔍</button>
|
||||
{#if pickToCompare}
|
||||
<button on:click={pickCompareItem}>🗃️</button>
|
||||
{:else}
|
||||
<!--🔍 -->
|
||||
<button on:click={compareSelected}>⮂</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<button disabled />
|
||||
{/if}
|
||||
@@ -307,12 +389,46 @@
|
||||
padding: 0 1em;
|
||||
line-height: var(--line-height-tight);
|
||||
}
|
||||
span.messages {
|
||||
/* span.messages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
} */
|
||||
:global(.is-mobile) .spacer {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.chip-wrap {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.chip {
|
||||
display: inline-block;
|
||||
border-radius: 2px;
|
||||
font-size: 0.8em;
|
||||
padding: 0 4px;
|
||||
margin: 0 2px;
|
||||
border-color: var(--tag-border-color);
|
||||
background-color: var(--tag-background);
|
||||
color: var(--tag-color);
|
||||
}
|
||||
.chip:empty {
|
||||
display: none;
|
||||
}
|
||||
.chip:not(:empty)::before {
|
||||
min-width: 1.8em;
|
||||
display: inline-block;
|
||||
}
|
||||
.chip.content:not(:empty)::before {
|
||||
content: "📄: ";
|
||||
}
|
||||
.chip.version:not(:empty)::before {
|
||||
content: "🏷️: ";
|
||||
}
|
||||
.chip.modified:not(:empty)::before {
|
||||
content: "📅: ";
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -324,7 +324,23 @@ export const SettingInformation: Partial<Record<keyof AllSettings, Configuration
|
||||
"notifyThresholdOfRemoteStorageSize": {
|
||||
name: "Notify when the estimated remote storage size exceeds on start up",
|
||||
desc: "MB (0 to disable)."
|
||||
}
|
||||
},
|
||||
"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;
|
||||
}
|
||||
90
updates.md
90
updates.md
@@ -18,6 +18,76 @@ I have a lot of respect for that plugin, even though it is sometimes treated as
|
||||
Hooray for open source, and generous licences, and the sharing of knowledge by experts.
|
||||
|
||||
#### Version history
|
||||
- 0.23.22:
|
||||
- Fixed:
|
||||
- Case-insensitive file handling
|
||||
- Full-lower-case files are no longer created during database checking.
|
||||
- Bulk chunk transfer
|
||||
- The default value will automatically adjust to an acceptable size when using IBM Cloudant.
|
||||
- 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.
|
||||
- 0.23.20:
|
||||
- Fixed:
|
||||
- Customisation Sync now checks the difference while storing or applying the configuration.
|
||||
- No longer storing the same configuration multiple times.
|
||||
- Time difference in the dialogue has been fixed.
|
||||
- Remote Storage Limit Notification dialogue has been fixed, now the chosen value is saved.
|
||||
- Improved:
|
||||
- The Enlarging button on the enlarging threshold dialogue now displays the new value.
|
||||
- 0.23.19:
|
||||
- Not released.
|
||||
- 0.23.18:
|
||||
- New feature:
|
||||
- Per-file-saved customization sync has been shipped.
|
||||
- We can synchronise plug-igs etc., more smoothly.
|
||||
- Default: disabled. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost compatibility with old versions.
|
||||
- Customisation sync has got beta3.
|
||||
- We can set `Flag` to each item to select the newest, automatically.
|
||||
- This configuration is per device.
|
||||
- Improved:
|
||||
- Start-up speed has been improved.
|
||||
- Fixed:
|
||||
- On the customisation sync dialogue, buttons are kept within the screen.
|
||||
- No more unnecessary entries on `data.json` for customisation sync.
|
||||
- Selections are no longer lost while updating customisation items.
|
||||
- Tidied on source codes:
|
||||
- Many typos have been fixed.
|
||||
- Some unnecessary type casting removed.
|
||||
- 0.23.17:
|
||||
- Improved:
|
||||
- Overall performance has been improved by using PouchDB 9.0.0.
|
||||
@@ -30,25 +100,5 @@ Hooray for open source, and generous licences, and the sharing of knowledge by e
|
||||
- We can get a notification about the storage usage of the remote database.
|
||||
- Default: We will be asked.
|
||||
- If the remote storage usage approaches the configured value, we will be asked whether we want to Rebuild or increase the limit.
|
||||
- 0.23.16:
|
||||
- Maintenance Update:
|
||||
- Library refining (Phase 1 - step 2). There are no significant changes on the user side.
|
||||
- Including the following fixes of potentially problems:
|
||||
- the problem which the path had been obfuscating twice has been resolved.
|
||||
- Note: Potential problems of the library; which has not happened in Self-hosted LiveSync for some reasons.
|
||||
- 0.23.15:
|
||||
- Maintenance Update:
|
||||
- Library refining (Phase 1). There are no significant changes on the user side.
|
||||
- 0.23.14:
|
||||
- Fixed:
|
||||
- No longer batch-saving ignores editor inputs.
|
||||
- The file-watching and serialisation processes have been changed to the one which is similar to previous implementations.
|
||||
- We can configure the settings (Especially about text-boxes) even if we have configured the device name.
|
||||
- Improved:
|
||||
- We can configure the delay of batch-saving.
|
||||
- Default: 5 seconds, the same as the previous hard-coded value. (Note: also, the previous behaviour was not correct).
|
||||
- Also, we can configure the limit of delaying batch-saving.
|
||||
- The performance of showing status indicators has been improved.
|
||||
|
||||
|
||||
Older notes is in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md).
|
||||
@@ -18,6 +18,25 @@ I have a lot of respect for that plugin, even though it is sometimes treated as
|
||||
Hooray for open source, and generous licences, and the sharing of knowledge by experts.
|
||||
|
||||
#### Version history
|
||||
- 0.23.16:
|
||||
- Maintenance Update:
|
||||
- Library refining (Phase 1 - step 2). There are no significant changes on the user side.
|
||||
- Including the following fixes of potentially problems:
|
||||
- the problem which the path had been obfuscating twice has been resolved.
|
||||
- Note: Potential problems of the library; which has not happened in Self-hosted LiveSync for some reasons.
|
||||
- 0.23.15:
|
||||
- Maintenance Update:
|
||||
- Library refining (Phase 1). There are no significant changes on the user side.
|
||||
- 0.23.14:
|
||||
- Fixed:
|
||||
- No longer batch-saving ignores editor inputs.
|
||||
- The file-watching and serialisation processes have been changed to the one which is similar to previous implementations.
|
||||
- We can configure the settings (Especially about text-boxes) even if we have configured the device name.
|
||||
- Improved:
|
||||
- We can configure the delay of batch-saving.
|
||||
- Default: 5 seconds, the same as the previous hard-coded value. (Note: also, the previous behaviour was not correct).
|
||||
- Also, we can configure the limit of delaying batch-saving.
|
||||
- The performance of showing status indicators has been improved.
|
||||
- 0.23.13:
|
||||
- Fixed:
|
||||
- No longer files have been trimmed even delimiters have been continuous.
|
||||
|
||||
Reference in New Issue
Block a user