From 375e7bde317bb5a8bbb4d93f9f37c80121f4efa2 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Wed, 9 Jul 2025 12:15:59 +0100 Subject: [PATCH] ### New Feature - New chunking algorithm `V3: Fine deduplication` has been added, and will be recommended after updates. - New language `ko` (Korean) has been added. - Chinese (Simplified) translation has been updated. ### Fixed - Numeric settings are now never lost the focus during the value changing. ### Improved - All translations have rewritten into YAML format, to easier manage and contribution. - Doctor recommendations have now shown in the user-friendly notation. ### Refactored - Never ending `ObsidianLiveSyncSettingTag.ts` finally had separated into each pane's file. - Some commented-out codes have been removed. --- eslint.config.mjs | 3 +- package-lock.json | 527 ++- package.json | 12 +- src/lib | 2 +- src/modules/essential/ModuleMigration.ts | 154 +- src/modules/features/ModuleObsidianSetting.ts | 17 + .../SettingDialogue/LiveSyncSetting.ts | 18 +- .../ObsidianLiveSyncSettingTab.ts | 3193 ++--------------- .../features/SettingDialogue/PaneAdvanced.ts | 44 + .../features/SettingDialogue/PaneChangeLog.ts | 33 + .../SettingDialogue/PaneCustomisationSync.ts | 77 + .../features/SettingDialogue/PaneGeneral.ts | 45 + .../features/SettingDialogue/PaneHatch.ts | 531 +++ .../SettingDialogue/PaneMaintenance.ts | 374 ++ .../features/SettingDialogue/PanePatches.ts | 82 + .../SettingDialogue/PanePowerUsers.ts | 71 + .../SettingDialogue/PaneRemoteConfig.ts | 708 ++++ .../features/SettingDialogue/PaneSelector.ts | 121 + .../features/SettingDialogue/PaneSetup.ts | 205 ++ .../SettingDialogue/PaneSyncSettings.ts | 346 ++ .../features/SettingDialogue/SettingPane.ts | 119 + .../SettingDialogue/settingConstants.ts | 4 + tsconfig.json | 2 +- 23 files changed, 3573 insertions(+), 3115 deletions(-) create mode 100644 src/modules/features/SettingDialogue/PaneAdvanced.ts create mode 100644 src/modules/features/SettingDialogue/PaneChangeLog.ts create mode 100644 src/modules/features/SettingDialogue/PaneCustomisationSync.ts create mode 100644 src/modules/features/SettingDialogue/PaneGeneral.ts create mode 100644 src/modules/features/SettingDialogue/PaneHatch.ts create mode 100644 src/modules/features/SettingDialogue/PaneMaintenance.ts create mode 100644 src/modules/features/SettingDialogue/PanePatches.ts create mode 100644 src/modules/features/SettingDialogue/PanePowerUsers.ts create mode 100644 src/modules/features/SettingDialogue/PaneRemoteConfig.ts create mode 100644 src/modules/features/SettingDialogue/PaneSelector.ts create mode 100644 src/modules/features/SettingDialogue/PaneSetup.ts create mode 100644 src/modules/features/SettingDialogue/PaneSyncSettings.ts create mode 100644 src/modules/features/SettingDialogue/SettingPane.ts diff --git a/eslint.config.mjs b/eslint.config.mjs index 7b1c9ec..98e8f50 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -40,8 +40,7 @@ export default [ "src/lib/test", "src/lib/src/cli", "**/main.js", - "src/lib/apps/webpeer/dist", - "src/lib/apps/webpeer/svelte.config.js", + "src/lib/apps/webpeer/*" ], }, ...compat.extends( diff --git a/package-lock.json b/package-lock.json index 97ba4cb..8cf89d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "eslint-plugin-import": "^2.31.0", "eslint-plugin-svelte": "^3.0.2", "events": "^3.3.0", + "glob": "^11.0.3", "obsidian": "^1.8.7", "postcss": "^8.5.3", "postcss-load-config": "^6.0.1", @@ -71,7 +72,8 @@ "transform-pouch": "^2.0.0", "tslib": "^2.8.1", "tsx": "^4.19.4", - "typescript": "5.7.3" + "typescript": "5.7.3", + "yaml": "^2.8.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2451,6 +2453,130 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", @@ -5009,7 +5135,8 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/base64-js": { "version": "1.5.1", @@ -5074,6 +5201,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -5657,6 +5785,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -6748,6 +6883,23 @@ "is-callable": "^1.1.3" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -6912,6 +7064,30 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/glob": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -7831,6 +8007,22 @@ "npm": ">=7.0.0" } }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/js-sdsl": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", @@ -8193,11 +8385,12 @@ } }, "node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { "node": "20 || >=22" @@ -8215,6 +8408,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -8645,6 +8848,13 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8692,6 +8902,33 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/path-to-regexp": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", @@ -9906,6 +10143,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -10013,6 +10263,22 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -10077,6 +10343,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -10918,6 +11198,25 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -10962,18 +11261,16 @@ } }, "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "dev": true, "license": "ISC", - "optional": true, - "peer": true, "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" } }, "node_modules/yargs": { @@ -12670,6 +12967,84 @@ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true }, + "@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==" + }, + "@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "requires": { + "@isaacs/balanced-match": "^4.0.1" + } + }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, "@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", @@ -14616,7 +14991,8 @@ "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "base64-js": { "version": "1.5.1", @@ -14660,6 +15036,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "requires": { "balanced-match": "^1.0.0" } @@ -15049,6 +15426,12 @@ "gopd": "^1.2.0" } }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -15847,6 +16230,16 @@ "is-callable": "^1.1.3" } }, + "foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -15953,6 +16346,20 @@ "resolve-pkg-maps": "^1.0.0" } }, + "glob": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "dev": true, + "requires": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + } + }, "glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -16602,6 +17009,15 @@ "ws": "^8.4.0" } }, + "jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2" + } + }, "js-sdsl": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", @@ -16879,11 +17295,11 @@ } }, "minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "requires": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" } }, "minimist": { @@ -16891,6 +17307,12 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true + }, "moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -17168,6 +17590,12 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -17200,6 +17628,24 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "requires": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "dependencies": { + "lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "dev": true + } + } + }, "path-to-regexp": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", @@ -18061,6 +18507,12 @@ "side-channel-map": "^1.0.1" } }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + }, "smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -18137,6 +18589,17 @@ "strip-ansi": "^6.0.1" } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, "string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -18179,6 +18642,15 @@ "ansi-regex": "^5.0.1" } }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -18743,6 +19215,17 @@ "strip-ansi": "^6.0.0" } }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -18766,12 +19249,10 @@ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, "yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", - "dev": true, - "optional": true, - "peer": true + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "dev": true }, "yargs": { "version": "17.7.2", diff --git a/package.json b/package.json index 4ebe945..4684cf9 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,13 @@ "type": "module", "scripts": { "bakei18n": "npx tsx ./src/lib/_tools/bakei18n.ts", + "i18n:bakejson": "npx tsx ./src/lib/_tools/bakei18n.ts", + "i18n:yaml2json": "npx tsx ./src/lib/_tools/yaml2json.ts", + "i18n:json2yaml": "npx tsx ./src/lib/_tools/json2yaml.ts", + "prettyjson": "prettier --config ./.prettierrc ./src/lib/src/common/messagesJson/*.json --write --log-level error", "postbakei18n": "prettier --config ./.prettierrc ./src/lib/src/common/messages/*.ts --write --log-level error", + "posti18n:yaml2json": "npm run prettyjson", + "predev": "npm run bakei18n", "dev": "node esbuild.config.mjs", "prebuild": "npm run bakei18n", "build": "node esbuild.config.mjs production", @@ -47,6 +53,7 @@ "eslint-plugin-import": "^2.31.0", "eslint-plugin-svelte": "^3.0.2", "events": "^3.3.0", + "glob": "^11.0.3", "obsidian": "^1.8.7", "postcss": "^8.5.3", "postcss-load-config": "^6.0.1", @@ -67,13 +74,14 @@ "transform-pouch": "^2.0.0", "tslib": "^2.8.1", "tsx": "^4.19.4", - "typescript": "5.7.3" + "typescript": "5.7.3", + "yaml": "^2.8.0" }, "dependencies": { "@aws-sdk/client-s3": "^3.808.0", + "@smithy/fetch-http-handler": "^5.0.2", "@smithy/md5-js": "^4.0.2", "@smithy/middleware-apply-body-checksum": "^4.1.0", - "@smithy/fetch-http-handler": "^5.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/querystring-builder": "^4.0.2", "diff-match-patch": "^1.0.5", diff --git a/src/lib b/src/lib index 89d9e4e..dfbd635 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit 89d9e4e3e0145973776359f25f346dcb33dde31d +Subproject commit dfbd6358b1fd31b827079293d957265beb74eebb diff --git a/src/modules/essential/ModuleMigration.ts b/src/modules/essential/ModuleMigration.ts index c9fc265..3ba0f6c 100644 --- a/src/modules/essential/ModuleMigration.ts +++ b/src/modules/essential/ModuleMigration.ts @@ -85,7 +85,8 @@ export class ModuleMigration extends AbstractModule implements ICoreModule { name: getConfName(key as AllSettingItemKey), current: `${this.settings[key]}`, reason: value.reason ?? " N/A ", - ideal: `${value.value}`, + ideal: `${value.valueDisplay ?? value.value}`, + //@ts-ignore level: `${level}`, note: note, }), @@ -147,157 +148,6 @@ export class ModuleMigration extends AbstractModule implements ICoreModule { await this.saveSettings(); } } - // async migrationCheck() { - // const old = this.settings.settingVersion; - // const current = SETTING_VERSION_SUPPORT_CASE_INSENSITIVE; - // // Check each migrations(old -> current) - // if (!(await this.migrateToCaseInsensitive(old, current))) { - // this._log( - // $msg("moduleMigration.logMigrationFailed", { - // old: old.toString(), - // current: current.toString(), - // }), - // LOG_LEVEL_NOTICE - // ); - // return; - // } - // } - // async migrateToCaseInsensitive(old: number, current: number) { - // if ( - // this.settings.handleFilenameCaseSensitive !== undefined && - // this.settings.doNotUseFixedRevisionForChunks !== undefined - // ) { - // if (current < SETTING_VERSION_SUPPORT_CASE_INSENSITIVE) { - // this.settings.settingVersion = SETTING_VERSION_SUPPORT_CASE_INSENSITIVE; - // await this.saveSettings(); - // } - // return true; - // } - // if ( - // old >= SETTING_VERSION_SUPPORT_CASE_INSENSITIVE && - // this.settings.handleFilenameCaseSensitive !== undefined && - // this.settings.doNotUseFixedRevisionForChunks !== undefined - // ) { - // return true; - // } - - // let remoteHandleFilenameCaseSensitive: undefined | boolean = undefined; - // let remoteDoNotUseFixedRevisionForChunks: undefined | boolean = undefined; - // let remoteChecked = false; - // try { - // const remoteInfo = await this.core.replicator.getRemotePreferredTweakValues(this.settings); - // if (remoteInfo) { - // remoteHandleFilenameCaseSensitive = - // "handleFilenameCaseSensitive" in remoteInfo ? remoteInfo.handleFilenameCaseSensitive : false; - // remoteDoNotUseFixedRevisionForChunks = - // "doNotUseFixedRevisionForChunks" in remoteInfo ? remoteInfo.doNotUseFixedRevisionForChunks : false; - // if ( - // remoteHandleFilenameCaseSensitive !== undefined || - // remoteDoNotUseFixedRevisionForChunks !== undefined - // ) { - // remoteChecked = true; - // } - // } else { - // this._log($msg("moduleMigration.logFetchRemoteTweakFailed"), LOG_LEVEL_INFO); - // } - // } catch (ex) { - // this._log($msg("moduleMigration.logRemoteTweakUnavailable"), LOG_LEVEL_INFO); - // this._log(ex, LOG_LEVEL_VERBOSE); - // } - - // if (remoteChecked) { - // // The case that the remote could be checked. - // if (remoteHandleFilenameCaseSensitive && remoteDoNotUseFixedRevisionForChunks) { - // // Migrated, but configured as same as old behaviour. - // this.settings.handleFilenameCaseSensitive = true; - // this.settings.doNotUseFixedRevisionForChunks = true; - // this.settings.settingVersion = SETTING_VERSION_SUPPORT_CASE_INSENSITIVE; - // this._log( - // $msg("moduleMigration.logMigratedSameBehaviour", { - // current: current.toString(), - // }), - // LOG_LEVEL_INFO - // ); - // await this.saveSettings(); - // return true; - // } - // const message = $msg("moduleMigration.msgFetchRemoteAgain"); - // const OPTION_FETCH = $msg("moduleMigration.optionYesFetchAgain"); - // const DISMISS = $msg("moduleMigration.optionNoAskAgain"); - // const options = [OPTION_FETCH, DISMISS]; - // const ret = await this.core.confirm.confirmWithMessage( - // $msg("moduleMigration.titleCaseSensitivity"), - // message, - // options, - // DISMISS, - // 40 - // ); - // if (ret == OPTION_FETCH) { - // this.settings.handleFilenameCaseSensitive = remoteHandleFilenameCaseSensitive || false; - // this.settings.doNotUseFixedRevisionForChunks = remoteDoNotUseFixedRevisionForChunks || false; - // this.settings.settingVersion = SETTING_VERSION_SUPPORT_CASE_INSENSITIVE; - // await this.saveSettings(); - // try { - // await this.core.rebuilder.scheduleFetch(); - // return; - // } catch (ex) { - // this._log($msg("moduleMigration.logRedflag2CreationFail"), LOG_LEVEL_VERBOSE); - // this._log(ex, LOG_LEVEL_VERBOSE); - // } - // return false; - // } else { - // return false; - // } - // } - - // const ENABLE_BOTH = $msg("moduleMigration.optionEnableBoth"); - // const ENABLE_FILENAME_CASE_INSENSITIVE = $msg("moduleMigration.optionEnableFilenameCaseInsensitive"); - // const ENABLE_FIXED_REVISION_FOR_CHUNKS = $msg("moduleMigration.optionEnableFixedRevisionForChunks"); - // const ADJUST_TO_REMOTE = $msg("moduleMigration.optionAdjustRemote"); - // const KEEP = $msg("moduleMigration.optionKeepPreviousBehaviour"); - // const DISMISS = $msg("moduleMigration.optionDecideLater"); - // const message = $msg("moduleMigration.msgSinceV02321"); - // const options = [ENABLE_BOTH, ENABLE_FILENAME_CASE_INSENSITIVE, ENABLE_FIXED_REVISION_FOR_CHUNKS]; - // if (remoteChecked) { - // options.push(ADJUST_TO_REMOTE); - // } - // options.push(KEEP, DISMISS); - // const ret = await this.core.confirm.confirmWithMessage( - // $msg("moduleMigration.titleCaseSensitivity"), - // message, - // options, - // DISMISS, - // 40 - // ); - // console.dir(ret); - // switch (ret) { - // case ENABLE_BOTH: - // this.settings.handleFilenameCaseSensitive = false; - // this.settings.doNotUseFixedRevisionForChunks = false; - // break; - // case ENABLE_FILENAME_CASE_INSENSITIVE: - // this.settings.handleFilenameCaseSensitive = false; - // this.settings.doNotUseFixedRevisionForChunks = true; - // break; - // case ENABLE_FIXED_REVISION_FOR_CHUNKS: - // this.settings.doNotUseFixedRevisionForChunks = false; - // this.settings.handleFilenameCaseSensitive = true; - // break; - // case KEEP: - // this.settings.handleFilenameCaseSensitive = true; - // this.settings.doNotUseFixedRevisionForChunks = true; - // this.settings.settingVersion = SETTING_VERSION_SUPPORT_CASE_INSENSITIVE; - // await this.saveSettings(); - // return true; - // case DISMISS: - // default: - // return false; - // } - // this.settings.settingVersion = SETTING_VERSION_SUPPORT_CASE_INSENSITIVE; - // await this.saveSettings(); - // await this.core.rebuilder.scheduleRebuild(); - // await this.core.$$performRestart(); - // } async initialMessage() { const message = $msg("moduleMigration.msgInitialSetup", { diff --git a/src/modules/features/ModuleObsidianSetting.ts b/src/modules/features/ModuleObsidianSetting.ts index 99d7e8b..4eb2aa8 100644 --- a/src/modules/features/ModuleObsidianSetting.ts +++ b/src/modules/features/ModuleObsidianSetting.ts @@ -3,6 +3,7 @@ import { type IObsidianModule, AbstractObsidianModule } from "../AbstractObsidia import { EVENT_REQUEST_RELOAD_SETTING_TAB, EVENT_SETTING_SAVED, eventHub } from "../../common/events"; import { type BucketSyncSetting, + ChunkAlgorithmNames, type ConfigPassphraseStore, type CouchDBConnection, DEFAULT_SETTINGS, @@ -273,6 +274,22 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO this.settings.usePluginSync = false; } } + + // Splitter configurations have been replaced with chunkSplitterVersion. + if (this.settings.chunkSplitterVersion == "") { + if (this.settings.enableChunkSplitterV2) { + if (this.settings.useSegmenter) { + this.settings.chunkSplitterVersion = "v2-segmenter"; + } else { + this.settings.chunkSplitterVersion = "v2"; + } + } else { + this.settings.chunkSplitterVersion = ""; + } + } else if (!(this.settings.chunkSplitterVersion in ChunkAlgorithmNames)) { + this.settings.chunkSplitterVersion = ""; + } + // this.core.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim()); eventHub.emitEvent(EVENT_REQUEST_RELOAD_SETTING_TAB); } diff --git a/src/modules/features/SettingDialogue/LiveSyncSetting.ts b/src/modules/features/SettingDialogue/LiveSyncSetting.ts index 14d3ea6..4735f39 100644 --- a/src/modules/features/SettingDialogue/LiveSyncSetting.ts +++ b/src/modules/features/SettingDialogue/LiveSyncSetting.ts @@ -14,14 +14,7 @@ import { statusDisplay, type ConfigurationItem, } from "../../../lib/src/common/types.ts"; -import { - type ObsidianLiveSyncSettingTab, - type AutoWireOption, - wrapMemo, - type OnUpdateResult, - createStub, - findAttrFromParent, -} from "./ObsidianLiveSyncSettingTab.ts"; +import { createStub, type ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; import { type AllSettingItemKey, getConfig, @@ -31,6 +24,7 @@ import { type AllBooleanItemKey, } from "./settingConstants.ts"; import { $msg } from "src/lib/src/common/i18n.ts"; +import { findAttrFromParent, wrapMemo, type AutoWireOption, type OnUpdateResult } from "./SettingPane.ts"; export class LiveSyncSetting extends Setting { autoWiredComponent?: TextComponent | ToggleComponent | DropdownComponent | ButtonComponent | TextAreaComponent; @@ -184,10 +178,10 @@ export class LiveSyncSetting extends Setting { const conf = this.autoWireSetting(key, opt); this.addText((text) => { this.autoWiredComponent = text; - if (opt.clampMin) { + if (opt.clampMin !== undefined) { text.inputEl.setAttribute("min", `${opt.clampMin}`); } - if (opt.clampMax) { + if (opt.clampMax !== undefined) { text.inputEl.setAttribute("max", `${opt.clampMax}`); } let lastError = false; @@ -203,8 +197,8 @@ export class LiveSyncSetting extends Setting { const value = parsedValue; let hasError = false; if (isNaN(value)) hasError = true; - if (opt.clampMax && opt.clampMax < value) hasError = true; - if (opt.clampMin && opt.clampMin > value) { + if (opt.clampMax !== undefined && opt.clampMax < value) hasError = true; + if (opt.clampMin !== undefined && opt.clampMin > value) { if (opt.acceptZero && value == 0) { // This is ok. } else { diff --git a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts index 1d28767..00f7ef4 100644 --- a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts +++ b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts @@ -1,59 +1,29 @@ -import { App, PluginSettingTab, MarkdownRenderer, stringifyYaml } from "../../../deps.ts"; +import { App, PluginSettingTab } from "../../../deps.ts"; import { - DEFAULT_SETTINGS, type ObsidianLiveSyncSettings, - type ConfigPassphraseStore, type RemoteDBSettings, - type FilePathWithPrefix, - type HashAlgorithm, - type DocumentID, LOG_LEVEL_NOTICE, - LOG_LEVEL_VERBOSE, - LOG_LEVEL_INFO, - type LoadedEntry, - PREFERRED_SETTING_CLOUDANT, - PREFERRED_SETTING_SELF_HOSTED, FLAGMD_REDFLAG2_HR, FLAGMD_REDFLAG3_HR, REMOTE_COUCHDB, REMOTE_MINIO, - PREFERRED_JOURNAL_SYNC, - FLAGMD_REDFLAG, type ConfigLevel, LEVEL_POWER_USER, LEVEL_ADVANCED, LEVEL_EDGE_CASE, - type MetaEntry, - type FilePath, REMOTE_P2P, - type CustomRegExpSource, } from "../../../lib/src/common/types.ts"; -import { - constructCustomRegExpList, - createBlob, - delay, - getFileRegExp, - isDocContentSame, - isObjectDifferent, - parseHeaderValues, - readAsBlob, - sizeToHumanReadable, - splitCustomRegExpList, -} from "../../../lib/src/common/utils.ts"; -import { arrayBufferToBase64Single, versionNumberString2Number } from "../../../lib/src/string_and_binary/convert.ts"; +import { delay, isObjectDifferent, sizeToHumanReadable } from "../../../lib/src/common/utils.ts"; +import { versionNumberString2Number } from "../../../lib/src/string_and_binary/convert.ts"; import { Logger } from "../../../lib/src/common/logger.ts"; import { balanceChunkPurgedDBs, checkSyncInfo, - isCloudantURI, purgeUnreferencedChunks, } from "../../../lib/src/pouchdb/utils_couchdb.ts"; import { testCrypt } from "../../../lib/src/encryption/e2ee_v2.ts"; import ObsidianLiveSyncPlugin from "../../../main.ts"; -import { getPath, requestToCouchDBWithCredentials, scheduleTask } from "../../../common/utils.ts"; -import { request } from "obsidian"; -import { addPrefix, shouldBeIgnored, stripAllPrefixes } from "../../../lib/src/string_and_binary/path.ts"; -import MultipleRegExpControl from "./MultipleRegExpControl.svelte"; +import { scheduleTask } from "../../../common/utils.ts"; import { LiveSyncCouchDBReplicator } from "../../../lib/src/replication/couchdb/LiveSyncReplicator.ts"; import { type AllSettingItemKey, @@ -65,88 +35,38 @@ import { type OnDialogSettings, getConfName, } from "./settingConstants.ts"; -import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "../../../lib/src/common/rosetta.ts"; -import { $t, $msg } from "../../../lib/src/common/i18n.ts"; -import { Semaphore } from "octagonal-wheels/concurrency/semaphore"; +import { $msg } from "../../../lib/src/common/i18n.ts"; import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; import { fireAndForget, yieldNextAnimationFrame } from "octagonal-wheels/promises"; import { confirmWithMessage } from "../../coreObsidian/UILib/dialogs.ts"; -import { - EVENT_REQUEST_COPY_SETUP_URI, - EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, - EVENT_REQUEST_OPEN_SETUP_URI, - EVENT_REQUEST_RELOAD_SETTING_TAB, - EVENT_REQUEST_RUN_DOCTOR, - EVENT_REQUEST_SHOW_SETUP_QR, - eventHub, -} from "../../../common/events.ts"; +import { EVENT_REQUEST_RELOAD_SETTING_TAB, eventHub } from "../../../common/events.ts"; import { skipIfDuplicated } from "octagonal-wheels/concurrency/lock"; import { JournalSyncMinio } from "../../../lib/src/replication/journal/objectstore/JournalSyncMinio.ts"; -import { ICHeader, ICXHeader, PSCHeader } from "../../../common/types.ts"; -import { HiddenFileSync } from "../../../features/HiddenFileSync/CmdHiddenFileSync.ts"; -import { EVENT_REQUEST_SHOW_HISTORY } from "../../../common/obsidianEvents.ts"; -import { LocalDatabaseMaintenance } from "../../../features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts"; -import { mount } from "svelte"; -import { getWebCrypto } from "../../../lib/src/mods.ts"; -import { generateCredentialObject } from "../../../lib/src/replication/httplib.ts"; - -export type OnUpdateResult = { - visibility?: boolean; - disabled?: boolean; - classes?: string[]; - isCta?: boolean; - isWarning?: boolean; -}; -type OnUpdateFunc = () => OnUpdateResult; -type UpdateFunction = () => void; - -export type AutoWireOption = { - placeHolder?: string; - holdValue?: boolean; - isPassword?: boolean; - invert?: boolean; - onUpdate?: OnUpdateFunc; - obsolete?: boolean; -}; - -function visibleOnly(cond: () => boolean): OnUpdateFunc { - return () => ({ - visibility: cond(), - }); -} -function enableOnly(cond: () => boolean): OnUpdateFunc { - return () => ({ - disabled: !cond(), - }); -} - -type OnSavedHandlerFunc = (value: AllSettings[T]) => Promise | void; -type OnSavedHandler = { - key: T; - handler: OnSavedHandlerFunc; -}; - -function getLevelStr(level: ConfigLevel) { - return level == LEVEL_POWER_USER - ? $msg("obsidianLiveSyncSettingTab.levelPowerUser") - : level == LEVEL_ADVANCED - ? $msg("obsidianLiveSyncSettingTab.levelAdvanced") - : level == LEVEL_EDGE_CASE - ? $msg("obsidianLiveSyncSettingTab.levelEdgeCase") - : ""; -} - -export function findAttrFromParent(el: HTMLElement, attr: string): string { - let current: HTMLElement | null = el; - while (current) { - const value = current.getAttribute(attr); - if (value) { - return value; - } - current = current.parentElement; - } - return ""; -} +import { paneChangeLog } from "./PaneChangeLog.ts"; +import { + enableOnly, + findAttrFromParent, + getLevelStr, + setLevelClass, + setStyle, + visibleOnly, + type OnSavedHandler, + type OnUpdateFunc, + type OnUpdateResult, + type PageFunctions, + type UpdateFunction, +} from "./SettingPane.ts"; +import { paneSetup } from "./PaneSetup.ts"; +import { paneGeneral } from "./PaneGeneral.ts"; +import { paneRemoteConfig } from "./PaneRemoteConfig.ts"; +import { paneSelector } from "./PaneSelector.ts"; +import { paneSyncSettings } from "./PaneSyncSettings.ts"; +import { paneCustomisationSync } from "./PaneCustomisationSync.ts"; +import { paneHatch } from "./PaneHatch.ts"; +import { paneAdvanced } from "./PaneAdvanced.ts"; +import { panePowerUsers } from "./PanePowerUsers.ts"; +import { panePatches } from "./PanePatches.ts"; +import { paneMaintenance } from "./PaneMaintenance.ts"; // For creating a document const toc = new Set(); @@ -169,16 +89,6 @@ export function createStub(name: string, key: string, value: string, panel: stri } } -export function wrapMemo(func: (arg: T) => void) { - let buf: T | undefined = undefined; - return (arg: T) => { - if (buf !== arg) { - func(arg); - buf = arg; - } - }; -} - export class ObsidianLiveSyncSettingTab extends PluginSettingTab { plugin: ObsidianLiveSyncPlugin; selectedScreen = ""; @@ -473,7 +383,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { const keys = Object.keys(newConf) as (keyof ObsidianLiveSyncSettings)[]; let hasLoaded = false; for (const k of keys) { - if (k === "deviceAndVaultName") continue; if (isObjectDifferent(newConf[k], this.initialSettings?.[k])) { // Something has changed if (this.isDirty(k as AllSettingItemKey)) { @@ -494,6 +403,9 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { } else { // not modified this.refreshSetting(k as AllSettingItemKey); + if (k in OnDialogSettingsDefault) { + continue; + } hasLoaded = true; } } @@ -508,6 +420,13 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { } } + //@ts-ignore + manifestVersion: string = MANIFEST_VERSION || "-"; + //@ts-ignore + updateInformation: string = UPDATE_INFO || ""; + + lastVersion = ~~(versionNumberString2Number(this.manifestVersion) / 1000); + screenElements: { [key: string]: HTMLElement[] } = {}; changeDisplay(screen: string) { for (const k in this.screenElements) { @@ -546,6 +465,196 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { } menuEl?: HTMLElement; + addScreenElement(key: string, element: HTMLElement) { + if (!(key in this.screenElements)) { + this.screenElements[key] = []; + } + this.screenElements[key].push(element); + } + + selectPane(event: Event) { + const target = event.target as HTMLElement; + if (target.tagName == "INPUT") { + const value = target.getAttribute("value"); + if (value && this.selectedScreen != value) { + this.changeDisplay(value); + } + } + } + + isNeedRebuildLocal() { + return this.isSomeDirty([ + "useIndexedDBAdapter", + "doNotUseFixedRevisionForChunks", + "handleFilenameCaseSensitive", + "passphrase", + "useDynamicIterationCount", + "usePathObfuscation", + "encrypt", + // "remoteType", + ]); + } + isNeedRebuildRemote() { + return this.isSomeDirty([ + "doNotUseFixedRevisionForChunks", + "handleFilenameCaseSensitive", + "passphrase", + "useDynamicIterationCount", + "usePathObfuscation", + "encrypt", + ]); + } + isAnySyncEnabled() { + if (this.isConfiguredAs("isConfigured", false)) return false; + if (this.isConfiguredAs("liveSync", true)) return true; + if (this.isConfiguredAs("periodicReplication", true)) return true; + if (this.isConfiguredAs("syncOnFileOpen", true)) return true; + if (this.isConfiguredAs("syncOnSave", true)) return true; + if (this.isConfiguredAs("syncOnEditorSave", true)) return true; + if (this.isConfiguredAs("syncOnStart", true)) return true; + if (this.isConfiguredAs("syncAfterMerge", true)) return true; + if (this.isConfiguredAs("syncOnFileOpen", true)) return true; + if (this.plugin?.replicator?.syncStatus == "CONNECTED") return true; + if (this.plugin?.replicator?.syncStatus == "PAUSED") return true; + return false; + } + + enableOnlySyncDisabled = enableOnly(() => !this.isAnySyncEnabled()); + + onlyOnP2POrCouchDB = () => + ({ + visibility: + this.isConfiguredAs("remoteType", REMOTE_P2P) || this.isConfiguredAs("remoteType", REMOTE_COUCHDB), + }) as OnUpdateResult; + + onlyOnCouchDB = () => + ({ + visibility: this.isConfiguredAs("remoteType", REMOTE_COUCHDB), + }) as OnUpdateResult; + onlyOnMinIO = () => + ({ + visibility: this.isConfiguredAs("remoteType", REMOTE_MINIO), + }) as OnUpdateResult; + onlyOnOnlyP2P = () => + ({ + visibility: this.isConfiguredAs("remoteType", REMOTE_P2P), + }) as OnUpdateResult; + onlyOnCouchDBOrMinIO = () => + ({ + visibility: + this.isConfiguredAs("remoteType", REMOTE_COUCHDB) || this.isConfiguredAs("remoteType", REMOTE_MINIO), + }) as OnUpdateResult; + // E2EE Function + checkWorkingPassphrase = async (): Promise => { + if (this.editingSettings.remoteType == REMOTE_MINIO) return true; + + const settingForCheck: RemoteDBSettings = { + ...this.editingSettings, + }; + const replicator = this.plugin.$anyNewReplicator(settingForCheck); + if (!(replicator instanceof LiveSyncCouchDBReplicator)) return true; + + const db = await replicator.connectRemoteCouchDBWithSetting(settingForCheck, this.plugin.$$isMobile(), true); + if (typeof db === "string") { + Logger($msg("obsidianLiveSyncSettingTab.logCheckPassphraseFailed", { db }), LOG_LEVEL_NOTICE); + return false; + } else { + if (await checkSyncInfo(db.db)) { + // Logger($msg("obsidianLiveSyncSettingTab.logDatabaseConnected"), LOG_LEVEL_NOTICE); + return true; + } else { + Logger($msg("obsidianLiveSyncSettingTab.logPassphraseNotCompatible"), LOG_LEVEL_NOTICE); + return false; + } + } + }; + isPassphraseValid = async () => { + if (this.editingSettings.encrypt && this.editingSettings.passphrase == "") { + Logger($msg("obsidianLiveSyncSettingTab.logEncryptionNoPassphrase"), LOG_LEVEL_NOTICE); + return false; + } + if (this.editingSettings.encrypt && !(await testCrypt())) { + Logger($msg("obsidianLiveSyncSettingTab.logEncryptionNoSupport"), LOG_LEVEL_NOTICE); + return false; + } + return true; + }; + + rebuildDB = async (method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks") => { + if (this.editingSettings.encrypt && this.editingSettings.passphrase == "") { + Logger($msg("obsidianLiveSyncSettingTab.logEncryptionNoPassphrase"), LOG_LEVEL_NOTICE); + return; + } + if (this.editingSettings.encrypt && !(await testCrypt())) { + Logger($msg("obsidianLiveSyncSettingTab.logEncryptionNoSupport"), LOG_LEVEL_NOTICE); + return; + } + if (!this.editingSettings.encrypt) { + this.editingSettings.passphrase = ""; + } + this.applyAllSettings(); + await this.plugin.$allSuspendAllSync(); + await this.plugin.$allSuspendExtraSync(); + this.reloadAllSettings(); + this.editingSettings.isConfigured = true; + Logger($msg("obsidianLiveSyncSettingTab.logRebuildNote"), LOG_LEVEL_NOTICE); + await this.saveAllDirtySettings(); + this.closeSetting(); + await delay(2000); + await this.plugin.rebuilder.$performRebuildDB(method); + }; + async confirmRebuild() { + if (!(await this.isPassphraseValid())) { + Logger(`Passphrase is not valid, please fix it.`, LOG_LEVEL_NOTICE); + return; + } + const OPTION_FETCH = $msg("obsidianLiveSyncSettingTab.optionFetchFromRemote"); + const OPTION_REBUILD_BOTH = $msg("obsidianLiveSyncSettingTab.optionRebuildBoth"); + const OPTION_ONLY_SETTING = $msg("obsidianLiveSyncSettingTab.optionSaveOnlySettings"); + const OPTION_CANCEL = $msg("obsidianLiveSyncSettingTab.optionCancel"); + const title = $msg("obsidianLiveSyncSettingTab.titleRebuildRequired"); + const note = $msg("obsidianLiveSyncSettingTab.msgRebuildRequired", { + OPTION_REBUILD_BOTH, + OPTION_FETCH, + OPTION_ONLY_SETTING, + }); + const buttons = [ + OPTION_FETCH, + OPTION_REBUILD_BOTH, // OPTION_REBUILD_REMOTE, + OPTION_ONLY_SETTING, + OPTION_CANCEL, + ]; + const result = await confirmWithMessage(this.plugin, title, note, buttons, OPTION_CANCEL, 0); + if (result == OPTION_CANCEL) return; + if (result == OPTION_FETCH) { + if (!(await this.checkWorkingPassphrase())) { + if ( + (await this.plugin.confirm.askYesNoDialog($msg("obsidianLiveSyncSettingTab.msgAreYouSureProceed"), { + defaultOption: "No", + })) != "yes" + ) + return; + } + } + if (!this.editingSettings.encrypt) { + this.editingSettings.passphrase = ""; + } + await this.saveAllDirtySettings(); + await this.applyAllSettings(); + if (result == OPTION_FETCH) { + await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG3_HR, ""); + this.plugin.$$scheduleAppReload(); + this.closeSetting(); + // await rebuildDB("localOnly"); + } else if (result == OPTION_REBUILD_BOTH) { + await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG2_HR, ""); + this.plugin.$$scheduleAppReload(); + this.closeSetting(); + } else if (result == OPTION_ONLY_SETTING) { + await this.plugin.saveSettings(); + } + } + display(): void { const changeDisplay = this.changeDisplay.bind(this); const { containerEl } = this; @@ -566,25 +675,11 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { containerEl.addClass("sls-setting"); containerEl.removeClass("isWizard"); - const setStyle = (el: HTMLElement, styleHead: string, condition: () => boolean) => { - if (condition()) { - el.addClass(`${styleHead}-enabled`); - el.removeClass(`${styleHead}-disabled`); - } else { - el.addClass(`${styleHead}-disabled`); - el.removeClass(`${styleHead}-enabled`); - } - }; setStyle(containerEl, "menu-setting-poweruser", () => this.isConfiguredAs("usePowerUserMode", true)); setStyle(containerEl, "menu-setting-advanced", () => this.isConfiguredAs("useAdvancedMode", true)); setStyle(containerEl, "menu-setting-edgecase", () => this.isConfiguredAs("useEdgeCaseMode", true)); - const addScreenElement = (key: string, element: HTMLElement) => { - if (!(key in this.screenElements)) { - this.screenElements[key] = []; - } - this.screenElements[key].push(element); - }; + // const addScreenElement = (key: string, element: HTMLElement) => addScreenElement.bind(this)(key, element); const menuWrapper = this.createEl(containerEl, "div", { cls: "sls-setting-menu-wrapper" }); if (this.menuEl) { @@ -593,91 +688,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { this.menuEl = menuWrapper.createDiv(""); this.menuEl.addClass("sls-setting-menu"); const menuTabs = this.menuEl.querySelectorAll(".sls-setting-label"); - const selectPane = (event: Event) => { - const target = event.target as HTMLElement; - if (target.tagName == "INPUT") { - const value = target.getAttribute("value"); - if (value && this.selectedScreen != value) { - changeDisplay(value); - } - } - }; - const isNeedRebuildLocal = () => { - return this.isSomeDirty([ - "useIndexedDBAdapter", - "doNotUseFixedRevisionForChunks", - "handleFilenameCaseSensitive", - "passphrase", - "useDynamicIterationCount", - "usePathObfuscation", - "encrypt", - // "remoteType", - ]); - }; - const isNeedRebuildRemote = () => { - return this.isSomeDirty([ - "doNotUseFixedRevisionForChunks", - "handleFilenameCaseSensitive", - "passphrase", - "useDynamicIterationCount", - "usePathObfuscation", - "encrypt", - ]); - }; - const confirmRebuild = async () => { - if (!(await isPassphraseValid())) { - Logger(`Passphrase is not valid, please fix it.`, LOG_LEVEL_NOTICE); - return; - } - const OPTION_FETCH = $msg("obsidianLiveSyncSettingTab.optionFetchFromRemote"); - const OPTION_REBUILD_BOTH = $msg("obsidianLiveSyncSettingTab.optionRebuildBoth"); - const OPTION_ONLY_SETTING = $msg("obsidianLiveSyncSettingTab.optionSaveOnlySettings"); - const OPTION_CANCEL = $msg("obsidianLiveSyncSettingTab.optionCancel"); - const title = $msg("obsidianLiveSyncSettingTab.titleRebuildRequired"); - const note = $msg("obsidianLiveSyncSettingTab.msgRebuildRequired", { - OPTION_REBUILD_BOTH, - OPTION_FETCH, - OPTION_ONLY_SETTING, - }); - const buttons = [ - OPTION_FETCH, - OPTION_REBUILD_BOTH, // OPTION_REBUILD_REMOTE, - OPTION_ONLY_SETTING, - OPTION_CANCEL, - ]; - const result = await confirmWithMessage(this.plugin, title, note, buttons, OPTION_CANCEL, 0); - if (result == OPTION_CANCEL) return; - if (result == OPTION_FETCH) { - if (!(await checkWorkingPassphrase())) { - if ( - (await this.plugin.confirm.askYesNoDialog( - $msg("obsidianLiveSyncSettingTab.msgAreYouSureProceed"), - { - defaultOption: "No", - } - )) != "yes" - ) - return; - } - } - if (!this.editingSettings.encrypt) { - this.editingSettings.passphrase = ""; - } - await this.saveAllDirtySettings(); - await this.applyAllSettings(); - if (result == OPTION_FETCH) { - await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG3_HR, ""); - this.plugin.$$scheduleAppReload(); - this.closeSetting(); - // await rebuildDB("localOnly"); - } else if (result == OPTION_REBUILD_BOTH) { - await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG2_HR, ""); - this.plugin.$$scheduleAppReload(); - this.closeSetting(); - } else if (result == OPTION_ONLY_SETTING) { - await this.plugin.saveSettings(); - } - }; + this.createEl( menuWrapper, "div", @@ -690,27 +701,15 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { "button", { text: $msg("obsidianLiveSyncSettingTab.optionApply"), cls: "mod-warning" }, (buttonEl) => { - buttonEl.addEventListener("click", () => fireAndForget(async () => await confirmRebuild())); + buttonEl.addEventListener("click", () => + fireAndForget(async () => await this.confirmRebuild()) + ); } ); }, - visibleOnly(() => isNeedRebuildLocal() || isNeedRebuildRemote()) + visibleOnly(() => this.isNeedRebuildLocal() || this.isNeedRebuildRemote()) ); - const setLevelClass = (el: HTMLElement, level?: ConfigLevel) => { - switch (level) { - case LEVEL_POWER_USER: - el.addClass("sls-setting-poweruser"); - break; - case LEVEL_ADVANCED: - el.addClass("sls-setting-advanced"); - break; - case LEVEL_EDGE_CASE: - el.addClass("sls-setting-edgecase"); - break; - default: - // NO OP. - } - }; + let paneNo = 0; const addPane = ( parentEl: HTMLElement, @@ -750,12 +749,12 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { text: icon, title: title, }); - inputEl.addEventListener("change", selectPane); - inputEl.addEventListener("click", selectPane); + inputEl.addEventListener("change", (evt) => this.selectPane(evt)); + inputEl.addEventListener("click", (evt) => this.selectPane(evt)); } ); } - addScreenElement(`${order}`, el); + this.addScreenElement(`${order}`, el); const p = Promise.resolve(el); // fireAndForget // p.finally(() => { @@ -800,2708 +799,58 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { }); }); - //@ts-ignore - const manifestVersion: string = MANIFEST_VERSION || "-"; - //@ts-ignore - const updateInformation: string = UPDATE_INFO || ""; - - const lastVersion = ~~(versionNumberString2Number(manifestVersion) / 1000); - - const isAnySyncEnabled = (): boolean => { - if (this.isConfiguredAs("isConfigured", false)) return false; - if (this.isConfiguredAs("liveSync", true)) return true; - if (this.isConfiguredAs("periodicReplication", true)) return true; - if (this.isConfiguredAs("syncOnFileOpen", true)) return true; - if (this.isConfiguredAs("syncOnSave", true)) return true; - if (this.isConfiguredAs("syncOnEditorSave", true)) return true; - if (this.isConfiguredAs("syncOnStart", true)) return true; - if (this.isConfiguredAs("syncAfterMerge", true)) return true; - if (this.isConfiguredAs("syncOnFileOpen", true)) return true; - if (this.plugin?.replicator?.syncStatus == "CONNECTED") return true; - if (this.plugin?.replicator?.syncStatus == "PAUSED") return true; - return false; - }; - const enableOnlySyncDisabled = enableOnly(() => !isAnySyncEnabled()); - const combineOnUpdate = (func1: OnUpdateFunc, func2: OnUpdateFunc): OnUpdateFunc => { - return () => ({ - ...func1(), - ...func2(), - }); - }; - const onlyOnP2POrCouchDB = () => - ({ - visibility: - this.isConfiguredAs("remoteType", REMOTE_P2P) || this.isConfiguredAs("remoteType", REMOTE_COUCHDB), - }) as OnUpdateResult; - - const onlyOnCouchDB = () => - ({ - visibility: this.isConfiguredAs("remoteType", REMOTE_COUCHDB), - }) as OnUpdateResult; - const onlyOnMinIO = () => - ({ - visibility: this.isConfiguredAs("remoteType", REMOTE_MINIO), - }) as OnUpdateResult; - const onlyOnOnlyP2P = () => - ({ - visibility: this.isConfiguredAs("remoteType", REMOTE_P2P), - }) as OnUpdateResult; - const onlyOnCouchDBOrMinIO = () => - ({ - visibility: - this.isConfiguredAs("remoteType", REMOTE_COUCHDB) || - this.isConfiguredAs("remoteType", REMOTE_MINIO), - }) as OnUpdateResult; - // E2EE Function - const checkWorkingPassphrase = async (): Promise => { - if (this.editingSettings.remoteType == REMOTE_MINIO) return true; - - const settingForCheck: RemoteDBSettings = { - ...this.editingSettings, - }; - const replicator = this.plugin.$anyNewReplicator(settingForCheck); - if (!(replicator instanceof LiveSyncCouchDBReplicator)) return true; - - const db = await replicator.connectRemoteCouchDBWithSetting( - settingForCheck, - this.plugin.$$isMobile(), - true - ); - if (typeof db === "string") { - Logger($msg("obsidianLiveSyncSettingTab.logCheckPassphraseFailed", { db }), LOG_LEVEL_NOTICE); - return false; - } else { - if (await checkSyncInfo(db.db)) { - // Logger($msg("obsidianLiveSyncSettingTab.logDatabaseConnected"), LOG_LEVEL_NOTICE); - return true; - } else { - Logger($msg("obsidianLiveSyncSettingTab.logPassphraseNotCompatible"), LOG_LEVEL_NOTICE); - return false; - } - } - }; - const isPassphraseValid = async () => { - if (this.editingSettings.encrypt && this.editingSettings.passphrase == "") { - Logger($msg("obsidianLiveSyncSettingTab.logEncryptionNoPassphrase"), LOG_LEVEL_NOTICE); - return false; - } - if (this.editingSettings.encrypt && !(await testCrypt())) { - Logger($msg("obsidianLiveSyncSettingTab.logEncryptionNoSupport"), LOG_LEVEL_NOTICE); - return false; - } - return true; - }; - - const rebuildDB = async ( - method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks" - ) => { - if (this.editingSettings.encrypt && this.editingSettings.passphrase == "") { - Logger($msg("obsidianLiveSyncSettingTab.logEncryptionNoPassphrase"), LOG_LEVEL_NOTICE); - return; - } - if (this.editingSettings.encrypt && !(await testCrypt())) { - Logger($msg("obsidianLiveSyncSettingTab.logEncryptionNoSupport"), LOG_LEVEL_NOTICE); - return; - } - if (!this.editingSettings.encrypt) { - this.editingSettings.passphrase = ""; - } - this.applyAllSettings(); - await this.plugin.$allSuspendAllSync(); - await this.plugin.$allSuspendExtraSync(); - this.reloadAllSettings(); - this.editingSettings.isConfigured = true; - Logger($msg("obsidianLiveSyncSettingTab.logRebuildNote"), LOG_LEVEL_NOTICE); - await this.saveAllDirtySettings(); - this.closeSetting(); - await delay(2000); - await this.plugin.rebuilder.$performRebuildDB(method); - }; // Panes - void addPane(containerEl, $msg("obsidianLiveSyncSettingTab.panelChangeLog"), "💬", 100, false).then( - (paneEl) => { - const informationDivEl = this.createEl(paneEl, "div", { text: "" }); - - const tmpDiv = createDiv(); - // tmpDiv.addClass("sls-header-button"); - tmpDiv.addClass("op-warn-info"); - - tmpDiv.innerHTML = `

${$msg("obsidianLiveSyncSettingTab.msgNewVersionNote")}

`; - if (lastVersion > (this.editingSettings?.lastReadUpdates || 0)) { - const informationButtonDiv = informationDivEl.appendChild(tmpDiv); - informationButtonDiv.querySelector("button")?.addEventListener("click", () => { - fireAndForget(async () => { - this.editingSettings.lastReadUpdates = lastVersion; - await this.saveAllDirtySettings(); - informationButtonDiv.remove(); - }); - }); - } - fireAndForget(() => - MarkdownRenderer.render(this.plugin.app, updateInformation, informationDivEl, "/", this.plugin) - ); - } - ); - - void addPane(containerEl, $msg("obsidianLiveSyncSettingTab.panelSetup"), "🧙‍♂️", 110, false).then((paneEl) => { - void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleQuickSetup")).then((paneEl) => { - new Setting(paneEl) - .setName($msg("obsidianLiveSyncSettingTab.nameConnectSetupURI")) - .setDesc($msg("obsidianLiveSyncSettingTab.descConnectSetupURI")) - .addButton((text) => { - text.setButtonText($msg("obsidianLiveSyncSettingTab.btnUse")).onClick(() => { - this.closeSetting(); - eventHub.emitEvent(EVENT_REQUEST_OPEN_SETUP_URI); - }); - }); - - new Setting(paneEl) - .setName($msg("obsidianLiveSyncSettingTab.nameManualSetup")) - .setDesc($msg("obsidianLiveSyncSettingTab.descManualSetup")) - .addButton((text) => { - text.setButtonText($msg("obsidianLiveSyncSettingTab.btnStart")).onClick(async () => { - await this.enableMinimalSetup(); - }); - }); - - new Setting(paneEl) - .setName($msg("obsidianLiveSyncSettingTab.nameEnableLiveSync")) - .setDesc($msg("obsidianLiveSyncSettingTab.descEnableLiveSync")) - .addOnUpdate(visibleOnly(() => !this.isConfiguredAs("isConfigured", true))) - .addButton((text) => { - text.setButtonText($msg("obsidianLiveSyncSettingTab.btnEnable")).onClick(async () => { - this.editingSettings.isConfigured = true; - await this.saveAllDirtySettings(); - this.plugin.$$askReload(); - }); - }); - }); - - void addPanel( - paneEl, - $msg("obsidianLiveSyncSettingTab.titleSetupOtherDevices"), - undefined, - visibleOnly(() => this.isConfiguredAs("isConfigured", true)) - ).then((paneEl) => { - new Setting(paneEl) - .setName($msg("obsidianLiveSyncSettingTab.nameCopySetupURI")) - .setDesc($msg("obsidianLiveSyncSettingTab.descCopySetupURI")) - .addButton((text) => { - text.setButtonText($msg("obsidianLiveSyncSettingTab.btnCopy")).onClick(() => { - // await this.plugin.addOnSetup.command_copySetupURI(); - eventHub.emitEvent(EVENT_REQUEST_COPY_SETUP_URI); - }); - }); - new Setting(paneEl) - .setName($msg("Setup.ShowQRCode")) - .setDesc($msg("Setup.ShowQRCode.Desc")) - .addButton((text) => { - text.setButtonText($msg("Setup.ShowQRCode")).onClick(() => { - eventHub.emitEvent(EVENT_REQUEST_SHOW_SETUP_QR); - }); - }); - }); - - void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleReset")).then((paneEl) => { - new Setting(paneEl) - .setName($msg("obsidianLiveSyncSettingTab.nameDiscardSettings")) - .addButton((text) => { - text.setButtonText($msg("obsidianLiveSyncSettingTab.btnDiscard")) - .onClick(async () => { - if ( - (await this.plugin.confirm.askYesNoDialog( - $msg("obsidianLiveSyncSettingTab.msgDiscardConfirmation"), - { defaultOption: "No" } - )) == "yes" - ) { - this.editingSettings = { ...this.editingSettings, ...DEFAULT_SETTINGS }; - await this.saveAllDirtySettings(); - this.plugin.settings = { ...DEFAULT_SETTINGS }; - await this.plugin.$$saveSettingData(); - await this.plugin.$$resetLocalDatabase(); - // await this.plugin.initializeDatabase(); - this.plugin.$$askReload(); - } - }) - .setWarning(); - }) - .addOnUpdate(visibleOnly(() => this.isConfiguredAs("isConfigured", true))); - }); - - void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleExtraFeatures")).then((paneEl) => { - new Setting(paneEl).autoWireToggle("useAdvancedMode"); - - new Setting(paneEl).autoWireToggle("usePowerUserMode"); - new Setting(paneEl).autoWireToggle("useEdgeCaseMode"); - - this.addOnSaved("useAdvancedMode", () => this.display()); - this.addOnSaved("usePowerUserMode", () => this.display()); - this.addOnSaved("useEdgeCaseMode", () => this.display()); - }); - - void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleOnlineTips")).then((paneEl) => { - // this.createEl(paneEl, "h3", { text: $msg("obsidianLiveSyncSettingTab.titleOnlineTips") }); - const repo = "vrtmrz/obsidian-livesync"; - const topPath = $msg("obsidianLiveSyncSettingTab.linkTroubleshooting"); - const rawRepoURI = `https://raw.githubusercontent.com/${repo}/main`; - this.createEl( - paneEl, - "div", - "", - (el) => - (el.innerHTML = `${$msg("obsidianLiveSyncSettingTab.linkOpenInBrowser")}`) - ); - const troubleShootEl = this.createEl(paneEl, "div", { - text: "", - cls: "sls-troubleshoot-preview", + const bindPane = ( + paneFunc: (this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, funcs: PageFunctions) => void + ): ((paneEl: HTMLElement) => void) => { + const callback = (paneEl: HTMLElement) => { + paneFunc.call(this, paneEl, { + addPane, + addPanel, }); - const loadMarkdownPage = async (pathAll: string, basePathParam: string = "") => { - troubleShootEl.style.minHeight = troubleShootEl.clientHeight + "px"; - troubleShootEl.empty(); - const fullPath = pathAll.startsWith("/") ? pathAll : `${basePathParam}/${pathAll}`; - - const directoryArr = fullPath.split("/"); - const filename = directoryArr.pop(); - const directly = directoryArr.join("/"); - const basePath = directly; - - let remoteTroubleShootMDSrc = ""; - try { - remoteTroubleShootMDSrc = await request(`${rawRepoURI}${basePath}/${filename}`); - } catch (ex: any) { - remoteTroubleShootMDSrc = `${$msg("obsidianLiveSyncSettingTab.logErrorOccurred")}\n${ex.toString()}`; - } - const remoteTroubleShootMD = remoteTroubleShootMDSrc.replace( - /\((.*?(.png)|(.jpg))\)/g, - `(${rawRepoURI}${basePath}/$1)` - ); - // Render markdown - await MarkdownRenderer.render( - this.plugin.app, - ` [${$msg("obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting")}](${topPath}) [${$msg("obsidianLiveSyncSettingTab.linkPageTop")}](${filename})\n\n${remoteTroubleShootMD}`, - troubleShootEl, - `${rawRepoURI}`, - this.plugin - ); - // Menu - troubleShootEl - .querySelector(".sls-troubleshoot-anchor") - ?.parentElement?.setCssStyles({ - position: "sticky", - top: "-1em", - backgroundColor: "var(--modal-background)", - }); - // Trap internal links. - troubleShootEl.querySelectorAll("a.internal-link").forEach((anchorEl) => { - anchorEl.addEventListener("click", (evt) => { - fireAndForget(async () => { - const uri = anchorEl.getAttr("data-href"); - if (!uri) return; - if (uri.startsWith("#")) { - evt.preventDefault(); - const elements = Array.from( - troubleShootEl.querySelectorAll("[data-heading]") - ); - const p = elements.find( - (e) => - e.getAttr("data-heading")?.toLowerCase().split(" ").join("-") == - uri.substring(1).toLowerCase() - ); - if (p) { - p.setCssStyles({ scrollMargin: "3em" }); - p.scrollIntoView({ - behavior: "instant", - block: "start", - }); - } - } else { - evt.preventDefault(); - await loadMarkdownPage(uri, basePath); - troubleShootEl.setCssStyles({ scrollMargin: "1em" }); - troubleShootEl.scrollIntoView({ - behavior: "instant", - block: "start", - }); - } - }); - }); - }); - troubleShootEl.style.minHeight = ""; - }; - void loadMarkdownPage(topPath); - }); - }); - void addPane(containerEl, $msg("obsidianLiveSyncSettingTab.panelGeneralSettings"), "⚙️", 20, false).then( - (paneEl) => { - void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleAppearance")).then((paneEl) => { - const languages = Object.fromEntries([ - // ["", $msg("obsidianLiveSyncSettingTab.defaultLanguage")], - ...SUPPORTED_I18N_LANGS.map((e) => [e, $t(`lang-${e}`)]), - ]) as Record; - new Setting(paneEl).autoWireDropDown("displayLanguage", { - options: languages, - }); - this.addOnSaved("displayLanguage", () => this.display()); - new Setting(paneEl).autoWireToggle("showStatusOnEditor"); - new Setting(paneEl).autoWireToggle("showOnlyIconsOnEditor", { - onUpdate: visibleOnly(() => this.isConfiguredAs("showStatusOnEditor", true)), - }); - new Setting(paneEl).autoWireToggle("showStatusOnStatusbar"); - new Setting(paneEl).autoWireToggle("hideFileWarningNotice"); - }); - void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleLogging")).then((paneEl) => { - paneEl.addClass("wizardHidden"); - - new Setting(paneEl).autoWireToggle("lessInformationInLog"); - - new Setting(paneEl).autoWireToggle("showVerboseLog", { - onUpdate: visibleOnly(() => this.isConfiguredAs("lessInformationInLog", false)), - }); - }); - new Setting(paneEl).setClass("wizardOnly").addButton((button) => - button - .setButtonText($msg("obsidianLiveSyncSettingTab.btnNext")) - .setCta() - .onClick(() => { - this.changeDisplay("0"); - }) - ); - } - ); - let checkResultDiv: HTMLDivElement; - const checkConfig = async (checkResultDiv: HTMLDivElement | undefined) => { - Logger($msg("obsidianLiveSyncSettingTab.logCheckingDbConfig"), LOG_LEVEL_INFO); - let isSuccessful = true; - const emptyDiv = createDiv(); - emptyDiv.innerHTML = ""; - checkResultDiv?.replaceChildren(...[emptyDiv]); - const addResult = (msg: string, classes?: string[]) => { - const tmpDiv = createDiv(); - tmpDiv.addClass("ob-btn-config-fix"); - if (classes) { - tmpDiv.addClasses(classes); - } - tmpDiv.innerHTML = `${msg}`; - checkResultDiv?.appendChild(tmpDiv); }; - try { - if (isCloudantURI(this.editingSettings.couchDB_URI)) { - Logger($msg("obsidianLiveSyncSettingTab.logCannotUseCloudant"), LOG_LEVEL_NOTICE); - return; - } - // Tip: Add log for cloudant as Logger($msg("obsidianLiveSyncSettingTab.logServerConfigurationCheck")); - const customHeaders = parseHeaderValues(this.editingSettings.couchDB_CustomHeaders); - const credential = generateCredentialObject(this.editingSettings); - const r = await requestToCouchDBWithCredentials( - this.editingSettings.couchDB_URI, - credential, - window.origin, - undefined, - undefined, - undefined, - customHeaders - ); - const responseConfig = r.json; - - const addConfigFixButton = (title: string, key: string, value: string) => { - if (!checkResultDiv) return; - const tmpDiv = createDiv(); - tmpDiv.addClass("ob-btn-config-fix"); - tmpDiv.innerHTML = ``; - const x = checkResultDiv.appendChild(tmpDiv); - x.querySelector("button")?.addEventListener("click", () => { - fireAndForget(async () => { - Logger($msg("obsidianLiveSyncSettingTab.logCouchDbConfigSet", { title, key, value })); - const res = await requestToCouchDBWithCredentials( - this.editingSettings.couchDB_URI, - credential, - undefined, - key, - value, - undefined, - customHeaders - ); - if (res.status == 200) { - Logger( - $msg("obsidianLiveSyncSettingTab.logCouchDbConfigUpdated", { title }), - LOG_LEVEL_NOTICE - ); - checkResultDiv.removeChild(x); - await checkConfig(checkResultDiv); - } else { - Logger( - $msg("obsidianLiveSyncSettingTab.logCouchDbConfigFail", { title }), - LOG_LEVEL_NOTICE - ); - Logger(res.text, LOG_LEVEL_VERBOSE); - } - }); - }); - }; - addResult($msg("obsidianLiveSyncSettingTab.msgNotice"), ["ob-btn-config-head"]); - addResult($msg("obsidianLiveSyncSettingTab.msgIfConfigNotPersistent"), ["ob-btn-config-info"]); - addResult($msg("obsidianLiveSyncSettingTab.msgConfigCheck"), ["ob-btn-config-head"]); - - // Admin check - // for database creation and deletion - if (!(this.editingSettings.couchDB_USER in responseConfig.admins)) { - addResult($msg("obsidianLiveSyncSettingTab.warnNoAdmin")); - } else { - addResult($msg("obsidianLiveSyncSettingTab.okAdminPrivileges")); - } - // HTTP user-authorization check - if (responseConfig?.chttpd?.require_valid_user != "true") { - isSuccessful = false; - addResult($msg("obsidianLiveSyncSettingTab.errRequireValidUser")); - addConfigFixButton( - $msg("obsidianLiveSyncSettingTab.msgSetRequireValidUser"), - "chttpd/require_valid_user", - "true" - ); - } else { - addResult($msg("obsidianLiveSyncSettingTab.okRequireValidUser")); - } - if (responseConfig?.chttpd_auth?.require_valid_user != "true") { - isSuccessful = false; - addResult($msg("obsidianLiveSyncSettingTab.errRequireValidUserAuth")); - addConfigFixButton( - $msg("obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth"), - "chttpd_auth/require_valid_user", - "true" - ); - } else { - addResult($msg("obsidianLiveSyncSettingTab.okRequireValidUserAuth")); - } - // HTTPD check - // Check Authentication header - if (!responseConfig?.httpd["WWW-Authenticate"]) { - isSuccessful = false; - addResult($msg("obsidianLiveSyncSettingTab.errMissingWwwAuth")); - addConfigFixButton( - $msg("obsidianLiveSyncSettingTab.msgSetWwwAuth"), - "httpd/WWW-Authenticate", - 'Basic realm="couchdb"' - ); - } else { - addResult($msg("obsidianLiveSyncSettingTab.okWwwAuth")); - } - if (responseConfig?.httpd?.enable_cors != "true") { - isSuccessful = false; - addResult($msg("obsidianLiveSyncSettingTab.errEnableCors")); - addConfigFixButton($msg("obsidianLiveSyncSettingTab.msgEnableCors"), "httpd/enable_cors", "true"); - } else { - addResult($msg("obsidianLiveSyncSettingTab.okEnableCors")); - } - // If the server is not cloudant, configure request size - if (!isCloudantURI(this.editingSettings.couchDB_URI)) { - // REQUEST SIZE - if (Number(responseConfig?.chttpd?.max_http_request_size ?? 0) < 4294967296) { - isSuccessful = false; - addResult($msg("obsidianLiveSyncSettingTab.errMaxRequestSize")); - addConfigFixButton( - $msg("obsidianLiveSyncSettingTab.msgSetMaxRequestSize"), - "chttpd/max_http_request_size", - "4294967296" - ); - } else { - addResult($msg("obsidianLiveSyncSettingTab.okMaxRequestSize")); - } - if (Number(responseConfig?.couchdb?.max_document_size ?? 0) < 50000000) { - isSuccessful = false; - addResult($msg("obsidianLiveSyncSettingTab.errMaxDocumentSize")); - addConfigFixButton( - $msg("obsidianLiveSyncSettingTab.msgSetMaxDocSize"), - "couchdb/max_document_size", - "50000000" - ); - } else { - addResult($msg("obsidianLiveSyncSettingTab.okMaxDocumentSize")); - } - } - // CORS check - // checking connectivity for mobile - if (responseConfig?.cors?.credentials != "true") { - isSuccessful = false; - addResult($msg("obsidianLiveSyncSettingTab.errCorsCredentials")); - addConfigFixButton( - $msg("obsidianLiveSyncSettingTab.msgSetCorsCredentials"), - "cors/credentials", - "true" - ); - } else { - addResult($msg("obsidianLiveSyncSettingTab.okCorsCredentials")); - } - const ConfiguredOrigins = ((responseConfig?.cors?.origins ?? "") + "").split(","); - if ( - responseConfig?.cors?.origins == "*" || - (ConfiguredOrigins.indexOf("app://obsidian.md") !== -1 && - ConfiguredOrigins.indexOf("capacitor://localhost") !== -1 && - ConfiguredOrigins.indexOf("http://localhost") !== -1) - ) { - addResult($msg("obsidianLiveSyncSettingTab.okCorsOrigins")); - } else { - addResult($msg("obsidianLiveSyncSettingTab.errCorsOrigins")); - addConfigFixButton( - $msg("obsidianLiveSyncSettingTab.msgSetCorsOrigins"), - "cors/origins", - "app://obsidian.md,capacitor://localhost,http://localhost" - ); - isSuccessful = false; - } - addResult($msg("obsidianLiveSyncSettingTab.msgConnectionCheck"), ["ob-btn-config-head"]); - addResult($msg("obsidianLiveSyncSettingTab.msgCurrentOrigin", { origin: window.location.origin })); - - // Request header check - const origins = ["app://obsidian.md", "capacitor://localhost", "http://localhost"]; - for (const org of origins) { - const rr = await requestToCouchDBWithCredentials( - this.editingSettings.couchDB_URI, - credential, - org, - undefined, - undefined, - undefined, - customHeaders - ); - const responseHeaders = Object.fromEntries( - Object.entries(rr.headers).map((e) => { - e[0] = `${e[0]}`.toLowerCase(); - return e; - }) - ); - addResult($msg("obsidianLiveSyncSettingTab.msgOriginCheck", { org })); - if (responseHeaders["access-control-allow-credentials"] != "true") { - addResult($msg("obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials")); - isSuccessful = false; - } else { - addResult($msg("obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin")); - } - if (responseHeaders["access-control-allow-origin"] != org) { - addResult( - $msg("obsidianLiveSyncSettingTab.warnCorsOriginUnmatched", { - from: origin, - to: responseHeaders["access-control-allow-origin"], - }) - ); - } else { - addResult($msg("obsidianLiveSyncSettingTab.okCorsOriginMatched")); - } - } - addResult($msg("obsidianLiveSyncSettingTab.msgDone"), ["ob-btn-config-head"]); - addResult($msg("obsidianLiveSyncSettingTab.msgConnectionProxyNote"), ["ob-btn-config-info"]); - Logger($msg("obsidianLiveSyncSettingTab.logCheckingConfigDone"), LOG_LEVEL_INFO); - } catch (ex: any) { - if (ex?.status == 401) { - isSuccessful = false; - addResult($msg("obsidianLiveSyncSettingTab.errAccessForbidden")); - addResult($msg("obsidianLiveSyncSettingTab.errCannotContinueTest")); - Logger($msg("obsidianLiveSyncSettingTab.logCheckingConfigDone"), LOG_LEVEL_INFO); - } else { - Logger($msg("obsidianLiveSyncSettingTab.logCheckingConfigFailed"), LOG_LEVEL_NOTICE); - Logger(ex); - isSuccessful = false; - } - } - return isSuccessful; + return callback; }; + void addPane(containerEl, $msg("obsidianLiveSyncSettingTab.panelChangeLog"), "💬", 100, false).then( + bindPane(paneChangeLog) + ); + void addPane(containerEl, $msg("obsidianLiveSyncSettingTab.panelSetup"), "🧙‍♂️", 110, false).then( + bindPane(paneSetup) + ); + void addPane(containerEl, $msg("obsidianLiveSyncSettingTab.panelGeneralSettings"), "⚙️", 20, false).then( + bindPane(paneGeneral) + ); void addPane(containerEl, $msg("obsidianLiveSyncSettingTab.panelRemoteConfiguration"), "🛰️", 0, false).then( - (paneEl) => { - void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleRemoteServer")).then((paneEl) => { - // const containerRemoteDatabaseEl = containerEl.createDiv(); - this.createEl( - paneEl, - "div", - { - text: $msg("obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync"), - }, - undefined, - visibleOnly(() => isAnySyncEnabled()) - ).addClass("op-warn-info"); - new Setting(paneEl).autoWireDropDown("remoteType", { - holdValue: true, - options: { - [REMOTE_COUCHDB]: $msg("obsidianLiveSyncSettingTab.optionCouchDB"), - [REMOTE_MINIO]: $msg("obsidianLiveSyncSettingTab.optionMinioS3R2"), - [REMOTE_P2P]: "Only Peer-to-Peer", - }, - onUpdate: enableOnlySyncDisabled, - }); - void addPanel(paneEl, "Peer-to-Peer", undefined, onlyOnOnlyP2P).then((paneEl) => { - const syncWarnP2P = this.createEl(paneEl, "div", { - text: "", - }); - const p2pMessage = `This feature is a Work In Progress, and configurable on \`P2P Replicator\` Pane. -The pane also can be launched by \`P2P Replicator\` command from the Command Palette. -`; - - void MarkdownRenderer.render(this.plugin.app, p2pMessage, syncWarnP2P, "/", this.plugin); - syncWarnP2P.addClass("op-warn-info"); - new Setting(paneEl) - .setName("Apply Settings") - .setClass("wizardHidden") - .addApplyButton(["remoteType"]); - // .addOnUpdate(onlyOnMinIO); - // new Setting(paneEl).addButton((button) => - // button - // .setButtonText("Open P2P Replicator") - // .onClick(() => { - // const addOn = this.plugin.getAddOn(P2PReplicator.name); - // void addOn?.openPane(); - // this.closeSetting(); - // }) - // ); - }); - void addPanel( - paneEl, - $msg("obsidianLiveSyncSettingTab.titleMinioS3R2"), - undefined, - onlyOnMinIO - ).then((paneEl) => { - const syncWarnMinio = this.createEl(paneEl, "div", { - text: "", - }); - const ObjectStorageMessage = $msg("obsidianLiveSyncSettingTab.msgObjectStorageWarning"); - - void MarkdownRenderer.render( - this.plugin.app, - ObjectStorageMessage, - syncWarnMinio, - "/", - this.plugin - ); - syncWarnMinio.addClass("op-warn-info"); - - new Setting(paneEl).autoWireText("endpoint", { holdValue: true }); - new Setting(paneEl).autoWireText("accessKey", { holdValue: true }); - - new Setting(paneEl).autoWireText("secretKey", { - holdValue: true, - isPassword: true, - }); - - new Setting(paneEl).autoWireText("region", { holdValue: true }); - - new Setting(paneEl).autoWireText("bucket", { holdValue: true }); - new Setting(paneEl).autoWireText("bucketPrefix", { - holdValue: true, - placeHolder: "vaultname/", - }); - - new Setting(paneEl).autoWireToggle("useCustomRequestHandler", { holdValue: true }); - new Setting(paneEl).autoWireTextArea("bucketCustomHeaders", { - holdValue: true, - placeHolder: "x-custom-header: value\n x-custom-header2: value2", - }); - new Setting(paneEl) - .setName($msg("obsidianLiveSyncSettingTab.nameTestConnection")) - .addButton((button) => - button - .setButtonText($msg("obsidianLiveSyncSettingTab.btnTest")) - .setDisabled(false) - .onClick(async () => { - await this.testConnection(this.editingSettings); - }) - ); - new Setting(paneEl) - .setName($msg("obsidianLiveSyncSettingTab.nameApplySettings")) - .setClass("wizardHidden") - .addApplyButton([ - "remoteType", - "endpoint", - "region", - "accessKey", - "secretKey", - "bucket", - "useCustomRequestHandler", - "bucketCustomHeaders", - "bucketPrefix", - ]) - .addOnUpdate(onlyOnMinIO); - }); - - void addPanel( - paneEl, - $msg("obsidianLiveSyncSettingTab.titleCouchDB"), - undefined, - onlyOnCouchDB - ).then((paneEl) => { - if (this.plugin.$$isMobile()) { - this.createEl( - paneEl, - "div", - { - text: $msg("obsidianLiveSyncSettingTab.msgNonHTTPSWarning"), - }, - undefined, - visibleOnly(() => !this.editingSettings.couchDB_URI.startsWith("https://")) - ).addClass("op-warn"); - } else { - this.createEl( - paneEl, - "div", - { - text: $msg("obsidianLiveSyncSettingTab.msgNonHTTPSInfo"), - }, - undefined, - visibleOnly(() => !this.editingSettings.couchDB_URI.startsWith("https://")) - ).addClass("op-warn-info"); - } - - new Setting(paneEl).autoWireText("couchDB_URI", { - holdValue: true, - onUpdate: enableOnlySyncDisabled, - }); - new Setting(paneEl).autoWireToggle("useJWT", { - holdValue: true, - onUpdate: enableOnlySyncDisabled, - }); - new Setting(paneEl).autoWireText("couchDB_USER", { - holdValue: true, - onUpdate: combineOnUpdate( - enableOnlySyncDisabled, - visibleOnly(() => !this.editingSettings.useJWT) - ), - }); - new Setting(paneEl).autoWireText("couchDB_PASSWORD", { - holdValue: true, - isPassword: true, - onUpdate: combineOnUpdate( - enableOnlySyncDisabled, - visibleOnly(() => !this.editingSettings.useJWT) - ), - }); - const algorithms = { - ["HS256"]: "HS256", - ["HS512"]: "HS512", - ["ES256"]: "ES256", - ["ES512"]: "ES512", - } as const; - new Setting(paneEl).autoWireDropDown("jwtAlgorithm", { - options: algorithms, - onUpdate: combineOnUpdate( - enableOnlySyncDisabled, - visibleOnly(() => this.editingSettings.useJWT) - ), - }); - new Setting(paneEl).autoWireTextArea("jwtKey", { - holdValue: true, - onUpdate: combineOnUpdate( - enableOnlySyncDisabled, - visibleOnly(() => this.editingSettings.useJWT) - ), - }); - // eslint-disable-next-line prefer-const - let generatedKeyDivEl: HTMLDivElement; - new Setting(paneEl) - .setDesc("Generate ES256 Keypair for testing") - .addButton((button) => - button.setButtonText("Generate").onClick(async () => { - const crypto = await getWebCrypto(); - const keyPair = await crypto.subtle.generateKey( - { name: "ECDSA", namedCurve: "P-256" }, - true, - ["sign", "verify"] - ); - const pubKey = await crypto.subtle.exportKey("spki", keyPair.publicKey); - const privateKey = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey); - const encodedPublicKey = await arrayBufferToBase64Single(pubKey); - const encodedPrivateKey = await arrayBufferToBase64Single(privateKey); - - const privateKeyPem = `> -----BEGIN PRIVATE KEY-----\n> ${encodedPrivateKey}\n> -----END PRIVATE KEY-----`; - const publicKeyPem = `> -----BEGIN PUBLIC KEY-----\\n${encodedPublicKey}\\n-----END PUBLIC KEY-----`; - - const title = $msg("Setting.GenerateKeyPair.Title"); - const msg = $msg("Setting.GenerateKeyPair.Desc", { - public_key: publicKeyPem, - private_key: privateKeyPem, - }); - await MarkdownRenderer.render( - this.plugin.app, - "## " + title + "\n\n" + msg, - generatedKeyDivEl, - "/", - this.plugin - ); - }) - ) - .addOnUpdate( - combineOnUpdate( - enableOnlySyncDisabled, - visibleOnly(() => this.editingSettings.useJWT) - ) - ); - generatedKeyDivEl = this.createEl( - paneEl, - "div", - { text: "" }, - (el) => {}, - visibleOnly(() => this.editingSettings.useJWT) - ); - - new Setting(paneEl).autoWireText("jwtKid", { - holdValue: true, - onUpdate: combineOnUpdate( - enableOnlySyncDisabled, - visibleOnly(() => this.editingSettings.useJWT) - ), - }); - new Setting(paneEl).autoWireText("jwtSub", { - holdValue: true, - onUpdate: combineOnUpdate( - enableOnlySyncDisabled, - visibleOnly(() => this.editingSettings.useJWT) - ), - }); - new Setting(paneEl).autoWireNumeric("jwtExpDuration", { - holdValue: true, - onUpdate: combineOnUpdate( - enableOnlySyncDisabled, - visibleOnly(() => this.editingSettings.useJWT) - ), - }); - new Setting(paneEl).autoWireText("couchDB_DBNAME", { - holdValue: true, - onUpdate: enableOnlySyncDisabled, - }); - new Setting(paneEl).autoWireTextArea("couchDB_CustomHeaders", { holdValue: true }); - new Setting(paneEl).autoWireToggle("useRequestAPI", { - holdValue: true, - onUpdate: enableOnlySyncDisabled, - }); - new Setting(paneEl) - .setName($msg("obsidianLiveSyncSettingTab.nameTestDatabaseConnection")) - .setClass("wizardHidden") - .setDesc($msg("obsidianLiveSyncSettingTab.descTestDatabaseConnection")) - .addButton((button) => - button - .setButtonText($msg("obsidianLiveSyncSettingTab.btnTest")) - .setDisabled(false) - .onClick(async () => { - await this.testConnection(); - }) - ); - - new Setting(paneEl) - .setName($msg("obsidianLiveSyncSettingTab.nameValidateDatabaseConfig")) - .setDesc($msg("obsidianLiveSyncSettingTab.descValidateDatabaseConfig")) - .addButton((button) => - button - .setButtonText($msg("obsidianLiveSyncSettingTab.btnCheck")) - .setDisabled(false) - .onClick(async () => { - await checkConfig(checkResultDiv); - }) - ); - checkResultDiv = this.createEl(paneEl, "div", { - text: "", - }); - - new Setting(paneEl) - .setName($msg("obsidianLiveSyncSettingTab.nameApplySettings")) - .setClass("wizardHidden") - .addApplyButton([ - "remoteType", - "couchDB_URI", - "couchDB_USER", - "couchDB_PASSWORD", - "couchDB_DBNAME", - "jwtAlgorithm", - "jwtExpDuration", - "jwtKey", - "jwtSub", - "jwtKid", - "useJWT", - "couchDB_CustomHeaders", - "useRequestAPI", - ]) - .addOnUpdate(onlyOnCouchDB); - }); - }); - void addPanel( - paneEl, - $msg("obsidianLiveSyncSettingTab.titleNotification"), - () => {}, - onlyOnCouchDB - ).then((paneEl) => { - paneEl.addClass("wizardHidden"); - new Setting(paneEl) - .autoWireNumeric("notifyThresholdOfRemoteStorageSize", {}) - .setClass("wizardHidden"); - }); - - void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.panelPrivacyEncryption")).then((paneEl) => { - new Setting(paneEl).autoWireToggle("encrypt", { holdValue: true }); - - const isEncryptEnabled = visibleOnly(() => this.isConfiguredAs("encrypt", true)); - - new Setting(paneEl).autoWireText("passphrase", { - holdValue: true, - isPassword: true, - onUpdate: isEncryptEnabled, - }); - - new Setting(paneEl).autoWireToggle("usePathObfuscation", { - holdValue: true, - onUpdate: isEncryptEnabled, - }); - new Setting(paneEl) - .autoWireToggle("useDynamicIterationCount", { - holdValue: true, - onUpdate: isEncryptEnabled, - }) - .setClass("wizardHidden"); - }); - - void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleFetchSettings")).then((paneEl) => { - new Setting(paneEl) - .setName($msg("obsidianLiveSyncSettingTab.titleFetchConfigFromRemote")) - .setDesc($msg("obsidianLiveSyncSettingTab.descFetchConfigFromRemote")) - .addButton((button) => - button - .setButtonText($msg("obsidianLiveSyncSettingTab.buttonFetch")) - .setDisabled(false) - .onClick(async () => { - const trialSetting = { ...this.initialSettings, ...this.editingSettings }; - const newTweaks = - await this.plugin.$$checkAndAskUseRemoteConfiguration(trialSetting); - if (newTweaks.result !== false) { - if (this.inWizard) { - this.editingSettings = { ...this.editingSettings, ...newTweaks.result }; - this.requestUpdate(); - return; - } else { - this.closeSetting(); - this.plugin.settings = { ...this.plugin.settings, ...newTweaks.result }; - if (newTweaks.requireFetch) { - if ( - (await this.plugin.confirm.askYesNoDialog( - $msg("SettingTab.Message.AskRebuild"), - { - defaultOption: "Yes", - } - )) == "no" - ) { - await this.plugin.$$saveSettingData(); - return; - } - await this.plugin.$$saveSettingData(); - await this.plugin.rebuilder.scheduleFetch(); - await this.plugin.$$scheduleAppReload(); - return; - } else { - await this.plugin.$$saveSettingData(); - } - } - } - }) - ); - }); - new Setting(paneEl).setClass("wizardOnly").addButton((button) => - button - .setButtonText($msg("obsidianLiveSyncSettingTab.buttonNext")) - .setCta() - .setDisabled(false) - .onClick(async () => { - if (!(await checkConfig(checkResultDiv))) { - if ( - (await this.plugin.confirm.askYesNoDialog( - $msg("obsidianLiveSyncSettingTab.msgConfigCheckFailed"), - { - defaultOption: "No", - title: $msg("obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed"), - } - )) == "no" - ) { - return; - } - } - const isEncryptionFullyEnabled = - !this.editingSettings.encrypt || !this.editingSettings.usePathObfuscation; - if (isEncryptionFullyEnabled) { - if ( - (await this.plugin.confirm.askYesNoDialog( - $msg("obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation"), - { - defaultOption: "No", - title: $msg("obsidianLiveSyncSettingTab.titleEncryptionNotEnabled"), - } - )) == "no" - ) { - return; - } - } - if (!this.editingSettings.encrypt) { - this.editingSettings.passphrase = ""; - } - if (!(await isPassphraseValid())) { - if ( - (await this.plugin.confirm.askYesNoDialog( - $msg("obsidianLiveSyncSettingTab.msgInvalidPassphrase"), - { - defaultOption: "No", - title: $msg("obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid"), - } - )) == "no" - ) { - return; - } - } - if (isCloudantURI(this.editingSettings.couchDB_URI)) { - this.editingSettings = { ...this.editingSettings, ...PREFERRED_SETTING_CLOUDANT }; - } else if (this.editingSettings.remoteType == REMOTE_MINIO) { - this.editingSettings = { ...this.editingSettings, ...PREFERRED_JOURNAL_SYNC }; - } else { - this.editingSettings = { ...this.editingSettings, ...PREFERRED_SETTING_SELF_HOSTED }; - } - if ( - (await this.plugin.confirm.askYesNoDialog( - $msg("obsidianLiveSyncSettingTab.msgFetchConfigFromRemote"), - { defaultOption: "Yes", title: $msg("obsidianLiveSyncSettingTab.titleFetchConfig") } - )) == "yes" - ) { - const trialSetting = { ...this.initialSettings, ...this.editingSettings }; - const newTweaks = await this.plugin.$$checkAndAskUseRemoteConfiguration(trialSetting); - if (newTweaks.result !== false) { - this.editingSettings = { ...this.editingSettings, ...newTweaks.result }; - this.requestUpdate(); - } else { - // Messages should be already shown. - } - } - changeDisplay("30"); - }) - ); - } + bindPane(paneRemoteConfig) ); void addPane(containerEl, $msg("obsidianLiveSyncSettingTab.titleSyncSettings"), "🔄", 30, false).then( - (paneEl) => { - if (this.editingSettings.versionUpFlash != "") { - const c = this.createEl( - paneEl, - "div", - { - text: this.editingSettings.versionUpFlash, - cls: "op-warn sls-setting-hidden", - }, - (el) => { - this.createEl( - el, - "button", - { text: $msg("obsidianLiveSyncSettingTab.btnGotItAndUpdated") }, - (e) => { - e.addClass("mod-cta"); - e.addEventListener("click", () => { - fireAndForget(async () => { - this.editingSettings.versionUpFlash = ""; - await this.saveAllDirtySettings(); - c.remove(); - }); - }); - } - ); - }, - visibleOnly(() => !this.isConfiguredAs("versionUpFlash", "")) - ); - } - - this.createEl(paneEl, "div", { - text: $msg("obsidianLiveSyncSettingTab.msgSelectAndApplyPreset"), - cls: "wizardOnly", - }).addClasses(["op-warn-info"]); - - void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleSynchronizationPreset")).then((paneEl) => { - const options: Record = - this.editingSettings.remoteType == REMOTE_COUCHDB - ? { - NONE: "", - LIVESYNC: $msg("obsidianLiveSyncSettingTab.optionLiveSync"), - PERIODIC: $msg("obsidianLiveSyncSettingTab.optionPeriodicWithBatch"), - DISABLE: $msg("obsidianLiveSyncSettingTab.optionDisableAllAutomatic"), - } - : { - NONE: "", - PERIODIC: $msg("obsidianLiveSyncSettingTab.optionPeriodicWithBatch"), - DISABLE: $msg("obsidianLiveSyncSettingTab.optionDisableAllAutomatic"), - }; - - new Setting(paneEl) - .autoWireDropDown("preset", { - options: options, - holdValue: true, - }) - .addButton((button) => { - button.setButtonText($msg("obsidianLiveSyncSettingTab.btnApply")); - button.onClick(async () => { - // await this.saveSettings(["preset"]); - await this.saveAllDirtySettings(); - }); - }); - - this.addOnSaved("preset", async (currentPreset) => { - if (currentPreset == "") { - Logger($msg("obsidianLiveSyncSettingTab.logSelectAnyPreset"), LOG_LEVEL_NOTICE); - return; - } - const presetAllDisabled = { - batchSave: false, - liveSync: false, - periodicReplication: false, - syncOnSave: false, - syncOnEditorSave: false, - syncOnStart: false, - syncOnFileOpen: false, - syncAfterMerge: false, - } as Partial; - const presetLiveSync = { - ...presetAllDisabled, - liveSync: true, - } as Partial; - const presetPeriodic = { - ...presetAllDisabled, - batchSave: true, - periodicReplication: true, - syncOnSave: false, - syncOnEditorSave: false, - syncOnStart: true, - syncOnFileOpen: true, - syncAfterMerge: true, - } as Partial; - - if (currentPreset == "LIVESYNC") { - this.editingSettings = { - ...this.editingSettings, - ...presetLiveSync, - }; - Logger($msg("obsidianLiveSyncSettingTab.logConfiguredLiveSync"), LOG_LEVEL_NOTICE); - } else if (currentPreset == "PERIODIC") { - this.editingSettings = { - ...this.editingSettings, - ...presetPeriodic, - }; - Logger($msg("obsidianLiveSyncSettingTab.logConfiguredPeriodic"), LOG_LEVEL_NOTICE); - } else { - Logger($msg("obsidianLiveSyncSettingTab.logConfiguredDisabled"), LOG_LEVEL_NOTICE); - this.editingSettings = { - ...this.editingSettings, - ...presetAllDisabled, - }; - } - - if (this.inWizard) { - this.closeSetting(); - this.inWizard = false; - if (!this.editingSettings.isConfigured) { - this.editingSettings.isConfigured = true; - await this.saveAllDirtySettings(); - await this.plugin.$$realizeSettingSyncMode(); - await rebuildDB("localOnly"); - // this.resetEditingSettings(); - if ( - (await this.plugin.confirm.askYesNoDialog( - $msg("obsidianLiveSyncSettingTab.msgGenerateSetupURI"), - { - defaultOption: "Yes", - title: $msg("obsidianLiveSyncSettingTab.titleCongratulations"), - } - )) == "yes" - ) { - eventHub.emitEvent(EVENT_REQUEST_COPY_SETUP_URI); - } - } else { - if (isNeedRebuildLocal() || isNeedRebuildRemote()) { - await confirmRebuild(); - } else { - await this.saveAllDirtySettings(); - await this.plugin.$$realizeSettingSyncMode(); - this.plugin.$$askReload(); - } - } - } else { - await this.saveAllDirtySettings(); - await this.plugin.$$realizeSettingSyncMode(); - } - }); - }); - void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleSynchronizationMethod")).then((paneEl) => { - paneEl.addClass("wizardHidden"); - - // const onlyOnLiveSync = visibleOnly(() => this.isConfiguredAs("syncMode", "LIVESYNC")); - const onlyOnNonLiveSync = visibleOnly(() => !this.isConfiguredAs("syncMode", "LIVESYNC")); - const onlyOnPeriodic = visibleOnly(() => this.isConfiguredAs("syncMode", "PERIODIC")); - - const optionsSyncMode = - this.editingSettings.remoteType == REMOTE_COUCHDB - ? { - ONEVENTS: $msg("obsidianLiveSyncSettingTab.optionOnEvents"), - PERIODIC: $msg("obsidianLiveSyncSettingTab.optionPeriodicAndEvents"), - LIVESYNC: $msg("obsidianLiveSyncSettingTab.optionLiveSync"), - } - : { - ONEVENTS: $msg("obsidianLiveSyncSettingTab.optionOnEvents"), - PERIODIC: $msg("obsidianLiveSyncSettingTab.optionPeriodicAndEvents"), - }; - - new Setting(paneEl) - .autoWireDropDown("syncMode", { - //@ts-ignore - options: optionsSyncMode, - }) - .setClass("wizardHidden"); - this.addOnSaved("syncMode", async (value) => { - this.editingSettings.liveSync = false; - this.editingSettings.periodicReplication = false; - if (value == "LIVESYNC") { - this.editingSettings.liveSync = true; - } else if (value == "PERIODIC") { - this.editingSettings.periodicReplication = true; - } - await this.saveSettings(["liveSync", "periodicReplication"]); - - await this.plugin.$$realizeSettingSyncMode(); - }); - - new Setting(paneEl) - .autoWireNumeric("periodicReplicationInterval", { - clampMax: 5000, - onUpdate: onlyOnPeriodic, - }) - .setClass("wizardHidden"); - - new Setting(paneEl).autoWireNumeric("syncMinimumInterval", { - onUpdate: onlyOnNonLiveSync, - }); - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("syncOnSave", { onUpdate: onlyOnNonLiveSync }); - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("syncOnEditorSave", { onUpdate: onlyOnNonLiveSync }); - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("syncOnFileOpen", { onUpdate: onlyOnNonLiveSync }); - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("syncOnStart", { onUpdate: onlyOnNonLiveSync }); - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("syncAfterMerge", { onUpdate: onlyOnNonLiveSync }); - }); - - void addPanel( - paneEl, - $msg("obsidianLiveSyncSettingTab.titleUpdateThinning"), - undefined, - visibleOnly(() => !this.isConfiguredAs("syncMode", "LIVESYNC")) - ).then((paneEl) => { - paneEl.addClass("wizardHidden"); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("batchSave"); - new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("batchSaveMinimumDelay", { - acceptZero: true, - onUpdate: visibleOnly(() => this.isConfiguredAs("batchSave", true)), - }); - new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("batchSaveMaximumDelay", { - acceptZero: true, - onUpdate: visibleOnly(() => this.isConfiguredAs("batchSave", true)), - }); - }); - - void addPanel( - paneEl, - $msg("obsidianLiveSyncSettingTab.titleDeletionPropagation"), - undefined, - undefined, - LEVEL_ADVANCED - ).then((paneEl) => { - paneEl.addClass("wizardHidden"); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("trashInsteadDelete"); - - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("doNotDeleteFolder"); - }); - void addPanel( - paneEl, - $msg("obsidianLiveSyncSettingTab.titleConflictResolution"), - undefined, - undefined, - LEVEL_ADVANCED - ).then((paneEl) => { - paneEl.addClass("wizardHidden"); - - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("resolveConflictsByNewerFile"); - - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("checkConflictOnlyOnOpen"); - - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("showMergeDialogOnlyOnActive"); - }); - - void addPanel( - paneEl, - $msg("obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown"), - undefined, - undefined, - LEVEL_ADVANCED - ).then((paneEl) => { - paneEl.addClass("wizardHidden"); - new Setting(paneEl) - .autoWireText("settingSyncFile", { holdValue: true }) - .addApplyButton(["settingSyncFile"]); - - new Setting(paneEl).autoWireToggle("writeCredentialsForSettingSync"); - - new Setting(paneEl).autoWireToggle("notifyAllSettingSyncFile"); - }); - - void addPanel( - paneEl, - $msg("obsidianLiveSyncSettingTab.titleHiddenFiles"), - undefined, - undefined, - LEVEL_ADVANCED - ).then((paneEl) => { - paneEl.addClass("wizardHidden"); - - const LABEL_ENABLED = $msg("obsidianLiveSyncSettingTab.labelEnabled"); - const LABEL_DISABLED = $msg("obsidianLiveSyncSettingTab.labelDisabled"); - - const hiddenFileSyncSetting = new Setting(paneEl) - .setName($msg("obsidianLiveSyncSettingTab.nameHiddenFileSynchronization")) - .setClass("wizardHidden"); - const hiddenFileSyncSettingEl = hiddenFileSyncSetting.settingEl; - const hiddenFileSyncSettingDiv = hiddenFileSyncSettingEl.createDiv(""); - hiddenFileSyncSettingDiv.innerText = this.editingSettings.syncInternalFiles - ? LABEL_ENABLED - : LABEL_DISABLED; - if (this.editingSettings.syncInternalFiles) { - new Setting(paneEl) - .setName($msg("obsidianLiveSyncSettingTab.nameDisableHiddenFileSync")) - .setClass("wizardHidden") - .addButton((button) => { - button - .setButtonText($msg("obsidianLiveSyncSettingTab.btnDisable")) - .onClick(async () => { - this.editingSettings.syncInternalFiles = false; - await this.saveAllDirtySettings(); - this.display(); - }); - }); - } else { - new Setting(paneEl) - .setName($msg("obsidianLiveSyncSettingTab.nameEnableHiddenFileSync")) - .setClass("wizardHidden") - .addButton((button) => { - button.setButtonText("Merge").onClick(async () => { - this.closeSetting(); - // this.resetEditingSettings(); - await this.plugin.$anyConfigureOptionalSyncFeature("MERGE"); - }); - }) - .addButton((button) => { - button.setButtonText("Fetch").onClick(async () => { - this.closeSetting(); - // this.resetEditingSettings(); - await this.plugin.$anyConfigureOptionalSyncFeature("FETCH"); - }); - }) - .addButton((button) => { - button.setButtonText("Overwrite").onClick(async () => { - this.closeSetting(); - // this.resetEditingSettings(); - await this.plugin.$anyConfigureOptionalSyncFeature("OVERWRITE"); - }); - }); - } - - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("suppressNotifyHiddenFilesChange", {}); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncInternalFilesBeforeReplication", { - onUpdate: visibleOnly(() => this.isConfiguredAs("watchInternalFileChanges", true)), - }); - - new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("syncInternalFilesInterval", { - clampMin: 10, - acceptZero: true, - }); - }); - } + bindPane(paneSyncSettings) + ); + void addPane(containerEl, "Selector", "🚦", 33, false, LEVEL_ADVANCED).then(bindPane(paneSelector)); + void addPane(containerEl, "Customization sync", "🔌", 60, false, LEVEL_ADVANCED).then( + bindPane(paneCustomisationSync) ); - void addPane(containerEl, "Selector", "🚦", 33, false, LEVEL_ADVANCED).then((paneEl) => { - void addPanel(paneEl, "Normal Files").then((paneEl) => { - paneEl.addClass("wizardHidden"); - const syncFilesSetting = new Setting(paneEl) - .setName("Synchronising files") - .setDesc( - "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files." - ) - .setClass("wizardHidden"); - mount(MultipleRegExpControl, { - target: syncFilesSetting.controlEl, - props: { - patterns: splitCustomRegExpList(this.editingSettings.syncOnlyRegEx, "|[]|"), - originals: splitCustomRegExpList(this.editingSettings.syncOnlyRegEx, "|[]|"), - apply: async (newPatterns: CustomRegExpSource[]) => { - this.editingSettings.syncOnlyRegEx = constructCustomRegExpList(newPatterns, "|[]|"); - await this.saveAllDirtySettings(); - this.display(); - }, - }, - }); + void addPane(containerEl, "Hatch", "🧰", 50, true).then(bindPane(paneHatch)); + void addPane(containerEl, "Advanced", "🔧", 46, false, LEVEL_ADVANCED).then(bindPane(paneAdvanced)); + void addPane(containerEl, "Power users", "💪", 47, true, LEVEL_POWER_USER).then(bindPane(panePowerUsers)); - const nonSyncFilesSetting = new Setting(paneEl) - .setName("Non-Synchronising files") - .setDesc( - "(RegExp) If this is set, any changes to local and remote files that match this will be skipped." - ) - .setClass("wizardHidden"); + void addPane(containerEl, "Patches", "🩹", 51, false, LEVEL_EDGE_CASE).then(bindPane(panePatches)); - mount(MultipleRegExpControl, { - target: nonSyncFilesSetting.controlEl, - props: { - patterns: splitCustomRegExpList(this.editingSettings.syncIgnoreRegEx, "|[]|"), - originals: splitCustomRegExpList(this.editingSettings.syncIgnoreRegEx, "|[]|"), - apply: async (newPatterns: CustomRegExpSource[]) => { - this.editingSettings.syncIgnoreRegEx = constructCustomRegExpList(newPatterns, "|[]|"); - await this.saveAllDirtySettings(); - this.display(); - }, - }, - }); - new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("syncMaxSizeInMB", { clampMin: 0 }); + void addPane(containerEl, "Maintenance", "🎛️", 70, true).then(bindPane(paneMaintenance)); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("useIgnoreFiles"); - new Setting(paneEl).setClass("wizardHidden").autoWireTextArea("ignoreFiles", { - onUpdate: visibleOnly(() => this.isConfiguredAs("useIgnoreFiles", true)), - }); - }); - void addPanel(paneEl, "Hidden Files", undefined, undefined, LEVEL_ADVANCED).then((paneEl) => { - const targetPatternSetting = new Setting(paneEl) - .setName("Target patterns") - .setClass("wizardHidden") - .setDesc("Patterns to match files for syncing"); - const patTarget = splitCustomRegExpList(this.editingSettings.syncInternalFilesTargetPatterns, ","); - mount(MultipleRegExpControl, { - target: targetPatternSetting.controlEl, - props: { - patterns: patTarget, - originals: [...patTarget], - apply: async (newPatterns: CustomRegExpSource[]) => { - this.editingSettings.syncInternalFilesTargetPatterns = constructCustomRegExpList( - newPatterns, - "," - ); - await this.saveAllDirtySettings(); - this.display(); - }, - }, - }); - - const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, ^\\.git\\/, \\/obsidian-livesync\\/"; - const defaultSkipPatternXPlat = - defaultSkipPattern + ",\\/workspace$ ,\\/workspace.json$,\\/workspace-mobile.json$"; - - const pat = splitCustomRegExpList(this.editingSettings.syncInternalFilesIgnorePatterns, ","); - const patSetting = new Setting(paneEl).setName("Ignore patterns").setClass("wizardHidden").setDesc(""); - - mount(MultipleRegExpControl, { - target: patSetting.controlEl, - props: { - patterns: pat, - originals: [...pat], - apply: async (newPatterns: CustomRegExpSource[]) => { - this.editingSettings.syncInternalFilesIgnorePatterns = constructCustomRegExpList( - newPatterns, - "," - ); - await this.saveAllDirtySettings(); - this.display(); - }, - }, - }); - - const addDefaultPatterns = async (patterns: string) => { - const oldList = splitCustomRegExpList(this.editingSettings.syncInternalFilesIgnorePatterns, ","); - const newList = splitCustomRegExpList( - patterns as unknown as typeof this.editingSettings.syncInternalFilesIgnorePatterns, - "," - ); - const allSet = new Set([...oldList, ...newList]); - this.editingSettings.syncInternalFilesIgnorePatterns = constructCustomRegExpList([...allSet], ","); - await this.saveAllDirtySettings(); - this.display(); - }; - - new Setting(paneEl) - .setName("Add default patterns") - .setClass("wizardHidden") - .addButton((button) => { - button.setButtonText("Default").onClick(async () => { - await addDefaultPatterns(defaultSkipPattern); - }); - }) - .addButton((button) => { - button.setButtonText("Cross-platform").onClick(async () => { - await addDefaultPatterns(defaultSkipPatternXPlat); - }); - }); - }); - }); - - void addPane(containerEl, "Customization sync", "🔌", 60, false, LEVEL_ADVANCED).then((paneEl) => { - // With great respect, thank you TfTHacker! - // Refer: https://github.com/TfTHacker/obsidian42-brat/blob/main/src/features/BetaPlugins.ts - void addPanel(paneEl, "Customization Sync").then((paneEl) => { - const enableOnlyOnPluginSyncIsNotEnabled = enableOnly(() => - this.isConfiguredAs("usePluginSync", false) - ); - const visibleOnlyOnPluginSyncEnabled = visibleOnly(() => this.isConfiguredAs("usePluginSync", true)); - - this.createEl( - paneEl, - "div", - { - text: "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.", - cls: "op-warn", - }, - (c) => {}, - visibleOnly(() => this.isConfiguredAs("deviceAndVaultName", "")) - ); - this.createEl( - paneEl, - "div", - { - text: "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.", - cls: "op-warn-info", - }, - (c) => {}, - visibleOnly(() => this.isConfiguredAs("usePluginSync", true)) - ); - - new Setting(paneEl).autoWireText("deviceAndVaultName", { - placeHolder: "desktop", - onUpdate: enableOnlyOnPluginSyncIsNotEnabled, - }); - - new Setting(paneEl).autoWireToggle("usePluginSyncV2"); - - new Setting(paneEl).autoWireToggle("usePluginSync", { - onUpdate: enableOnly(() => !this.isConfiguredAs("deviceAndVaultName", "")), - }); - - new Setting(paneEl).autoWireToggle("autoSweepPlugins", { - onUpdate: visibleOnlyOnPluginSyncEnabled, - }); - - new Setting(paneEl).autoWireToggle("autoSweepPluginsPeriodic", { - onUpdate: visibleOnly( - () => - this.isConfiguredAs("usePluginSync", true) && this.isConfiguredAs("autoSweepPlugins", true) - ), - }); - new Setting(paneEl).autoWireToggle("notifyPluginOrSettingUpdated", { - onUpdate: visibleOnlyOnPluginSyncEnabled, - }); - - new Setting(paneEl) - .setName("Open") - .setDesc("Open the dialog") - .addButton((button) => { - button - .setButtonText("Open") - .setDisabled(false) - .onClick(() => { - // this.plugin.getAddOn(ConfigSync.name)?.showPluginSyncModal(); - // this.plugin.addOnConfigSync.showPluginSyncModal(); - eventHub.emitEvent(EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG); - }); - }) - .addOnUpdate(visibleOnlyOnPluginSyncEnabled); - }); - }); - - void addPane(containerEl, "Hatch", "🧰", 50, true).then((paneEl) => { - // const hatchWarn = this.createEl(paneEl, "div", { text: `To stop the boot up sequence for fixing problems on databases, you can put redflag.md on top of your vault (Rebooting obsidian is required).` }); - // hatchWarn.addClass("op-warn-info"); - void addPanel(paneEl, $msg("Setting.TroubleShooting")).then((paneEl) => { - new Setting(paneEl) - .setName($msg("Setting.TroubleShooting.Doctor")) - .setDesc($msg("Setting.TroubleShooting.Doctor.Desc")) - .addButton((button) => - button - .setButtonText("Run Doctor") - .setCta() - .setDisabled(false) - .onClick(() => { - this.closeSetting(); - eventHub.emitEvent(EVENT_REQUEST_RUN_DOCTOR, "you wanted(Thank you)!"); - }) - ); - new Setting(paneEl).setName("Prepare the 'report' to create an issue").addButton((button) => - button - .setButtonText("Copy Report to clipboard") - .setCta() - .setDisabled(false) - .onClick(async () => { - let responseConfig: any = {}; - const REDACTED = "𝑅𝐸𝐷𝐴𝐶𝑇𝐸𝐷"; - if (this.editingSettings.remoteType == REMOTE_COUCHDB) { - try { - const credential = generateCredentialObject(this.editingSettings); - const customHeaders = parseHeaderValues(this.editingSettings.couchDB_CustomHeaders); - const r = await requestToCouchDBWithCredentials( - this.editingSettings.couchDB_URI, - credential, - window.origin, - undefined, - undefined, - undefined, - customHeaders - ); - - Logger(JSON.stringify(r.json, null, 2)); - - responseConfig = r.json; - responseConfig["couch_httpd_auth"].secret = REDACTED; - responseConfig["couch_httpd_auth"].authentication_db = REDACTED; - responseConfig["couch_httpd_auth"].authentication_redirect = REDACTED; - responseConfig["couchdb"].uuid = REDACTED; - responseConfig["admins"] = REDACTED; - } catch (ex) { - Logger(ex, LOG_LEVEL_VERBOSE); - responseConfig = - "Requesting information from the remote CouchDB has failed. If you are using IBM Cloudant, this is normal behaviour."; - } - } else if (this.editingSettings.remoteType == REMOTE_MINIO) { - responseConfig = "Object Storage Synchronisation"; - // - } - const pluginConfig = JSON.parse( - JSON.stringify(this.editingSettings) - ) as ObsidianLiveSyncSettings; - pluginConfig.couchDB_DBNAME = REDACTED; - pluginConfig.couchDB_PASSWORD = REDACTED; - const scheme = pluginConfig.couchDB_URI.startsWith("http:") - ? "(HTTP)" - : pluginConfig.couchDB_URI.startsWith("https:") - ? "(HTTPS)" - : ""; - pluginConfig.couchDB_URI = isCloudantURI(pluginConfig.couchDB_URI) - ? "cloudant" - : `self-hosted${scheme}`; - pluginConfig.couchDB_USER = REDACTED; - pluginConfig.passphrase = REDACTED; - pluginConfig.encryptedPassphrase = REDACTED; - pluginConfig.encryptedCouchDBConnection = REDACTED; - pluginConfig.accessKey = REDACTED; - pluginConfig.secretKey = REDACTED; - const redact = (source: string) => `${REDACTED}(${source.length} letters)`; - pluginConfig.region = redact(pluginConfig.region); - pluginConfig.bucket = redact(pluginConfig.bucket); - pluginConfig.pluginSyncExtendedSetting = {}; - pluginConfig.P2P_AppID = redact(pluginConfig.P2P_AppID); - pluginConfig.P2P_passphrase = redact(pluginConfig.P2P_passphrase); - pluginConfig.P2P_roomID = redact(pluginConfig.P2P_roomID); - pluginConfig.P2P_relays = redact(pluginConfig.P2P_relays); - pluginConfig.jwtKey = redact(pluginConfig.jwtKey); - pluginConfig.jwtSub = redact(pluginConfig.jwtSub); - pluginConfig.jwtKid = redact(pluginConfig.jwtKid); - pluginConfig.bucketCustomHeaders = redact(pluginConfig.bucketCustomHeaders); - pluginConfig.couchDB_CustomHeaders = redact(pluginConfig.couchDB_CustomHeaders); - const endpoint = pluginConfig.endpoint; - if (endpoint == "") { - pluginConfig.endpoint = "Not configured or AWS"; - } else { - 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: ${navigator.userAgent} -FileSystem: ${this.plugin.$$isStorageInsensitive() ? "insensitive" : "sensitive"}`; - const msgConfig = `---- Obsidian info ---- -${obsidianInfo} ----- remote config ---- -${stringifyYaml(responseConfig)} ----- Plug-in config --- -version:${manifestVersion} -${stringifyYaml(pluginConfig)}`; - console.log(msgConfig); - await navigator.clipboard.writeText(msgConfig); - Logger( - `Generated report has been copied to clipboard. Please report the issue with this! Thank you for your cooperation!`, - LOG_LEVEL_NOTICE - ); - }) - ); - new Setting(paneEl).autoWireToggle("writeLogToTheFile"); - }); - - void addPanel(paneEl, "Scram Switches").then((paneEl) => { - new Setting(paneEl).autoWireToggle("suspendFileWatching"); - this.addOnSaved("suspendFileWatching", () => this.plugin.$$askReload()); - - new Setting(paneEl).autoWireToggle("suspendParseReplicationResult"); - this.addOnSaved("suspendParseReplicationResult", () => this.plugin.$$askReload()); - }); - - void addPanel(paneEl, "Recovery and Repair").then((paneEl) => { - const addResult = async ( - path: string, - file: FilePathWithPrefix | false, - fileOnDB: LoadedEntry | false - ) => { - const storageFileStat = file ? await this.plugin.storageAccess.statHidden(file) : null; - resultArea.appendChild( - this.createEl(resultArea, "div", {}, (el) => { - el.appendChild(this.createEl(el, "h6", { text: path })); - el.appendChild( - this.createEl(el, "div", {}, (infoGroupEl) => { - infoGroupEl.appendChild( - this.createEl(infoGroupEl, "div", { - text: `Storage : Modified: ${!storageFileStat ? `Missing:` : `${new Date(storageFileStat.mtime).toLocaleString()}, Size:${storageFileStat.size}`}`, - }) - ); - infoGroupEl.appendChild( - this.createEl(infoGroupEl, "div", { - text: `Database: Modified: ${!fileOnDB ? `Missing:` : `${new Date(fileOnDB.mtime).toLocaleString()}, Size:${fileOnDB.size}`}`, - }) - ); - }) - ); - if (fileOnDB && file) { - el.appendChild( - this.createEl(el, "button", { text: "Show history" }, (buttonEl) => { - buttonEl.onClickEvent(() => { - eventHub.emitEvent(EVENT_REQUEST_SHOW_HISTORY, { - file: file, - fileOnDB: fileOnDB, - }); - }); - }) - ); - } - if (file) { - el.appendChild( - this.createEl(el, "button", { text: "Storage -> Database" }, (buttonEl) => { - buttonEl.onClickEvent(async () => { - if (file.startsWith(".")) { - const addOn = this.plugin.getAddOn(HiddenFileSync.name); - if (addOn) { - const file = (await addOn.scanInternalFiles()).find( - (e) => e.path == path - ); - if (!file) { - Logger( - `Failed to find the file in the internal files: ${path}`, - LOG_LEVEL_NOTICE - ); - return; - } - if (!(await addOn.storeInternalFileToDatabase(file, true))) { - Logger( - `Failed to store the file to the database (Hidden file): ${file}`, - LOG_LEVEL_NOTICE - ); - return; - } - } - } else { - if ( - !(await this.plugin.fileHandler.storeFileToDB( - file as FilePath, - true - )) - ) { - Logger( - `Failed to store the file to the database: ${file}`, - LOG_LEVEL_NOTICE - ); - return; - } - } - el.remove(); - }); - }) - ); - } - if (fileOnDB) { - el.appendChild( - this.createEl(el, "button", { text: "Database -> Storage" }, (buttonEl) => { - buttonEl.onClickEvent(async () => { - if (fileOnDB.path.startsWith(ICHeader)) { - const addOn = this.plugin.getAddOn(HiddenFileSync.name); - if (addOn) { - if ( - !(await addOn.extractInternalFileFromDatabase( - path as FilePath, - true - )) - ) { - Logger( - `Failed to store the file to the database (Hidden file): ${file}`, - LOG_LEVEL_NOTICE - ); - return; - } - } - } else { - if ( - !(await this.plugin.fileHandler.dbToStorage( - fileOnDB as MetaEntry, - null, - true - )) - ) { - Logger( - `Failed to store the file to the storage: ${fileOnDB.path}`, - LOG_LEVEL_NOTICE - ); - return; - } - } - el.remove(); - }); - }) - ); - } - return el; - }) - ); - }; - - const checkBetweenStorageAndDatabase = async (file: FilePathWithPrefix, fileOnDB: LoadedEntry) => { - const dataContent = readAsBlob(fileOnDB); - const content = createBlob(await this.plugin.storageAccess.readHiddenFileBinary(file)); - if (await isDocContentSame(content, dataContent)) { - Logger(`Compare: SAME: ${file}`); - } else { - Logger(`Compare: CONTENT IS NOT MATCHED! ${file}`, LOG_LEVEL_NOTICE); - void addResult(file, file, fileOnDB); - } - }; - new Setting(paneEl) - .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.fileHandler.createAllChunks(true); - }) - ); - new Setting(paneEl) - .setName("Resolve All conflicted files by the newer one") - .setDesc( - "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one." - ) - .addButton((button) => - button - .setButtonText("Resolve All") - .setCta() - .onClick(async () => { - await this.plugin.rebuilder.resolveAllConflictedFilesByNewerOnes(); - }) - ); - - new Setting(paneEl) - .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." - ) - .addButton((button) => - button - .setButtonText("Verify all") - .setDisabled(false) - .setCta() - .onClick(async () => { - Logger("Start verifying all files", LOG_LEVEL_NOTICE, "verify"); - const ignorePatterns = getFileRegExp( - this.plugin.settings, - "syncInternalFilesIgnorePatterns" - ); - const targetPatterns = getFileRegExp( - this.plugin.settings, - "syncInternalFilesTargetPatterns" - ); - this.plugin.localDatabase.hashCaches.clear(); - Logger("Start verifying all files", LOG_LEVEL_NOTICE, "verify"); - const files = this.plugin.settings.syncInternalFiles - ? await this.plugin.storageAccess.getFilesIncludeHidden( - "/", - targetPatterns, - ignorePatterns - ) - : await this.plugin.storageAccess.getFileNames(); - const documents = [] as FilePath[]; - - const adn = this.plugin.localDatabase.findAllDocs(); - for await (const i of adn) { - const path = getPath(i); - if (path.startsWith(ICXHeader)) continue; - if (path.startsWith(PSCHeader)) continue; - if (!this.plugin.settings.syncInternalFiles && path.startsWith(ICHeader)) continue; - documents.push(stripAllPrefixes(path)); - } - const allPaths = [...new Set([...documents, ...files])]; - let i = 0; - const incProc = () => { - i++; - if (i % 25 == 0) - Logger( - `Checking ${i}/${allPaths.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 stat = (await this.plugin.storageAccess.isExistsIncludeHidden(path)) - ? await this.plugin.storageAccess.statHidden(path) - : false; - const fileOnStorage = stat != null ? stat : false; - if (!(await this.plugin.$$isTargetFile(path))) return incProc(); - const releaser = await semaphore.acquire(1); - if (fileOnStorage && this.plugin.$$isFileSizeExceeded(fileOnStorage.size)) - return incProc(); - try { - const isHiddenFile = path.startsWith("."); - const dbPath = isHiddenFile ? addPrefix(path, ICHeader) : path; - const fileOnDB = await this.plugin.localDatabase.getDBEntry(dbPath); - if (fileOnDB && this.plugin.$$isFileSizeExceeded(fileOnDB.size)) - return incProc(); - - if (!fileOnDB && fileOnStorage) { - Logger( - `Compare: Not found on the local database: ${path}`, - LOG_LEVEL_NOTICE - ); - void addResult(path, path, false); - return incProc(); - } - if (fileOnDB && !fileOnStorage) { - Logger(`Compare: Not found on the storage: ${path}`, LOG_LEVEL_NOTICE); - void addResult(path, false, fileOnDB); - return incProc(); - } - if (fileOnStorage && fileOnDB) { - await checkBetweenStorageAndDatabase(path, 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 = paneEl.createDiv({ text: "" }); - new Setting(paneEl) - .setName("Check and convert non-path-obfuscated files") - .setDesc("") - .addButton((button) => - button - .setButtonText("Perform") - .setDisabled(false) - .setWarning() - .onClick(async () => { - for await (const docName of this.plugin.localDatabase.findAllDocNames()) { - if (!docName.startsWith("f:")) { - const idEncoded = await this.plugin.$$path2id(docName as FilePathWithPrefix); - const doc = await this.plugin.localDatabase.getRaw(docName as DocumentID); - if (!doc) continue; - if (doc.type != "newnote" && doc.type != "plain") { - continue; - } - if (doc?.deleted ?? false) continue; - const newDoc = { ...doc }; - //Prepare converted data - newDoc._id = idEncoded; - newDoc.path = docName as FilePathWithPrefix; - // @ts-ignore - delete newDoc._rev; - try { - const obfuscatedDoc = await this.plugin.localDatabase.getRaw(idEncoded, { - revs_info: true, - }); - // Unfortunately we have to delete one of them. - // Just now, save it as a conflicted document. - obfuscatedDoc._revs_info?.shift(); // Drop latest revision. - const previousRev = obfuscatedDoc._revs_info?.shift(); // Use second revision. - if (previousRev) { - newDoc._rev = previousRev.rev; - } else { - //If there are no revisions, set the possibly unique one - newDoc._rev = - "1-" + - `00000000000000000000000000000000${~~(Math.random() * 1e9)}${~~(Math.random() * 1e9)}${~~(Math.random() * 1e9)}${~~(Math.random() * 1e9)}`.slice( - -32 - ); - } - const ret = await this.plugin.localDatabase.putRaw(newDoc, { force: true }); - if (ret.ok) { - Logger( - `${docName} has been converted as conflicted document`, - LOG_LEVEL_NOTICE - ); - doc._deleted = true; - if ((await this.plugin.localDatabase.putRaw(doc)).ok) { - Logger(`Old ${docName} has been deleted`, LOG_LEVEL_NOTICE); - } - await this.plugin.$$queueConflictCheckIfOpen( - docName as FilePathWithPrefix - ); - } else { - Logger(`Converting ${docName} Failed!`, LOG_LEVEL_NOTICE); - Logger(ret, LOG_LEVEL_VERBOSE); - } - } catch (ex: any) { - if (ex?.status == 404) { - // We can perform this safely - if ((await this.plugin.localDatabase.putRaw(newDoc)).ok) { - Logger(`${docName} has been converted`, LOG_LEVEL_NOTICE); - doc._deleted = true; - if ((await this.plugin.localDatabase.putRaw(doc)).ok) { - Logger(`Old ${docName} has been deleted`, LOG_LEVEL_NOTICE); - } - } - } else { - Logger( - `Something went wrong while converting ${docName}`, - LOG_LEVEL_NOTICE - ); - Logger(ex, LOG_LEVEL_VERBOSE); - // Something wrong. - } - } - } - } - Logger(`Converting finished`, LOG_LEVEL_NOTICE); - }) - ); - }); - void addPanel(paneEl, "Reset").then((paneEl) => { - new Setting(paneEl).setName("Back to non-configured").addButton((button) => - button - .setButtonText("Back") - .setDisabled(false) - .onClick(async () => { - this.editingSettings.isConfigured = false; - await this.saveAllDirtySettings(); - this.plugin.$$askReload(); - }) - ); - - new Setting(paneEl).setName("Delete all customization sync data").addButton((button) => - button - .setButtonText("Delete") - .setDisabled(false) - .setWarning() - .onClick(async () => { - Logger(`Deleting customization sync data`, LOG_LEVEL_NOTICE); - const entriesToDelete = await this.plugin.localDatabase.allDocsRaw({ - startkey: "ix:", - endkey: "ix:\u{10ffff}", - include_docs: true, - }); - const newData = entriesToDelete.rows.map((e) => ({ - ...e.doc, - _deleted: true, - })); - const r = await this.plugin.localDatabase.bulkDocsRaw(newData as any[]); - // Do not care about the result. - Logger( - `${r.length} items have been removed, to confirm how many items are left, please perform it again.`, - LOG_LEVEL_NOTICE - ); - }) - ); - }); - }); - void addPane(containerEl, "Advanced", "🔧", 46, false, LEVEL_ADVANCED).then((paneEl) => { - void addPanel(paneEl, "Memory cache").then((paneEl) => { - new Setting(paneEl).autoWireNumeric("hashCacheMaxCount", { clampMin: 10 }); - new Setting(paneEl).autoWireNumeric("hashCacheMaxAmount", { clampMin: 1 }); - }); - void addPanel(paneEl, "Local Database Tweak").then((paneEl) => { - paneEl.addClass("wizardHidden"); - - new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("customChunkSize", { clampMin: 0 }); - - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("enableChunkSplitterV2", { - onUpdate: enableOnly(() => this.isConfiguredAs("useSegmenter", false)), - }); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("useSegmenter", { - onUpdate: enableOnly(() => this.isConfiguredAs("enableChunkSplitterV2", false)), - }); - }); - - void addPanel(paneEl, "Transfer Tweak").then((paneEl) => { - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("readChunksOnline", { onUpdate: onlyOnCouchDB }); - - new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("concurrencyOfReadChunksOnline", { - clampMin: 10, - onUpdate: onlyOnCouchDB, - }); - - new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("minimumIntervalOfReadChunksOnline", { - clampMin: 10, - onUpdate: onlyOnCouchDB, - }); - // new Setting(paneEl) - // .setClass("wizardHidden") - // .autoWireToggle("sendChunksBulk", { onUpdate: onlyOnCouchDB }) - // new Setting(paneEl) - // .setClass("wizardHidden") - // .autoWireNumeric("sendChunksBulkMaxSize", { - // clampMax: 100, clampMin: 1, onUpdate: onlyOnCouchDB - // }) - }); - }); - - void addPane(containerEl, "Power users", "💪", 47, true, LEVEL_POWER_USER).then((paneEl) => { - void addPanel(paneEl, "Remote Database Tweak").then((paneEl) => { - new Setting(paneEl).autoWireToggle("useEden").setClass("wizardHidden"); - const onlyUsingEden = visibleOnly(() => this.isConfiguredAs("useEden", true)); - new Setting(paneEl) - .autoWireNumeric("maxChunksInEden", { onUpdate: onlyUsingEden }) - .setClass("wizardHidden"); - new Setting(paneEl) - .autoWireNumeric("maxTotalLengthInEden", { onUpdate: onlyUsingEden }) - .setClass("wizardHidden"); - new Setting(paneEl) - .autoWireNumeric("maxAgeInEden", { onUpdate: onlyUsingEden }) - .setClass("wizardHidden"); - - new Setting(paneEl).autoWireToggle("enableCompression").setClass("wizardHidden"); - }); - - void addPanel(paneEl, "CouchDB Connection Tweak", undefined, onlyOnCouchDB).then((paneEl) => { - paneEl.addClass("wizardHidden"); - - this.createEl( - paneEl, - "div", - { - text: `If you reached the payload size limit when using IBM Cloudant, please decrease batch size and batch limit to a lower value.`, - }, - undefined, - onlyOnCouchDB - ).addClass("wizardHidden"); - - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireNumeric("batch_size", { clampMin: 2, onUpdate: onlyOnCouchDB }); - new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("batches_limit", { - clampMin: 2, - onUpdate: onlyOnCouchDB, - }); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("useTimeouts", { onUpdate: onlyOnCouchDB }); - }); - void addPanel(paneEl, "Configuration Encryption").then((paneEl) => { - const passphrase_options: Record = { - "": "Default", - LOCALSTORAGE: "Use a custom passphrase", - ASK_AT_LAUNCH: "Ask an passphrase at every launch", - }; - - new Setting(paneEl) - .setName("Encrypting sensitive configuration items") - .autoWireDropDown("configPassphraseStore", { - options: passphrase_options, - holdValue: true, - }) - .setClass("wizardHidden"); - - new Setting(paneEl) - .autoWireText("configPassphrase", { isPassword: true, holdValue: true }) - .setClass("wizardHidden") - .addOnUpdate(() => ({ - disabled: !this.isConfiguredAs("configPassphraseStore", "LOCALSTORAGE"), - })); - new Setting(paneEl) - .addApplyButton(["configPassphrase", "configPassphraseStore"]) - .setClass("wizardHidden"); - }); - void addPanel(paneEl, "Developer").then((paneEl) => { - new Setting(paneEl).autoWireToggle("enableDebugTools").setClass("wizardHidden"); - }); - }); - - void addPane(containerEl, "Patches", "🩹", 51, false, LEVEL_EDGE_CASE).then((paneEl) => { - void addPanel(paneEl, "Compatibility (Metadata)").then((paneEl) => { - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("deleteMetadataOfDeletedFiles"); - - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireNumeric("automaticallyDeleteMetadataOfDeletedFiles", { - onUpdate: visibleOnly(() => this.isConfiguredAs("deleteMetadataOfDeletedFiles", true)), - }); - }); - - void addPanel(paneEl, "Compatibility (Conflict Behaviour)").then((paneEl) => { - paneEl.addClass("wizardHidden"); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("disableMarkdownAutoMerge"); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("writeDocumentsIfConflicted"); - }); - - void addPanel(paneEl, "Compatibility (Database structure)").then((paneEl) => { - new Setting(paneEl).autoWireToggle("useIndexedDBAdapter", { invert: true, holdValue: true }); - - new Setting(paneEl) - .autoWireToggle("doNotUseFixedRevisionForChunks", { holdValue: true }) - .setClass("wizardHidden"); - new Setting(paneEl) - .autoWireToggle("handleFilenameCaseSensitive", { holdValue: true }) - .setClass("wizardHidden"); - - this.addOnSaved("useIndexedDBAdapter", async () => { - await this.saveAllDirtySettings(); - await rebuildDB("localOnly"); - }); - }); - - void addPanel(paneEl, "Compatibility (Internal API Usage)").then((paneEl) => { - new Setting(paneEl).autoWireToggle("watchInternalFileChanges", { invert: true }); - }); - - void addPanel(paneEl, "Edge case addressing (Database)").then((paneEl) => { - new Setting(paneEl) - .autoWireText("additionalSuffixOfDatabaseName", { holdValue: true }) - .addApplyButton(["additionalSuffixOfDatabaseName"]); - - this.addOnSaved("additionalSuffixOfDatabaseName", async (key) => { - Logger("Suffix has been changed. Reopening database...", LOG_LEVEL_NOTICE); - await this.plugin.$$initializeDatabase(); - }); - - new Setting(paneEl).autoWireDropDown("hashAlg", { - options: { - "": "Old Algorithm", - xxhash32: "xxhash32 (Fast but less collision resistance)", - xxhash64: "xxhash64 (Fastest)", - "mixed-purejs": "PureJS fallback (Fast, W/O WebAssembly)", - sha1: "Older fallback (Slow, W/O WebAssembly)", - } as Record, - }); - this.addOnSaved("hashAlg", async () => { - await this.plugin.localDatabase._prepareHashFunctions(); - }); - }); - void addPanel(paneEl, "Edge case addressing (Behaviour)").then((paneEl) => { - new Setting(paneEl).autoWireToggle("doNotSuspendOnFetching"); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("doNotDeleteFolder"); - }); - - void addPanel(paneEl, "Edge case addressing (Processing)").then((paneEl) => { - new Setting(paneEl).autoWireToggle("disableWorkerForGeneratingChunks"); - - new Setting(paneEl).autoWireToggle("processSmallFilesInUIThread", { - onUpdate: visibleOnly(() => this.isConfiguredAs("disableWorkerForGeneratingChunks", false)), - }); - }); - // void addPanel(paneEl, "Edge case addressing (Networking)").then((paneEl) => { - // new Setting(paneEl).autoWireToggle("useRequestAPI"); - // }); - void addPanel(paneEl, "Compatibility (Trouble addressed)").then((paneEl) => { - new Setting(paneEl).autoWireToggle("disableCheckingConfigMismatch"); - }); - }); - - void addPane(containerEl, "Maintenance", "🎛️", 70, true).then((paneEl) => { - const isRemoteLockedAndDeviceNotAccepted = () => this.plugin?.replicator?.remoteLockedAndDeviceNotAccepted; - const isRemoteLocked = () => this.plugin?.replicator?.remoteLocked; - // if (this.plugin?.replicator?.remoteLockedAndDeviceNotAccepted) { - this.createEl( - paneEl, - "div", - { - text: "The remote database is locked for synchronization to prevent vault corruption because this device isn't marked as 'resolved'. Please backup your vault, reset the local database, and select 'Mark this device as resolved'. This warning will persist until the device is confirmed as resolved by replication.", - cls: "op-warn", - }, - (c) => { - this.createEl( - c, - "button", - { - text: "I've made a backup, mark this device 'resolved'", - cls: "mod-warning", - }, - (e) => { - e.addEventListener("click", () => { - fireAndForget(async () => { - await this.plugin.$$markRemoteResolved(); - this.display(); - }); - }); - } - ); - }, - visibleOnly(isRemoteLockedAndDeviceNotAccepted) - ); - this.createEl( - paneEl, - "div", - { - text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization. (This device is marked 'resolved') When all your devices are marked 'resolved', unlock the database. This warning kept showing until confirming the device is resolved by the replication", - cls: "op-warn", - }, - (c) => - this.createEl( - c, - "button", - { - text: "I'm ready, unlock the database", - cls: "mod-warning", - }, - (e) => { - e.addEventListener("click", () => { - fireAndForget(async () => { - await this.plugin.$$markRemoteUnlocked(); - this.display(); - }); - }); - } - ), - visibleOnly(isRemoteLocked) - ); - - void addPanel(paneEl, "Scram!").then((paneEl) => { - new Setting(paneEl) - .setName("Lock Server") - .setDesc("Lock the remote server to prevent synchronization with other devices.") - .addButton((button) => - button - .setButtonText("Lock") - .setDisabled(false) - .setWarning() - .onClick(async () => { - await this.plugin.$$markRemoteLocked(); - }) - ) - .addOnUpdate(onlyOnCouchDBOrMinIO); - - new Setting(paneEl) - .setName("Emergency restart") - .setDesc("Disables all synchronization and restart.") - .addButton((button) => - button - .setButtonText("Flag and restart") - .setDisabled(false) - .setWarning() - .onClick(async () => { - await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG, ""); - this.plugin.$$performRestart(); - }) - ); - }); - - void addPanel(paneEl, "Syncing", () => {}, onlyOnCouchDBOrMinIO).then((paneEl) => { - new Setting(paneEl) - .setName("Resend") - .setDesc("Resend all chunks to the remote.") - .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); - } - }) - ) - .addOnUpdate(onlyOnCouchDB); - - new Setting(paneEl) - .setName("Reset journal received history") - .setDesc( - "Initialise journal received history. On the next sync, every item except this device sent will be downloaded again." - ) - .addButton((button) => - button - .setButtonText("Reset received") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await this.getMinioJournalSyncClient().updateCheckPointInfo((info) => ({ - ...info, - receivedFiles: new Set(), - knownIDs: new Set(), - })); - Logger(`Journal received history has been cleared.`, LOG_LEVEL_NOTICE); - }) - ) - .addOnUpdate(onlyOnMinIO); - - new Setting(paneEl) - .setName("Reset journal sent history") - .setDesc( - "Initialise journal sent history. On the next sync, every item except this device received will be sent again." - ) - .addButton((button) => - button - .setButtonText("Reset sent history") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await this.getMinioJournalSyncClient().updateCheckPointInfo((info) => ({ - ...info, - lastLocalSeq: 0, - sentIDs: new Set(), - sentFiles: new Set(), - })); - Logger(`Journal sent history has been cleared.`, LOG_LEVEL_NOTICE); - }) - ) - .addOnUpdate(onlyOnMinIO); - }); - void addPanel(paneEl, "Garbage Collection (Beta)", (e) => e, onlyOnP2POrCouchDB).then((paneEl) => { - new Setting(paneEl) - .setName("Remove all orphaned chunks") - .setDesc("Remove all orphaned chunks from the local database.") - .addButton((button) => - button - .setButtonText("Remove") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await this.plugin - .getAddOn(LocalDatabaseMaintenance.name) - ?.removeUnusedChunks(); - }) - ); - - new Setting(paneEl) - .setName("Resurrect deleted chunks") - .setDesc( - "If you have deleted chunks before fully synchronised and missed some chunks, you possibly can resurrect them." - ) - .addButton((button) => - button - .setButtonText("Try resurrect") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await this.plugin - .getAddOn(LocalDatabaseMaintenance.name) - ?.resurrectChunks(); - }) - ); - new Setting(paneEl) - .setName("Commit File Deletion") - .setDesc("Completely delete all deleted documents from the local database.") - .addButton((button) => - button - .setButtonText("Delete") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await this.plugin - .getAddOn(LocalDatabaseMaintenance.name) - ?.commitFileDeletion(); - }) - ); - }); - void addPanel(paneEl, "Rebuilding Operations (Local)").then((paneEl) => { - new Setting(paneEl) - .setName("Fetch from remote") - .setDesc("Restore or reconstruct local database from remote.") - .addButton((button) => - button - .setButtonText("Fetch") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG3_HR, ""); - this.plugin.$$performRestart(); - }) - ) - .addButton((button) => - button - .setButtonText("Fetch w/o restarting") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await rebuildDB("localOnly"); - }) - ); - - new Setting(paneEl) - .setName("Fetch rebuilt DB (Save local documents before)") - .setDesc("Restore or reconstruct local database from remote database but use local chunks.") - .addButton((button) => - button - .setButtonText("Save and Fetch") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await rebuildDB("localOnlyWithChunks"); - }) - ) - .addOnUpdate(onlyOnCouchDB); - }); - - void addPanel(paneEl, "Total Overhaul", () => {}, onlyOnCouchDBOrMinIO).then((paneEl) => { - new Setting(paneEl) - .setName("Rebuild everything") - .setDesc("Rebuild local and remote database with local files.") - .addButton((button) => - button - .setButtonText("Rebuild") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG2_HR, ""); - this.plugin.$$performRestart(); - }) - ) - .addButton((button) => - button - .setButtonText("Rebuild w/o restarting") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await rebuildDB("rebuildBothByThisDevice"); - }) - ); - }); - void addPanel(paneEl, "Rebuilding Operations (Remote Only)", () => {}, onlyOnCouchDBOrMinIO).then( - (paneEl) => { - new Setting(paneEl) - .setName("Perform cleanup") - .setDesc( - "Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client." - ) - .addButton((button) => - button - .setButtonText("Perform") - .setDisabled(false) - .onClick(async () => { - const replicator = this.plugin.replicator as LiveSyncCouchDBReplicator; - Logger(`Cleanup has been began`, LOG_LEVEL_NOTICE, "compaction"); - if (await replicator.compactRemote(this.editingSettings)) { - Logger(`Cleanup has been completed!`, LOG_LEVEL_NOTICE, "compaction"); - } else { - Logger(`Cleanup has been failed!`, LOG_LEVEL_NOTICE, "compaction"); - } - }) - ) - .addOnUpdate(onlyOnCouchDB); - - new Setting(paneEl) - .setName("Overwrite remote") - .setDesc("Overwrite remote with local DB and passphrase.") - .addButton((button) => - button - .setButtonText("Send") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await rebuildDB("remoteOnly"); - }) - ); - - new Setting(paneEl) - .setName("Reset all journal counter") - .setDesc( - "Initialise all journal history, On the next sync, every item will be received and sent." - ) - .addButton((button) => - button - .setButtonText("Reset all") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await this.getMinioJournalSyncClient().resetCheckpointInfo(); - Logger(`Journal exchange history has been cleared.`, LOG_LEVEL_NOTICE); - }) - ) - .addOnUpdate(onlyOnMinIO); - - new Setting(paneEl) - .setName("Purge all journal counter") - .setDesc("Purge all download/upload cache.") - .addButton((button) => - button - .setButtonText("Reset all") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await this.getMinioJournalSyncClient().resetAllCaches(); - Logger(`Journal download/upload cache has been cleared.`, LOG_LEVEL_NOTICE); - }) - ) - .addOnUpdate(onlyOnMinIO); - - new Setting(paneEl) - .setName("Fresh Start Wipe") - .setDesc("Delete all data on the remote server.") - .addButton((button) => - button - .setButtonText("Delete") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await this.getMinioJournalSyncClient().updateCheckPointInfo((info) => ({ - ...info, - receivedFiles: new Set(), - knownIDs: new Set(), - lastLocalSeq: 0, - sentIDs: new Set(), - sentFiles: new Set(), - })); - await this.resetRemoteBucket(); - Logger(`Deleted all data on remote server`, LOG_LEVEL_NOTICE); - }) - ) - .addOnUpdate(onlyOnMinIO); - } - ); - - void addPanel(paneEl, "Reset").then((paneEl) => { - new Setting(paneEl) - .setName("Delete local database to reset or uninstall Self-hosted LiveSync") - .addButton((button) => - button - .setButtonText("Delete") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await this.plugin.$$resetLocalDatabase(); - await this.plugin.$$initializeDatabase(); - }) - ); - }); - }); void yieldNextAnimationFrame().then(() => { if (this.selectedScreen == "") { - if (lastVersion != this.editingSettings.lastReadUpdates) { + if (this.lastVersion != this.editingSettings.lastReadUpdates) { if (this.editingSettings.isConfigured) { changeDisplay("100"); } else { changeDisplay("110"); } } else { - if (isAnySyncEnabled()) { + if (this.isAnySyncEnabled()) { changeDisplay("20"); } else { changeDisplay("110"); diff --git a/src/modules/features/SettingDialogue/PaneAdvanced.ts b/src/modules/features/SettingDialogue/PaneAdvanced.ts new file mode 100644 index 0000000..5cbf666 --- /dev/null +++ b/src/modules/features/SettingDialogue/PaneAdvanced.ts @@ -0,0 +1,44 @@ +import { ChunkAlgorithmNames } from "../../../lib/src/common/types.ts"; +import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; +import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; +import type { PageFunctions } from "./SettingPane.ts"; + +export function paneAdvanced(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void { + void addPanel(paneEl, "Memory cache").then((paneEl) => { + new Setting(paneEl).autoWireNumeric("hashCacheMaxCount", { clampMin: 10 }); + new Setting(paneEl).autoWireNumeric("hashCacheMaxAmount", { clampMin: 1 }); + }); + void addPanel(paneEl, "Local Database Tweak").then((paneEl) => { + paneEl.addClass("wizardHidden"); + + const items = ChunkAlgorithmNames; + new Setting(paneEl).autoWireDropDown("chunkSplitterVersion", { + options: items, + }); + new Setting(paneEl).autoWireNumeric("customChunkSize", { clampMin: 0, acceptZero: true }); + }); + + void addPanel(paneEl, "Transfer Tweak").then((paneEl) => { + new Setting(paneEl) + .setClass("wizardHidden") + .autoWireToggle("readChunksOnline", { onUpdate: this.onlyOnCouchDB }); + + new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("concurrencyOfReadChunksOnline", { + clampMin: 10, + onUpdate: this.onlyOnCouchDB, + }); + + new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("minimumIntervalOfReadChunksOnline", { + clampMin: 10, + onUpdate: this.onlyOnCouchDB, + }); + // new Setting(paneEl) + // .setClass("wizardHidden") + // .autoWireToggle("sendChunksBulk", { onUpdate: onlyOnCouchDB }) + // new Setting(paneEl) + // .setClass("wizardHidden") + // .autoWireNumeric("sendChunksBulkMaxSize", { + // clampMax: 100, clampMin: 1, onUpdate: onlyOnCouchDB + // }) + }); +} diff --git a/src/modules/features/SettingDialogue/PaneChangeLog.ts b/src/modules/features/SettingDialogue/PaneChangeLog.ts new file mode 100644 index 0000000..f77d2e1 --- /dev/null +++ b/src/modules/features/SettingDialogue/PaneChangeLog.ts @@ -0,0 +1,33 @@ +import { MarkdownRenderer } from "../../../deps.ts"; +import { versionNumberString2Number } from "../../../lib/src/string_and_binary/convert.ts"; +import { $msg } from "../../../lib/src/common/i18n.ts"; +import { fireAndForget } from "octagonal-wheels/promises"; +import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; +//@ts-ignore +const manifestVersion: string = MANIFEST_VERSION || "-"; +//@ts-ignore +const updateInformation: string = UPDATE_INFO || ""; + +const lastVersion = ~~(versionNumberString2Number(manifestVersion) / 1000); +export function paneChangeLog(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement): void { + const informationDivEl = this.createEl(paneEl, "div", { text: "" }); + + const tmpDiv = createDiv(); + // tmpDiv.addClass("sls-header-button"); + tmpDiv.addClass("op-warn-info"); + + tmpDiv.innerHTML = `

${$msg("obsidianLiveSyncSettingTab.msgNewVersionNote")}

`; + if (lastVersion > (this.editingSettings?.lastReadUpdates || 0)) { + const informationButtonDiv = informationDivEl.appendChild(tmpDiv); + informationButtonDiv.querySelector("button")?.addEventListener("click", () => { + fireAndForget(async () => { + this.editingSettings.lastReadUpdates = lastVersion; + await this.saveAllDirtySettings(); + informationButtonDiv.remove(); + }); + }); + } + fireAndForget(() => + MarkdownRenderer.render(this.plugin.app, updateInformation, informationDivEl, "/", this.plugin) + ); +} diff --git a/src/modules/features/SettingDialogue/PaneCustomisationSync.ts b/src/modules/features/SettingDialogue/PaneCustomisationSync.ts new file mode 100644 index 0000000..b040369 --- /dev/null +++ b/src/modules/features/SettingDialogue/PaneCustomisationSync.ts @@ -0,0 +1,77 @@ +import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; +import { EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, eventHub } from "../../../common/events.ts"; +import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; +import type { PageFunctions } from "./SettingPane.ts"; +import { enableOnly, visibleOnly } from "./SettingPane.ts"; +export function paneCustomisationSync( + this: ObsidianLiveSyncSettingTab, + paneEl: HTMLElement, + { addPanel }: PageFunctions +): void { + // With great respect, thank you TfTHacker! + // Refer: https://github.com/TfTHacker/obsidian42-brat/blob/main/src/features/BetaPlugins.ts + void addPanel(paneEl, "Customization Sync").then((paneEl) => { + const enableOnlyOnPluginSyncIsNotEnabled = enableOnly(() => this.isConfiguredAs("usePluginSync", false)); + const visibleOnlyOnPluginSyncEnabled = visibleOnly(() => this.isConfiguredAs("usePluginSync", true)); + + this.createEl( + paneEl, + "div", + { + text: "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.", + cls: "op-warn", + }, + (c) => {}, + visibleOnly(() => this.isConfiguredAs("deviceAndVaultName", "")) + ); + this.createEl( + paneEl, + "div", + { + text: "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.", + cls: "op-warn-info", + }, + (c) => {}, + visibleOnly(() => this.isConfiguredAs("usePluginSync", true)) + ); + + new Setting(paneEl).autoWireText("deviceAndVaultName", { + placeHolder: "desktop", + onUpdate: enableOnlyOnPluginSyncIsNotEnabled, + }); + + new Setting(paneEl).autoWireToggle("usePluginSyncV2"); + + new Setting(paneEl).autoWireToggle("usePluginSync", { + onUpdate: enableOnly(() => !this.isConfiguredAs("deviceAndVaultName", "")), + }); + + new Setting(paneEl).autoWireToggle("autoSweepPlugins", { + onUpdate: visibleOnlyOnPluginSyncEnabled, + }); + + new Setting(paneEl).autoWireToggle("autoSweepPluginsPeriodic", { + onUpdate: visibleOnly( + () => this.isConfiguredAs("usePluginSync", true) && this.isConfiguredAs("autoSweepPlugins", true) + ), + }); + new Setting(paneEl).autoWireToggle("notifyPluginOrSettingUpdated", { + onUpdate: visibleOnlyOnPluginSyncEnabled, + }); + + new Setting(paneEl) + .setName("Open") + .setDesc("Open the dialog") + .addButton((button) => { + button + .setButtonText("Open") + .setDisabled(false) + .onClick(() => { + // this.plugin.getAddOn(ConfigSync.name)?.showPluginSyncModal(); + // this.plugin.addOnConfigSync.showPluginSyncModal(); + eventHub.emitEvent(EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG); + }); + }) + .addOnUpdate(visibleOnlyOnPluginSyncEnabled); + }); +} diff --git a/src/modules/features/SettingDialogue/PaneGeneral.ts b/src/modules/features/SettingDialogue/PaneGeneral.ts new file mode 100644 index 0000000..0a92b7b --- /dev/null +++ b/src/modules/features/SettingDialogue/PaneGeneral.ts @@ -0,0 +1,45 @@ +import { $msg, $t } from "../../../lib/src/common/i18n.ts"; +import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "../../../lib/src/common/rosetta.ts"; +import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; +import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; +import type { PageFunctions } from "./SettingPane.ts"; +import { visibleOnly } from "./SettingPane.ts"; +export function paneGeneral( + this: ObsidianLiveSyncSettingTab, + paneEl: HTMLElement, + { addPanel, addPane }: PageFunctions +): void { + void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleAppearance")).then((paneEl) => { + const languages = Object.fromEntries([ + // ["", $msg("obsidianLiveSyncSettingTab.defaultLanguage")], + ...SUPPORTED_I18N_LANGS.map((e) => [e, $t(`lang-${e}`)]), + ]) as Record; + new Setting(paneEl).autoWireDropDown("displayLanguage", { + options: languages, + }); + this.addOnSaved("displayLanguage", () => this.display()); + new Setting(paneEl).autoWireToggle("showStatusOnEditor"); + new Setting(paneEl).autoWireToggle("showOnlyIconsOnEditor", { + onUpdate: visibleOnly(() => this.isConfiguredAs("showStatusOnEditor", true)), + }); + new Setting(paneEl).autoWireToggle("showStatusOnStatusbar"); + new Setting(paneEl).autoWireToggle("hideFileWarningNotice"); + }); + void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleLogging")).then((paneEl) => { + paneEl.addClass("wizardHidden"); + + new Setting(paneEl).autoWireToggle("lessInformationInLog"); + + new Setting(paneEl).autoWireToggle("showVerboseLog", { + onUpdate: visibleOnly(() => this.isConfiguredAs("lessInformationInLog", false)), + }); + }); + new Setting(paneEl).setClass("wizardOnly").addButton((button) => + button + .setButtonText($msg("obsidianLiveSyncSettingTab.btnNext")) + .setCta() + .onClick(() => { + this.changeDisplay("0"); + }) + ); +} diff --git a/src/modules/features/SettingDialogue/PaneHatch.ts b/src/modules/features/SettingDialogue/PaneHatch.ts new file mode 100644 index 0000000..7fe06ed --- /dev/null +++ b/src/modules/features/SettingDialogue/PaneHatch.ts @@ -0,0 +1,531 @@ +import { stringifyYaml } from "../../../deps.ts"; +import { + type ObsidianLiveSyncSettings, + type FilePathWithPrefix, + type DocumentID, + LOG_LEVEL_NOTICE, + LOG_LEVEL_VERBOSE, + type LoadedEntry, + REMOTE_COUCHDB, + REMOTE_MINIO, + type MetaEntry, + type FilePath, + DEFAULT_SETTINGS, +} from "../../../lib/src/common/types.ts"; +import { + createBlob, + getFileRegExp, + isDocContentSame, + parseHeaderValues, + readAsBlob, +} from "../../../lib/src/common/utils.ts"; +import { Logger } from "../../../lib/src/common/logger.ts"; +import { isCloudantURI } from "../../../lib/src/pouchdb/utils_couchdb.ts"; +import { getPath, requestToCouchDBWithCredentials } from "../../../common/utils.ts"; +import { addPrefix, shouldBeIgnored, stripAllPrefixes } from "../../../lib/src/string_and_binary/path.ts"; +import { $msg } from "../../../lib/src/common/i18n.ts"; +import { Semaphore } from "octagonal-wheels/concurrency/semaphore"; +import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; +import { EVENT_REQUEST_RUN_DOCTOR, eventHub } from "../../../common/events.ts"; +import { ICHeader, ICXHeader, PSCHeader } from "../../../common/types.ts"; +import { HiddenFileSync } from "../../../features/HiddenFileSync/CmdHiddenFileSync.ts"; +import { EVENT_REQUEST_SHOW_HISTORY } from "../../../common/obsidianEvents.ts"; +import { generateCredentialObject } from "../../../lib/src/replication/httplib.ts"; +import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; +import type { PageFunctions } from "./SettingPane.ts"; +export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void { + // const hatchWarn = this.createEl(paneEl, "div", { text: `To stop the boot up sequence for fixing problems on databases, you can put redflag.md on top of your vault (Rebooting obsidian is required).` }); + // hatchWarn.addClass("op-warn-info"); + void addPanel(paneEl, $msg("Setting.TroubleShooting")).then((paneEl) => { + new Setting(paneEl) + .setName($msg("Setting.TroubleShooting.Doctor")) + .setDesc($msg("Setting.TroubleShooting.Doctor.Desc")) + .addButton((button) => + button + .setButtonText("Run Doctor") + .setCta() + .setDisabled(false) + .onClick(() => { + this.closeSetting(); + eventHub.emitEvent(EVENT_REQUEST_RUN_DOCTOR, "you wanted(Thank you)!"); + }) + ); + new Setting(paneEl).setName("Prepare the 'report' to create an issue").addButton((button) => + button + .setButtonText("Copy Report to clipboard") + .setCta() + .setDisabled(false) + .onClick(async () => { + let responseConfig: any = {}; + const REDACTED = "𝑅𝐸𝐷𝐴𝐶𝑇𝐸𝐷"; + if (this.editingSettings.remoteType == REMOTE_COUCHDB) { + try { + const credential = generateCredentialObject(this.editingSettings); + const customHeaders = parseHeaderValues(this.editingSettings.couchDB_CustomHeaders); + const r = await requestToCouchDBWithCredentials( + this.editingSettings.couchDB_URI, + credential, + window.origin, + undefined, + undefined, + undefined, + customHeaders + ); + + Logger(JSON.stringify(r.json, null, 2)); + + responseConfig = r.json; + responseConfig["couch_httpd_auth"].secret = REDACTED; + responseConfig["couch_httpd_auth"].authentication_db = REDACTED; + responseConfig["couch_httpd_auth"].authentication_redirect = REDACTED; + responseConfig["couchdb"].uuid = REDACTED; + responseConfig["admins"] = REDACTED; + delete responseConfig["jwt_keys"]; + if ("secret" in responseConfig["chttpd_auth"]) + responseConfig["chttpd_auth"].secret = REDACTED; + } catch (ex) { + Logger(ex, LOG_LEVEL_VERBOSE); + responseConfig = { + error: "Requesting information from the remote CouchDB has failed. If you are using IBM Cloudant, this is normal behaviour.", + }; + } + } else if (this.editingSettings.remoteType == REMOTE_MINIO) { + responseConfig = { error: "Object Storage Synchronisation" }; + // + } + const defaultKeys = Object.keys(DEFAULT_SETTINGS) as (keyof ObsidianLiveSyncSettings)[]; + const pluginConfig = JSON.parse(JSON.stringify(this.editingSettings)) as ObsidianLiveSyncSettings; + const pluginKeys = Object.keys(pluginConfig); + for (const key of pluginKeys) { + if (defaultKeys.includes(key as any)) continue; + delete pluginConfig[key as keyof ObsidianLiveSyncSettings]; + } + + pluginConfig.couchDB_DBNAME = REDACTED; + pluginConfig.couchDB_PASSWORD = REDACTED; + const scheme = pluginConfig.couchDB_URI.startsWith("http:") + ? "(HTTP)" + : pluginConfig.couchDB_URI.startsWith("https:") + ? "(HTTPS)" + : ""; + pluginConfig.couchDB_URI = isCloudantURI(pluginConfig.couchDB_URI) + ? "cloudant" + : `self-hosted${scheme}`; + pluginConfig.couchDB_USER = REDACTED; + pluginConfig.passphrase = REDACTED; + pluginConfig.encryptedPassphrase = REDACTED; + pluginConfig.encryptedCouchDBConnection = REDACTED; + pluginConfig.accessKey = REDACTED; + pluginConfig.secretKey = REDACTED; + const redact = (source: string) => `${REDACTED}(${source.length} letters)`; + pluginConfig.region = redact(pluginConfig.region); + pluginConfig.bucket = redact(pluginConfig.bucket); + pluginConfig.pluginSyncExtendedSetting = {}; + pluginConfig.P2P_AppID = redact(pluginConfig.P2P_AppID); + pluginConfig.P2P_passphrase = redact(pluginConfig.P2P_passphrase); + pluginConfig.P2P_roomID = redact(pluginConfig.P2P_roomID); + pluginConfig.P2P_relays = redact(pluginConfig.P2P_relays); + pluginConfig.jwtKey = redact(pluginConfig.jwtKey); + pluginConfig.jwtSub = redact(pluginConfig.jwtSub); + pluginConfig.jwtKid = redact(pluginConfig.jwtKid); + pluginConfig.bucketCustomHeaders = redact(pluginConfig.bucketCustomHeaders); + pluginConfig.couchDB_CustomHeaders = redact(pluginConfig.couchDB_CustomHeaders); + const endpoint = pluginConfig.endpoint; + if (endpoint == "") { + pluginConfig.endpoint = "Not configured or AWS"; + } else { + 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: navigator.userAgent, + fileSystem: this.plugin.$$isStorageInsensitive() ? "insensitive" : "sensitive", + }; + const msgConfig = `# ---- Obsidian info ---- +${stringifyYaml(obsidianInfo)} +--- +# ---- remote config ---- +${stringifyYaml(responseConfig)} +--- +# ---- Plug-in config ---- +${stringifyYaml({ + version: this.manifestVersion, + ...pluginConfig, +})}`; + console.log(msgConfig); + await navigator.clipboard.writeText(msgConfig); + Logger( + `Generated report has been copied to clipboard. Please report the issue with this! Thank you for your cooperation!`, + LOG_LEVEL_NOTICE + ); + }) + ); + new Setting(paneEl).autoWireToggle("writeLogToTheFile"); + }); + + void addPanel(paneEl, "Scram Switches").then((paneEl) => { + new Setting(paneEl).autoWireToggle("suspendFileWatching"); + this.addOnSaved("suspendFileWatching", () => this.plugin.$$askReload()); + + new Setting(paneEl).autoWireToggle("suspendParseReplicationResult"); + this.addOnSaved("suspendParseReplicationResult", () => this.plugin.$$askReload()); + }); + + void addPanel(paneEl, "Recovery and Repair").then((paneEl) => { + const addResult = async (path: string, file: FilePathWithPrefix | false, fileOnDB: LoadedEntry | false) => { + const storageFileStat = file ? await this.plugin.storageAccess.statHidden(file) : null; + resultArea.appendChild( + this.createEl(resultArea, "div", {}, (el) => { + el.appendChild(this.createEl(el, "h6", { text: path })); + el.appendChild( + this.createEl(el, "div", {}, (infoGroupEl) => { + infoGroupEl.appendChild( + this.createEl(infoGroupEl, "div", { + text: `Storage : Modified: ${!storageFileStat ? `Missing:` : `${new Date(storageFileStat.mtime).toLocaleString()}, Size:${storageFileStat.size}`}`, + }) + ); + infoGroupEl.appendChild( + this.createEl(infoGroupEl, "div", { + text: `Database: Modified: ${!fileOnDB ? `Missing:` : `${new Date(fileOnDB.mtime).toLocaleString()}, Size:${fileOnDB.size}`}`, + }) + ); + }) + ); + if (fileOnDB && file) { + el.appendChild( + this.createEl(el, "button", { text: "Show history" }, (buttonEl) => { + buttonEl.onClickEvent(() => { + eventHub.emitEvent(EVENT_REQUEST_SHOW_HISTORY, { + file: file, + fileOnDB: fileOnDB, + }); + }); + }) + ); + } + if (file) { + el.appendChild( + this.createEl(el, "button", { text: "Storage -> Database" }, (buttonEl) => { + buttonEl.onClickEvent(async () => { + if (file.startsWith(".")) { + const addOn = this.plugin.getAddOn(HiddenFileSync.name); + if (addOn) { + const file = (await addOn.scanInternalFiles()).find((e) => e.path == path); + if (!file) { + Logger( + `Failed to find the file in the internal files: ${path}`, + LOG_LEVEL_NOTICE + ); + return; + } + if (!(await addOn.storeInternalFileToDatabase(file, true))) { + Logger( + `Failed to store the file to the database (Hidden file): ${file}`, + LOG_LEVEL_NOTICE + ); + return; + } + } + } else { + if (!(await this.plugin.fileHandler.storeFileToDB(file as FilePath, true))) { + Logger( + `Failed to store the file to the database: ${file}`, + LOG_LEVEL_NOTICE + ); + return; + } + } + el.remove(); + }); + }) + ); + } + if (fileOnDB) { + el.appendChild( + this.createEl(el, "button", { text: "Database -> Storage" }, (buttonEl) => { + buttonEl.onClickEvent(async () => { + if (fileOnDB.path.startsWith(ICHeader)) { + const addOn = this.plugin.getAddOn(HiddenFileSync.name); + if (addOn) { + if ( + !(await addOn.extractInternalFileFromDatabase(path as FilePath, true)) + ) { + Logger( + `Failed to store the file to the database (Hidden file): ${file}`, + LOG_LEVEL_NOTICE + ); + return; + } + } + } else { + if ( + !(await this.plugin.fileHandler.dbToStorage( + fileOnDB as MetaEntry, + null, + true + )) + ) { + Logger( + `Failed to store the file to the storage: ${fileOnDB.path}`, + LOG_LEVEL_NOTICE + ); + return; + } + } + el.remove(); + }); + }) + ); + } + return el; + }) + ); + }; + + const checkBetweenStorageAndDatabase = async (file: FilePathWithPrefix, fileOnDB: LoadedEntry) => { + const dataContent = readAsBlob(fileOnDB); + const content = createBlob(await this.plugin.storageAccess.readHiddenFileBinary(file)); + if (await isDocContentSame(content, dataContent)) { + Logger(`Compare: SAME: ${file}`); + } else { + Logger(`Compare: CONTENT IS NOT MATCHED! ${file}`, LOG_LEVEL_NOTICE); + void addResult(file, file, fileOnDB); + } + }; + new Setting(paneEl) + .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.fileHandler.createAllChunks(true); + }) + ); + new Setting(paneEl) + .setName("Resolve All conflicted files by the newer one") + .setDesc( + "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one." + ) + .addButton((button) => + button + .setButtonText("Resolve All") + .setCta() + .onClick(async () => { + await this.plugin.rebuilder.resolveAllConflictedFilesByNewerOnes(); + }) + ); + + new Setting(paneEl) + .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." + ) + .addButton((button) => + button + .setButtonText("Verify all") + .setDisabled(false) + .setCta() + .onClick(async () => { + Logger("Start verifying all files", LOG_LEVEL_NOTICE, "verify"); + const ignorePatterns = getFileRegExp(this.plugin.settings, "syncInternalFilesIgnorePatterns"); + const targetPatterns = getFileRegExp(this.plugin.settings, "syncInternalFilesTargetPatterns"); + this.plugin.localDatabase.hashCaches.clear(); + Logger("Start verifying all files", LOG_LEVEL_NOTICE, "verify"); + const files = this.plugin.settings.syncInternalFiles + ? await this.plugin.storageAccess.getFilesIncludeHidden("/", targetPatterns, ignorePatterns) + : await this.plugin.storageAccess.getFileNames(); + const documents = [] as FilePath[]; + + const adn = this.plugin.localDatabase.findAllDocs(); + for await (const i of adn) { + const path = getPath(i); + if (path.startsWith(ICXHeader)) continue; + if (path.startsWith(PSCHeader)) continue; + if (!this.plugin.settings.syncInternalFiles && path.startsWith(ICHeader)) continue; + documents.push(stripAllPrefixes(path)); + } + const allPaths = [...new Set([...documents, ...files])]; + let i = 0; + const incProc = () => { + i++; + if (i % 25 == 0) + Logger( + `Checking ${i}/${allPaths.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 stat = (await this.plugin.storageAccess.isExistsIncludeHidden(path)) + ? await this.plugin.storageAccess.statHidden(path) + : false; + const fileOnStorage = stat != null ? stat : false; + if (!(await this.plugin.$$isTargetFile(path))) return incProc(); + const releaser = await semaphore.acquire(1); + if (fileOnStorage && this.plugin.$$isFileSizeExceeded(fileOnStorage.size)) + return incProc(); + try { + const isHiddenFile = path.startsWith("."); + const dbPath = isHiddenFile ? addPrefix(path, ICHeader) : path; + const fileOnDB = await this.plugin.localDatabase.getDBEntry(dbPath); + if (fileOnDB && this.plugin.$$isFileSizeExceeded(fileOnDB.size)) return incProc(); + + if (!fileOnDB && fileOnStorage) { + Logger(`Compare: Not found on the local database: ${path}`, LOG_LEVEL_NOTICE); + void addResult(path, path, false); + return incProc(); + } + if (fileOnDB && !fileOnStorage) { + Logger(`Compare: Not found on the storage: ${path}`, LOG_LEVEL_NOTICE); + void addResult(path, false, fileOnDB); + return incProc(); + } + if (fileOnStorage && fileOnDB) { + await checkBetweenStorageAndDatabase(path, 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 = paneEl.createDiv({ text: "" }); + new Setting(paneEl) + .setName("Check and convert non-path-obfuscated files") + .setDesc("") + .addButton((button) => + button + .setButtonText("Perform") + .setDisabled(false) + .setWarning() + .onClick(async () => { + for await (const docName of this.plugin.localDatabase.findAllDocNames()) { + if (!docName.startsWith("f:")) { + const idEncoded = await this.plugin.$$path2id(docName as FilePathWithPrefix); + const doc = await this.plugin.localDatabase.getRaw(docName as DocumentID); + if (!doc) continue; + if (doc.type != "newnote" && doc.type != "plain") { + continue; + } + if (doc?.deleted ?? false) continue; + const newDoc = { ...doc }; + //Prepare converted data + newDoc._id = idEncoded; + newDoc.path = docName as FilePathWithPrefix; + // @ts-ignore + delete newDoc._rev; + try { + const obfuscatedDoc = await this.plugin.localDatabase.getRaw(idEncoded, { + revs_info: true, + }); + // Unfortunately we have to delete one of them. + // Just now, save it as a conflicted document. + obfuscatedDoc._revs_info?.shift(); // Drop latest revision. + const previousRev = obfuscatedDoc._revs_info?.shift(); // Use second revision. + if (previousRev) { + newDoc._rev = previousRev.rev; + } else { + //If there are no revisions, set the possibly unique one + newDoc._rev = + "1-" + + `00000000000000000000000000000000${~~(Math.random() * 1e9)}${~~(Math.random() * 1e9)}${~~(Math.random() * 1e9)}${~~(Math.random() * 1e9)}`.slice( + -32 + ); + } + const ret = await this.plugin.localDatabase.putRaw(newDoc, { force: true }); + if (ret.ok) { + Logger( + `${docName} has been converted as conflicted document`, + LOG_LEVEL_NOTICE + ); + doc._deleted = true; + if ((await this.plugin.localDatabase.putRaw(doc)).ok) { + Logger(`Old ${docName} has been deleted`, LOG_LEVEL_NOTICE); + } + await this.plugin.$$queueConflictCheckIfOpen(docName as FilePathWithPrefix); + } else { + Logger(`Converting ${docName} Failed!`, LOG_LEVEL_NOTICE); + Logger(ret, LOG_LEVEL_VERBOSE); + } + } catch (ex: any) { + if (ex?.status == 404) { + // We can perform this safely + if ((await this.plugin.localDatabase.putRaw(newDoc)).ok) { + Logger(`${docName} has been converted`, LOG_LEVEL_NOTICE); + doc._deleted = true; + if ((await this.plugin.localDatabase.putRaw(doc)).ok) { + Logger(`Old ${docName} has been deleted`, LOG_LEVEL_NOTICE); + } + } + } else { + Logger(`Something went wrong while converting ${docName}`, LOG_LEVEL_NOTICE); + Logger(ex, LOG_LEVEL_VERBOSE); + // Something wrong. + } + } + } + } + Logger(`Converting finished`, LOG_LEVEL_NOTICE); + }) + ); + }); + void addPanel(paneEl, "Reset").then((paneEl) => { + new Setting(paneEl).setName("Back to non-configured").addButton((button) => + button + .setButtonText("Back") + .setDisabled(false) + .onClick(async () => { + this.editingSettings.isConfigured = false; + await this.saveAllDirtySettings(); + this.plugin.$$askReload(); + }) + ); + + new Setting(paneEl).setName("Delete all customization sync data").addButton((button) => + button + .setButtonText("Delete") + .setDisabled(false) + .setWarning() + .onClick(async () => { + Logger(`Deleting customization sync data`, LOG_LEVEL_NOTICE); + const entriesToDelete = await this.plugin.localDatabase.allDocsRaw({ + startkey: "ix:", + endkey: "ix:\u{10ffff}", + include_docs: true, + }); + const newData = entriesToDelete.rows.map((e) => ({ + ...e.doc, + _deleted: true, + })); + const r = await this.plugin.localDatabase.bulkDocsRaw(newData as any[]); + // Do not care about the result. + Logger( + `${r.length} items have been removed, to confirm how many items are left, please perform it again.`, + LOG_LEVEL_NOTICE + ); + }) + ); + }); +} diff --git a/src/modules/features/SettingDialogue/PaneMaintenance.ts b/src/modules/features/SettingDialogue/PaneMaintenance.ts new file mode 100644 index 0000000..71f408b --- /dev/null +++ b/src/modules/features/SettingDialogue/PaneMaintenance.ts @@ -0,0 +1,374 @@ +import { LocalDatabaseMaintenance } from "../../../features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts"; +import { LOG_LEVEL_NOTICE, Logger } from "../../../lib/src/common/logger.ts"; +import { FLAGMD_REDFLAG, FLAGMD_REDFLAG2_HR, FLAGMD_REDFLAG3_HR } from "../../../lib/src/common/types.ts"; +import { fireAndForget } from "../../../lib/src/common/utils.ts"; +import { LiveSyncCouchDBReplicator } from "../../../lib/src/replication/couchdb/LiveSyncReplicator.ts"; +import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; +import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab"; +import { visibleOnly, type PageFunctions } from "./SettingPane"; +export function paneMaintenance( + this: ObsidianLiveSyncSettingTab, + paneEl: HTMLElement, + { addPanel }: PageFunctions +): void { + const isRemoteLockedAndDeviceNotAccepted = () => this.plugin?.replicator?.remoteLockedAndDeviceNotAccepted; + const isRemoteLocked = () => this.plugin?.replicator?.remoteLocked; + // if (this.plugin?.replicator?.remoteLockedAndDeviceNotAccepted) { + this.createEl( + paneEl, + "div", + { + text: "The remote database is locked for synchronization to prevent vault corruption because this device isn't marked as 'resolved'. Please backup your vault, reset the local database, and select 'Mark this device as resolved'. This warning will persist until the device is confirmed as resolved by replication.", + cls: "op-warn", + }, + (c) => { + this.createEl( + c, + "button", + { + text: "I've made a backup, mark this device 'resolved'", + cls: "mod-warning", + }, + (e) => { + e.addEventListener("click", () => { + fireAndForget(async () => { + await this.plugin.$$markRemoteResolved(); + this.display(); + }); + }); + } + ); + }, + visibleOnly(isRemoteLockedAndDeviceNotAccepted) + ); + this.createEl( + paneEl, + "div", + { + text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization. (This device is marked 'resolved') When all your devices are marked 'resolved', unlock the database. This warning kept showing until confirming the device is resolved by the replication", + cls: "op-warn", + }, + (c) => + this.createEl( + c, + "button", + { + text: "I'm ready, unlock the database", + cls: "mod-warning", + }, + (e) => { + e.addEventListener("click", () => { + fireAndForget(async () => { + await this.plugin.$$markRemoteUnlocked(); + this.display(); + }); + }); + } + ), + visibleOnly(isRemoteLocked) + ); + + void addPanel(paneEl, "Scram!").then((paneEl) => { + new Setting(paneEl) + .setName("Lock Server") + .setDesc("Lock the remote server to prevent synchronization with other devices.") + .addButton((button) => + button + .setButtonText("Lock") + .setDisabled(false) + .setWarning() + .onClick(async () => { + await this.plugin.$$markRemoteLocked(); + }) + ) + .addOnUpdate(this.onlyOnCouchDBOrMinIO); + + new Setting(paneEl) + .setName("Emergency restart") + .setDesc("Disables all synchronization and restart.") + .addButton((button) => + button + .setButtonText("Flag and restart") + .setDisabled(false) + .setWarning() + .onClick(async () => { + await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG, ""); + this.plugin.$$performRestart(); + }) + ); + }); + + void addPanel(paneEl, "Syncing", () => {}, this.onlyOnCouchDBOrMinIO).then((paneEl) => { + new Setting(paneEl) + .setName("Resend") + .setDesc("Resend all chunks to the remote.") + .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); + } + }) + ) + .addOnUpdate(this.onlyOnCouchDB); + + new Setting(paneEl) + .setName("Reset journal received history") + .setDesc( + "Initialise journal received history. On the next sync, every item except this device sent will be downloaded again." + ) + .addButton((button) => + button + .setButtonText("Reset received") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.getMinioJournalSyncClient().updateCheckPointInfo((info) => ({ + ...info, + receivedFiles: new Set(), + knownIDs: new Set(), + })); + Logger(`Journal received history has been cleared.`, LOG_LEVEL_NOTICE); + }) + ) + .addOnUpdate(this.onlyOnMinIO); + + new Setting(paneEl) + .setName("Reset journal sent history") + .setDesc( + "Initialise journal sent history. On the next sync, every item except this device received will be sent again." + ) + .addButton((button) => + button + .setButtonText("Reset sent history") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.getMinioJournalSyncClient().updateCheckPointInfo((info) => ({ + ...info, + lastLocalSeq: 0, + sentIDs: new Set(), + sentFiles: new Set(), + })); + Logger(`Journal sent history has been cleared.`, LOG_LEVEL_NOTICE); + }) + ) + .addOnUpdate(this.onlyOnMinIO); + }); + void addPanel(paneEl, "Garbage Collection (Beta)", (e) => e, this.onlyOnP2POrCouchDB).then((paneEl) => { + new Setting(paneEl) + .setName("Remove all orphaned chunks") + .setDesc("Remove all orphaned chunks from the local database.") + .addButton((button) => + button + .setButtonText("Remove") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.plugin + .getAddOn(LocalDatabaseMaintenance.name) + ?.removeUnusedChunks(); + }) + ); + + new Setting(paneEl) + .setName("Resurrect deleted chunks") + .setDesc( + "If you have deleted chunks before fully synchronised and missed some chunks, you possibly can resurrect them." + ) + .addButton((button) => + button + .setButtonText("Try resurrect") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.plugin + .getAddOn(LocalDatabaseMaintenance.name) + ?.resurrectChunks(); + }) + ); + new Setting(paneEl) + .setName("Commit File Deletion") + .setDesc("Completely delete all deleted documents from the local database.") + .addButton((button) => + button + .setButtonText("Delete") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.plugin + .getAddOn(LocalDatabaseMaintenance.name) + ?.commitFileDeletion(); + }) + ); + }); + void addPanel(paneEl, "Rebuilding Operations (Local)").then((paneEl) => { + new Setting(paneEl) + .setName("Fetch from remote") + .setDesc("Restore or reconstruct local database from remote.") + .addButton((button) => + button + .setButtonText("Fetch") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG3_HR, ""); + this.plugin.$$performRestart(); + }) + ) + .addButton((button) => + button + .setButtonText("Fetch w/o restarting") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.rebuildDB("localOnly"); + }) + ); + + new Setting(paneEl) + .setName("Fetch rebuilt DB (Save local documents before)") + .setDesc("Restore or reconstruct local database from remote database but use local chunks.") + .addButton((button) => + button + .setButtonText("Save and Fetch") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.rebuildDB("localOnlyWithChunks"); + }) + ) + .addOnUpdate(this.onlyOnCouchDB); + }); + + void addPanel(paneEl, "Total Overhaul", () => {}, this.onlyOnCouchDBOrMinIO).then((paneEl) => { + new Setting(paneEl) + .setName("Rebuild everything") + .setDesc("Rebuild local and remote database with local files.") + .addButton((button) => + button + .setButtonText("Rebuild") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG2_HR, ""); + this.plugin.$$performRestart(); + }) + ) + .addButton((button) => + button + .setButtonText("Rebuild w/o restarting") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.rebuildDB("rebuildBothByThisDevice"); + }) + ); + }); + void addPanel(paneEl, "Rebuilding Operations (Remote Only)", () => {}, this.onlyOnCouchDBOrMinIO).then((paneEl) => { + new Setting(paneEl) + .setName("Perform cleanup") + .setDesc( + "Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client." + ) + .addButton((button) => + button + .setButtonText("Perform") + .setDisabled(false) + .onClick(async () => { + const replicator = this.plugin.replicator as LiveSyncCouchDBReplicator; + Logger(`Cleanup has been began`, LOG_LEVEL_NOTICE, "compaction"); + if (await replicator.compactRemote(this.editingSettings)) { + Logger(`Cleanup has been completed!`, LOG_LEVEL_NOTICE, "compaction"); + } else { + Logger(`Cleanup has been failed!`, LOG_LEVEL_NOTICE, "compaction"); + } + }) + ) + .addOnUpdate(this.onlyOnCouchDB); + + new Setting(paneEl) + .setName("Overwrite remote") + .setDesc("Overwrite remote with local DB and passphrase.") + .addButton((button) => + button + .setButtonText("Send") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.rebuildDB("remoteOnly"); + }) + ); + + new Setting(paneEl) + .setName("Reset all journal counter") + .setDesc("Initialise all journal history, On the next sync, every item will be received and sent.") + .addButton((button) => + button + .setButtonText("Reset all") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.getMinioJournalSyncClient().resetCheckpointInfo(); + Logger(`Journal exchange history has been cleared.`, LOG_LEVEL_NOTICE); + }) + ) + .addOnUpdate(this.onlyOnMinIO); + + new Setting(paneEl) + .setName("Purge all journal counter") + .setDesc("Purge all download/upload cache.") + .addButton((button) => + button + .setButtonText("Reset all") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.getMinioJournalSyncClient().resetAllCaches(); + Logger(`Journal download/upload cache has been cleared.`, LOG_LEVEL_NOTICE); + }) + ) + .addOnUpdate(this.onlyOnMinIO); + + new Setting(paneEl) + .setName("Fresh Start Wipe") + .setDesc("Delete all data on the remote server.") + .addButton((button) => + button + .setButtonText("Delete") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.getMinioJournalSyncClient().updateCheckPointInfo((info) => ({ + ...info, + receivedFiles: new Set(), + knownIDs: new Set(), + lastLocalSeq: 0, + sentIDs: new Set(), + sentFiles: new Set(), + })); + await this.resetRemoteBucket(); + Logger(`Deleted all data on remote server`, LOG_LEVEL_NOTICE); + }) + ) + .addOnUpdate(this.onlyOnMinIO); + }); + + void addPanel(paneEl, "Reset").then((paneEl) => { + new Setting(paneEl) + .setName("Delete local database to reset or uninstall Self-hosted LiveSync") + .addButton((button) => + button + .setButtonText("Delete") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.plugin.$$resetLocalDatabase(); + await this.plugin.$$initializeDatabase(); + }) + ); + }); +} diff --git a/src/modules/features/SettingDialogue/PanePatches.ts b/src/modules/features/SettingDialogue/PanePatches.ts new file mode 100644 index 0000000..eed7c5c --- /dev/null +++ b/src/modules/features/SettingDialogue/PanePatches.ts @@ -0,0 +1,82 @@ +import { type HashAlgorithm, LOG_LEVEL_NOTICE } from "../../../lib/src/common/types.ts"; +import { Logger } from "../../../lib/src/common/logger.ts"; +import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; +import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; +import type { PageFunctions } from "./SettingPane.ts"; +import { visibleOnly } from "./SettingPane.ts"; + +export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void { + void addPanel(paneEl, "Compatibility (Metadata)").then((paneEl) => { + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("deleteMetadataOfDeletedFiles"); + + new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("automaticallyDeleteMetadataOfDeletedFiles", { + onUpdate: visibleOnly(() => this.isConfiguredAs("deleteMetadataOfDeletedFiles", true)), + }); + }); + + void addPanel(paneEl, "Compatibility (Conflict Behaviour)").then((paneEl) => { + paneEl.addClass("wizardHidden"); + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("disableMarkdownAutoMerge"); + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("writeDocumentsIfConflicted"); + }); + + void addPanel(paneEl, "Compatibility (Database structure)").then((paneEl) => { + new Setting(paneEl).autoWireToggle("useIndexedDBAdapter", { invert: true, holdValue: true }); + + new Setting(paneEl) + .autoWireToggle("doNotUseFixedRevisionForChunks", { holdValue: true }) + .setClass("wizardHidden"); + new Setting(paneEl).autoWireToggle("handleFilenameCaseSensitive", { holdValue: true }).setClass("wizardHidden"); + + this.addOnSaved("useIndexedDBAdapter", async () => { + await this.saveAllDirtySettings(); + await this.rebuildDB("localOnly"); + }); + }); + + void addPanel(paneEl, "Compatibility (Internal API Usage)").then((paneEl) => { + new Setting(paneEl).autoWireToggle("watchInternalFileChanges", { invert: true }); + }); + + void addPanel(paneEl, "Edge case addressing (Database)").then((paneEl) => { + new Setting(paneEl) + .autoWireText("additionalSuffixOfDatabaseName", { holdValue: true }) + .addApplyButton(["additionalSuffixOfDatabaseName"]); + + this.addOnSaved("additionalSuffixOfDatabaseName", async (key) => { + Logger("Suffix has been changed. Reopening database...", LOG_LEVEL_NOTICE); + await this.plugin.$$initializeDatabase(); + }); + + new Setting(paneEl).autoWireDropDown("hashAlg", { + options: { + "": "Old Algorithm", + xxhash32: "xxhash32 (Fast but less collision resistance)", + xxhash64: "xxhash64 (Fastest)", + "mixed-purejs": "PureJS fallback (Fast, W/O WebAssembly)", + sha1: "Older fallback (Slow, W/O WebAssembly)", + } as Record, + }); + this.addOnSaved("hashAlg", async () => { + await this.plugin.localDatabase._prepareHashFunctions(); + }); + }); + void addPanel(paneEl, "Edge case addressing (Behaviour)").then((paneEl) => { + new Setting(paneEl).autoWireToggle("doNotSuspendOnFetching"); + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("doNotDeleteFolder"); + }); + + void addPanel(paneEl, "Edge case addressing (Processing)").then((paneEl) => { + new Setting(paneEl).autoWireToggle("disableWorkerForGeneratingChunks"); + + new Setting(paneEl).autoWireToggle("processSmallFilesInUIThread", { + onUpdate: visibleOnly(() => this.isConfiguredAs("disableWorkerForGeneratingChunks", false)), + }); + }); + // void addPanel(paneEl, "Edge case addressing (Networking)").then((paneEl) => { + // new Setting(paneEl).autoWireToggle("useRequestAPI"); + // }); + void addPanel(paneEl, "Compatibility (Trouble addressed)").then((paneEl) => { + new Setting(paneEl).autoWireToggle("disableCheckingConfigMismatch"); + }); +} diff --git a/src/modules/features/SettingDialogue/PanePowerUsers.ts b/src/modules/features/SettingDialogue/PanePowerUsers.ts new file mode 100644 index 0000000..4d6edd5 --- /dev/null +++ b/src/modules/features/SettingDialogue/PanePowerUsers.ts @@ -0,0 +1,71 @@ +import { type ConfigPassphraseStore } from "../../../lib/src/common/types.ts"; +import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; +import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; +import type { PageFunctions } from "./SettingPane.ts"; +import { visibleOnly } from "./SettingPane.ts"; +export function panePowerUsers( + this: ObsidianLiveSyncSettingTab, + paneEl: HTMLElement, + { addPanel }: PageFunctions +): void { + void addPanel(paneEl, "Remote Database Tweak").then((paneEl) => { + new Setting(paneEl).autoWireToggle("useEden").setClass("wizardHidden"); + const onlyUsingEden = visibleOnly(() => this.isConfiguredAs("useEden", true)); + new Setting(paneEl).autoWireNumeric("maxChunksInEden", { onUpdate: onlyUsingEden }).setClass("wizardHidden"); + new Setting(paneEl) + .autoWireNumeric("maxTotalLengthInEden", { onUpdate: onlyUsingEden }) + .setClass("wizardHidden"); + new Setting(paneEl).autoWireNumeric("maxAgeInEden", { onUpdate: onlyUsingEden }).setClass("wizardHidden"); + + new Setting(paneEl).autoWireToggle("enableCompression").setClass("wizardHidden"); + }); + + void addPanel(paneEl, "CouchDB Connection Tweak", undefined, this.onlyOnCouchDB).then((paneEl) => { + paneEl.addClass("wizardHidden"); + + this.createEl( + paneEl, + "div", + { + text: `If you reached the payload size limit when using IBM Cloudant, please decrease batch size and batch limit to a lower value.`, + }, + undefined, + this.onlyOnCouchDB + ).addClass("wizardHidden"); + + new Setting(paneEl) + .setClass("wizardHidden") + .autoWireNumeric("batch_size", { clampMin: 2, onUpdate: this.onlyOnCouchDB }); + new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("batches_limit", { + clampMin: 2, + onUpdate: this.onlyOnCouchDB, + }); + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("useTimeouts", { onUpdate: this.onlyOnCouchDB }); + }); + void addPanel(paneEl, "Configuration Encryption").then((paneEl) => { + const passphrase_options: Record = { + "": "Default", + LOCALSTORAGE: "Use a custom passphrase", + ASK_AT_LAUNCH: "Ask an passphrase at every launch", + }; + + new Setting(paneEl) + .setName("Encrypting sensitive configuration items") + .autoWireDropDown("configPassphraseStore", { + options: passphrase_options, + holdValue: true, + }) + .setClass("wizardHidden"); + + new Setting(paneEl) + .autoWireText("configPassphrase", { isPassword: true, holdValue: true }) + .setClass("wizardHidden") + .addOnUpdate(() => ({ + disabled: !this.isConfiguredAs("configPassphraseStore", "LOCALSTORAGE"), + })); + new Setting(paneEl).addApplyButton(["configPassphrase", "configPassphraseStore"]).setClass("wizardHidden"); + }); + void addPanel(paneEl, "Developer").then((paneEl) => { + new Setting(paneEl).autoWireToggle("enableDebugTools").setClass("wizardHidden"); + }); +} diff --git a/src/modules/features/SettingDialogue/PaneRemoteConfig.ts b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts new file mode 100644 index 0000000..83adcfe --- /dev/null +++ b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts @@ -0,0 +1,708 @@ +import { MarkdownRenderer } from "../../../deps.ts"; +import { + LOG_LEVEL_NOTICE, + LOG_LEVEL_VERBOSE, + PREFERRED_JOURNAL_SYNC, + PREFERRED_SETTING_CLOUDANT, + PREFERRED_SETTING_SELF_HOSTED, + REMOTE_COUCHDB, + REMOTE_MINIO, + REMOTE_P2P, +} from "../../../lib/src/common/types.ts"; +import { parseHeaderValues } from "../../../lib/src/common/utils.ts"; +import { LOG_LEVEL_INFO, Logger } from "../../../lib/src/common/logger.ts"; +import { isCloudantURI } from "../../../lib/src/pouchdb/utils_couchdb.ts"; +import { requestToCouchDBWithCredentials } from "../../../common/utils.ts"; +import { $msg } from "../../../lib/src/common/i18n.ts"; +import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; +import { fireAndForget } from "octagonal-wheels/promises"; +import { generateCredentialObject } from "../../../lib/src/replication/httplib.ts"; +import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; +import type { PageFunctions } from "./SettingPane.ts"; +import { combineOnUpdate, visibleOnly } from "./SettingPane.ts"; +import { getWebCrypto } from "../../../lib/src/mods.ts"; +import { arrayBufferToBase64Single } from "../../../lib/src/string_and_binary/convert.ts"; +export function paneRemoteConfig( + this: ObsidianLiveSyncSettingTab, + paneEl: HTMLElement, + { addPanel, addPane }: PageFunctions +): void { + let checkResultDiv: HTMLDivElement; + const checkConfig = async (checkResultDiv: HTMLDivElement | undefined) => { + Logger($msg("obsidianLiveSyncSettingTab.logCheckingDbConfig"), LOG_LEVEL_INFO); + let isSuccessful = true; + const emptyDiv = createDiv(); + emptyDiv.innerHTML = ""; + checkResultDiv?.replaceChildren(...[emptyDiv]); + const addResult = (msg: string, classes?: string[]) => { + const tmpDiv = createDiv(); + tmpDiv.addClass("ob-btn-config-fix"); + if (classes) { + tmpDiv.addClasses(classes); + } + tmpDiv.innerHTML = `${msg}`; + checkResultDiv?.appendChild(tmpDiv); + }; + try { + if (isCloudantURI(this.editingSettings.couchDB_URI)) { + Logger($msg("obsidianLiveSyncSettingTab.logCannotUseCloudant"), LOG_LEVEL_NOTICE); + return; + } + // Tip: Add log for cloudant as Logger($msg("obsidianLiveSyncSettingTab.logServerConfigurationCheck")); + const customHeaders = parseHeaderValues(this.editingSettings.couchDB_CustomHeaders); + const credential = generateCredentialObject(this.editingSettings); + const r = await requestToCouchDBWithCredentials( + this.editingSettings.couchDB_URI, + credential, + window.origin, + undefined, + undefined, + undefined, + customHeaders + ); + const responseConfig = r.json; + + const addConfigFixButton = (title: string, key: string, value: string) => { + if (!checkResultDiv) return; + const tmpDiv = createDiv(); + tmpDiv.addClass("ob-btn-config-fix"); + tmpDiv.innerHTML = ``; + const x = checkResultDiv.appendChild(tmpDiv); + x.querySelector("button")?.addEventListener("click", () => { + fireAndForget(async () => { + Logger($msg("obsidianLiveSyncSettingTab.logCouchDbConfigSet", { title, key, value })); + const res = await requestToCouchDBWithCredentials( + this.editingSettings.couchDB_URI, + credential, + undefined, + key, + value, + undefined, + customHeaders + ); + if (res.status == 200) { + Logger( + $msg("obsidianLiveSyncSettingTab.logCouchDbConfigUpdated", { title }), + LOG_LEVEL_NOTICE + ); + checkResultDiv.removeChild(x); + await checkConfig(checkResultDiv); + } else { + Logger( + $msg("obsidianLiveSyncSettingTab.logCouchDbConfigFail", { title }), + LOG_LEVEL_NOTICE + ); + Logger(res.text, LOG_LEVEL_VERBOSE); + } + }); + }); + }; + addResult($msg("obsidianLiveSyncSettingTab.msgNotice"), ["ob-btn-config-head"]); + addResult($msg("obsidianLiveSyncSettingTab.msgIfConfigNotPersistent"), ["ob-btn-config-info"]); + addResult($msg("obsidianLiveSyncSettingTab.msgConfigCheck"), ["ob-btn-config-head"]); + + // Admin check + // for database creation and deletion + if (!(this.editingSettings.couchDB_USER in responseConfig.admins)) { + addResult($msg("obsidianLiveSyncSettingTab.warnNoAdmin")); + } else { + addResult($msg("obsidianLiveSyncSettingTab.okAdminPrivileges")); + } + // HTTP user-authorization check + if (responseConfig?.chttpd?.require_valid_user != "true") { + isSuccessful = false; + addResult($msg("obsidianLiveSyncSettingTab.errRequireValidUser")); + addConfigFixButton( + $msg("obsidianLiveSyncSettingTab.msgSetRequireValidUser"), + "chttpd/require_valid_user", + "true" + ); + } else { + addResult($msg("obsidianLiveSyncSettingTab.okRequireValidUser")); + } + if (responseConfig?.chttpd_auth?.require_valid_user != "true") { + isSuccessful = false; + addResult($msg("obsidianLiveSyncSettingTab.errRequireValidUserAuth")); + addConfigFixButton( + $msg("obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth"), + "chttpd_auth/require_valid_user", + "true" + ); + } else { + addResult($msg("obsidianLiveSyncSettingTab.okRequireValidUserAuth")); + } + // HTTPD check + // Check Authentication header + if (!responseConfig?.httpd["WWW-Authenticate"]) { + isSuccessful = false; + addResult($msg("obsidianLiveSyncSettingTab.errMissingWwwAuth")); + addConfigFixButton( + $msg("obsidianLiveSyncSettingTab.msgSetWwwAuth"), + "httpd/WWW-Authenticate", + 'Basic realm="couchdb"' + ); + } else { + addResult($msg("obsidianLiveSyncSettingTab.okWwwAuth")); + } + if (responseConfig?.httpd?.enable_cors != "true") { + isSuccessful = false; + addResult($msg("obsidianLiveSyncSettingTab.errEnableCors")); + addConfigFixButton($msg("obsidianLiveSyncSettingTab.msgEnableCors"), "httpd/enable_cors", "true"); + } else { + addResult($msg("obsidianLiveSyncSettingTab.okEnableCors")); + } + // If the server is not cloudant, configure request size + if (!isCloudantURI(this.editingSettings.couchDB_URI)) { + // REQUEST SIZE + if (Number(responseConfig?.chttpd?.max_http_request_size ?? 0) < 4294967296) { + isSuccessful = false; + addResult($msg("obsidianLiveSyncSettingTab.errMaxRequestSize")); + addConfigFixButton( + $msg("obsidianLiveSyncSettingTab.msgSetMaxRequestSize"), + "chttpd/max_http_request_size", + "4294967296" + ); + } else { + addResult($msg("obsidianLiveSyncSettingTab.okMaxRequestSize")); + } + if (Number(responseConfig?.couchdb?.max_document_size ?? 0) < 50000000) { + isSuccessful = false; + addResult($msg("obsidianLiveSyncSettingTab.errMaxDocumentSize")); + addConfigFixButton( + $msg("obsidianLiveSyncSettingTab.msgSetMaxDocSize"), + "couchdb/max_document_size", + "50000000" + ); + } else { + addResult($msg("obsidianLiveSyncSettingTab.okMaxDocumentSize")); + } + } + // CORS check + // checking connectivity for mobile + if (responseConfig?.cors?.credentials != "true") { + isSuccessful = false; + addResult($msg("obsidianLiveSyncSettingTab.errCorsCredentials")); + addConfigFixButton( + $msg("obsidianLiveSyncSettingTab.msgSetCorsCredentials"), + "cors/credentials", + "true" + ); + } else { + addResult($msg("obsidianLiveSyncSettingTab.okCorsCredentials")); + } + const ConfiguredOrigins = ((responseConfig?.cors?.origins ?? "") + "").split(","); + if ( + responseConfig?.cors?.origins == "*" || + (ConfiguredOrigins.indexOf("app://obsidian.md") !== -1 && + ConfiguredOrigins.indexOf("capacitor://localhost") !== -1 && + ConfiguredOrigins.indexOf("http://localhost") !== -1) + ) { + addResult($msg("obsidianLiveSyncSettingTab.okCorsOrigins")); + } else { + addResult($msg("obsidianLiveSyncSettingTab.errCorsOrigins")); + addConfigFixButton( + $msg("obsidianLiveSyncSettingTab.msgSetCorsOrigins"), + "cors/origins", + "app://obsidian.md,capacitor://localhost,http://localhost" + ); + isSuccessful = false; + } + addResult($msg("obsidianLiveSyncSettingTab.msgConnectionCheck"), ["ob-btn-config-head"]); + addResult($msg("obsidianLiveSyncSettingTab.msgCurrentOrigin", { origin: window.location.origin })); + + // Request header check + const origins = ["app://obsidian.md", "capacitor://localhost", "http://localhost"]; + for (const org of origins) { + const rr = await requestToCouchDBWithCredentials( + this.editingSettings.couchDB_URI, + credential, + org, + undefined, + undefined, + undefined, + customHeaders + ); + const responseHeaders = Object.fromEntries( + Object.entries(rr.headers).map((e) => { + e[0] = `${e[0]}`.toLowerCase(); + return e; + }) + ); + addResult($msg("obsidianLiveSyncSettingTab.msgOriginCheck", { org })); + if (responseHeaders["access-control-allow-credentials"] != "true") { + addResult($msg("obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials")); + isSuccessful = false; + } else { + addResult($msg("obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin")); + } + if (responseHeaders["access-control-allow-origin"] != org) { + addResult( + $msg("obsidianLiveSyncSettingTab.warnCorsOriginUnmatched", { + from: origin, + to: responseHeaders["access-control-allow-origin"], + }) + ); + } else { + addResult($msg("obsidianLiveSyncSettingTab.okCorsOriginMatched")); + } + } + addResult($msg("obsidianLiveSyncSettingTab.msgDone"), ["ob-btn-config-head"]); + addResult($msg("obsidianLiveSyncSettingTab.msgConnectionProxyNote"), ["ob-btn-config-info"]); + Logger($msg("obsidianLiveSyncSettingTab.logCheckingConfigDone"), LOG_LEVEL_INFO); + } catch (ex: any) { + if (ex?.status == 401) { + isSuccessful = false; + addResult($msg("obsidianLiveSyncSettingTab.errAccessForbidden")); + addResult($msg("obsidianLiveSyncSettingTab.errCannotContinueTest")); + Logger($msg("obsidianLiveSyncSettingTab.logCheckingConfigDone"), LOG_LEVEL_INFO); + } else { + Logger($msg("obsidianLiveSyncSettingTab.logCheckingConfigFailed"), LOG_LEVEL_NOTICE); + Logger(ex); + isSuccessful = false; + } + } + return isSuccessful; + }; + void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleRemoteServer")).then((paneEl) => { + // const containerRemoteDatabaseEl = containerEl.createDiv(); + this.createEl( + paneEl, + "div", + { + text: $msg("obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync"), + }, + undefined, + visibleOnly(() => this.isAnySyncEnabled()) + ).addClass("op-warn-info"); + new Setting(paneEl).autoWireDropDown("remoteType", { + holdValue: true, + options: { + [REMOTE_COUCHDB]: $msg("obsidianLiveSyncSettingTab.optionCouchDB"), + [REMOTE_MINIO]: $msg("obsidianLiveSyncSettingTab.optionMinioS3R2"), + [REMOTE_P2P]: "Only Peer-to-Peer", + }, + onUpdate: this.enableOnlySyncDisabled, + }); + void addPanel(paneEl, "Peer-to-Peer", undefined, this.onlyOnOnlyP2P).then((paneEl) => { + const syncWarnP2P = this.createEl(paneEl, "div", { + text: "", + }); + const p2pMessage = `This feature is a Work In Progress, and configurable on \`P2P Replicator\` Pane. +The pane also can be launched by \`P2P Replicator\` command from the Command Palette. +`; + + void MarkdownRenderer.render(this.plugin.app, p2pMessage, syncWarnP2P, "/", this.plugin); + syncWarnP2P.addClass("op-warn-info"); + new Setting(paneEl).setName("Apply Settings").setClass("wizardHidden").addApplyButton(["remoteType"]); + // .addOnUpdate(onlyOnMinIO); + // new Setting(paneEl).addButton((button) => + // button + // .setButtonText("Open P2P Replicator") + // .onClick(() => { + // const addOn = this.plugin.getAddOn(P2PReplicator.name); + // void addOn?.openPane(); + // this.closeSetting(); + // }) + // ); + }); + void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleMinioS3R2"), undefined, this.onlyOnMinIO).then( + (paneEl) => { + const syncWarnMinio = this.createEl(paneEl, "div", { + text: "", + }); + const ObjectStorageMessage = $msg("obsidianLiveSyncSettingTab.msgObjectStorageWarning"); + + void MarkdownRenderer.render(this.plugin.app, ObjectStorageMessage, syncWarnMinio, "/", this.plugin); + syncWarnMinio.addClass("op-warn-info"); + + new Setting(paneEl).autoWireText("endpoint", { holdValue: true }); + new Setting(paneEl).autoWireText("accessKey", { holdValue: true }); + + new Setting(paneEl).autoWireText("secretKey", { + holdValue: true, + isPassword: true, + }); + + new Setting(paneEl).autoWireText("region", { holdValue: true }); + + new Setting(paneEl).autoWireText("bucket", { holdValue: true }); + new Setting(paneEl).autoWireText("bucketPrefix", { + holdValue: true, + placeHolder: "vaultname/", + }); + + new Setting(paneEl).autoWireToggle("useCustomRequestHandler", { holdValue: true }); + new Setting(paneEl).autoWireTextArea("bucketCustomHeaders", { + holdValue: true, + placeHolder: "x-custom-header: value\n x-custom-header2: value2", + }); + new Setting(paneEl).setName($msg("obsidianLiveSyncSettingTab.nameTestConnection")).addButton((button) => + button + .setButtonText($msg("obsidianLiveSyncSettingTab.btnTest")) + .setDisabled(false) + .onClick(async () => { + await this.testConnection(this.editingSettings); + }) + ); + new Setting(paneEl) + .setName($msg("obsidianLiveSyncSettingTab.nameApplySettings")) + .setClass("wizardHidden") + .addApplyButton([ + "remoteType", + "endpoint", + "region", + "accessKey", + "secretKey", + "bucket", + "useCustomRequestHandler", + "bucketCustomHeaders", + "bucketPrefix", + ]) + .addOnUpdate(this.onlyOnMinIO); + } + ); + + void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleCouchDB"), undefined, this.onlyOnCouchDB).then( + (paneEl) => { + if (this.plugin.$$isMobile()) { + this.createEl( + paneEl, + "div", + { + text: $msg("obsidianLiveSyncSettingTab.msgNonHTTPSWarning"), + }, + undefined, + visibleOnly(() => !this.editingSettings.couchDB_URI.startsWith("https://")) + ).addClass("op-warn"); + } else { + this.createEl( + paneEl, + "div", + { + text: $msg("obsidianLiveSyncSettingTab.msgNonHTTPSInfo"), + }, + undefined, + visibleOnly(() => !this.editingSettings.couchDB_URI.startsWith("https://")) + ).addClass("op-warn-info"); + } + + new Setting(paneEl).autoWireText("couchDB_URI", { + holdValue: true, + onUpdate: this.enableOnlySyncDisabled, + }); + new Setting(paneEl).autoWireToggle("useJWT", { + holdValue: true, + onUpdate: this.enableOnlySyncDisabled, + }); + new Setting(paneEl).autoWireText("couchDB_USER", { + holdValue: true, + onUpdate: combineOnUpdate( + this.enableOnlySyncDisabled, + visibleOnly(() => !this.editingSettings.useJWT) + ), + }); + new Setting(paneEl).autoWireText("couchDB_PASSWORD", { + holdValue: true, + isPassword: true, + onUpdate: combineOnUpdate( + this.enableOnlySyncDisabled, + visibleOnly(() => !this.editingSettings.useJWT) + ), + }); + const algorithms = { + ["HS256"]: "HS256", + ["HS512"]: "HS512", + ["ES256"]: "ES256", + ["ES512"]: "ES512", + } as const; + new Setting(paneEl).autoWireDropDown("jwtAlgorithm", { + options: algorithms, + onUpdate: combineOnUpdate( + this.enableOnlySyncDisabled, + visibleOnly(() => this.editingSettings.useJWT) + ), + }); + new Setting(paneEl).autoWireTextArea("jwtKey", { + holdValue: true, + onUpdate: combineOnUpdate( + this.enableOnlySyncDisabled, + visibleOnly(() => this.editingSettings.useJWT) + ), + }); + // eslint-disable-next-line prefer-const + let generatedKeyDivEl: HTMLDivElement; + new Setting(paneEl) + .setDesc("Generate ES256 Keypair for testing") + .addButton((button) => + button.setButtonText("Generate").onClick(async () => { + const crypto = await getWebCrypto(); + const keyPair = await crypto.subtle.generateKey( + { name: "ECDSA", namedCurve: "P-256" }, + true, + ["sign", "verify"] + ); + const pubKey = await crypto.subtle.exportKey("spki", keyPair.publicKey); + const privateKey = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey); + const encodedPublicKey = await arrayBufferToBase64Single(pubKey); + const encodedPrivateKey = await arrayBufferToBase64Single(privateKey); + + const privateKeyPem = `> -----BEGIN PRIVATE KEY-----\n> ${encodedPrivateKey}\n> -----END PRIVATE KEY-----`; + const publicKeyPem = `> -----BEGIN PUBLIC KEY-----\\n${encodedPublicKey}\\n-----END PUBLIC KEY-----`; + + const title = $msg("Setting.GenerateKeyPair.Title"); + const msg = $msg("Setting.GenerateKeyPair.Desc", { + public_key: publicKeyPem, + private_key: privateKeyPem, + }); + await MarkdownRenderer.render( + this.plugin.app, + "## " + title + "\n\n" + msg, + generatedKeyDivEl, + "/", + this.plugin + ); + }) + ) + .addOnUpdate( + combineOnUpdate( + this.enableOnlySyncDisabled, + visibleOnly(() => this.editingSettings.useJWT) + ) + ); + generatedKeyDivEl = this.createEl( + paneEl, + "div", + { text: "" }, + (el) => {}, + visibleOnly(() => this.editingSettings.useJWT) + ); + + new Setting(paneEl).autoWireText("jwtKid", { + holdValue: true, + onUpdate: combineOnUpdate( + this.enableOnlySyncDisabled, + visibleOnly(() => this.editingSettings.useJWT) + ), + }); + new Setting(paneEl).autoWireText("jwtSub", { + holdValue: true, + onUpdate: combineOnUpdate( + this.enableOnlySyncDisabled, + visibleOnly(() => this.editingSettings.useJWT) + ), + }); + new Setting(paneEl).autoWireNumeric("jwtExpDuration", { + holdValue: true, + onUpdate: combineOnUpdate( + this.enableOnlySyncDisabled, + visibleOnly(() => this.editingSettings.useJWT) + ), + }); + new Setting(paneEl).autoWireText("couchDB_DBNAME", { + holdValue: true, + onUpdate: this.enableOnlySyncDisabled, + }); + new Setting(paneEl).autoWireTextArea("couchDB_CustomHeaders", { holdValue: true }); + new Setting(paneEl).autoWireToggle("useRequestAPI", { + holdValue: true, + onUpdate: this.enableOnlySyncDisabled, + }); + new Setting(paneEl) + .setName($msg("obsidianLiveSyncSettingTab.nameTestDatabaseConnection")) + .setClass("wizardHidden") + .setDesc($msg("obsidianLiveSyncSettingTab.descTestDatabaseConnection")) + .addButton((button) => + button + .setButtonText($msg("obsidianLiveSyncSettingTab.btnTest")) + .setDisabled(false) + .onClick(async () => { + await this.testConnection(); + }) + ); + + new Setting(paneEl) + .setName($msg("obsidianLiveSyncSettingTab.nameValidateDatabaseConfig")) + .setDesc($msg("obsidianLiveSyncSettingTab.descValidateDatabaseConfig")) + .addButton((button) => + button + .setButtonText($msg("obsidianLiveSyncSettingTab.btnCheck")) + .setDisabled(false) + .onClick(async () => { + await checkConfig(checkResultDiv); + }) + ); + checkResultDiv = this.createEl(paneEl, "div", { + text: "", + }); + + new Setting(paneEl) + .setName($msg("obsidianLiveSyncSettingTab.nameApplySettings")) + .setClass("wizardHidden") + .addApplyButton([ + "remoteType", + "couchDB_URI", + "couchDB_USER", + "couchDB_PASSWORD", + "couchDB_DBNAME", + "jwtAlgorithm", + "jwtExpDuration", + "jwtKey", + "jwtSub", + "jwtKid", + "useJWT", + "couchDB_CustomHeaders", + "useRequestAPI", + ]) + .addOnUpdate(this.onlyOnCouchDB); + } + ); + }); + void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleNotification"), () => {}, this.onlyOnCouchDB).then( + (paneEl) => { + paneEl.addClass("wizardHidden"); + new Setting(paneEl).autoWireNumeric("notifyThresholdOfRemoteStorageSize", {}).setClass("wizardHidden"); + } + ); + + void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.panelPrivacyEncryption")).then((paneEl) => { + new Setting(paneEl).autoWireToggle("encrypt", { holdValue: true }); + + const isEncryptEnabled = visibleOnly(() => this.isConfiguredAs("encrypt", true)); + + new Setting(paneEl).autoWireText("passphrase", { + holdValue: true, + isPassword: true, + onUpdate: isEncryptEnabled, + }); + + new Setting(paneEl).autoWireToggle("usePathObfuscation", { + holdValue: true, + onUpdate: isEncryptEnabled, + }); + new Setting(paneEl) + .autoWireToggle("useDynamicIterationCount", { + holdValue: true, + onUpdate: isEncryptEnabled, + }) + .setClass("wizardHidden"); + }); + + void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleFetchSettings")).then((paneEl) => { + new Setting(paneEl) + .setName($msg("obsidianLiveSyncSettingTab.titleFetchConfigFromRemote")) + .setDesc($msg("obsidianLiveSyncSettingTab.descFetchConfigFromRemote")) + .addButton((button) => + button + .setButtonText($msg("obsidianLiveSyncSettingTab.buttonFetch")) + .setDisabled(false) + .onClick(async () => { + const trialSetting = { ...this.initialSettings, ...this.editingSettings }; + const newTweaks = await this.plugin.$$checkAndAskUseRemoteConfiguration(trialSetting); + if (newTweaks.result !== false) { + if (this.inWizard) { + this.editingSettings = { ...this.editingSettings, ...newTweaks.result }; + this.requestUpdate(); + return; + } else { + this.closeSetting(); + this.plugin.settings = { ...this.plugin.settings, ...newTweaks.result }; + if (newTweaks.requireFetch) { + if ( + (await this.plugin.confirm.askYesNoDialog( + $msg("SettingTab.Message.AskRebuild"), + { + defaultOption: "Yes", + } + )) == "no" + ) { + await this.plugin.$$saveSettingData(); + return; + } + await this.plugin.$$saveSettingData(); + await this.plugin.rebuilder.scheduleFetch(); + await this.plugin.$$scheduleAppReload(); + return; + } else { + await this.plugin.$$saveSettingData(); + } + } + } + }) + ); + }); + new Setting(paneEl).setClass("wizardOnly").addButton((button) => + button + .setButtonText($msg("obsidianLiveSyncSettingTab.buttonNext")) + .setCta() + .setDisabled(false) + .onClick(async () => { + if (!(await checkConfig(checkResultDiv))) { + if ( + (await this.plugin.confirm.askYesNoDialog( + $msg("obsidianLiveSyncSettingTab.msgConfigCheckFailed"), + { + defaultOption: "No", + title: $msg("obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed"), + } + )) == "no" + ) { + return; + } + } + const isEncryptionFullyEnabled = + !this.editingSettings.encrypt || !this.editingSettings.usePathObfuscation; + if (isEncryptionFullyEnabled) { + if ( + (await this.plugin.confirm.askYesNoDialog( + $msg("obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation"), + { + defaultOption: "No", + title: $msg("obsidianLiveSyncSettingTab.titleEncryptionNotEnabled"), + } + )) == "no" + ) { + return; + } + } + if (!this.editingSettings.encrypt) { + this.editingSettings.passphrase = ""; + } + if (!(await this.isPassphraseValid())) { + if ( + (await this.plugin.confirm.askYesNoDialog( + $msg("obsidianLiveSyncSettingTab.msgInvalidPassphrase"), + { + defaultOption: "No", + title: $msg("obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid"), + } + )) == "no" + ) { + return; + } + } + if (isCloudantURI(this.editingSettings.couchDB_URI)) { + this.editingSettings = { ...this.editingSettings, ...PREFERRED_SETTING_CLOUDANT }; + } else if (this.editingSettings.remoteType == REMOTE_MINIO) { + this.editingSettings = { ...this.editingSettings, ...PREFERRED_JOURNAL_SYNC }; + } else { + this.editingSettings = { ...this.editingSettings, ...PREFERRED_SETTING_SELF_HOSTED }; + } + if ( + (await this.plugin.confirm.askYesNoDialog( + $msg("obsidianLiveSyncSettingTab.msgFetchConfigFromRemote"), + { defaultOption: "Yes", title: $msg("obsidianLiveSyncSettingTab.titleFetchConfig") } + )) == "yes" + ) { + const trialSetting = { ...this.initialSettings, ...this.editingSettings }; + const newTweaks = await this.plugin.$$checkAndAskUseRemoteConfiguration(trialSetting); + if (newTweaks.result !== false) { + this.editingSettings = { ...this.editingSettings, ...newTweaks.result }; + this.requestUpdate(); + } else { + // Messages should be already shown. + } + } + this.changeDisplay("30"); + }) + ); +} diff --git a/src/modules/features/SettingDialogue/PaneSelector.ts b/src/modules/features/SettingDialogue/PaneSelector.ts new file mode 100644 index 0000000..3e79cb3 --- /dev/null +++ b/src/modules/features/SettingDialogue/PaneSelector.ts @@ -0,0 +1,121 @@ +import { LEVEL_ADVANCED, type CustomRegExpSource } from "../../../lib/src/common/types.ts"; +import { constructCustomRegExpList, splitCustomRegExpList } from "../../../lib/src/common/utils.ts"; +import MultipleRegExpControl from "./MultipleRegExpControl.svelte"; +import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; +import { mount } from "svelte"; +import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; +import type { PageFunctions } from "./SettingPane.ts"; +import { visibleOnly } from "./SettingPane.ts"; +export function paneSelector(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void { + void addPanel(paneEl, "Normal Files").then((paneEl) => { + paneEl.addClass("wizardHidden"); + + const syncFilesSetting = new Setting(paneEl) + .setName("Synchronising files") + .setDesc( + "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files." + ) + .setClass("wizardHidden"); + mount(MultipleRegExpControl, { + target: syncFilesSetting.controlEl, + props: { + patterns: splitCustomRegExpList(this.editingSettings.syncOnlyRegEx, "|[]|"), + originals: splitCustomRegExpList(this.editingSettings.syncOnlyRegEx, "|[]|"), + apply: async (newPatterns: CustomRegExpSource[]) => { + this.editingSettings.syncOnlyRegEx = constructCustomRegExpList(newPatterns, "|[]|"); + await this.saveAllDirtySettings(); + this.display(); + }, + }, + }); + + const nonSyncFilesSetting = new Setting(paneEl) + .setName("Non-Synchronising files") + .setDesc("(RegExp) If this is set, any changes to local and remote files that match this will be skipped.") + .setClass("wizardHidden"); + + mount(MultipleRegExpControl, { + target: nonSyncFilesSetting.controlEl, + props: { + patterns: splitCustomRegExpList(this.editingSettings.syncIgnoreRegEx, "|[]|"), + originals: splitCustomRegExpList(this.editingSettings.syncIgnoreRegEx, "|[]|"), + apply: async (newPatterns: CustomRegExpSource[]) => { + this.editingSettings.syncIgnoreRegEx = constructCustomRegExpList(newPatterns, "|[]|"); + await this.saveAllDirtySettings(); + this.display(); + }, + }, + }); + new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("syncMaxSizeInMB", { clampMin: 0 }); + + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("useIgnoreFiles"); + new Setting(paneEl).setClass("wizardHidden").autoWireTextArea("ignoreFiles", { + onUpdate: visibleOnly(() => this.isConfiguredAs("useIgnoreFiles", true)), + }); + }); + void addPanel(paneEl, "Hidden Files", undefined, undefined, LEVEL_ADVANCED).then((paneEl) => { + const targetPatternSetting = new Setting(paneEl) + .setName("Target patterns") + .setClass("wizardHidden") + .setDesc("Patterns to match files for syncing"); + const patTarget = splitCustomRegExpList(this.editingSettings.syncInternalFilesTargetPatterns, ","); + mount(MultipleRegExpControl, { + target: targetPatternSetting.controlEl, + props: { + patterns: patTarget, + originals: [...patTarget], + apply: async (newPatterns: CustomRegExpSource[]) => { + this.editingSettings.syncInternalFilesTargetPatterns = constructCustomRegExpList(newPatterns, ","); + await this.saveAllDirtySettings(); + this.display(); + }, + }, + }); + + const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, ^\\.git\\/, \\/obsidian-livesync\\/"; + const defaultSkipPatternXPlat = + defaultSkipPattern + ",\\/workspace$ ,\\/workspace.json$,\\/workspace-mobile.json$"; + + const pat = splitCustomRegExpList(this.editingSettings.syncInternalFilesIgnorePatterns, ","); + const patSetting = new Setting(paneEl).setName("Ignore patterns").setClass("wizardHidden").setDesc(""); + + mount(MultipleRegExpControl, { + target: patSetting.controlEl, + props: { + patterns: pat, + originals: [...pat], + apply: async (newPatterns: CustomRegExpSource[]) => { + this.editingSettings.syncInternalFilesIgnorePatterns = constructCustomRegExpList(newPatterns, ","); + await this.saveAllDirtySettings(); + this.display(); + }, + }, + }); + + const addDefaultPatterns = async (patterns: string) => { + const oldList = splitCustomRegExpList(this.editingSettings.syncInternalFilesIgnorePatterns, ","); + const newList = splitCustomRegExpList( + patterns as unknown as typeof this.editingSettings.syncInternalFilesIgnorePatterns, + "," + ); + const allSet = new Set([...oldList, ...newList]); + this.editingSettings.syncInternalFilesIgnorePatterns = constructCustomRegExpList([...allSet], ","); + await this.saveAllDirtySettings(); + this.display(); + }; + + new Setting(paneEl) + .setName("Add default patterns") + .setClass("wizardHidden") + .addButton((button) => { + button.setButtonText("Default").onClick(async () => { + await addDefaultPatterns(defaultSkipPattern); + }); + }) + .addButton((button) => { + button.setButtonText("Cross-platform").onClick(async () => { + await addDefaultPatterns(defaultSkipPatternXPlat); + }); + }); + }); +} diff --git a/src/modules/features/SettingDialogue/PaneSetup.ts b/src/modules/features/SettingDialogue/PaneSetup.ts new file mode 100644 index 0000000..4dd8aab --- /dev/null +++ b/src/modules/features/SettingDialogue/PaneSetup.ts @@ -0,0 +1,205 @@ +import { MarkdownRenderer } from "../../../deps.ts"; +import { $msg } from "../../../lib/src/common/i18n.ts"; +import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; +import { fireAndForget } from "octagonal-wheels/promises"; +import { + EVENT_REQUEST_COPY_SETUP_URI, + EVENT_REQUEST_OPEN_SETUP_URI, + EVENT_REQUEST_SHOW_SETUP_QR, + eventHub, +} from "../../../common/events.ts"; +import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; +import type { PageFunctions } from "./SettingPane.ts"; +import { visibleOnly } from "./SettingPane.ts"; +import { DEFAULT_SETTINGS } from "../../../lib/src/common/types.ts"; +import { request } from "obsidian"; +export function paneSetup( + this: ObsidianLiveSyncSettingTab, + paneEl: HTMLElement, + { addPanel, addPane }: PageFunctions +): void { + void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleQuickSetup")).then((paneEl) => { + new Setting(paneEl) + .setName($msg("obsidianLiveSyncSettingTab.nameConnectSetupURI")) + .setDesc($msg("obsidianLiveSyncSettingTab.descConnectSetupURI")) + .addButton((text) => { + text.setButtonText($msg("obsidianLiveSyncSettingTab.btnUse")).onClick(() => { + this.closeSetting(); + eventHub.emitEvent(EVENT_REQUEST_OPEN_SETUP_URI); + }); + }); + + new Setting(paneEl) + .setName($msg("obsidianLiveSyncSettingTab.nameManualSetup")) + .setDesc($msg("obsidianLiveSyncSettingTab.descManualSetup")) + .addButton((text) => { + text.setButtonText($msg("obsidianLiveSyncSettingTab.btnStart")).onClick(async () => { + await this.enableMinimalSetup(); + }); + }); + + new Setting(paneEl) + .setName($msg("obsidianLiveSyncSettingTab.nameEnableLiveSync")) + .setDesc($msg("obsidianLiveSyncSettingTab.descEnableLiveSync")) + .addOnUpdate(visibleOnly(() => !this.isConfiguredAs("isConfigured", true))) + .addButton((text) => { + text.setButtonText($msg("obsidianLiveSyncSettingTab.btnEnable")).onClick(async () => { + this.editingSettings.isConfigured = true; + await this.saveAllDirtySettings(); + this.plugin.$$askReload(); + }); + }); + }); + + void addPanel( + paneEl, + $msg("obsidianLiveSyncSettingTab.titleSetupOtherDevices"), + undefined, + visibleOnly(() => this.isConfiguredAs("isConfigured", true)) + ).then((paneEl) => { + new Setting(paneEl) + .setName($msg("obsidianLiveSyncSettingTab.nameCopySetupURI")) + .setDesc($msg("obsidianLiveSyncSettingTab.descCopySetupURI")) + .addButton((text) => { + text.setButtonText($msg("obsidianLiveSyncSettingTab.btnCopy")).onClick(() => { + // await this.plugin.addOnSetup.command_copySetupURI(); + eventHub.emitEvent(EVENT_REQUEST_COPY_SETUP_URI); + }); + }); + new Setting(paneEl) + .setName($msg("Setup.ShowQRCode")) + .setDesc($msg("Setup.ShowQRCode.Desc")) + .addButton((text) => { + text.setButtonText($msg("Setup.ShowQRCode")).onClick(() => { + eventHub.emitEvent(EVENT_REQUEST_SHOW_SETUP_QR); + }); + }); + }); + + void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleReset")).then((paneEl) => { + new Setting(paneEl) + .setName($msg("obsidianLiveSyncSettingTab.nameDiscardSettings")) + .addButton((text) => { + text.setButtonText($msg("obsidianLiveSyncSettingTab.btnDiscard")) + .onClick(async () => { + if ( + (await this.plugin.confirm.askYesNoDialog( + $msg("obsidianLiveSyncSettingTab.msgDiscardConfirmation"), + { defaultOption: "No" } + )) == "yes" + ) { + this.editingSettings = { ...this.editingSettings, ...DEFAULT_SETTINGS }; + await this.saveAllDirtySettings(); + this.plugin.settings = { ...DEFAULT_SETTINGS }; + await this.plugin.$$saveSettingData(); + await this.plugin.$$resetLocalDatabase(); + // await this.plugin.initializeDatabase(); + this.plugin.$$askReload(); + } + }) + .setWarning(); + }) + .addOnUpdate(visibleOnly(() => this.isConfiguredAs("isConfigured", true))); + }); + + void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleExtraFeatures")).then((paneEl) => { + new Setting(paneEl).autoWireToggle("useAdvancedMode"); + + new Setting(paneEl).autoWireToggle("usePowerUserMode"); + new Setting(paneEl).autoWireToggle("useEdgeCaseMode"); + + this.addOnSaved("useAdvancedMode", () => this.display()); + this.addOnSaved("usePowerUserMode", () => this.display()); + this.addOnSaved("useEdgeCaseMode", () => this.display()); + }); + + void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleOnlineTips")).then((paneEl) => { + // this.createEl(paneEl, "h3", { text: $msg("obsidianLiveSyncSettingTab.titleOnlineTips") }); + const repo = "vrtmrz/obsidian-livesync"; + const topPath = $msg("obsidianLiveSyncSettingTab.linkTroubleshooting"); + const rawRepoURI = `https://raw.githubusercontent.com/${repo}/main`; + this.createEl( + paneEl, + "div", + "", + (el) => + (el.innerHTML = `${$msg("obsidianLiveSyncSettingTab.linkOpenInBrowser")}`) + ); + const troubleShootEl = this.createEl(paneEl, "div", { + text: "", + cls: "sls-troubleshoot-preview", + }); + const loadMarkdownPage = async (pathAll: string, basePathParam: string = "") => { + troubleShootEl.style.minHeight = troubleShootEl.clientHeight + "px"; + troubleShootEl.empty(); + const fullPath = pathAll.startsWith("/") ? pathAll : `${basePathParam}/${pathAll}`; + + const directoryArr = fullPath.split("/"); + const filename = directoryArr.pop(); + const directly = directoryArr.join("/"); + const basePath = directly; + + let remoteTroubleShootMDSrc = ""; + try { + remoteTroubleShootMDSrc = await request(`${rawRepoURI}${basePath}/${filename}`); + } catch (ex: any) { + remoteTroubleShootMDSrc = `${$msg("obsidianLiveSyncSettingTab.logErrorOccurred")}\n${ex.toString()}`; + } + const remoteTroubleShootMD = remoteTroubleShootMDSrc.replace( + /\((.*?(.png)|(.jpg))\)/g, + `(${rawRepoURI}${basePath}/$1)` + ); + // Render markdown + await MarkdownRenderer.render( + this.plugin.app, + ` [${$msg("obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting")}](${topPath}) [${$msg("obsidianLiveSyncSettingTab.linkPageTop")}](${filename})\n\n${remoteTroubleShootMD}`, + troubleShootEl, + `${rawRepoURI}`, + this.plugin + ); + // Menu + troubleShootEl.querySelector(".sls-troubleshoot-anchor")?.parentElement?.setCssStyles({ + position: "sticky", + top: "-1em", + backgroundColor: "var(--modal-background)", + }); + // Trap internal links. + troubleShootEl.querySelectorAll("a.internal-link").forEach((anchorEl) => { + anchorEl.addEventListener("click", (evt) => { + fireAndForget(async () => { + const uri = anchorEl.getAttr("data-href"); + if (!uri) return; + if (uri.startsWith("#")) { + evt.preventDefault(); + const elements = Array.from( + troubleShootEl.querySelectorAll("[data-heading]") + ); + const p = elements.find( + (e) => + e.getAttr("data-heading")?.toLowerCase().split(" ").join("-") == + uri.substring(1).toLowerCase() + ); + if (p) { + p.setCssStyles({ scrollMargin: "3em" }); + p.scrollIntoView({ + behavior: "instant", + block: "start", + }); + } + } else { + evt.preventDefault(); + await loadMarkdownPage(uri, basePath); + troubleShootEl.setCssStyles({ scrollMargin: "1em" }); + troubleShootEl.scrollIntoView({ + behavior: "instant", + block: "start", + }); + } + }); + }); + }); + troubleShootEl.style.minHeight = ""; + }; + void loadMarkdownPage(topPath); + }); +} diff --git a/src/modules/features/SettingDialogue/PaneSyncSettings.ts b/src/modules/features/SettingDialogue/PaneSyncSettings.ts new file mode 100644 index 0000000..42982c9 --- /dev/null +++ b/src/modules/features/SettingDialogue/PaneSyncSettings.ts @@ -0,0 +1,346 @@ +import { + type ObsidianLiveSyncSettings, + LOG_LEVEL_NOTICE, + REMOTE_COUCHDB, + LEVEL_ADVANCED, +} from "../../../lib/src/common/types.ts"; +import { Logger } from "../../../lib/src/common/logger.ts"; +import { $msg } from "../../../lib/src/common/i18n.ts"; +import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; +import { fireAndForget } from "octagonal-wheels/promises"; +import { EVENT_REQUEST_COPY_SETUP_URI, eventHub } from "../../../common/events.ts"; +import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; +import type { PageFunctions } from "./SettingPane.ts"; +import { visibleOnly } from "./SettingPane.ts"; +export function paneSyncSettings( + this: ObsidianLiveSyncSettingTab, + paneEl: HTMLElement, + { addPanel, addPane }: PageFunctions +): void { + if (this.editingSettings.versionUpFlash != "") { + const c = this.createEl( + paneEl, + "div", + { + text: this.editingSettings.versionUpFlash, + cls: "op-warn sls-setting-hidden", + }, + (el) => { + this.createEl(el, "button", { text: $msg("obsidianLiveSyncSettingTab.btnGotItAndUpdated") }, (e) => { + e.addClass("mod-cta"); + e.addEventListener("click", () => { + fireAndForget(async () => { + this.editingSettings.versionUpFlash = ""; + await this.saveAllDirtySettings(); + c.remove(); + }); + }); + }); + }, + visibleOnly(() => !this.isConfiguredAs("versionUpFlash", "")) + ); + } + + this.createEl(paneEl, "div", { + text: $msg("obsidianLiveSyncSettingTab.msgSelectAndApplyPreset"), + cls: "wizardOnly", + }).addClasses(["op-warn-info"]); + + void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleSynchronizationPreset")).then((paneEl) => { + const options: Record = + this.editingSettings.remoteType == REMOTE_COUCHDB + ? { + NONE: "", + LIVESYNC: $msg("obsidianLiveSyncSettingTab.optionLiveSync"), + PERIODIC: $msg("obsidianLiveSyncSettingTab.optionPeriodicWithBatch"), + DISABLE: $msg("obsidianLiveSyncSettingTab.optionDisableAllAutomatic"), + } + : { + NONE: "", + PERIODIC: $msg("obsidianLiveSyncSettingTab.optionPeriodicWithBatch"), + DISABLE: $msg("obsidianLiveSyncSettingTab.optionDisableAllAutomatic"), + }; + + new Setting(paneEl) + .autoWireDropDown("preset", { + options: options, + holdValue: true, + }) + .addButton((button) => { + button.setButtonText($msg("obsidianLiveSyncSettingTab.btnApply")); + button.onClick(async () => { + // await this.saveSettings(["preset"]); + await this.saveAllDirtySettings(); + }); + }); + + this.addOnSaved("preset", async (currentPreset) => { + if (currentPreset == "") { + Logger($msg("obsidianLiveSyncSettingTab.logSelectAnyPreset"), LOG_LEVEL_NOTICE); + return; + } + const presetAllDisabled = { + batchSave: false, + liveSync: false, + periodicReplication: false, + syncOnSave: false, + syncOnEditorSave: false, + syncOnStart: false, + syncOnFileOpen: false, + syncAfterMerge: false, + } as Partial; + const presetLiveSync = { + ...presetAllDisabled, + liveSync: true, + } as Partial; + const presetPeriodic = { + ...presetAllDisabled, + batchSave: true, + periodicReplication: true, + syncOnSave: false, + syncOnEditorSave: false, + syncOnStart: true, + syncOnFileOpen: true, + syncAfterMerge: true, + } as Partial; + + if (currentPreset == "LIVESYNC") { + this.editingSettings = { + ...this.editingSettings, + ...presetLiveSync, + }; + Logger($msg("obsidianLiveSyncSettingTab.logConfiguredLiveSync"), LOG_LEVEL_NOTICE); + } else if (currentPreset == "PERIODIC") { + this.editingSettings = { + ...this.editingSettings, + ...presetPeriodic, + }; + Logger($msg("obsidianLiveSyncSettingTab.logConfiguredPeriodic"), LOG_LEVEL_NOTICE); + } else { + Logger($msg("obsidianLiveSyncSettingTab.logConfiguredDisabled"), LOG_LEVEL_NOTICE); + this.editingSettings = { + ...this.editingSettings, + ...presetAllDisabled, + }; + } + + if (this.inWizard) { + this.closeSetting(); + this.inWizard = false; + if (!this.editingSettings.isConfigured) { + this.editingSettings.isConfigured = true; + await this.saveAllDirtySettings(); + await this.plugin.$$realizeSettingSyncMode(); + await this.rebuildDB("localOnly"); + // this.resetEditingSettings(); + if ( + (await this.plugin.confirm.askYesNoDialog( + $msg("obsidianLiveSyncSettingTab.msgGenerateSetupURI"), + { + defaultOption: "Yes", + title: $msg("obsidianLiveSyncSettingTab.titleCongratulations"), + } + )) == "yes" + ) { + eventHub.emitEvent(EVENT_REQUEST_COPY_SETUP_URI); + } + } else { + if (this.isNeedRebuildLocal() || this.isNeedRebuildRemote()) { + await this.confirmRebuild(); + } else { + await this.saveAllDirtySettings(); + await this.plugin.$$realizeSettingSyncMode(); + this.plugin.$$askReload(); + } + } + } else { + await this.saveAllDirtySettings(); + await this.plugin.$$realizeSettingSyncMode(); + } + }); + }); + void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleSynchronizationMethod")).then((paneEl) => { + paneEl.addClass("wizardHidden"); + + // const onlyOnLiveSync = visibleOnly(() => this.isConfiguredAs("syncMode", "LIVESYNC")); + const onlyOnNonLiveSync = visibleOnly(() => !this.isConfiguredAs("syncMode", "LIVESYNC")); + const onlyOnPeriodic = visibleOnly(() => this.isConfiguredAs("syncMode", "PERIODIC")); + + const optionsSyncMode = + this.editingSettings.remoteType == REMOTE_COUCHDB + ? { + ONEVENTS: $msg("obsidianLiveSyncSettingTab.optionOnEvents"), + PERIODIC: $msg("obsidianLiveSyncSettingTab.optionPeriodicAndEvents"), + LIVESYNC: $msg("obsidianLiveSyncSettingTab.optionLiveSync"), + } + : { + ONEVENTS: $msg("obsidianLiveSyncSettingTab.optionOnEvents"), + PERIODIC: $msg("obsidianLiveSyncSettingTab.optionPeriodicAndEvents"), + }; + + new Setting(paneEl) + .autoWireDropDown("syncMode", { + //@ts-ignore + options: optionsSyncMode, + }) + .setClass("wizardHidden"); + this.addOnSaved("syncMode", async (value) => { + this.editingSettings.liveSync = false; + this.editingSettings.periodicReplication = false; + if (value == "LIVESYNC") { + this.editingSettings.liveSync = true; + } else if (value == "PERIODIC") { + this.editingSettings.periodicReplication = true; + } + await this.saveSettings(["liveSync", "periodicReplication"]); + + await this.plugin.$$realizeSettingSyncMode(); + }); + + new Setting(paneEl) + .autoWireNumeric("periodicReplicationInterval", { + clampMax: 5000, + onUpdate: onlyOnPeriodic, + }) + .setClass("wizardHidden"); + + new Setting(paneEl).autoWireNumeric("syncMinimumInterval", { + onUpdate: onlyOnNonLiveSync, + }); + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncOnSave", { onUpdate: onlyOnNonLiveSync }); + new Setting(paneEl) + .setClass("wizardHidden") + .autoWireToggle("syncOnEditorSave", { onUpdate: onlyOnNonLiveSync }); + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncOnFileOpen", { onUpdate: onlyOnNonLiveSync }); + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncOnStart", { onUpdate: onlyOnNonLiveSync }); + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncAfterMerge", { onUpdate: onlyOnNonLiveSync }); + }); + + void addPanel( + paneEl, + $msg("obsidianLiveSyncSettingTab.titleUpdateThinning"), + undefined, + visibleOnly(() => !this.isConfiguredAs("syncMode", "LIVESYNC")) + ).then((paneEl) => { + paneEl.addClass("wizardHidden"); + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("batchSave"); + new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("batchSaveMinimumDelay", { + acceptZero: true, + onUpdate: visibleOnly(() => this.isConfiguredAs("batchSave", true)), + }); + new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("batchSaveMaximumDelay", { + acceptZero: true, + onUpdate: visibleOnly(() => this.isConfiguredAs("batchSave", true)), + }); + }); + + void addPanel( + paneEl, + $msg("obsidianLiveSyncSettingTab.titleDeletionPropagation"), + undefined, + undefined, + LEVEL_ADVANCED + ).then((paneEl) => { + paneEl.addClass("wizardHidden"); + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("trashInsteadDelete"); + + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("doNotDeleteFolder"); + }); + void addPanel( + paneEl, + $msg("obsidianLiveSyncSettingTab.titleConflictResolution"), + undefined, + undefined, + LEVEL_ADVANCED + ).then((paneEl) => { + paneEl.addClass("wizardHidden"); + + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("resolveConflictsByNewerFile"); + + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("checkConflictOnlyOnOpen"); + + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("showMergeDialogOnlyOnActive"); + }); + + void addPanel( + paneEl, + $msg("obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown"), + undefined, + undefined, + LEVEL_ADVANCED + ).then((paneEl) => { + paneEl.addClass("wizardHidden"); + new Setting(paneEl).autoWireText("settingSyncFile", { holdValue: true }).addApplyButton(["settingSyncFile"]); + + new Setting(paneEl).autoWireToggle("writeCredentialsForSettingSync"); + + new Setting(paneEl).autoWireToggle("notifyAllSettingSyncFile"); + }); + + void addPanel( + paneEl, + $msg("obsidianLiveSyncSettingTab.titleHiddenFiles"), + undefined, + undefined, + LEVEL_ADVANCED + ).then((paneEl) => { + paneEl.addClass("wizardHidden"); + + const LABEL_ENABLED = $msg("obsidianLiveSyncSettingTab.labelEnabled"); + const LABEL_DISABLED = $msg("obsidianLiveSyncSettingTab.labelDisabled"); + + const hiddenFileSyncSetting = new Setting(paneEl) + .setName($msg("obsidianLiveSyncSettingTab.nameHiddenFileSynchronization")) + .setClass("wizardHidden"); + const hiddenFileSyncSettingEl = hiddenFileSyncSetting.settingEl; + const hiddenFileSyncSettingDiv = hiddenFileSyncSettingEl.createDiv(""); + hiddenFileSyncSettingDiv.innerText = this.editingSettings.syncInternalFiles ? LABEL_ENABLED : LABEL_DISABLED; + if (this.editingSettings.syncInternalFiles) { + new Setting(paneEl) + .setName($msg("obsidianLiveSyncSettingTab.nameDisableHiddenFileSync")) + .setClass("wizardHidden") + .addButton((button) => { + button.setButtonText($msg("obsidianLiveSyncSettingTab.btnDisable")).onClick(async () => { + this.editingSettings.syncInternalFiles = false; + await this.saveAllDirtySettings(); + this.display(); + }); + }); + } else { + new Setting(paneEl) + .setName($msg("obsidianLiveSyncSettingTab.nameEnableHiddenFileSync")) + .setClass("wizardHidden") + .addButton((button) => { + button.setButtonText("Merge").onClick(async () => { + this.closeSetting(); + // this.resetEditingSettings(); + await this.plugin.$anyConfigureOptionalSyncFeature("MERGE"); + }); + }) + .addButton((button) => { + button.setButtonText("Fetch").onClick(async () => { + this.closeSetting(); + // this.resetEditingSettings(); + await this.plugin.$anyConfigureOptionalSyncFeature("FETCH"); + }); + }) + .addButton((button) => { + button.setButtonText("Overwrite").onClick(async () => { + this.closeSetting(); + // this.resetEditingSettings(); + await this.plugin.$anyConfigureOptionalSyncFeature("OVERWRITE"); + }); + }); + } + + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("suppressNotifyHiddenFilesChange", {}); + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncInternalFilesBeforeReplication", { + onUpdate: visibleOnly(() => this.isConfiguredAs("watchInternalFileChanges", true)), + }); + + new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("syncInternalFilesInterval", { + clampMin: 10, + acceptZero: true, + }); + }); +} diff --git a/src/modules/features/SettingDialogue/SettingPane.ts b/src/modules/features/SettingDialogue/SettingPane.ts new file mode 100644 index 0000000..2fe8f56 --- /dev/null +++ b/src/modules/features/SettingDialogue/SettingPane.ts @@ -0,0 +1,119 @@ +import { $msg } from "../../../lib/src/common/i18n"; +import { LEVEL_ADVANCED, LEVEL_EDGE_CASE, LEVEL_POWER_USER, type ConfigLevel } from "../../../lib/src/common/types"; +import type { AllSettingItemKey, AllSettings } from "./settingConstants"; + +export const combineOnUpdate = (func1: OnUpdateFunc, func2: OnUpdateFunc): OnUpdateFunc => { + return () => ({ + ...func1(), + ...func2(), + }); +}; +export const setLevelClass = (el: HTMLElement, level?: ConfigLevel) => { + switch (level) { + case LEVEL_POWER_USER: + el.addClass("sls-setting-poweruser"); + break; + case LEVEL_ADVANCED: + el.addClass("sls-setting-advanced"); + break; + case LEVEL_EDGE_CASE: + el.addClass("sls-setting-edgecase"); + break; + default: + // NO OP. + } +}; +export function setStyle(el: HTMLElement, styleHead: string, condition: () => boolean) { + if (condition()) { + el.addClass(`${styleHead}-enabled`); + el.removeClass(`${styleHead}-disabled`); + } else { + el.addClass(`${styleHead}-disabled`); + el.removeClass(`${styleHead}-enabled`); + } +} + +export function visibleOnly(cond: () => boolean): OnUpdateFunc { + return () => ({ + visibility: cond(), + }); +} +export function enableOnly(cond: () => boolean): OnUpdateFunc { + return () => ({ + disabled: !cond(), + }); +} + +export type OnUpdateResult = { + visibility?: boolean; + disabled?: boolean; + classes?: string[]; + isCta?: boolean; + isWarning?: boolean; +}; +export type OnUpdateFunc = () => OnUpdateResult; +export type UpdateFunction = () => void; + +export type OnSavedHandlerFunc = (value: AllSettings[T]) => Promise | void; +export type OnSavedHandler = { + key: T; + handler: OnSavedHandlerFunc; +}; + +export function getLevelStr(level: ConfigLevel) { + return level == LEVEL_POWER_USER + ? $msg("obsidianLiveSyncSettingTab.levelPowerUser") + : level == LEVEL_ADVANCED + ? $msg("obsidianLiveSyncSettingTab.levelAdvanced") + : level == LEVEL_EDGE_CASE + ? $msg("obsidianLiveSyncSettingTab.levelEdgeCase") + : ""; +} + +export type AutoWireOption = { + placeHolder?: string; + holdValue?: boolean; + isPassword?: boolean; + invert?: boolean; + onUpdate?: OnUpdateFunc; + obsolete?: boolean; +}; + +export function findAttrFromParent(el: HTMLElement, attr: string): string { + let current: HTMLElement | null = el; + while (current) { + const value = current.getAttribute(attr); + if (value) { + return value; + } + current = current.parentElement; + } + return ""; +} + +export function wrapMemo(func: (arg: T) => void) { + let buf: T | undefined = undefined; + return (arg: T) => { + if (buf !== arg) { + func(arg); + buf = arg; + } + }; +} +export type PageFunctions = { + addPane: ( + parentEl: HTMLElement, + title: string, + icon: string, + order: number, + wizardHidden: boolean, + level?: ConfigLevel + ) => Promise; + addPanel: ( + parentEl: HTMLElement, + title: string, + callback?: (el: HTMLDivElement) => void, + func?: OnUpdateFunc, + level?: ConfigLevel + ) => Promise; +}; diff --git a/src/modules/features/SettingDialogue/settingConstants.ts b/src/modules/features/SettingDialogue/settingConstants.ts index deed783..e2b016c 100644 --- a/src/modules/features/SettingDialogue/settingConstants.ts +++ b/src/modules/features/SettingDialogue/settingConstants.ts @@ -389,6 +389,10 @@ export const SettingInformation: Partial