mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-03-17 23:35:18 +00:00
Compare commits
4 Commits
0.25.52-pa
...
beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0717093d81 | ||
|
|
1f87a9fd3d | ||
|
|
fdd3a3aecb | ||
|
|
d8281390c4 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.25.52-patched-3",
|
||||
"version": "0.25.53",
|
||||
"minAppVersion": "0.9.12",
|
||||
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"author": "vorotamoroz",
|
||||
|
||||
144
package-lock.json
generated
144
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.52-patched-3",
|
||||
"version": "0.25.53",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.52-patched-3",
|
||||
"version": "0.25.53",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.808.0",
|
||||
@@ -19,6 +19,7 @@
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"fflate": "^0.8.2",
|
||||
"idb": "^8.0.3",
|
||||
"markdown-it": "^14.1.1",
|
||||
"minimatch": "^10.2.2",
|
||||
"node-datachannel": "^0.32.1",
|
||||
"octagonal-wheels": "^0.1.45",
|
||||
@@ -36,6 +37,7 @@
|
||||
"@tsconfig/svelte": "^5.0.8",
|
||||
"@types/deno": "^2.5.0",
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/node": "^24.10.13",
|
||||
"@types/pouchdb": "^6.4.2",
|
||||
"@types/pouchdb-adapter-http": "^6.1.6",
|
||||
@@ -5086,6 +5088,13 @@
|
||||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
|
||||
@@ -5101,6 +5110,24 @@
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "14.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/linkify-it": "^5",
|
||||
"@types/mdurl": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/minimatch": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
|
||||
@@ -6586,7 +6613,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/aria-query": {
|
||||
@@ -8135,7 +8161,6 @@
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
@@ -11459,6 +11484,15 @@
|
||||
"url": "https://github.com/sponsors/antonk52"
|
||||
}
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"uc.micro": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-app": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-app/-/locate-app-2.5.0.tgz",
|
||||
@@ -11637,6 +11671,23 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it": {
|
||||
"version": "14.1.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
|
||||
"integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "^4.4.0",
|
||||
"linkify-it": "^5.0.0",
|
||||
"mdurl": "^2.0.0",
|
||||
"punycode.js": "^2.3.1",
|
||||
"uc.micro": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"markdown-it": "bin/markdown-it.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -11647,6 +11698,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/memdown": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/memdown/-/memdown-1.4.1.tgz",
|
||||
@@ -13429,6 +13486,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode.js": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode-generator": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.5.2.tgz",
|
||||
@@ -15680,6 +15746,12 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uint8-varint": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/uint8-varint/-/uint8-varint-2.0.4.tgz",
|
||||
@@ -20527,6 +20599,12 @@
|
||||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
|
||||
@@ -20540,6 +20618,22 @@
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"@types/markdown-it": {
|
||||
"version": "14.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/linkify-it": "^5",
|
||||
"@types/mdurl": "^2"
|
||||
}
|
||||
},
|
||||
"@types/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/minimatch": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
|
||||
@@ -21613,8 +21707,7 @@
|
||||
"argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"aria-query": {
|
||||
"version": "5.3.2",
|
||||
@@ -22633,8 +22726,7 @@
|
||||
"entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
|
||||
},
|
||||
"errno": {
|
||||
"version": "0.1.8",
|
||||
@@ -24921,6 +25013,14 @@
|
||||
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
|
||||
"dev": true
|
||||
},
|
||||
"linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
|
||||
"requires": {
|
||||
"uc.micro": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"locate-app": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-app/-/locate-app-2.5.0.tgz",
|
||||
@@ -25054,12 +25154,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"markdown-it": {
|
||||
"version": "14.1.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
|
||||
"integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
|
||||
"requires": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "^4.4.0",
|
||||
"linkify-it": "^5.0.0",
|
||||
"mdurl": "^2.0.0",
|
||||
"punycode.js": "^2.3.1",
|
||||
"uc.micro": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true
|
||||
},
|
||||
"mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="
|
||||
},
|
||||
"memdown": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/memdown/-/memdown-1.4.1.tgz",
|
||||
@@ -26294,6 +26412,11 @@
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
||||
},
|
||||
"punycode.js": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="
|
||||
},
|
||||
"qrcode-generator": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.5.2.tgz",
|
||||
@@ -27660,6 +27783,11 @@
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"uc.micro": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="
|
||||
},
|
||||
"uint8-varint": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/uint8-varint/-/uint8-varint-2.0.4.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.52-patched-3",
|
||||
"version": "0.25.53",
|
||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"main": "main.js",
|
||||
"type": "module",
|
||||
@@ -68,6 +68,7 @@
|
||||
"@tsconfig/svelte": "^5.0.8",
|
||||
"@types/deno": "^2.5.0",
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/node": "^24.10.13",
|
||||
"@types/pouchdb": "^6.4.2",
|
||||
"@types/pouchdb-adapter-http": "^6.1.6",
|
||||
@@ -134,6 +135,7 @@
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"fflate": "^0.8.2",
|
||||
"idb": "^8.0.3",
|
||||
"markdown-it": "^14.1.1",
|
||||
"minimatch": "^10.2.2",
|
||||
"node-datachannel": "^0.32.1",
|
||||
"octagonal-wheels": "^0.1.45",
|
||||
|
||||
@@ -55,8 +55,8 @@ The built files will be in the `dist` directory.
|
||||
|
||||
### Usage
|
||||
|
||||
1. Open the webapp in your browser
|
||||
2. Grant directory access when prompted
|
||||
1. Open the webapp in your browser (`webapp.html`)
|
||||
2. Select a vault from history or grant access to a new directory
|
||||
3. Configure CouchDB connection by editing `.livesync/settings.json` in your vault
|
||||
- You can also copy data.json from Obsidian's plug-in folder.
|
||||
|
||||
@@ -98,8 +98,11 @@ webapp/
|
||||
│ ├── ServiceFileAccessImpl.ts
|
||||
│ ├── DatabaseFileAccess.ts
|
||||
│ └── FSAPIServiceModules.ts
|
||||
├── main.ts # Application entry point
|
||||
├── index.html # HTML entry
|
||||
├── bootstrap.ts # Vault picker + startup orchestration
|
||||
├── main.ts # LiveSync core bootstrap (after vault selected)
|
||||
├── vaultSelector.ts # FileSystem handle history and permission flow
|
||||
├── webapp.html # Main HTML entry
|
||||
├── index.html # Redirect entry for compatibility
|
||||
├── package.json
|
||||
├── vite.config.ts
|
||||
└── README.md
|
||||
|
||||
139
src/apps/webapp/bootstrap.ts
Normal file
139
src/apps/webapp/bootstrap.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { LiveSyncWebApp } from "./main";
|
||||
import { VaultHistoryStore, type VaultHistoryItem } from "./vaultSelector";
|
||||
|
||||
const historyStore = new VaultHistoryStore();
|
||||
let app: LiveSyncWebApp | null = null;
|
||||
|
||||
function getRequiredElement<T extends HTMLElement>(id: string): T {
|
||||
const element = document.getElementById(id);
|
||||
if (!element) {
|
||||
throw new Error(`Missing element: #${id}`);
|
||||
}
|
||||
return element as T;
|
||||
}
|
||||
|
||||
function setStatus(kind: "info" | "warning" | "error" | "success", message: string): void {
|
||||
const statusEl = getRequiredElement<HTMLDivElement>("status");
|
||||
statusEl.className = kind;
|
||||
statusEl.textContent = message;
|
||||
}
|
||||
|
||||
function setBusyState(isBusy: boolean): void {
|
||||
const pickNewBtn = getRequiredElement<HTMLButtonElement>("pick-new-vault");
|
||||
pickNewBtn.disabled = isBusy;
|
||||
|
||||
const historyButtons = document.querySelectorAll<HTMLButtonElement>(".vault-item button");
|
||||
historyButtons.forEach((button) => {
|
||||
button.disabled = isBusy;
|
||||
});
|
||||
}
|
||||
|
||||
function formatLastUsed(unixMillis: number): string {
|
||||
if (!unixMillis) {
|
||||
return "unknown";
|
||||
}
|
||||
return new Date(unixMillis).toLocaleString();
|
||||
}
|
||||
|
||||
async function renderHistoryList(): Promise<VaultHistoryItem[]> {
|
||||
const listEl = getRequiredElement<HTMLDivElement>("vault-history-list");
|
||||
const emptyEl = getRequiredElement<HTMLParagraphElement>("vault-history-empty");
|
||||
|
||||
const [items, lastUsedId] = await Promise.all([historyStore.getVaultHistory(), historyStore.getLastUsedVaultId()]);
|
||||
|
||||
listEl.innerHTML = "";
|
||||
emptyEl.classList.toggle("is-hidden", items.length > 0);
|
||||
|
||||
for (const item of items) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "vault-item";
|
||||
|
||||
const info = document.createElement("div");
|
||||
info.className = "vault-item-info";
|
||||
|
||||
const name = document.createElement("div");
|
||||
name.className = "vault-item-name";
|
||||
name.textContent = item.name;
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "vault-item-meta";
|
||||
const label = item.id === lastUsedId ? "Last used" : "Used";
|
||||
meta.textContent = `${label}: ${formatLastUsed(item.lastUsedAt)}`;
|
||||
|
||||
info.append(name, meta);
|
||||
|
||||
const useButton = document.createElement("button");
|
||||
useButton.type = "button";
|
||||
useButton.textContent = "Use this vault";
|
||||
useButton.addEventListener("click", () => {
|
||||
void startWithHistory(item);
|
||||
});
|
||||
|
||||
row.append(info, useButton);
|
||||
listEl.appendChild(row);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
async function startWithHandle(handle: FileSystemDirectoryHandle): Promise<void> {
|
||||
setStatus("info", `Starting LiveSync with vault: ${handle.name}`);
|
||||
app = new LiveSyncWebApp(handle);
|
||||
await app.initialize();
|
||||
|
||||
const selectorEl = getRequiredElement<HTMLDivElement>("vault-selector");
|
||||
selectorEl.classList.add("is-hidden");
|
||||
}
|
||||
|
||||
async function startWithHistory(item: VaultHistoryItem): Promise<void> {
|
||||
setBusyState(true);
|
||||
try {
|
||||
const handle = await historyStore.activateHistoryItem(item);
|
||||
await startWithHandle(handle);
|
||||
} catch (error) {
|
||||
console.error("[Directory] Failed to open history vault:", error);
|
||||
setStatus("error", `Failed to open saved vault: ${String(error)}`);
|
||||
setBusyState(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function startWithNewPicker(): Promise<void> {
|
||||
setBusyState(true);
|
||||
try {
|
||||
const handle = await historyStore.pickNewVault();
|
||||
await startWithHandle(handle);
|
||||
} catch (error) {
|
||||
console.error("[Directory] Failed to pick vault:", error);
|
||||
setStatus("warning", `Vault selection was cancelled or failed: ${String(error)}`);
|
||||
setBusyState(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeVaultSelector(): Promise<void> {
|
||||
setStatus("info", "Select a vault folder to start LiveSync.");
|
||||
|
||||
const pickNewBtn = getRequiredElement<HTMLButtonElement>("pick-new-vault");
|
||||
pickNewBtn.addEventListener("click", () => {
|
||||
void startWithNewPicker();
|
||||
});
|
||||
|
||||
await renderHistoryList();
|
||||
}
|
||||
|
||||
window.addEventListener("load", async () => {
|
||||
try {
|
||||
await initializeVaultSelector();
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize vault selector:", error);
|
||||
setStatus("error", `Initialization failed: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
void app?.shutdown();
|
||||
});
|
||||
|
||||
(window as any).livesyncApp = {
|
||||
getApp: () => app,
|
||||
historyStore,
|
||||
};
|
||||
@@ -3,207 +3,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Self-hosted LiveSync WebApp</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
padding: 40px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#status {
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#status.error {
|
||||
background: #fee;
|
||||
color: #c33;
|
||||
border: 1px solid #fcc;
|
||||
}
|
||||
|
||||
#status.warning {
|
||||
background: #ffeaa7;
|
||||
color: #d63031;
|
||||
border: 1px solid #fdcb6e;
|
||||
}
|
||||
|
||||
#status.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
#status.info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
margin-top: 30px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.info-section h2 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.info-section ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.info-section li {
|
||||
padding: 8px 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-section li::before {
|
||||
content: "•";
|
||||
color: #667eea;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
margin-left: -1em;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.feature-list h3 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #e9ecef;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.console-link {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.container {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<title>Self-hosted LiveSync WebApp Launcher</title>
|
||||
<meta http-equiv="refresh" content="0; url=./webapp.html">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔄 Self-hosted LiveSync</h1>
|
||||
<p class="subtitle">Browser-based Self-hosted LiveSync using FileSystem API</p>
|
||||
|
||||
<div id="status" class="info">
|
||||
Initialising...
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<h2>About This Application</h2>
|
||||
<ul>
|
||||
<li>Runs entirely in your browser</li>
|
||||
<li>Uses FileSystem API to access your local vault</li>
|
||||
<li>Syncs with CouchDB server (like Obsidian plugin)</li>
|
||||
<li>Settings stored in <code>.livesync/settings.json</code></li>
|
||||
<li>Real-time file watching with FileSystemObserver (Chrome 124+)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<h2>How to Use</h2>
|
||||
<ul>
|
||||
<li>Grant directory access when prompted</li>
|
||||
<li>Create <code>.livesync/settings.json</code> in your vault folder. (Compatible with Obsidian's Self-hosted LiveSync)</li>
|
||||
<li>Add your CouchDB connection details</li>
|
||||
<li>Your files will be synced automatically</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="console-link">
|
||||
💡 Open browser console (F12) for detailed logs
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>
|
||||
Powered by
|
||||
<a href="https://github.com/vrtmrz/obsidian-livesync" target="_blank">
|
||||
Self-hosted LiveSync
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="./main.ts"></script>
|
||||
<p>Redirecting to <a href="./webapp.html">WebApp</a>...</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -13,9 +13,10 @@ import type { InjectableSettingService } from "@lib/services/implements/injectab
|
||||
import { useOfflineScanner } from "@lib/serviceFeatures/offlineScanner";
|
||||
import { useRedFlagFeatures } from "@/serviceFeatures/redFlag";
|
||||
import { useCheckRemoteSize } from "@lib/serviceFeatures/checkRemoteSize";
|
||||
import { useSetupQRCodeFeature } from "@lib/serviceFeatures/setupObsidian/qrCode";
|
||||
import { useSetupURIFeature } from "@lib/serviceFeatures/setupObsidian/setupUri";
|
||||
import { SetupManager } from "@/modules/features/SetupManager";
|
||||
// import { ModuleObsidianSettingsAsMarkdown } from "@/modules/features/ModuleObsidianSettingAsMarkdown";
|
||||
import { ModuleSetupObsidian } from "@/modules/features/ModuleSetupObsidian";
|
||||
// import { ModuleObsidianMenu } from "@/modules/essentialObsidian/ModuleObsidianMenu";
|
||||
|
||||
const SETTINGS_DIR = ".livesync";
|
||||
@@ -47,21 +48,18 @@ const DEFAULT_SETTINGS: Partial<ObsidianLiveSyncSettings> = {
|
||||
};
|
||||
|
||||
class LiveSyncWebApp {
|
||||
private rootHandle: FileSystemDirectoryHandle | null = null;
|
||||
private rootHandle: FileSystemDirectoryHandle;
|
||||
private core: LiveSyncBaseCore<ServiceContext, any> | null = null;
|
||||
private serviceHub: BrowserServiceHub<ServiceContext> | null = null;
|
||||
|
||||
constructor(rootHandle: FileSystemDirectoryHandle) {
|
||||
this.rootHandle = rootHandle;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
console.log("Self-hosted LiveSync WebApp");
|
||||
console.log("Initializing...");
|
||||
|
||||
// Request directory access
|
||||
await this.requestDirectoryAccess();
|
||||
|
||||
if (!this.rootHandle) {
|
||||
throw new Error("Failed to get directory access");
|
||||
}
|
||||
|
||||
console.log(`Vault directory: ${this.rootHandle.name}`);
|
||||
|
||||
// Create service context and hub
|
||||
@@ -102,14 +100,12 @@ class LiveSyncWebApp {
|
||||
this.core = new LiveSyncBaseCore(
|
||||
this.serviceHub,
|
||||
(core, serviceHub) => {
|
||||
return initialiseServiceModulesFSAPI(this.rootHandle!, core, serviceHub);
|
||||
return initialiseServiceModulesFSAPI(this.rootHandle, core, serviceHub);
|
||||
},
|
||||
(core) => [
|
||||
// new ModuleObsidianEvents(this, core),
|
||||
// new ModuleObsidianSettingDialogue(this, core),
|
||||
// new ModuleObsidianMenu(core),
|
||||
new ModuleSetupObsidian(core),
|
||||
new SetupManager(core),
|
||||
// new ModuleObsidianSettingsAsMarkdown(core),
|
||||
// new ModuleLog(this, core),
|
||||
// new ModuleObsidianDocumentHistory(this, core),
|
||||
@@ -119,12 +115,15 @@ class LiveSyncWebApp {
|
||||
// new ModuleReplicateTest(this, core),
|
||||
// new ModuleIntegratedTest(this, core),
|
||||
// new SetupManager(core),
|
||||
new SetupManager(core), // this should be moved to core?
|
||||
],
|
||||
() => [], // No add-ons
|
||||
(core) => {
|
||||
useOfflineScanner(core);
|
||||
useRedFlagFeatures(core);
|
||||
useCheckRemoteSize(core);
|
||||
useSetupQRCodeFeature(core);
|
||||
useSetupURIFeature(core);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -133,8 +132,6 @@ class LiveSyncWebApp {
|
||||
}
|
||||
|
||||
private async saveSettingsToFile(data: ObsidianLiveSyncSettings): Promise<void> {
|
||||
if (!this.rootHandle) return;
|
||||
|
||||
try {
|
||||
// Create .livesync directory if it doesn't exist
|
||||
const livesyncDir = await this.rootHandle.getDirectoryHandle(SETTINGS_DIR, { create: true });
|
||||
@@ -151,8 +148,6 @@ class LiveSyncWebApp {
|
||||
}
|
||||
|
||||
private async loadSettingsFromFile(): Promise<Partial<ObsidianLiveSyncSettings> | null> {
|
||||
if (!this.rootHandle) return null;
|
||||
|
||||
try {
|
||||
const livesyncDir = await this.rootHandle.getDirectoryHandle(SETTINGS_DIR);
|
||||
const fileHandle = await livesyncDir.getFileHandle(SETTINGS_FILE);
|
||||
@@ -165,90 +160,6 @@ class LiveSyncWebApp {
|
||||
}
|
||||
}
|
||||
|
||||
private async requestDirectoryAccess() {
|
||||
try {
|
||||
// Check if we have a cached directory handle
|
||||
const cached = await this.loadCachedDirectoryHandle();
|
||||
if (cached) {
|
||||
// Verify permission (cast to any for compatibility)
|
||||
try {
|
||||
const permission = await (cached as any).queryPermission({ mode: "readwrite" });
|
||||
if (permission === "granted") {
|
||||
this.rootHandle = cached;
|
||||
console.log("[Directory] Using cached directory handle");
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// queryPermission might not be supported, try to use anyway
|
||||
console.log("[Directory] Could not verify permission, requesting new access");
|
||||
}
|
||||
}
|
||||
|
||||
// Request new directory access
|
||||
console.log("[Directory] Requesting directory access...");
|
||||
this.rootHandle = await (window as any).showDirectoryPicker({
|
||||
mode: "readwrite",
|
||||
startIn: "documents",
|
||||
});
|
||||
|
||||
// Save the handle for next time
|
||||
await this.saveCachedDirectoryHandle(this.rootHandle);
|
||||
console.log("[Directory] Directory access granted");
|
||||
} catch (error) {
|
||||
console.error("[Directory] Failed to get directory access:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async saveCachedDirectoryHandle(handle: FileSystemDirectoryHandle) {
|
||||
try {
|
||||
// Use IndexedDB to store the directory handle
|
||||
const db = await this.openHandleDB();
|
||||
const transaction = db.transaction(["handles"], "readwrite");
|
||||
const store = transaction.objectStore("handles");
|
||||
await new Promise((resolve, reject) => {
|
||||
const request = store.put(handle, "rootHandle");
|
||||
request.onsuccess = resolve;
|
||||
request.onerror = reject;
|
||||
});
|
||||
db.close();
|
||||
} catch (error) {
|
||||
console.error("[Directory] Failed to cache handle:", error);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadCachedDirectoryHandle(): Promise<FileSystemDirectoryHandle | null> {
|
||||
try {
|
||||
const db = await this.openHandleDB();
|
||||
const transaction = db.transaction(["handles"], "readonly");
|
||||
const store = transaction.objectStore("handles");
|
||||
const handle = await new Promise<FileSystemDirectoryHandle | null>((resolve, reject) => {
|
||||
const request = store.get("rootHandle");
|
||||
request.onsuccess = () => resolve(request.result || null);
|
||||
request.onerror = reject;
|
||||
});
|
||||
db.close();
|
||||
return handle;
|
||||
} catch (error) {
|
||||
console.error("[Directory] Failed to load cached handle:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async openHandleDB(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open("livesync-webapp-handles", 1);
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
if (!db.objectStoreNames.contains("handles")) {
|
||||
db.createObjectStore("handles");
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async start() {
|
||||
if (!this.core) {
|
||||
throw new Error("Core not initialized");
|
||||
@@ -333,21 +244,4 @@ class LiveSyncWebApp {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on load
|
||||
const app = new LiveSyncWebApp();
|
||||
|
||||
window.addEventListener("load", async () => {
|
||||
try {
|
||||
await app.initialize();
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize:", error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle page unload
|
||||
window.addEventListener("beforeunload", () => {
|
||||
void app.shutdown();
|
||||
});
|
||||
|
||||
// Export for debugging
|
||||
(window as any).livesyncApp = app;
|
||||
export { LiveSyncWebApp };
|
||||
|
||||
191
src/apps/webapp/vaultSelector.ts
Normal file
191
src/apps/webapp/vaultSelector.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
const HANDLE_DB_NAME = "livesync-webapp-handles";
|
||||
const HANDLE_STORE_NAME = "handles";
|
||||
const LAST_USED_KEY = "meta:lastUsedVaultId";
|
||||
const VAULT_KEY_PREFIX = "vault:";
|
||||
const MAX_HISTORY_COUNT = 10;
|
||||
|
||||
export type VaultHistoryItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
handle: FileSystemDirectoryHandle;
|
||||
lastUsedAt: number;
|
||||
};
|
||||
|
||||
type VaultHistoryValue = VaultHistoryItem;
|
||||
|
||||
function makeVaultKey(id: string): string {
|
||||
return `${VAULT_KEY_PREFIX}${id}`;
|
||||
}
|
||||
|
||||
function parseVaultId(key: string): string | null {
|
||||
if (!key.startsWith(VAULT_KEY_PREFIX)) {
|
||||
return null;
|
||||
}
|
||||
return key.slice(VAULT_KEY_PREFIX.length);
|
||||
}
|
||||
|
||||
function randomId(): string {
|
||||
const n = Math.random().toString(36).slice(2, 10);
|
||||
return `${Date.now()}-${n}`;
|
||||
}
|
||||
|
||||
async function hasReadWritePermission(handle: FileSystemDirectoryHandle, requestIfNeeded: boolean): Promise<boolean> {
|
||||
const h = handle as any;
|
||||
if (typeof h.queryPermission === "function") {
|
||||
const queried = await h.queryPermission({ mode: "readwrite" });
|
||||
if (queried === "granted") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!requestIfNeeded) {
|
||||
return false;
|
||||
}
|
||||
if (typeof h.requestPermission === "function") {
|
||||
const requested = await h.requestPermission({ mode: "readwrite" });
|
||||
return requested === "granted";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export class VaultHistoryStore {
|
||||
private async openHandleDB(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(HANDLE_DB_NAME, 1);
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
if (!db.objectStoreNames.contains(HANDLE_STORE_NAME)) {
|
||||
db.createObjectStore(HANDLE_STORE_NAME);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async withStore<T>(mode: IDBTransactionMode, task: (store: IDBObjectStore) => Promise<T>): Promise<T> {
|
||||
const db = await this.openHandleDB();
|
||||
try {
|
||||
const tx = db.transaction([HANDLE_STORE_NAME], mode);
|
||||
const store = tx.objectStore(HANDLE_STORE_NAME);
|
||||
return await task(store);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
private async requestAsPromise<T>(request: IDBRequest<T>): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async getLastUsedVaultId(): Promise<string | null> {
|
||||
return this.withStore("readonly", async (store) => {
|
||||
const value = await this.requestAsPromise(store.get(LAST_USED_KEY));
|
||||
return typeof value === "string" ? value : null;
|
||||
});
|
||||
}
|
||||
|
||||
async getVaultHistory(): Promise<VaultHistoryItem[]> {
|
||||
return this.withStore("readonly", async (store) => {
|
||||
const keys = (await this.requestAsPromise(store.getAllKeys())) as IDBValidKey[];
|
||||
const values = (await this.requestAsPromise(store.getAll())) as unknown[];
|
||||
const items: VaultHistoryItem[] = [];
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = String(keys[i]);
|
||||
const id = parseVaultId(key);
|
||||
const value = values[i] as Partial<VaultHistoryValue> | undefined;
|
||||
if (!id || !value || !value.handle || !value.name) {
|
||||
continue;
|
||||
}
|
||||
items.push({
|
||||
id,
|
||||
name: String(value.name),
|
||||
handle: value.handle,
|
||||
lastUsedAt: Number(value.lastUsedAt || 0),
|
||||
});
|
||||
}
|
||||
items.sort((a, b) => b.lastUsedAt - a.lastUsedAt);
|
||||
return items;
|
||||
});
|
||||
}
|
||||
|
||||
async saveSelectedVault(handle: FileSystemDirectoryHandle): Promise<VaultHistoryItem> {
|
||||
const now = Date.now();
|
||||
const existing = await this.getVaultHistory();
|
||||
|
||||
let matched: VaultHistoryItem | null = null;
|
||||
for (const item of existing) {
|
||||
try {
|
||||
if (await item.handle.isSameEntry(handle)) {
|
||||
matched = item;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Ignore handles that cannot be compared, keep scanning.
|
||||
}
|
||||
}
|
||||
|
||||
const item: VaultHistoryItem = {
|
||||
id: matched?.id ?? randomId(),
|
||||
name: handle.name,
|
||||
handle,
|
||||
lastUsedAt: now,
|
||||
};
|
||||
|
||||
await this.withStore("readwrite", async (store): Promise<void> => {
|
||||
await this.requestAsPromise(store.put(item, makeVaultKey(item.id)));
|
||||
await this.requestAsPromise(store.put(item.id, LAST_USED_KEY));
|
||||
|
||||
const merged = [...existing.filter((v) => v.id !== item.id), item].sort(
|
||||
(a, b) => b.lastUsedAt - a.lastUsedAt
|
||||
);
|
||||
const stale = merged.slice(MAX_HISTORY_COUNT);
|
||||
for (const old of stale) {
|
||||
await this.requestAsPromise(store.delete(makeVaultKey(old.id)));
|
||||
}
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
async activateHistoryItem(item: VaultHistoryItem): Promise<FileSystemDirectoryHandle> {
|
||||
const granted = await hasReadWritePermission(item.handle, true);
|
||||
if (!granted) {
|
||||
throw new Error("Vault permissions were not granted");
|
||||
}
|
||||
|
||||
const activated: VaultHistoryItem = {
|
||||
...item,
|
||||
lastUsedAt: Date.now(),
|
||||
};
|
||||
|
||||
await this.withStore("readwrite", async (store): Promise<void> => {
|
||||
await this.requestAsPromise(store.put(activated, makeVaultKey(activated.id)));
|
||||
await this.requestAsPromise(store.put(activated.id, LAST_USED_KEY));
|
||||
});
|
||||
|
||||
return item.handle;
|
||||
}
|
||||
|
||||
async pickNewVault(): Promise<FileSystemDirectoryHandle> {
|
||||
const picker = (window as any).showDirectoryPicker;
|
||||
if (typeof picker !== "function") {
|
||||
throw new Error("FileSystem API showDirectoryPicker is not supported in this browser");
|
||||
}
|
||||
|
||||
const handle = (await picker({
|
||||
mode: "readwrite",
|
||||
startIn: "documents",
|
||||
})) as FileSystemDirectoryHandle;
|
||||
|
||||
const granted = await hasReadWritePermission(handle, true);
|
||||
if (!granted) {
|
||||
throw new Error("Vault permissions were not granted");
|
||||
}
|
||||
|
||||
await this.saveSelectedVault(handle);
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,9 @@ export default defineConfig({
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: path.resolve(__dirname, "index.html"),
|
||||
webapp: path.resolve(__dirname, "webapp.html"),
|
||||
},
|
||||
external: ["crypto"],
|
||||
},
|
||||
},
|
||||
define: {
|
||||
|
||||
369
src/apps/webapp/webapp.css
Normal file
369
src/apps/webapp/webapp.css
Normal file
@@ -0,0 +1,369 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
padding: 40px;
|
||||
max-width: 700px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #666;
|
||||
margin-bottom: 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#status {
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#status.error {
|
||||
background: #fee;
|
||||
color: #c33;
|
||||
border: 1px solid #fcc;
|
||||
}
|
||||
|
||||
#status.warning {
|
||||
background: #ffeaa7;
|
||||
color: #d63031;
|
||||
border: 1px solid #fdcb6e;
|
||||
}
|
||||
|
||||
#status.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
#status.info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
|
||||
.vault-selector {
|
||||
border: 1px solid #e6e9f2;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background: #fbfcff;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.vault-selector h2 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.vault-selector p {
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.vault-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.vault-item {
|
||||
border: 1px solid #d9deee;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.vault-item-info {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vault-item-name {
|
||||
font-weight: 600;
|
||||
color: #1f2a44;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.vault-item-meta {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: #63708f;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
background: #2f5ae5;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #1e4ad6;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.empty-note {
|
||||
font-size: 13px;
|
||||
color: #6c757d;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-note.is-hidden,
|
||||
.vault-selector.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.info-section h2 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.info-section ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.info-section li {
|
||||
padding: 7px 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-section li::before {
|
||||
content: "•";
|
||||
color: #667eea;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
margin-left: -1em;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #e9ecef;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
body.livesync-log-visible {
|
||||
min-height: 100vh;
|
||||
padding-bottom: 42vh;
|
||||
}
|
||||
|
||||
#livesync-log-panel {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 42vh;
|
||||
z-index: 900;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #0f172a;
|
||||
border-top: 1px solid #334155;
|
||||
}
|
||||
|
||||
.livesync-log-header {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #e2e8f0;
|
||||
background: #111827;
|
||||
border-bottom: 1px solid #334155;
|
||||
}
|
||||
|
||||
#livesync-log-viewport {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 8px 12px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: #e2e8f0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.livesync-log-line {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
#livesync-command-bar {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
max-width: 40vw;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.livesync-command-button {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.livesync-command-button:hover:not(:disabled) {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.livesync-command-button.is-disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
#livesync-window-root {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
bottom: calc(42vh + 16px);
|
||||
z-index: 850;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#livesync-window-tabs {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 8px;
|
||||
background: #f3f4f6;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
#livesync-window-body {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.livesync-window-tab {
|
||||
border: 1px solid #d1d5db;
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.livesync-window-tab.is-active {
|
||||
background: #e0e7ff;
|
||||
border-color: #818cf8;
|
||||
}
|
||||
|
||||
.livesync-window-panel {
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.livesync-window-panel.is-active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.container {
|
||||
padding: 28px 18px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.vault-item {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
#livesync-command-bar {
|
||||
max-width: calc(100vw - 24px);
|
||||
right: 12px;
|
||||
left: 12px;
|
||||
bottom: 12px;
|
||||
}
|
||||
}
|
||||
45
src/apps/webapp/webapp.html
Normal file
45
src/apps/webapp/webapp.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Self-hosted LiveSync WebApp</title>
|
||||
<link rel="stylesheet" href="./webapp.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Self-hosted LiveSync</h1>
|
||||
<p class="subtitle">Browser-based Self-hosted LiveSync using FileSystem API</p>
|
||||
|
||||
<div id="status" class="info">Initialising...</div>
|
||||
|
||||
<div id="vault-selector" class="vault-selector">
|
||||
<h2>Select Vault Folder</h2>
|
||||
<p>Open a vault you already used, or pick a new folder.</p>
|
||||
|
||||
<div id="vault-history-list" class="vault-list"></div>
|
||||
<p id="vault-history-empty" class="empty-note">No saved vaults yet.</p>
|
||||
<button id="pick-new-vault" type="button">Choose new vault folder</button>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<h2>How to Use</h2>
|
||||
<ul>
|
||||
<li>Select a vault folder and grant permission</li>
|
||||
<li>Create <code>.livesync/settings.json</code> in your vault folder</li>
|
||||
<li>Add your CouchDB connection details</li>
|
||||
<li>Your files will be synced automatically</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>
|
||||
Powered by
|
||||
<a href="https://github.com/vrtmrz/obsidian-livesync" target="_blank">Self-hosted LiveSync</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="./bootstrap.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: 9145013efa...94e44e8a03
11
src/main.ts
11
src/main.ts
@@ -29,7 +29,6 @@ import type { ServiceModules } from "./types.ts";
|
||||
import { setNoticeClass } from "@lib/mock_and_interop/wrapper.ts";
|
||||
import type { ObsidianServiceContext } from "./lib/src/services/implements/obsidian/ObsidianServiceContext.ts";
|
||||
import { LiveSyncBaseCore } from "./LiveSyncBaseCore.ts";
|
||||
import { ModuleSetupObsidian } from "./modules/features/ModuleSetupObsidian.ts";
|
||||
import { ModuleObsidianMenu } from "./modules/essentialObsidian/ModuleObsidianMenu.ts";
|
||||
import { ModuleObsidianSettingsAsMarkdown } from "./modules/features/ModuleObsidianSettingAsMarkdown.ts";
|
||||
import { SetupManager } from "./modules/features/SetupManager.ts";
|
||||
@@ -38,6 +37,10 @@ import { enableI18nFeature } from "./serviceFeatures/onLayoutReady/enablei18n.ts
|
||||
import { useOfflineScanner } from "./lib/src/serviceFeatures/offlineScanner.ts";
|
||||
import { useCheckRemoteSize } from "./lib/src/serviceFeatures/checkRemoteSize.ts";
|
||||
import { useRedFlagFeatures } from "./serviceFeatures/redFlag.ts";
|
||||
import { useSetupProtocolFeature } from "./serviceFeatures/setupObsidian/setupProtocol.ts";
|
||||
import { useSetupQRCodeFeature } from "@lib/serviceFeatures/setupObsidian/qrCode";
|
||||
import { useSetupURIFeature } from "@lib/serviceFeatures/setupObsidian/setupUri";
|
||||
import { useSetupManagerHandlersFeature } from "./serviceFeatures/setupObsidian/setupManagerHandlers.ts";
|
||||
export type LiveSyncCore = LiveSyncBaseCore<ObsidianServiceContext, LiveSyncCommands>;
|
||||
export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
core: LiveSyncCore;
|
||||
@@ -147,7 +150,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
new ModuleObsidianEvents(this, core),
|
||||
new ModuleObsidianSettingDialogue(this, core),
|
||||
new ModuleObsidianMenu(core),
|
||||
new ModuleSetupObsidian(core),
|
||||
new ModuleObsidianSettingsAsMarkdown(core),
|
||||
new ModuleLog(this, core),
|
||||
new ModuleObsidianDocumentHistory(this, core),
|
||||
@@ -174,6 +176,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
const featuresInitialiser = enableI18nFeature;
|
||||
const curriedFeature = () => featuresInitialiser(core);
|
||||
core.services.appLifecycle.onLayoutReady.addHandler(curriedFeature);
|
||||
const setupManager = core.getModule(SetupManager);
|
||||
useSetupProtocolFeature(core, setupManager);
|
||||
useSetupQRCodeFeature(core);
|
||||
useSetupURIFeature(core);
|
||||
useSetupManagerHandlersFeature(core, setupManager);
|
||||
useOfflineScanner(core);
|
||||
useRedFlagFeatures(core);
|
||||
useCheckRemoteSize(core);
|
||||
|
||||
34
src/serviceFeatures/setupObsidian/setupManagerHandlers.ts
Normal file
34
src/serviceFeatures/setupObsidian/setupManagerHandlers.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { type SetupManager, UserMode } from "@/modules/features/SetupManager";
|
||||
import type { SetupFeatureHost } from "@lib/serviceFeatures/setupObsidian/types";
|
||||
import { EVENT_REQUEST_OPEN_P2P_SETTINGS, EVENT_REQUEST_OPEN_SETUP_URI } from "@lib/events/coreEvents";
|
||||
import { eventHub } from "@lib/hub/hub";
|
||||
import { fireAndForget } from "@lib/common/utils";
|
||||
import type { NecessaryServices } from "@lib/interfaces/ServiceModule";
|
||||
|
||||
export async function openSetupURI(setupManager: SetupManager) {
|
||||
await setupManager.onUseSetupURI(UserMode.Unknown);
|
||||
}
|
||||
|
||||
export async function openP2PSettings(host: SetupFeatureHost, setupManager: SetupManager) {
|
||||
return await setupManager.onP2PManualSetup(UserMode.Update, host.services.setting.currentSettings(), false);
|
||||
}
|
||||
|
||||
export function useSetupManagerHandlersFeature(
|
||||
host: NecessaryServices<"API" | "UI" | "setting" | "appLifecycle", never>,
|
||||
setupManager: SetupManager
|
||||
) {
|
||||
host.services.appLifecycle.onLoaded.addHandler(() => {
|
||||
host.services.API.addCommand({
|
||||
id: "livesync-opensetupuri",
|
||||
name: "Use the copied setup URI (Formerly Open setup URI)",
|
||||
callback: () => fireAndForget(openSetupURI(setupManager)),
|
||||
});
|
||||
|
||||
eventHub.onEvent(EVENT_REQUEST_OPEN_SETUP_URI, () => fireAndForget(() => openSetupURI(setupManager)));
|
||||
eventHub.onEvent(EVENT_REQUEST_OPEN_P2P_SETTINGS, () =>
|
||||
fireAndForget(() => openP2PSettings(host, setupManager))
|
||||
);
|
||||
|
||||
return Promise.resolve(true);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { describe, expect, it, vi, afterEach } from "vitest";
|
||||
import { eventHub } from "@lib/hub/hub";
|
||||
import { EVENT_REQUEST_OPEN_P2P_SETTINGS, EVENT_REQUEST_OPEN_SETUP_URI } from "@lib/events/coreEvents";
|
||||
import { openP2PSettings, openSetupURI, useSetupManagerHandlersFeature } from "./setupManagerHandlers";
|
||||
|
||||
vi.mock("@/modules/features/SetupManager", () => {
|
||||
return {
|
||||
UserMode: {
|
||||
Unknown: "unknown",
|
||||
Update: "unknown",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe("setupObsidian/setupManagerHandlers", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("openSetupURI should delegate to SetupManager.onUseSetupURI", async () => {
|
||||
const setupManager = {
|
||||
onUseSetupURI: vi.fn(async () => true),
|
||||
} as any;
|
||||
|
||||
await openSetupURI(setupManager);
|
||||
expect(setupManager.onUseSetupURI).toHaveBeenCalledWith("unknown");
|
||||
});
|
||||
|
||||
it("openP2PSettings should delegate to SetupManager.onP2PManualSetup", async () => {
|
||||
const settings = { x: 1 };
|
||||
const host = {
|
||||
services: {
|
||||
setting: {
|
||||
currentSettings: vi.fn(() => settings),
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
const setupManager = {
|
||||
onP2PManualSetup: vi.fn(async () => true),
|
||||
} as any;
|
||||
|
||||
await openP2PSettings(host, setupManager);
|
||||
expect(setupManager.onP2PManualSetup).toHaveBeenCalledWith("unknown", settings, false);
|
||||
});
|
||||
|
||||
it("useSetupManagerHandlersFeature should register onLoaded handler that wires command and events", async () => {
|
||||
const addHandler = vi.fn();
|
||||
const addCommand = vi.fn();
|
||||
const onEventSpy = vi.spyOn(eventHub, "onEvent");
|
||||
|
||||
const host = {
|
||||
services: {
|
||||
API: {
|
||||
addCommand,
|
||||
},
|
||||
appLifecycle: {
|
||||
onLoaded: {
|
||||
addHandler,
|
||||
},
|
||||
},
|
||||
setting: {
|
||||
currentSettings: vi.fn(() => ({ x: 1 })),
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
const setupManager = {
|
||||
onUseSetupURI: vi.fn(async () => true),
|
||||
onP2PManualSetup: vi.fn(async () => true),
|
||||
} as any;
|
||||
|
||||
useSetupManagerHandlersFeature(host, setupManager);
|
||||
expect(addHandler).toHaveBeenCalledTimes(1);
|
||||
|
||||
const loadedHandler = addHandler.mock.calls[0][0] as () => Promise<boolean>;
|
||||
await loadedHandler();
|
||||
|
||||
expect(addCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: "livesync-opensetupuri",
|
||||
name: "Use the copied setup URI (Formerly Open setup URI)",
|
||||
})
|
||||
);
|
||||
expect(onEventSpy).toHaveBeenCalledWith(EVENT_REQUEST_OPEN_SETUP_URI, expect.any(Function));
|
||||
expect(onEventSpy).toHaveBeenCalledWith(EVENT_REQUEST_OPEN_P2P_SETTINGS, expect.any(Function));
|
||||
});
|
||||
});
|
||||
37
src/serviceFeatures/setupObsidian/setupProtocol.ts
Normal file
37
src/serviceFeatures/setupObsidian/setupProtocol.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "@lib/common/types";
|
||||
import type { LogFunction } from "@lib/services/lib/logUtils";
|
||||
import { createInstanceLogFunction } from "@lib/services/lib/logUtils";
|
||||
import type { SetupFeatureHost } from "@lib/serviceFeatures/setupObsidian/types";
|
||||
import { configURIBase } from "@/common/types";
|
||||
import type { NecessaryServices } from "@lib/interfaces/ServiceModule";
|
||||
import { type SetupManager, UserMode } from "@/modules/features/SetupManager";
|
||||
|
||||
async function handleSetupProtocol(setupManager: SetupManager, conf: Record<string, string>) {
|
||||
if (conf.settings) {
|
||||
await setupManager.onUseSetupURI(UserMode.Unknown, `${configURIBase}${encodeURIComponent(conf.settings)}`);
|
||||
} else if (conf.settingsQR) {
|
||||
await setupManager.decodeQR(conf.settingsQR);
|
||||
}
|
||||
}
|
||||
|
||||
export function registerSetupProtocolHandler(host: SetupFeatureHost, log: LogFunction, setupManager: SetupManager) {
|
||||
try {
|
||||
host.services.API.registerProtocolHandler("setuplivesync", async (conf) => {
|
||||
await handleSetupProtocol(setupManager, conf);
|
||||
});
|
||||
} catch (e) {
|
||||
log("Failed to register protocol handler. This feature may not work in some environments.", LOG_LEVEL_NOTICE);
|
||||
log(e, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
}
|
||||
|
||||
export function useSetupProtocolFeature(
|
||||
host: NecessaryServices<"API" | "UI" | "setting" | "appLifecycle", never>,
|
||||
setupManager: SetupManager
|
||||
) {
|
||||
const log = createInstanceLogFunction("SF:SetupProtocol", host.services.API);
|
||||
host.services.appLifecycle.onLoaded.addHandler(() => {
|
||||
registerSetupProtocolHandler(host, log, setupManager);
|
||||
return Promise.resolve(true);
|
||||
});
|
||||
}
|
||||
131
src/serviceFeatures/setupObsidian/setupProtocol.unit.spec.ts
Normal file
131
src/serviceFeatures/setupObsidian/setupProtocol.unit.spec.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { describe, expect, it, vi, afterEach } from "vitest";
|
||||
import { registerSetupProtocolHandler, useSetupProtocolFeature } from "./setupProtocol";
|
||||
|
||||
vi.mock("@/common/types", () => {
|
||||
return {
|
||||
configURIBase: "mock-config://",
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/modules/features/SetupManager", () => {
|
||||
return {
|
||||
UserMode: {
|
||||
Unknown: "unknown",
|
||||
Update: "unknown",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe("setupObsidian/setupProtocol", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("registerSetupProtocolHandler should route settings payload to onUseSetupURI", async () => {
|
||||
let protocolHandler: ((params: Record<string, string>) => Promise<void>) | undefined;
|
||||
const host = {
|
||||
services: {
|
||||
API: {
|
||||
registerProtocolHandler: vi.fn(
|
||||
(_action: string, handler: (params: Record<string, string>) => Promise<void>) => {
|
||||
protocolHandler = handler;
|
||||
}
|
||||
),
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
const log = vi.fn();
|
||||
const setupManager = {
|
||||
onUseSetupURI: vi.fn(async () => true),
|
||||
decodeQR: vi.fn(async () => true),
|
||||
} as any;
|
||||
|
||||
registerSetupProtocolHandler(host, log, setupManager);
|
||||
expect(host.services.API.registerProtocolHandler).toHaveBeenCalledWith("setuplivesync", expect.any(Function));
|
||||
|
||||
await protocolHandler!({ settings: "a b" });
|
||||
expect(setupManager.onUseSetupURI).toHaveBeenCalledWith(
|
||||
"unknown",
|
||||
`mock-config://${encodeURIComponent("a b")}`
|
||||
);
|
||||
expect(setupManager.decodeQR).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("registerSetupProtocolHandler should route settingsQR payload to decodeQR", async () => {
|
||||
let protocolHandler: ((params: Record<string, string>) => Promise<void>) | undefined;
|
||||
const host = {
|
||||
services: {
|
||||
API: {
|
||||
registerProtocolHandler: vi.fn(
|
||||
(_action: string, handler: (params: Record<string, string>) => Promise<void>) => {
|
||||
protocolHandler = handler;
|
||||
}
|
||||
),
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
const log = vi.fn();
|
||||
const setupManager = {
|
||||
onUseSetupURI: vi.fn(async () => true),
|
||||
decodeQR: vi.fn(async () => true),
|
||||
} as any;
|
||||
|
||||
registerSetupProtocolHandler(host, log, setupManager);
|
||||
await protocolHandler!({ settingsQR: "qr-data" });
|
||||
|
||||
expect(setupManager.decodeQR).toHaveBeenCalledWith("qr-data");
|
||||
expect(setupManager.onUseSetupURI).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("registerSetupProtocolHandler should log and continue when registration throws", () => {
|
||||
const host = {
|
||||
services: {
|
||||
API: {
|
||||
registerProtocolHandler: vi.fn(() => {
|
||||
throw new Error("register failed");
|
||||
}),
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
const log = vi.fn();
|
||||
const setupManager = {
|
||||
onUseSetupURI: vi.fn(),
|
||||
decodeQR: vi.fn(),
|
||||
} as any;
|
||||
|
||||
registerSetupProtocolHandler(host, log, setupManager);
|
||||
|
||||
expect(log).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("useSetupProtocolFeature should register onLoaded handler", async () => {
|
||||
const addHandler = vi.fn();
|
||||
const registerProtocolHandler = vi.fn();
|
||||
const host = {
|
||||
services: {
|
||||
API: {
|
||||
addLog: vi.fn(),
|
||||
registerProtocolHandler,
|
||||
},
|
||||
appLifecycle: {
|
||||
onLoaded: {
|
||||
addHandler,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
const setupManager = {
|
||||
onUseSetupURI: vi.fn(),
|
||||
decodeQR: vi.fn(),
|
||||
} as any;
|
||||
|
||||
useSetupProtocolFeature(host, setupManager);
|
||||
expect(addHandler).toHaveBeenCalledTimes(1);
|
||||
|
||||
const loadedHandler = addHandler.mock.calls[0][0] as () => Promise<boolean>;
|
||||
await loadedHandler();
|
||||
|
||||
expect(registerProtocolHandler).toHaveBeenCalledWith("setuplivesync", expect.any(Function));
|
||||
});
|
||||
});
|
||||
122
updates.md
122
updates.md
@@ -3,48 +3,43 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025)
|
||||
|
||||
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
|
||||
|
||||
## 0.25.52-patched-3
|
||||
## 0.25.53
|
||||
|
||||
16th March, 2026
|
||||
17th March, 2026
|
||||
|
||||
I did wonder whether I should have released a minor version update, but when I actually tested it, compatibility seemed to be intact, so I didn’t. Hmm.
|
||||
|
||||
### Fixed
|
||||
|
||||
#### P2P Synchronisation
|
||||
|
||||
- Fixed flaky timing issues in P2P synchronisation.
|
||||
- Fixed more binary file handling issues in CLI.
|
||||
- No longer unexpected `Unhandled Rejections` during P2P operations (waiting for acceptance).
|
||||
|
||||
#### Journal Sync
|
||||
|
||||
- Fixed an issue where some conflicts cannot be resolved in Journal Sync.
|
||||
- Many minor fixes have been made for better stability and reliability.
|
||||
|
||||
### Tests
|
||||
|
||||
- Rewrite P2P end-to-end tests to use the CLI as host.
|
||||
- Rewrite P2P end-to-end tests to use the CLI as a host.
|
||||
|
||||
### CLI
|
||||
|
||||
## 0.25.52-patched-2
|
||||
We have previously developed FileSystem LiveSync and various other components in a separate repository, but updates have been significantly delayed, and we have been plagued by compatibility issues. Now, a CLI tool using the same core logic is emerging. This does not directly manipulate the file system, but it offers a more convenient way of working and can also communicate with Object Storage. We can also resolve conflicts. Please refer to the code in `src/apps/cli` for the [self-hosted-livesync-cli](./src/apps/cli/README.md) for more details.
|
||||
- Add `self-hosted-livesync-cli` to `src/apps/cli` as a headless and dedicated version.
|
||||
- P2P sync and Object Storage are also supported in the CLI.
|
||||
- Yes, we have finally managed to 'get one file'.
|
||||
- Also, no more need for a [LiveSync PeerServer](https://github.com/vrtmrz/livesync-serverpeer) for virtual environments! The CLI can do it.
|
||||
|
||||
14th March, 2026
|
||||
- Now binary files are also supported in the CLI.
|
||||
|
||||
### Fixed
|
||||
### Refactored or internal changes
|
||||
|
||||
- No longer unexpected `Unhandled Rejections` during P2P operations (waiting acceptance).
|
||||
- Fixed an issue where conflicts cannot be resolved in Journal Sync
|
||||
|
||||
### CLI new features
|
||||
|
||||
- `mirror` command has been added to the CLI. This command is intended to mirror the storage to the local database.
|
||||
- `p2p-sync`, `p2p-peers`, and `p2p-host` commands have been added to the CLI. These commands are intended for P2P synchronisation.
|
||||
- Yes, no more need for a [LiveSync PeerServer](https://github.com/vrtmrz/livesync-serverpeer) for virtual environments! The CLI can handle it by itself.
|
||||
|
||||
## 0.25.52-patched-1
|
||||
|
||||
12th March, 2026
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Journal Sync had not been working on some timing, due to a compatibility issue (for a long time).
|
||||
- ServiceFileAccessBase now correctly handles the reading of binary files.
|
||||
- HeadlessAPIService now correctly provides the online status (always online) to the plug-in.
|
||||
- Non-worker version of bgWorker now correctly handles some functions.
|
||||
|
||||
### Refactored
|
||||
|
||||
- Separated `ObsidianLiveSyncPlugin` into `ObsidianLiveSyncPlugin` and `LiveSyncBaseCore`.
|
||||
- Now `LiveSyncCore` indicates the type specified version of `LiveSyncBaseCore`.
|
||||
- Referencing `plugin.xxx` has been rewritten to referencing the corresponding service or `core.xxx`.
|
||||
@@ -53,22 +48,9 @@ The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsid
|
||||
- ControlService now provides the readiness for processing operations.
|
||||
- DatabaseService is now able to modify database opening options on derived classes.
|
||||
- Now `useOfflineScanner`, `useCheckRemoteSize`, and `useRedFlagFeatures` are set from `main.ts`, instead of `LiveSyncBaseCore`.
|
||||
|
||||
### Internal API changes
|
||||
|
||||
- Storage Access APIs are now yielding Promises. This is to allow more limited storage platforms to be supported.
|
||||
- Journal Replicator now yields true after the replication is done.
|
||||
|
||||
### CLI
|
||||
|
||||
We have previously developed FileSystem LiveSync and various other components in a separate repository, but updates have been significantly delayed, and we have been plagued by compatibility issues. Now, a CLI tool using the same core logic is emerging. This does not directly manipulate the file system, but it offers a more convenient way of working and can also communicate with Object Storage. We can also resolve conflicts. Please refer to the code in `src/apps/cli` for the [self-hosted-livesync-cli](./src/apps/cli/README.md) for more details.
|
||||
|
||||
- Add `self-hosted-livesync-cli` to `src/apps/cli` as a headless and dedicated version.
|
||||
- Add more tests.
|
||||
- Object Storage support has also been confirmed (and fixed) in CLI.
|
||||
- Yes, we have finally managed to 'get one file'.
|
||||
- Now binary files are also supported in the CLI.
|
||||
|
||||
### R&D
|
||||
|
||||
- Browser-version of Self-hosted LiveSync is now in development. This is not intended for public use now, but I will eventually make it available for testing.
|
||||
@@ -271,67 +253,5 @@ This release is identical to 0.25.41-patched-3, except for the version number.
|
||||
- Migrated from the outdated, unstable platform abstraction layer to services.
|
||||
- A bit more services will be added in the future for better maintainability.
|
||||
|
||||
## 0.25.41
|
||||
|
||||
24th January, 2026
|
||||
|
||||
### Fixed
|
||||
|
||||
- No longer `No available splitter for settings!!` errors occur after fetching old remote settings while rebuilding local database. (#748)
|
||||
|
||||
### Improved
|
||||
|
||||
- Boot sequence warning is now kept in the in-editor notification area.
|
||||
|
||||
### New feature
|
||||
|
||||
- We can now set the maximum modified time for reflect events in the settings. (for #754)
|
||||
- This setting can be configured from `Patches` -> `Remediation` in the settings dialogue.
|
||||
- Enabling this setting will restrict the propagation from the database to storage to only those changes made before the specified date and time.
|
||||
- This feature is primarily intended for recovery purposes. After placing `redflag.md` in an empty vault and importing the Self-hosted LiveSync configuration, please perform this configuration, and then fetch the local database from the remote.
|
||||
- This feature is useful when we want to prevent recent unwanted changes from being reflected in the local storage.
|
||||
|
||||
### Refactored
|
||||
|
||||
- Module to service refactoring has been started for better maintainability:
|
||||
- UI module has been moved to UI service.
|
||||
|
||||
### Behaviour change
|
||||
|
||||
- Default chunk splitter version has been changed to `Rabin-Karp` for new installations.
|
||||
|
||||
## 0.25.40
|
||||
|
||||
23rd January, 2026
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue where some events were not triggered correctly after the refactoring in 0.25.39.
|
||||
|
||||
## 0.25.39
|
||||
|
||||
23rd January, 2026
|
||||
|
||||
Also no behaviour changes or fixes in this release. Just refactoring for better maintainability. Thank you for your patience! I will address some of the reported issues soon.
|
||||
However, this is not a minor refactoring, so please be careful. Let me know if you find any unexpected behaviour after this update.
|
||||
|
||||
### Refactored
|
||||
|
||||
- Rewrite the service's binding/handler assignment systems
|
||||
- Removed loopholes that allowed traversal between services to clarify dependencies.
|
||||
- Consolidated the hidden state-related state, the handler, and the addition of bindings to the handler into a single object.
|
||||
- Currently, functions that can have handlers added implement either addHandler or setHandler directly on the function itself.
|
||||
I understand there are differing opinions on this, but for now, this is how it stands.
|
||||
- Services now possess a Context. Please ensure each platform has a class that inherits from ServiceContext.
|
||||
- To permit services to be dynamically bound, the services themselves are now defined by interfaces.
|
||||
|
||||
## 0.25.38
|
||||
|
||||
17th January, 2026
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue where indexedDB would not close correctly on some environments, causing unexpected errors during database operations.
|
||||
|
||||
Full notes are in
|
||||
[updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md).
|
||||
|
||||
171
updates_old.md
171
updates_old.md
@@ -3,6 +3,177 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025)
|
||||
|
||||
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
|
||||
|
||||
## 0.25.53
|
||||
|
||||
17th March, 2026
|
||||
|
||||
I did wonder whether I should have released a minor version update, but when I actually tested it, compatibility seemed to be intact, so I didn’t. Hmm.
|
||||
|
||||
### Fixed
|
||||
|
||||
#### P2P Synchronisation
|
||||
|
||||
- Fixed flaky timing issues in P2P synchronisation.
|
||||
- No longer unexpected `Unhandled Rejections` during P2P operations (waiting for acceptance).
|
||||
|
||||
#### Journal Sync
|
||||
|
||||
- Fixed an issue where some conflicts cannot be resolved in Journal Sync.
|
||||
- Many minor fixes have been made for better stability and reliability.
|
||||
|
||||
### Tests
|
||||
|
||||
- Rewrite P2P end-to-end tests to use the CLI as a host.
|
||||
|
||||
### CLI
|
||||
|
||||
We have previously developed FileSystem LiveSync and various other components in a separate repository, but updates have been significantly delayed, and we have been plagued by compatibility issues. Now, a CLI tool using the same core logic is emerging. This does not directly manipulate the file system, but it offers a more convenient way of working and can also communicate with Object Storage. We can also resolve conflicts. Please refer to the code in `src/apps/cli` for the [self-hosted-livesync-cli](./src/apps/cli/README.md) for more details.
|
||||
- Add `self-hosted-livesync-cli` to `src/apps/cli` as a headless and dedicated version.
|
||||
- P2P sync and Object Storage are also supported in the CLI.
|
||||
- Yes, we have finally managed to 'get one file'.
|
||||
- Also, no more need for a [LiveSync PeerServer](https://github.com/vrtmrz/livesync-serverpeer) for virtual environments! The CLI can do it.
|
||||
|
||||
- Now binary files are also supported in the CLI.
|
||||
|
||||
### Refactored or internal changes
|
||||
|
||||
- ServiceFileAccessBase now correctly handles the reading of binary files.
|
||||
- HeadlessAPIService now correctly provides the online status (always online) to the plug-in.
|
||||
- Non-worker version of bgWorker now correctly handles some functions.
|
||||
- Separated `ObsidianLiveSyncPlugin` into `ObsidianLiveSyncPlugin` and `LiveSyncBaseCore`.
|
||||
- Now `LiveSyncCore` indicates the type specified version of `LiveSyncBaseCore`.
|
||||
- Referencing `plugin.xxx` has been rewritten to referencing the corresponding service or `core.xxx`.
|
||||
- Offline change scanner and the local database preparation have been separated.
|
||||
- Set default priority for processFileEvent and processSynchroniseResult for the place to add hooks.
|
||||
- ControlService now provides the readiness for processing operations.
|
||||
- DatabaseService is now able to modify database opening options on derived classes.
|
||||
- Now `useOfflineScanner`, `useCheckRemoteSize`, and `useRedFlagFeatures` are set from `main.ts`, instead of `LiveSyncBaseCore`.
|
||||
- Storage Access APIs are now yielding Promises. This is to allow more limited storage platforms to be supported.
|
||||
- Journal Replicator now yields true after the replication is done.
|
||||
|
||||
### R&D
|
||||
|
||||
- Browser-version of Self-hosted LiveSync is now in development. This is not intended for public use now, but I will eventually make it available for testing.
|
||||
- We can see the code in `src/apps/webapp` for the browser version.
|
||||
|
||||
|
||||
## 0.25.52-patched-3
|
||||
|
||||
16th March, 2026
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed flaky timing issues in P2P synchronisation.
|
||||
- Fixed more binary file handling issues in CLI.
|
||||
|
||||
### Tests
|
||||
|
||||
- Rewrite P2P end-to-end tests to use the CLI as host.
|
||||
|
||||
|
||||
## 0.25.52-patched-2
|
||||
|
||||
14th March, 2026
|
||||
|
||||
### Fixed
|
||||
|
||||
- No longer unexpected `Unhandled Rejections` during P2P operations (waiting acceptance).
|
||||
- Fixed an issue where conflicts cannot be resolved in Journal Sync
|
||||
|
||||
### CLI new features
|
||||
|
||||
- `mirror` command has been added to the CLI. This command is intended to mirror the storage to the local database.
|
||||
- `p2p-sync`, `p2p-peers`, and `p2p-host` commands have been added to the CLI. These commands are intended for P2P synchronisation.
|
||||
- Yes, no more need for a [LiveSync PeerServer](https://github.com/vrtmrz/livesync-serverpeer) for virtual environments! The CLI can handle it by itself.
|
||||
|
||||
## 0.25.52-patched-1
|
||||
|
||||
12th March, 2026
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Journal Sync had not been working on some timing, due to a compatibility issue (for a long time).
|
||||
- ServiceFileAccessBase now correctly handles the reading of binary files.
|
||||
- HeadlessAPIService now correctly provides the online status (always online) to the plug-in.
|
||||
- Non-worker version of bgWorker now correctly handles some functions.
|
||||
|
||||
### Refactored
|
||||
|
||||
- Separated `ObsidianLiveSyncPlugin` into `ObsidianLiveSyncPlugin` and `LiveSyncBaseCore`.
|
||||
- Now `LiveSyncCore` indicates the type specified version of `LiveSyncBaseCore`.
|
||||
- Referencing `plugin.xxx` has been rewritten to referencing the corresponding service or `core.xxx`.
|
||||
- Offline change scanner and the local database preparation have been separated.
|
||||
- Set default priority for processFileEvent and processSynchroniseResult for the place to add hooks.
|
||||
- ControlService now provides the readiness for processing operations.
|
||||
- DatabaseService is now able to modify database opening options on derived classes.
|
||||
- Now `useOfflineScanner`, `useCheckRemoteSize`, and `useRedFlagFeatures` are set from `main.ts`, instead of `LiveSyncBaseCore`.
|
||||
|
||||
### Internal API changes
|
||||
|
||||
- Storage Access APIs are now yielding Promises. This is to allow more limited storage platforms to be supported.
|
||||
- Journal Replicator now yields true after the replication is done.
|
||||
|
||||
### CLI
|
||||
|
||||
We have previously developed FileSystem LiveSync and various other components in a separate repository, but updates have been significantly delayed, and we have been plagued by compatibility issues. Now, a CLI tool using the same core logic is emerging. This does not directly manipulate the file system, but it offers a more convenient way of working and can also communicate with Object Storage. We can also resolve conflicts. Please refer to the code in `src/apps/cli` for the [self-hosted-livesync-cli](./src/apps/cli/README.md) for more details.
|
||||
|
||||
- Add `self-hosted-livesync-cli` to `src/apps/cli` as a headless and dedicated version.
|
||||
- Add more tests.
|
||||
- Object Storage support has also been confirmed (and fixed) in CLI.
|
||||
- Yes, we have finally managed to 'get one file'.
|
||||
- Now binary files are also supported in the CLI.
|
||||
|
||||
### R&D
|
||||
|
||||
- Browser-version of Self-hosted LiveSync is now in development. This is not intended for public use now, but I will eventually make it available for testing.
|
||||
- We can see the code in `src/apps/webapp` for the browser version.
|
||||
|
||||
|
||||
## 0.25.52
|
||||
|
||||
9th March, 2026
|
||||
|
||||
Excuses: Too much `I`.
|
||||
Whilst I had a fever, I could not figure it out at all, but once I felt better, I spotted the problem in about thirty seconds. I apologise for causing you concern. I am grateful for your patience.
|
||||
I would like to devise a mechanism for running simple test scenarios. Now that we have got the Obsidian CLI up and running, it seems the perfect opportunity.
|
||||
|
||||
To improve the bus factor, we really need to organise the source code more thoroughly. Your cooperation and contributions would be greatly appreciated.
|
||||
|
||||
### Fixed
|
||||
|
||||
- No longer unexpected deletion-propagation occurs when the parent directory is not empty (#813).
|
||||
|
||||
### Revert reversions
|
||||
|
||||
- Reverted the reversion of ModuleCheckRemoteSize. Now it is back to the service feature.
|
||||
|
||||
## 0.25.51
|
||||
|
||||
7th March, 2026
|
||||
|
||||
### Reverted
|
||||
|
||||
- Reverted to ModuleRedFlag and ModuleInitializerFile to the previous version because of some unexpected issues. (#813)
|
||||
- I will re-implement them in the future with better design and tests.
|
||||
|
||||
## 0.25.50
|
||||
|
||||
3rd March, 2026
|
||||
|
||||
Note: 0.25.49 has been skipped because of too verbose logging (credentials are logged in verbose level, but I realised that could lead to unexpected exposure on issue reporting). Please bump to 0.25.50 to get the fix if you are on 0.25.49. (No expected behaviour changes except the logging).
|
||||
|
||||
### Fixed
|
||||
|
||||
- No longer deleted files are not clickable in the Global History pane.
|
||||
- Diff view now uses more specific classes (#803).
|
||||
- A message of configuration mismatching slightly added for better understanding.
|
||||
- Now it says `When replication is initiated manually via the command palette or ribbon, a dialogue box will open to address this.` to make it clear that the user can fix the issue by themselves.
|
||||
|
||||
### Refactored
|
||||
|
||||
- `ModuleRedFlag` has been refactored to `serviceFeatures/redFlag` and also tested.
|
||||
- `ModuleInitializerFile` has been refactored to `lib/serviceFeatures/offlineScanner` and also tested.
|
||||
|
||||
## 0.25.48
|
||||
|
||||
2nd March, 2026
|
||||
|
||||
Reference in New Issue
Block a user