mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-02-03 10:57:14 +00:00
Merge branch 'beta'
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
"**/tests.ts",
|
||||
"**/**test.ts",
|
||||
"**/**.test.ts",
|
||||
"src/apps/**",
|
||||
"esbuild.*.mjs",
|
||||
"terser.*.mjs"
|
||||
],
|
||||
|
||||
@@ -21,14 +21,10 @@ const updateInfo = JSON.stringify(fs.readFileSync("./updates.md") + "");
|
||||
|
||||
const PATHS_TEST_INSTALL = process.env?.PATHS_TEST_INSTALL || "";
|
||||
const PATH_TEST_INSTALL = PATHS_TEST_INSTALL.split(path.delimiter).map(p => p.trim()).filter(p => p.length);
|
||||
if (!prod) {
|
||||
if (PATH_TEST_INSTALL) {
|
||||
console.log(`Built files will be copied to ${PATH_TEST_INSTALL}`);
|
||||
} else {
|
||||
console.log("Development build: You can install the plug-in to Obsidian for testing by exporting the PATHS_TEST_INSTALL environment variable with the paths to your vault plugins directories separated by your system path delimiter (':' on Unix, ';' on Windows).");
|
||||
}
|
||||
if (PATH_TEST_INSTALL) {
|
||||
console.log(`Built files will be copied to ${PATH_TEST_INSTALL}`);
|
||||
} else {
|
||||
console.log("Production build");
|
||||
console.log("Development build: You can install the plug-in to Obsidian for testing by exporting the PATHS_TEST_INSTALL environment variable with the paths to your vault plugins directories separated by your system path delimiter (':' on Unix, ';' on Windows).");
|
||||
}
|
||||
|
||||
const moduleAliasPlugin = {
|
||||
|
||||
@@ -40,7 +40,7 @@ export default [
|
||||
"src/lib/test",
|
||||
"src/lib/src/cli",
|
||||
"**/main.js",
|
||||
"src/lib/apps/webpeer/*",
|
||||
"src/apps/**/*",
|
||||
".prettierrc.*.mjs",
|
||||
".prettierrc.mjs",
|
||||
"*.config.mjs"
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.25.24.beta3",
|
||||
"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",
|
||||
"authorUrl": "https://github.com/vrtmrz",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.25.41",
|
||||
"version": "0.25.42",
|
||||
"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",
|
||||
|
||||
18
package-lock.json
generated
18
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.41",
|
||||
"version": "0.25.42",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.41",
|
||||
"version": "0.25.42",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.808.0",
|
||||
@@ -19,7 +19,7 @@
|
||||
"fflate": "^0.8.2",
|
||||
"idb": "^8.0.3",
|
||||
"minimatch": "^10.0.2",
|
||||
"octagonal-wheels": "^0.1.44",
|
||||
"octagonal-wheels": "^0.1.45",
|
||||
"qrcode-generator": "^1.4.4",
|
||||
"trystero": "^0.22.0",
|
||||
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
|
||||
@@ -11151,9 +11151,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/octagonal-wheels": {
|
||||
"version": "0.1.44",
|
||||
"resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.44.tgz",
|
||||
"integrity": "sha512-sUn/dkYQ2AbMB0R8CubVd75BjkcsteW9B14ArO99F6wM5JRwOo/yPIBBoxCUFE7JjBFOfuWG21C9E3NTga6XrA==",
|
||||
"version": "0.1.45",
|
||||
"resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.45.tgz",
|
||||
"integrity": "sha512-gXoCrwoUIXhmu57YN4BxAtBe+JaYNJNaXaZuVjqjopwYKpH5p2mn1om6KjA22rgGPiIJFXkse2U28FFXoT3/0Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"idb": "^8.0.3"
|
||||
@@ -23036,9 +23036,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"octagonal-wheels": {
|
||||
"version": "0.1.44",
|
||||
"resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.44.tgz",
|
||||
"integrity": "sha512-sUn/dkYQ2AbMB0R8CubVd75BjkcsteW9B14ArO99F6wM5JRwOo/yPIBBoxCUFE7JjBFOfuWG21C9E3NTga6XrA==",
|
||||
"version": "0.1.45",
|
||||
"resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.45.tgz",
|
||||
"integrity": "sha512-gXoCrwoUIXhmu57YN4BxAtBe+JaYNJNaXaZuVjqjopwYKpH5p2mn1om6KjA22rgGPiIJFXkse2U28FFXoT3/0Q==",
|
||||
"requires": {
|
||||
"idb": "^8.0.3"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.41",
|
||||
"version": "0.25.42",
|
||||
"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",
|
||||
@@ -129,7 +129,7 @@
|
||||
"fflate": "^0.8.2",
|
||||
"idb": "^8.0.3",
|
||||
"minimatch": "^10.0.2",
|
||||
"octagonal-wheels": "^0.1.44",
|
||||
"octagonal-wheels": "^0.1.45",
|
||||
"qrcode-generator": "^1.4.4",
|
||||
"trystero": "^0.22.0",
|
||||
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
|
||||
|
||||
24
src/apps/webpeer/.gitignore
vendored
Normal file
24
src/apps/webpeer/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
30
src/apps/webpeer/README.md
Normal file
30
src/apps/webpeer/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# A pseudo client for Self-hosted LiveSync Peer-to-Peer Sync mode
|
||||
|
||||
## What is it for?
|
||||
|
||||
This is a pseudo client for the Self-hosted LiveSync Peer-to-Peer Sync mode. It is a simple pure-client-side web-application that can be connected to the Self-hosted LiveSync in peer-to-peer.
|
||||
|
||||
As long as you have a browser, it starts up, so if you leave it opened some device, it can replace your existing remote servers such as CouchDB.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Of course, it has not been fully tested. Rather, it was created to be tested.
|
||||
|
||||
This pseudo client actually receives the data from other devices, and sends if some device requests it. However, it does not store **files** in the local storage. If you want to purge the data, please purge the browser's cache and indexedDB, local storage, etc.
|
||||
|
||||
## How to use it?
|
||||
|
||||
We can build the application by running the following command:
|
||||
|
||||
```bash
|
||||
$ deno task build
|
||||
```
|
||||
|
||||
Then, open the `dist/index.html` in the browser. It can be configured as the same as the Self-hosted LiveSync (Same components are used[^1]).
|
||||
|
||||
## Some notes
|
||||
|
||||
I will launch this application in the github pages later, so will be able to use it without building it. However, that shares the origin. Hence, the application that your have built and deployed would be more secure.
|
||||
|
||||
|
||||
[^1]: Congrats! I made it modular. Finally...
|
||||
|
||||
1101
src/apps/webpeer/deno.lock
generated
Normal file
1101
src/apps/webpeer/deno.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
src/apps/webpeer/index.html
Normal file
17
src/apps/webpeer/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="icon.svg" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Peer-to-Peer Daemon on Browser</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
25
src/apps/webpeer/package.json
Normal file
25
src/apps/webpeer/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "webpeer",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"eslint-plugin-svelte": "^3.12.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@tsconfig/svelte": "^5.0.5",
|
||||
"svelte": "5.41.1",
|
||||
"svelte-check": "^4.3.3",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "^7.3.0"
|
||||
},
|
||||
"imports": {
|
||||
"../../src/worker/bgWorker.ts": "../../src/worker/bgWorker.mock.ts"
|
||||
}
|
||||
}
|
||||
52
src/apps/webpeer/public/icon.svg
Normal file
52
src/apps/webpeer/public/icon.svg
Normal file
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="512"
|
||||
height="512"
|
||||
viewBox="0 0 511.99998 511.99998"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
id="layer1"
|
||||
transform="translate(-22.694448,-28.922305)">
|
||||
<g
|
||||
id="g4"
|
||||
transform="matrix(4.6921194,0,0,4.6921194,-266.26061,-494.11652)">
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.2366;stroke-opacity:1"
|
||||
id="rect2"
|
||||
width="109.11913"
|
||||
height="109.11913"
|
||||
x="61.583057"
|
||||
y="111.47176" />
|
||||
<g
|
||||
id="g3"
|
||||
transform="matrix(0.77702959,0,0,0.77702959,22.523192,34.973874)">
|
||||
<path
|
||||
d="m 104.50787,75.245039 h -3.77394 l -10.90251,-29.352906 c 25.15963,-14.257127 33.96551,-46.12600067 20.12771,-71.285639 -14.25713,-25.159637 -46.126,-33.96551 -71.28564,-20.12771 -12.16049,6.709237 -21.38569,18.450401 -24.74031,31.868875 l -38.99744,-4.193274 c -2.93529,-18.4504 -20.12771,-31.449546 -38.578109,-28.514255 -16.773091,2.515964 -28.933582,16.773091 -28.933582,33.546184 0,5.8705823 1.677309,11.7411643 4.6126,17.1924183 l -46.964659,46.1260007 c -8.80587,-6.709236 -19.70838,-10.483182 -31.03022,-10.483182 -28.93358,0 -52.41591,23.482328 -52.41591,52.415908 0,28.933581 23.48233,52.415911 52.41591,52.415911 l 10.48319,62.89909 c -17.19242,7.54789 -25.15964,27.6756 -17.61175,44.86802 7.54789,17.19242 27.6756,25.15964 44.86802,17.61175 15.93444,-6.70924 23.90165,-24.32098 19.28905,-41.09408 l 36.900806,-19.28905 c 23.901654,26.41762 64.9957237,28.51425 91.832674,4.6126 13.41847,-12.16049 21.38569,-29.77224 21.38569,-47.80331 0,-4.6126 -0.41933,-9.64453 -1.67731,-14.25713 l 40.67475,-20.96636 c 12.57982,14.25713 33.96551,15.51511 48.22264,2.93529 14.25712,-12.57982 15.51511,-33.96551 2.93529,-48.222641 -7.96722,-6.70924 -17.19242,-10.90251 -26.83695,-10.90251 z m -223.92077,140.055311 c -5.45125,-5.45125 -12.99914,-8.80587 -20.54703,-9.64452 l -10.48319,-62.8991 c 10.06386,-3.35461 18.86973,-9.64452 25.15964,-18.03107 l 38.997438,20.54704 c -6.289909,16.77309 -5.031927,35.64282 3.354619,51.57725 z m 41.094077,-85.54276 -38.578107,-20.12771 c 1.67731,-5.45126 2.51596,-10.902511 2.51596,-16.773091 0,-10.90251 -3.35462,-21.38569 -9.64453,-30.191565 l 46.964658,-45.706673 c 15.934437,9.644527 36.900802,5.031927 46.545332,-10.9025087 1.25798,-2.096637 2.09663,-4.193273 2.93529,-6.28990997 l 38.99744,4.19327297 c 0.83865,15.0957817 8.38654,29.3529097 20.54703,38.5781097 l -34.3848403,64.157075 c -27.2562697,-10.902511 -58.7058147,-1.25798 -75.8982327,23.063 z m 148.861183,-20.12771 c 0,2.51596 0.41933,5.03193 1.25798,7.54789 l -38.57811,20.12771 c -4.6126,-9.2252 -11.74116,-16.77309 -20.12771,-22.64367 l 34.38484,-64.157077 c 5.45126,2.096637 11.32184,2.935291 17.19242,2.935291 3.35462,0 6.70924,-0.419327 10.06385,-0.838654 l 10.90251,29.352909 c -10.06385,5.87058 -15.51511,16.35376 -15.09578,27.675601 z"
|
||||
id="path1-1"
|
||||
style="overflow:hidden;stroke-width:4.19327"
|
||||
transform="matrix(0.26458333,0,0,0.26458333,130.85167,139.42444)" />
|
||||
<path
|
||||
id="path1-8-1-7"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.073263;stroke-opacity:1"
|
||||
d="m 140.38615,132.15285 c -0.55386,0 -1.00708,0.45307 -1.00708,1.00708 v 12.05537 c 0,0.5546 0.45322,1.00707 1.00708,1.00707 h 0.504 v 1.51154 h 2.01461 v -1.51154 h 10.07399 v 1.51154 h 2.01461 v -1.51154 h 0.50354 c 0.55461,0 1.00754,-0.45247 1.00754,-1.00707 v -12.05537 c 0,-0.55401 -0.45293,-1.00708 -1.00754,-1.00708 z m 0.504,1.51108 h 14.10321 v 11.04783 h -14.10321 z m 1.00753,1.00754 v 9.03321 h 12.0886 v -9.03321 z m 3.52524,1.99776 c 1.3854,0 2.51906,1.13321 2.51906,2.51861 0,1.38467 -1.13366,2.51816 -2.51906,2.51816 -1.38467,0 -2.51771,-1.13349 -2.51771,-2.51816 0,-1.3854 1.13304,-2.51861 2.51771,-2.51861 z m 7.05183,0 c 0.27767,0 0.504,0.22706 0.504,0.504 v 4.02968 c 0,0.27694 -0.22633,0.50309 -0.504,0.50309 -0.27693,0 -0.50354,-0.22615 -0.50354,-0.50309 v -4.02968 c 0,-0.27694 0.22661,-0.504 0.50354,-0.504 z m -7.55492,0.504 v 0.60461 c -0.42786,0.15092 -0.75526,0.50306 -0.90692,0.90601 h -0.60461 v 1.00753 h 0.60461 c 0.15166,0.42859 0.47906,0.756 0.90692,0.90692 v 0.60461 h 1.00753 v -0.60461 c 0.42786,-0.15092 0.75509,-0.50397 0.90601,-0.90692 h 0.60462 v -1.00753 h -0.60462 c -0.15092,-0.42786 -0.47815,-0.75509 -0.90601,-0.90601 v -0.60461 z" />
|
||||
<path
|
||||
id="path1-8-1-7-3"
|
||||
style="fill:#ffffff;fill-opacity:1"
|
||||
d="m 2576.8666,993.66142 c -7.56,0 -13.7458,6.19115 -13.7458,13.75308 v 164.5449 c 0,7.57 6.1858,13.7458 13.7458,13.7458 h 6.8838 v 20.6369 h 27.499 v -20.6369 h 137.5019 v 20.6369 h 27.4989 v -20.6369 h 6.8693 c 7.57,0 13.7531,-6.1758 13.7531,-13.7458 v -164.5449 c 0,-7.56193 -6.1831,-13.75308 -13.7531,-13.75308 z m 6.8838,20.62968 h 192.4998 v 150.799 h -192.4998 z m 13.7531,13.753 v 123.2929 h 164.9936 v -123.2929 z m 48.1141,27.2674 c 18.91,0 34.3827,15.4654 34.3827,34.3754 0,18.9 -15.4727,34.3755 -34.3827,34.3755 -18.9,0 -34.3682,-15.4755 -34.3682,-34.3755 0,-18.91 15.4682,-34.3754 34.3682,-34.3754 z m 96.2499,0 c 3.79,0 6.8838,3.0965 6.8838,6.8765 v 55.0051 c 0,3.78 -3.0938,6.8693 -6.8838,6.8693 -3.78,0 -6.8693,-3.0893 -6.8693,-6.8693 v -55.0051 c 0,-3.78 3.0893,-6.8765 6.8693,-6.8765 z m -103.1192,6.8765 v 8.2519 c -5.84,2.06 -10.3078,6.8705 -12.3778,12.3705 h -8.2518 v 13.7531 h 8.2518 c 2.07,5.85 6.5378,10.3178 12.3778,12.3778 v 8.2518 h 13.7531 v -8.2518 c 5.84,-2.06 10.3105,-6.8778 12.3705,-12.3778 h 8.2446 v -13.7531 h -8.2446 c -2.06,-5.84 -6.5305,-10.3105 -12.3705,-12.3705 v -8.2519 z"
|
||||
transform="matrix(0.06289731,0,0,0.06289731,-82.022365,94.831671)" />
|
||||
<path
|
||||
id="path1-8-1"
|
||||
style="fill:#ffffff;fill-opacity:1"
|
||||
d="m 2576.8708,993.66021 c -7.56,0 -13.7505,6.18852 -13.7505,13.75049 v 164.5474 c 0,7.57 6.1905,13.7454 13.7505,13.7454 h 6.8778 v 20.6333 h 27.5009 v -20.6333 h 137.4996 v 20.6333 h 27.5009 v -20.6333 h 6.8727 c 7.57,0 13.7504,-6.1754 13.7504,-13.7454 v -164.5474 c 0,-7.56197 -6.1804,-13.75049 -13.7504,-13.75049 z m 6.8778,20.62819 h 192.5014 v 150.797 h -192.5014 z m 13.7504,13.7556 v 123.296 h 165.0005 v -123.296 z m 48.119,27.2668 c 18.91,0 34.3838,15.4687 34.3838,34.3787 0,18.9 -15.4738,34.3685 -34.3838,34.3685 -18.9,0 -34.3685,-15.4685 -34.3685,-34.3685 0,-18.91 15.4685,-34.3787 34.3685,-34.3787 z m 96.2533,0 c 3.79,0 6.8778,3.0977 6.8778,6.8777 v 55.0019 c 0,3.78 -3.0878,6.8676 -6.8778,6.8676 -3.78,0 -6.8727,-3.0876 -6.8727,-6.8676 v -55.0019 c 0,-3.78 3.0927,-6.8777 6.8727,-6.8777 z m -103.1209,6.8777 v 8.2523 c -5.84,2.06 -10.311,6.8709 -12.381,12.3709 h -8.2472 v 13.7505 h 8.2472 c 2.07,5.85 6.541,10.3159 12.381,12.3759 v 8.2523 h 13.7505 v -8.2523 c 5.84,-2.06 10.3108,-6.8759 12.3708,-12.3759 h 8.2473 v -13.7505 h -8.2473 c -2.06,-5.84 -6.5308,-10.3109 -12.3708,-12.3709 v -8.2523 z"
|
||||
transform="matrix(0.08943055,0,0,0.08943055,-115.49313,85.768735)" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
26
src/apps/webpeer/public/manifest.json
Normal file
26
src/apps/webpeer/public/manifest.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "WebPeer - Pseudo client for Self-hosted LiveSync Peer-to-Peer Replication",
|
||||
"short_name": "WepPeer",
|
||||
"description": "A web-based pseudo peer-to-peer replication client using as like server for background sync.",
|
||||
"start_url": "./",
|
||||
"display": "standalone",
|
||||
"background_color": "#fff",
|
||||
"theme_color": "#fff",
|
||||
"orientation": "any",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./icon.svg",
|
||||
"sizes": "512x512",
|
||||
"type": "image/svg+xml",
|
||||
"maskable": true
|
||||
}
|
||||
],
|
||||
"additional_icons": [
|
||||
{
|
||||
"src": "./icon.svg",
|
||||
"sizes": "512x512",
|
||||
"type": "image/svg+xml",
|
||||
"maskable": true
|
||||
}
|
||||
]
|
||||
}
|
||||
5
src/apps/webpeer/src/App.svelte
Normal file
5
src/apps/webpeer/src/App.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<script lang="ts">
|
||||
import SyncMain from "./SyncMain.svelte";
|
||||
</script>
|
||||
|
||||
<SyncMain></SyncMain>
|
||||
23
src/apps/webpeer/src/CommandsShim.ts
Normal file
23
src/apps/webpeer/src/CommandsShim.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { LOG_LEVEL_VERBOSE } from "@lib/common/types";
|
||||
|
||||
import { defaultLoggerEnv, setGlobalLogFunction } from "@lib/common/logger";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export const logs = writable([] as string[]);
|
||||
|
||||
let _logs = [] as string[];
|
||||
|
||||
const maxLines = 10000;
|
||||
setGlobalLogFunction((msg, level) => {
|
||||
console.log(msg);
|
||||
const msgstr = typeof msg === "string" ? msg : JSON.stringify(msg);
|
||||
const strLog = `${new Date().toISOString()}\u2001${msgstr}`;
|
||||
_logs.push(strLog);
|
||||
if (_logs.length > maxLines) {
|
||||
_logs = _logs.slice(_logs.length - maxLines);
|
||||
}
|
||||
logs.set(_logs);
|
||||
});
|
||||
defaultLoggerEnv.minLogLevel = LOG_LEVEL_VERBOSE;
|
||||
|
||||
export const storeP2PStatusLine = writable("");
|
||||
364
src/apps/webpeer/src/P2PReplicatorShim.ts
Normal file
364
src/apps/webpeer/src/P2PReplicatorShim.ts
Normal file
@@ -0,0 +1,364 @@
|
||||
import { PouchDB } from "@lib/pouchdb/pouchdb-browser";
|
||||
import {
|
||||
type EntryDoc,
|
||||
type LOG_LEVEL,
|
||||
type P2PSyncSetting,
|
||||
LOG_LEVEL_NOTICE,
|
||||
LOG_LEVEL_VERBOSE,
|
||||
P2P_DEFAULT_SETTINGS,
|
||||
REMOTE_P2P,
|
||||
} from "@lib/common/types";
|
||||
import { eventHub } from "@lib/hub/hub";
|
||||
|
||||
import type { Confirm } from "@lib/interfaces/Confirm";
|
||||
import { LOG_LEVEL_INFO, Logger } from "@lib/common/logger";
|
||||
import { storeP2PStatusLine } from "./CommandsShim";
|
||||
import {
|
||||
EVENT_P2P_PEER_SHOW_EXTRA_MENU,
|
||||
type CommandShim,
|
||||
type PeerStatus,
|
||||
type PluginShim,
|
||||
} from "@lib/replication/trystero/P2PReplicatorPaneCommon";
|
||||
import {
|
||||
closeP2PReplicator,
|
||||
openP2PReplicator,
|
||||
P2PLogCollector,
|
||||
type P2PReplicatorBase,
|
||||
} from "@lib/replication/trystero/P2PReplicatorCore";
|
||||
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
||||
import { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
|
||||
import { EVENT_SETTING_SAVED } from "@lib/events/coreEvents";
|
||||
import { unique } from "octagonal-wheels/collection";
|
||||
import { BrowserServiceHub } from "@lib/services/BrowserServices";
|
||||
import { TrysteroReplicator } from "@lib/replication/trystero/TrysteroReplicator";
|
||||
import { SETTING_KEY_P2P_DEVICE_NAME } from "@lib/common/types";
|
||||
import { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||
import type { InjectableServiceHub } from "@lib/services/InjectableServices";
|
||||
import { Menu } from "@/lib/src/services/implements/browser/Menu";
|
||||
|
||||
function addToList(item: string, list: string) {
|
||||
return unique(
|
||||
list
|
||||
.split(",")
|
||||
.map((e) => e.trim())
|
||||
.concat(item)
|
||||
.filter((p) => p)
|
||||
).join(",");
|
||||
}
|
||||
function removeFromList(item: string, list: string) {
|
||||
return list
|
||||
.split(",")
|
||||
.map((e) => e.trim())
|
||||
.filter((p) => p !== item)
|
||||
.filter((p) => p)
|
||||
.join(",");
|
||||
}
|
||||
|
||||
export class P2PReplicatorShim implements P2PReplicatorBase, CommandShim {
|
||||
storeP2PStatusLine = reactiveSource("");
|
||||
plugin!: PluginShim;
|
||||
// environment!: IEnvironment;
|
||||
confirm!: Confirm;
|
||||
// simpleStoreAPI!: ISimpleStoreAPI;
|
||||
db?: PouchDB.Database<EntryDoc>;
|
||||
services: InjectableServiceHub<ServiceContext>;
|
||||
|
||||
getDB() {
|
||||
if (!this.db) {
|
||||
throw new Error("DB not initialized");
|
||||
}
|
||||
return this.db;
|
||||
}
|
||||
_simpleStore!: SimpleStore<any>;
|
||||
async closeDB() {
|
||||
if (this.db) {
|
||||
await this.db.close();
|
||||
this.db = undefined;
|
||||
}
|
||||
}
|
||||
constructor() {
|
||||
const browserServiceHub = new BrowserServiceHub<ServiceContext>();
|
||||
this.services = browserServiceHub;
|
||||
this.services.vault.getVaultName.setHandler(() => "p2p-livesync-web-peer");
|
||||
}
|
||||
async init() {
|
||||
// const { simpleStoreAPI } = await getWrappedSynchromesh();
|
||||
// this.confirm = confirm;
|
||||
this.confirm = this.services.UI.confirm;
|
||||
// this.environment = environment;
|
||||
|
||||
if (this.db) {
|
||||
try {
|
||||
await this.closeDB();
|
||||
} catch (ex) {
|
||||
Logger("Error closing db", LOG_LEVEL_VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
}
|
||||
|
||||
const repStore = this.services.database.openSimpleStore<any>("p2p-livesync-web-peer");
|
||||
this._simpleStore = repStore;
|
||||
let _settings = (await repStore.get("settings")) || ({ ...P2P_DEFAULT_SETTINGS } as P2PSyncSetting);
|
||||
|
||||
this.plugin = {
|
||||
saveSettings: async () => {
|
||||
await repStore.set("settings", _settings);
|
||||
eventHub.emitEvent(EVENT_SETTING_SAVED, _settings);
|
||||
},
|
||||
get settings() {
|
||||
return _settings;
|
||||
},
|
||||
set settings(newSettings: P2PSyncSetting) {
|
||||
_settings = { ..._settings, ...newSettings };
|
||||
},
|
||||
rebuilder: null,
|
||||
services: this.services,
|
||||
// $$scheduleAppReload: () => {},
|
||||
// $$getVaultName: () => "p2p-livesync-web-peer",
|
||||
};
|
||||
// const deviceName = this.getDeviceName();
|
||||
const database_name = this.settings.P2P_AppID + "-" + this.settings.P2P_roomID + "p2p-livesync-web-peer";
|
||||
this.db = new PouchDB<EntryDoc>(database_name);
|
||||
setTimeout(() => {
|
||||
if (this.settings.P2P_AutoStart && this.settings.P2P_Enabled) {
|
||||
void this.open();
|
||||
}
|
||||
}, 1000);
|
||||
return this;
|
||||
}
|
||||
get settings() {
|
||||
return this.plugin.settings;
|
||||
}
|
||||
_log(msg: any, level?: LOG_LEVEL): void {
|
||||
Logger(msg, level);
|
||||
}
|
||||
_notice(msg: string, key?: string): void {
|
||||
Logger(msg, LOG_LEVEL_NOTICE, key);
|
||||
}
|
||||
getSettings(): P2PSyncSetting {
|
||||
return this.settings;
|
||||
}
|
||||
simpleStore(): SimpleStore<any> {
|
||||
return this._simpleStore;
|
||||
}
|
||||
handleReplicatedDocuments(docs: EntryDoc[]): Promise<void> {
|
||||
// No op. This is a client and does not need to process the docs
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
getPluginShim() {
|
||||
return {};
|
||||
}
|
||||
getConfig(key: string) {
|
||||
const vaultName = this.services.vault.getVaultName();
|
||||
const dbKey = `${vaultName}-${key}`;
|
||||
return localStorage.getItem(dbKey);
|
||||
}
|
||||
setConfig(key: string, value: string) {
|
||||
const vaultName = this.services.vault.getVaultName();
|
||||
const dbKey = `${vaultName}-${key}`;
|
||||
localStorage.setItem(dbKey, value);
|
||||
}
|
||||
|
||||
getDeviceName(): string {
|
||||
return this.getConfig(SETTING_KEY_P2P_DEVICE_NAME) ?? this.plugin.services.vault.getVaultName();
|
||||
}
|
||||
getPlatform(): string {
|
||||
return "pseudo-replicator";
|
||||
}
|
||||
m?: Menu;
|
||||
afterConstructor(): void {
|
||||
eventHub.onEvent(EVENT_P2P_PEER_SHOW_EXTRA_MENU, ({ peer, event }) => {
|
||||
if (this.m) {
|
||||
this.m.hide();
|
||||
}
|
||||
this.m = new Menu()
|
||||
.addItem((item) => item.setTitle("📥 Only Fetch").onClick(() => this.replicateFrom(peer)))
|
||||
.addItem((item) => item.setTitle("📤 Only Send").onClick(() => this.replicateTo(peer)))
|
||||
.addSeparator()
|
||||
// .addItem((item) => {
|
||||
// item.setTitle("🔧 Get Configuration").onClick(async () => {
|
||||
// await this.getRemoteConfig(peer);
|
||||
// });
|
||||
// })
|
||||
// .addSeparator()
|
||||
.addItem((item) => {
|
||||
const mark = peer.syncOnConnect ? "checkmark" : null;
|
||||
item.setTitle("Toggle Sync on connect")
|
||||
.onClick(async () => {
|
||||
await this.toggleProp(peer, "syncOnConnect");
|
||||
})
|
||||
.setIcon(mark);
|
||||
})
|
||||
.addItem((item) => {
|
||||
const mark = peer.watchOnConnect ? "checkmark" : null;
|
||||
item.setTitle("Toggle Watch on connect")
|
||||
.onClick(async () => {
|
||||
await this.toggleProp(peer, "watchOnConnect");
|
||||
})
|
||||
.setIcon(mark);
|
||||
})
|
||||
.addItem((item) => {
|
||||
const mark = peer.syncOnReplicationCommand ? "checkmark" : null;
|
||||
item.setTitle("Toggle Sync on `Replicate now` command")
|
||||
.onClick(async () => {
|
||||
await this.toggleProp(peer, "syncOnReplicationCommand");
|
||||
})
|
||||
.setIcon(mark);
|
||||
});
|
||||
void this.m.showAtPosition({ x: event.x, y: event.y });
|
||||
});
|
||||
this.p2pLogCollector.p2pReplicationLine.onChanged((line) => {
|
||||
storeP2PStatusLine.set(line.value);
|
||||
});
|
||||
}
|
||||
|
||||
_replicatorInstance?: TrysteroReplicator;
|
||||
p2pLogCollector = new P2PLogCollector();
|
||||
async open() {
|
||||
await openP2PReplicator(this);
|
||||
}
|
||||
async close() {
|
||||
await closeP2PReplicator(this);
|
||||
}
|
||||
enableBroadcastCastings() {
|
||||
return this?._replicatorInstance?.enableBroadcastChanges();
|
||||
}
|
||||
disableBroadcastCastings() {
|
||||
return this?._replicatorInstance?.disableBroadcastChanges();
|
||||
}
|
||||
|
||||
async initialiseP2PReplicator(): Promise<TrysteroReplicator> {
|
||||
await this.init();
|
||||
try {
|
||||
if (this._replicatorInstance) {
|
||||
await this._replicatorInstance.close();
|
||||
this._replicatorInstance = undefined;
|
||||
}
|
||||
|
||||
if (!this.settings.P2P_AppID) {
|
||||
this.settings.P2P_AppID = P2P_DEFAULT_SETTINGS.P2P_AppID;
|
||||
}
|
||||
const getInitialDeviceName = () =>
|
||||
this.getConfig(SETTING_KEY_P2P_DEVICE_NAME) || this.services.vault.getVaultName();
|
||||
|
||||
const getSettings = () => this.settings;
|
||||
const store = () => this.simpleStore();
|
||||
const getDB = () => this.getDB();
|
||||
|
||||
const getConfirm = () => this.confirm;
|
||||
const getPlatform = () => this.getPlatform();
|
||||
const env = {
|
||||
get db() {
|
||||
return getDB();
|
||||
},
|
||||
get confirm() {
|
||||
return getConfirm();
|
||||
},
|
||||
get deviceName() {
|
||||
return getInitialDeviceName();
|
||||
},
|
||||
get platform() {
|
||||
return getPlatform();
|
||||
},
|
||||
get settings() {
|
||||
return getSettings();
|
||||
},
|
||||
processReplicatedDocs: async (docs: EntryDoc[]): Promise<void> => {
|
||||
await this.handleReplicatedDocuments(docs);
|
||||
// No op. This is a client and does not need to process the docs
|
||||
},
|
||||
get simpleStore() {
|
||||
return store();
|
||||
},
|
||||
};
|
||||
this._replicatorInstance = new TrysteroReplicator(env);
|
||||
return this._replicatorInstance;
|
||||
} catch (e) {
|
||||
this._log(
|
||||
e instanceof Error ? e.message : "Something occurred on Initialising P2P Replicator",
|
||||
LOG_LEVEL_INFO
|
||||
);
|
||||
this._log(e, LOG_LEVEL_VERBOSE);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
get replicator() {
|
||||
return this._replicatorInstance!;
|
||||
}
|
||||
async replicateFrom(peer: PeerStatus) {
|
||||
await this.replicator.replicateFrom(peer.peerId);
|
||||
}
|
||||
async replicateTo(peer: PeerStatus) {
|
||||
await this.replicator.requestSynchroniseToPeer(peer.peerId);
|
||||
}
|
||||
async getRemoteConfig(peer: PeerStatus) {
|
||||
Logger(
|
||||
`Requesting remote config for ${peer.name}. Please input the passphrase on the remote device`,
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
const remoteConfig = await this.replicator.getRemoteConfig(peer.peerId);
|
||||
if (remoteConfig) {
|
||||
Logger(`Remote config for ${peer.name} is retrieved successfully`);
|
||||
const DROP = "Yes, and drop local database";
|
||||
const KEEP = "Yes, but keep local database";
|
||||
const CANCEL = "No, cancel";
|
||||
const yn = await this.confirm.askSelectStringDialogue(
|
||||
`Do you really want to apply the remote config? This will overwrite your current config immediately and restart.
|
||||
And you can also drop the local database to rebuild from the remote device.`,
|
||||
[DROP, KEEP, CANCEL] as const,
|
||||
{
|
||||
defaultAction: CANCEL,
|
||||
title: "Apply Remote Config ",
|
||||
}
|
||||
);
|
||||
if (yn === DROP || yn === KEEP) {
|
||||
if (yn === DROP) {
|
||||
if (remoteConfig.remoteType !== REMOTE_P2P) {
|
||||
const yn2 = await this.confirm.askYesNoDialog(
|
||||
`Do you want to set the remote type to "P2P Sync" to rebuild by "P2P replication"?`,
|
||||
{
|
||||
title: "Rebuild from remote device",
|
||||
}
|
||||
);
|
||||
if (yn2 === "yes") {
|
||||
remoteConfig.remoteType = REMOTE_P2P;
|
||||
remoteConfig.P2P_RebuildFrom = peer.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.plugin.settings = remoteConfig;
|
||||
await this.plugin.saveSettings();
|
||||
if (yn === DROP) {
|
||||
await this.plugin.rebuilder.scheduleFetch();
|
||||
} else {
|
||||
await this.plugin.services.appLifecycle.scheduleRestart();
|
||||
}
|
||||
} else {
|
||||
Logger(`Cancelled\nRemote config for ${peer.name} is not applied`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
} else {
|
||||
Logger(`Cannot retrieve remote config for ${peer.peerId}`);
|
||||
}
|
||||
}
|
||||
|
||||
async toggleProp(peer: PeerStatus, prop: "syncOnConnect" | "watchOnConnect" | "syncOnReplicationCommand") {
|
||||
const settingMap = {
|
||||
syncOnConnect: "P2P_AutoSyncPeers",
|
||||
watchOnConnect: "P2P_AutoWatchPeers",
|
||||
syncOnReplicationCommand: "P2P_SyncOnReplication",
|
||||
} as const;
|
||||
|
||||
const targetSetting = settingMap[prop];
|
||||
if (peer[prop]) {
|
||||
this.plugin.settings[targetSetting] = removeFromList(peer.name, this.plugin.settings[targetSetting]);
|
||||
await this.plugin.saveSettings();
|
||||
} else {
|
||||
this.plugin.settings[targetSetting] = addToList(peer.name, this.plugin.settings[targetSetting]);
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const cmdSyncShim = new P2PReplicatorShim();
|
||||
112
src/apps/webpeer/src/SyncMain.svelte
Normal file
112
src/apps/webpeer/src/SyncMain.svelte
Normal file
@@ -0,0 +1,112 @@
|
||||
<script lang="ts">
|
||||
import { storeP2PStatusLine, logs } from "./CommandsShim";
|
||||
import P2PReplicatorPane from "@/features/P2PSync/P2PReplicator/P2PReplicatorPane.svelte";
|
||||
import { onMount, tick } from "svelte";
|
||||
import { cmdSyncShim } from "./P2PReplicatorShim";
|
||||
import { eventHub } from "@lib/hub/hub";
|
||||
import { EVENT_LAYOUT_READY } from "@lib/events/coreEvents";
|
||||
|
||||
let synchronised = $state(cmdSyncShim.init());
|
||||
|
||||
onMount(() => {
|
||||
eventHub.emitEvent(EVENT_LAYOUT_READY);
|
||||
return () => {
|
||||
synchronised.then((e) => e.close());
|
||||
};
|
||||
});
|
||||
let elP: HTMLDivElement;
|
||||
logs.subscribe((log) => {
|
||||
tick().then(() => elP?.scrollTo({ top: elP.scrollHeight }));
|
||||
});
|
||||
let statusLine = $state("");
|
||||
storeP2PStatusLine.subscribe((status) => {
|
||||
statusLine = status;
|
||||
});
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<div class="control">
|
||||
{#await synchronised then cmdSync}
|
||||
<P2PReplicatorPane plugin={cmdSync.plugin} {cmdSync}></P2PReplicatorPane>
|
||||
{:catch error}
|
||||
<p>{error.message}</p>
|
||||
{/await}
|
||||
</div>
|
||||
<div class="log">
|
||||
<div class="status">
|
||||
{statusLine}
|
||||
</div>
|
||||
<div class="logslist" bind:this={elP}>
|
||||
{#each $logs as log}
|
||||
<p>{log}</p>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
max-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
main {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@media (device-orientation: portrait) {
|
||||
main {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
.log {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
padding: 1em;
|
||||
min-width: 50%;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.log {
|
||||
max-height: 50vh;
|
||||
}
|
||||
}
|
||||
@media (device-orientation: portrait) {
|
||||
.log {
|
||||
max-height: 50vh;
|
||||
}
|
||||
}
|
||||
.control {
|
||||
padding: 1em 1em;
|
||||
overflow-y: scroll;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.status {
|
||||
flex-grow: 0;
|
||||
/* max-height: 40px; */
|
||||
/* height: 40px; */
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.logslist {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
/* padding: 1em; */
|
||||
width: 100%;
|
||||
overflow-y: scroll;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
/* max-height: calc(100% - 40px); */
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
text-align: left;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
74
src/apps/webpeer/src/UITest.svelte
Normal file
74
src/apps/webpeer/src/UITest.svelte
Normal file
@@ -0,0 +1,74 @@
|
||||
<script lang="ts">
|
||||
import { Menu } from "@/lib/src/services/implements/browser/Menu";
|
||||
import { getDialogContext } from "@lib/services/implements/base/SvelteDialog";
|
||||
let result = $state<string | boolean>("");
|
||||
|
||||
const context = getDialogContext();
|
||||
|
||||
async function testUI() {
|
||||
const confirm = await context.services.confirm;
|
||||
const ret = await confirm.askString("Your name", "What is your name?", "John Doe", false);
|
||||
result = ret;
|
||||
}
|
||||
let resultPassword = $state<string | boolean>("");
|
||||
async function testPassword() {
|
||||
const confirm = await context.services.confirm;
|
||||
const ret = await confirm.askString("passphrase", "?", "anythingonlyyouknow", true);
|
||||
resultPassword = ret;
|
||||
}
|
||||
|
||||
async function testMenu(event: MouseEvent) {
|
||||
const m = new Menu()
|
||||
.addItem((item) => item.setTitle("📥 Only Fetch").onClick(() => {}))
|
||||
.addItem((item) => item.setTitle("📤 Only Send").onClick(() => {}))
|
||||
.addSeparator()
|
||||
.addItem((item) => {
|
||||
item.setTitle("🔧 Get Configuration").onClick(async () => {
|
||||
console.log("Get Configuration");
|
||||
});
|
||||
})
|
||||
.addSeparator()
|
||||
.addItem((item) => {
|
||||
const mark = "checkmark";
|
||||
item.setTitle("Toggle Sync on connect")
|
||||
.onClick(async () => {
|
||||
console.log("Toggle Sync on connect");
|
||||
// await this.toggleProp(peer, "syncOnConnect");
|
||||
})
|
||||
.setIcon(mark);
|
||||
})
|
||||
.addItem((item) => {
|
||||
const mark = null;
|
||||
item.setTitle("Toggle Watch on connect")
|
||||
.onClick(async () => {
|
||||
console.log("Toggle Watch on connect");
|
||||
// await this.toggleProp(peer, "watchOnConnect");
|
||||
})
|
||||
.setIcon(mark);
|
||||
})
|
||||
.addItem((item) => {
|
||||
const mark = null;
|
||||
item.setTitle("Toggle Sync on `Replicate now` command")
|
||||
.onClick(async () => {})
|
||||
.setIcon(mark);
|
||||
});
|
||||
m.showAtPosition({ x: event.x, y: event.y });
|
||||
}
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<h1>UI Test</h1>
|
||||
<article>
|
||||
<div>
|
||||
<button onclick={() => testUI()}> String input </button>
|
||||
→ {result}
|
||||
</div>
|
||||
<div>
|
||||
<button onclick={() => testPassword()}> Password Input </button>
|
||||
→ {resultPassword}
|
||||
</div>
|
||||
<div>
|
||||
<button onclick={testMenu}>Menu</button>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
112
src/apps/webpeer/src/app.css
Normal file
112
src/apps/webpeer/src/app.css
Normal file
@@ -0,0 +1,112 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
--background-primary: #ffffff;
|
||||
--background-primary-alt: #e9e9e9;
|
||||
--size-4-1: 0.25em;
|
||||
--tag-background: #f0f0f0;
|
||||
--tag-border-width: 1px;
|
||||
--tag-border-color: #cfffdd;
|
||||
--background-modifier-success: #d4f3e9;
|
||||
--background-secondary: #f0f0f0;
|
||||
--background-modifier-error: #f8d7da;
|
||||
--background-modifier-error-hover: #f5c6cb;
|
||||
--interactive-accent: #007bff;
|
||||
--interactive-accent-hover: #0056b3;
|
||||
--text-normal: #333;
|
||||
--text-warning: #f0ad4e;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #1a1a1a;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
1
src/apps/webpeer/src/assets/svelte.svg
Normal file
1
src/apps/webpeer/src/assets/svelte.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
9
src/apps/webpeer/src/main.ts
Normal file
9
src/apps/webpeer/src/main.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { mount } from "svelte";
|
||||
import "./app.css";
|
||||
import App from "./App.svelte";
|
||||
|
||||
const app = mount(App, {
|
||||
target: document.getElementById("app")!,
|
||||
});
|
||||
|
||||
export default app;
|
||||
9
src/apps/webpeer/src/uitest.ts
Normal file
9
src/apps/webpeer/src/uitest.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { mount } from "svelte";
|
||||
import "./app.css";
|
||||
import App from "./UITest.svelte";
|
||||
|
||||
const app = mount(App, {
|
||||
target: document.getElementById("app")!,
|
||||
});
|
||||
|
||||
export default app;
|
||||
2
src/apps/webpeer/src/vite-env.d.ts
vendored
Normal file
2
src/apps/webpeer/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
||||
7
src/apps/webpeer/svelte.config.js
Normal file
7
src/apps/webpeer/svelte.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
|
||||
export default {
|
||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
};
|
||||
25
src/apps/webpeer/tsconfig.app.json
Normal file
25
src/apps/webpeer/tsconfig.app.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"sourceRoot": "../",
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||
* Note that setting allowJs false does not prevent the use
|
||||
* of JS in `.svelte` files.
|
||||
*/
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"paths": {
|
||||
"@/*": ["../../*"],
|
||||
"@lib/*": ["../../lib/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||
}
|
||||
4
src/apps/webpeer/tsconfig.json
Normal file
4
src/apps/webpeer/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
28
src/apps/webpeer/tsconfig.node.json
Normal file
28
src/apps/webpeer/tsconfig.node.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"paths": {
|
||||
"@/*": ["../../*"],
|
||||
"@lib/*": ["../../lib/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
16
src/apps/webpeer/ui.html
Normal file
16
src/apps/webpeer/ui.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="icon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Peer-to-Peer Daemon on Browser</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./src/uitest.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
24
src/apps/webpeer/vite.config.ts
Normal file
24
src/apps/webpeer/vite.config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import path from "node:path";
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "../../"),
|
||||
"@lib": path.resolve(__dirname, "../../lib/src"),
|
||||
},
|
||||
},
|
||||
base: "./",
|
||||
build: {
|
||||
outDir: "dist",
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: "index.html",
|
||||
// uitest: "uitest.html",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -29,7 +29,7 @@ import { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
|
||||
import type { Confirm } from "../../lib/src/interfaces/Confirm.ts";
|
||||
import type ObsidianLiveSyncPlugin from "../../main.ts";
|
||||
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
||||
import { getPlatformName } from "../../lib/src/PlatformAPIs/obsidian/Environment.ts";
|
||||
// import { getPlatformName } from "../../lib/src/PlatformAPIs/obsidian/Environment.ts";
|
||||
import type { LiveSyncCore } from "../../main.ts";
|
||||
import { TrysteroReplicator } from "../../lib/src/replication/trystero/TrysteroReplicator.ts";
|
||||
import { SETTING_KEY_P2P_DEVICE_NAME } from "../../lib/src/common/types.ts";
|
||||
@@ -130,7 +130,7 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase
|
||||
const getDB = () => this.getDB();
|
||||
|
||||
const getConfirm = () => this.confirm;
|
||||
const getPlatform = () => this.getPlatform();
|
||||
const getPlatform = () => this.services.API.getPlatform();
|
||||
const env = {
|
||||
get db() {
|
||||
return getDB();
|
||||
@@ -166,9 +166,6 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
getPlatform(): string {
|
||||
return getPlatformName();
|
||||
}
|
||||
|
||||
onunload(): void {
|
||||
removeP2PReplicatorInstance();
|
||||
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: cd32d3d326...a02102b131
@@ -67,9 +67,10 @@ import { ModuleExtraSyncObsidian } from "./modules/extraFeaturesObsidian/ModuleE
|
||||
import { LocalDatabaseMaintenance } from "./features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts";
|
||||
import { P2PReplicator } from "./features/P2PSync/CmdP2PReplicator.ts";
|
||||
import type { LiveSyncManagers } from "./lib/src/managers/LiveSyncManagers.ts";
|
||||
import { ObsidianServiceHub } from "./modules/services/ObsidianServices.ts";
|
||||
import type { InjectableServiceHub } from "./lib/src/services/InjectableServices.ts";
|
||||
import type { ServiceContext } from "./lib/src/services/ServiceHub.ts";
|
||||
import type { InjectableServiceHub } from "./lib/src/services/implements/injectable/InjectableServiceHub.ts";
|
||||
import { ObsidianServiceHub } from "./modules/services/ObsidianServiceHub.ts";
|
||||
import type { ServiceContext } from "./lib/src/services/base/ServiceBase.ts";
|
||||
// import type { InjectableServiceHub } from "./lib/src/services/InjectableServices.ts";
|
||||
|
||||
export default class ObsidianLiveSyncPlugin
|
||||
extends Plugin
|
||||
|
||||
@@ -9,10 +9,11 @@ import {
|
||||
} from "../../lib/src/common/types.ts";
|
||||
import { AbstractModule } from "../AbstractModule.ts";
|
||||
import type { LiveSyncCore } from "../../main.ts";
|
||||
import { SvelteDialogManager } from "../features/SetupWizard/ObsidianSvelteDialog.ts";
|
||||
import FetchEverything from "../features/SetupWizard/dialogs/FetchEverything.svelte";
|
||||
import RebuildEverything from "../features/SetupWizard/dialogs/RebuildEverything.svelte";
|
||||
import { extractObject } from "octagonal-wheels/object";
|
||||
import { SvelteDialogManagerBase } from "@/lib/src/UI/svelteDialog.ts";
|
||||
import type { ServiceContext } from "@/lib/src/services/base/ServiceBase.ts";
|
||||
|
||||
export class ModuleRedFlag extends AbstractModule {
|
||||
async isFlagFileExist(path: string) {
|
||||
@@ -52,7 +53,10 @@ export class ModuleRedFlag extends AbstractModule {
|
||||
await this.deleteFlagFile(FlagFilesOriginal.FETCH_ALL);
|
||||
await this.deleteFlagFile(FlagFilesHumanReadable.FETCH_ALL);
|
||||
}
|
||||
dialogManager = new SvelteDialogManager(this.core);
|
||||
// dialogManager = new SvelteDialogManagerBase(this.core);
|
||||
get dialogManager(): SvelteDialogManagerBase<ServiceContext> {
|
||||
return this.core.services.UI.dialogManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust setting to remote if needed.
|
||||
|
||||
@@ -5,6 +5,8 @@ import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/log
|
||||
import { AbstractModule } from "../AbstractModule.ts";
|
||||
import type { LiveSyncCore } from "../../main.ts";
|
||||
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
||||
import type { InjectableServiceHub } from "@/lib/src/services/InjectableServices.ts";
|
||||
import type { ObsidianDatabaseService } from "../services/ObsidianServices.ts";
|
||||
|
||||
export class ModuleKeyValueDB extends AbstractModule {
|
||||
async tryCloseKvDB() {
|
||||
@@ -77,6 +79,7 @@ export class ModuleKeyValueDB extends AbstractModule {
|
||||
.filter((e) => e.startsWith(prefix))
|
||||
.map((e) => e.substring(prefix.length));
|
||||
},
|
||||
db: Promise.resolve(getDB()),
|
||||
} satisfies SimpleStore<T>;
|
||||
}
|
||||
_everyOnInitializeDatabase(db: LiveSyncLocalDB): Promise<boolean> {
|
||||
@@ -100,12 +103,12 @@ export class ModuleKeyValueDB extends AbstractModule {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
|
||||
services.databaseEvents.onUnloadDatabase.addHandler(this._onDBUnload.bind(this));
|
||||
services.databaseEvents.onCloseDatabase.addHandler(this._onDBClose.bind(this));
|
||||
services.databaseEvents.onDatabaseInitialisation.addHandler(this._everyOnInitializeDatabase.bind(this));
|
||||
services.databaseEvents.onResetDatabase.addHandler(this._everyOnResetDatabase.bind(this));
|
||||
services.database.openSimpleStore.setHandler(this._getSimpleStore.bind(this));
|
||||
(services.database as ObsidianDatabaseService).openSimpleStore.setHandler(this._getSimpleStore.bind(this));
|
||||
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import { replicationFilter } from "@/lib/src/pouchdb/compress.ts";
|
||||
import { disableEncryption } from "@/lib/src/pouchdb/encryption.ts";
|
||||
import { enableEncryption } from "@/lib/src/pouchdb/encryption.ts";
|
||||
import { setNoticeClass } from "../../lib/src/mock_and_interop/wrapper.ts";
|
||||
import { ObsHttpHandler } from "./APILib/ObsHttpHandler.ts";
|
||||
import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.ts";
|
||||
import { AuthorizationHeaderGenerator } from "../../lib/src/replication/httplib.ts";
|
||||
import type { LiveSyncCore } from "../../main.ts";
|
||||
@@ -27,10 +26,7 @@ async function fetchByAPI(request: RequestUrlParam, errorAsResult = false): Prom
|
||||
const ret = await requestUrl({ ...request, throw: !errorAsResult });
|
||||
return ret;
|
||||
}
|
||||
|
||||
export class ModuleObsidianAPI extends AbstractObsidianModule {
|
||||
_customHandler!: ObsHttpHandler;
|
||||
|
||||
_authHeader = new AuthorizationHeaderGenerator();
|
||||
_previousErrors = new Set<string>();
|
||||
|
||||
@@ -47,10 +43,7 @@ export class ModuleObsidianAPI extends AbstractObsidianModule {
|
||||
eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR);
|
||||
}
|
||||
last_successful_post = false;
|
||||
_customFetchHandler(): ObsHttpHandler {
|
||||
if (!this._customHandler) this._customHandler = new ObsHttpHandler(undefined, undefined);
|
||||
return this._customHandler;
|
||||
}
|
||||
|
||||
_getLastPostFailedBySize(): boolean {
|
||||
return !this.last_successful_post;
|
||||
}
|
||||
@@ -286,11 +279,6 @@ export class ModuleObsidianAPI extends AbstractObsidianModule {
|
||||
}
|
||||
}
|
||||
|
||||
_isMobile(): boolean {
|
||||
//@ts-ignore : internal API
|
||||
return this.app.isMobile;
|
||||
}
|
||||
|
||||
_vaultName(): string {
|
||||
return this.app.vault.getName();
|
||||
}
|
||||
@@ -308,38 +296,16 @@ export class ModuleObsidianAPI extends AbstractObsidianModule {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
_anyGetAppId(): string {
|
||||
return `${"appId" in this.app ? this.app.appId : ""}`;
|
||||
}
|
||||
|
||||
private _reportUnresolvedMessages(): Promise<(string | Error)[]> {
|
||||
return Promise.resolve([...this._previousErrors]);
|
||||
}
|
||||
|
||||
private _getAppVersion(): string {
|
||||
const navigatorString = globalThis.navigator?.userAgent ?? "";
|
||||
const match = navigatorString.match(/obsidian\/([0-9]+\.[0-9]+\.[0-9]+)/);
|
||||
if (match && match.length >= 2) {
|
||||
return match[1];
|
||||
}
|
||||
return "0.0.0";
|
||||
}
|
||||
|
||||
private _getPluginVersion(): string {
|
||||
return this.plugin.manifest.version;
|
||||
}
|
||||
|
||||
onBindFunction(core: LiveSyncCore, services: typeof core.services) {
|
||||
services.API.getCustomFetchHandler.setHandler(this._customFetchHandler.bind(this));
|
||||
services.API.isLastPostFailedDueToPayloadSize.setHandler(this._getLastPostFailedBySize.bind(this));
|
||||
services.remote.connect.setHandler(this._connectRemoteCouchDB.bind(this));
|
||||
services.API.isMobile.setHandler(this._isMobile.bind(this));
|
||||
services.vault.getVaultName.setHandler(this._getVaultName.bind(this));
|
||||
services.vault.vaultName.setHandler(this._vaultName.bind(this));
|
||||
services.vault.getActiveFilePath.setHandler(this._getActiveFilePath.bind(this));
|
||||
services.API.getAppID.setHandler(this._anyGetAppId.bind(this));
|
||||
services.API.getAppVersion.setHandler(this._getAppVersion.bind(this));
|
||||
services.API.getPluginVersion.setHandler(this._getPluginVersion.bind(this));
|
||||
services.appLifecycle.getUnresolvedMessages.addHandler(this._reportUnresolvedMessages.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,26 +113,8 @@ export class ModuleObsidianMenu extends AbstractObsidianModule {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
private async _showView(viewType: string) {
|
||||
const leaves = this.app.workspace.getLeavesOfType(viewType);
|
||||
if (leaves.length == 0) {
|
||||
await this.app.workspace.getLeaf(true).setViewState({
|
||||
type: viewType,
|
||||
active: true,
|
||||
});
|
||||
} else {
|
||||
await leaves[0].setViewState({
|
||||
type: viewType,
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
if (leaves.length > 0) {
|
||||
await this.app.workspace.revealLeaf(leaves[0]);
|
||||
}
|
||||
}
|
||||
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
|
||||
services.appLifecycle.onLoaded.addHandler(this._everyOnload.bind(this));
|
||||
services.API.showWindow.setHandler(this._showView.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
} from "../../lib/src/common/types.ts";
|
||||
import { generatePatchObj, isObjectDifferent } from "../../lib/src/common/utils.ts";
|
||||
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||
import { SvelteDialogManager } from "./SetupWizard/ObsidianSvelteDialog.ts";
|
||||
import Intro from "./SetupWizard/dialogs/Intro.svelte";
|
||||
import SelectMethodNewUser from "./SetupWizard/dialogs/SelectMethodNewUser.svelte";
|
||||
import SelectMethodExisting from "./SetupWizard/dialogs/SelectMethodExisting.svelte";
|
||||
@@ -52,10 +51,13 @@ export const enum UserMode {
|
||||
* Setup Manager to handle onboarding and configuration setup
|
||||
*/
|
||||
export class SetupManager extends AbstractObsidianModule {
|
||||
/**
|
||||
* Dialog manager for handling Svelte dialogs
|
||||
*/
|
||||
private dialogManager: SvelteDialogManager = new SvelteDialogManager(this.plugin);
|
||||
// /**
|
||||
// * Dialog manager for handling Svelte dialogs
|
||||
// */
|
||||
// private dialogManager: SvelteDialogManager = new SvelteDialogManager(this.plugin);
|
||||
get dialogManager() {
|
||||
return this.services.UI.dialogManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the onboarding process
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
import { eventHub, EVENT_PLUGIN_UNLOADED } from "@/common/events";
|
||||
import { Modal } from "@/deps";
|
||||
import type ObsidianLiveSyncPlugin from "@/main";
|
||||
import { mount, unmount } from "svelte";
|
||||
import DialogHost from "@lib/UI/DialogHost.svelte";
|
||||
import { fireAndForget, promiseWithResolvers, type PromiseWithResolvers } from "octagonal-wheels/promises";
|
||||
import { LOG_LEVEL_NOTICE, Logger } from "octagonal-wheels/common/logger";
|
||||
import {
|
||||
type DialogControlBase,
|
||||
type DialogSvelteComponentBaseProps,
|
||||
type ComponentHasResult,
|
||||
setupDialogContext,
|
||||
getDialogContext,
|
||||
type SvelteDialogManagerBase,
|
||||
} from "@/lib/src/UI/svelteDialog.ts";
|
||||
|
||||
export type DialogSvelteComponentProps = DialogSvelteComponentBaseProps & {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
services: ObsidianLiveSyncPlugin["services"];
|
||||
};
|
||||
|
||||
export type DialogControls<T = any, U = any> = DialogControlBase<T, U> & {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
services: ObsidianLiveSyncPlugin["services"];
|
||||
};
|
||||
|
||||
export type DialogMessageProps = Record<string, any>;
|
||||
// type DialogSvelteComponent<T extends DialogSvelteComponentProps = DialogSvelteComponentProps> = Component<SvelteComponent<T>,any>;
|
||||
|
||||
export class SvelteDialog<T, U> extends Modal {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
mountedComponent?: ReturnType<typeof mount>;
|
||||
component: ComponentHasResult<T, U>;
|
||||
result?: T;
|
||||
initialData?: U;
|
||||
title: string = "Obsidian LiveSync - Setup Wizard";
|
||||
constructor(plugin: ObsidianLiveSyncPlugin, component: ComponentHasResult<T, U>, initialData?: U) {
|
||||
super(plugin.app);
|
||||
this.plugin = plugin;
|
||||
this.component = component;
|
||||
this.initialData = initialData;
|
||||
}
|
||||
resolveResult() {
|
||||
this.resultPromiseWithResolvers?.resolve(this.result);
|
||||
this.resultPromiseWithResolvers = undefined;
|
||||
}
|
||||
resultPromiseWithResolvers?: PromiseWithResolvers<T | undefined>;
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const dialog = this;
|
||||
|
||||
if (this.resultPromiseWithResolvers) {
|
||||
this.resultPromiseWithResolvers.reject("Dialog opened again");
|
||||
}
|
||||
const pr = promiseWithResolvers<any>();
|
||||
eventHub.once(EVENT_PLUGIN_UNLOADED, () => {
|
||||
if (this.resultPromiseWithResolvers === pr) {
|
||||
pr.reject("Plugin unloaded");
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
this.resultPromiseWithResolvers = pr;
|
||||
this.mountedComponent = mount(DialogHost, {
|
||||
target: contentEl,
|
||||
props: {
|
||||
onSetupContext: (props: DialogSvelteComponentBaseProps) => {
|
||||
setupDialogContext({
|
||||
...props,
|
||||
plugin: this.plugin,
|
||||
services: this.plugin.services,
|
||||
});
|
||||
},
|
||||
setTitle: (title: string) => {
|
||||
dialog.setTitle(title);
|
||||
},
|
||||
closeDialog: () => {
|
||||
dialog.close();
|
||||
},
|
||||
setResult: (result: T) => {
|
||||
this.result = result;
|
||||
},
|
||||
getInitialData: () => this.initialData,
|
||||
mountComponent: this.component,
|
||||
},
|
||||
});
|
||||
}
|
||||
waitForClose(): Promise<T | undefined> {
|
||||
if (!this.resultPromiseWithResolvers) {
|
||||
throw new Error("Dialog not opened yet");
|
||||
}
|
||||
return this.resultPromiseWithResolvers.promise;
|
||||
}
|
||||
onClose() {
|
||||
this.resolveResult();
|
||||
fireAndForget(async () => {
|
||||
if (this.mountedComponent) {
|
||||
await unmount(this.mountedComponent);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function openSvelteDialog<T, U>(
|
||||
plugin: ObsidianLiveSyncPlugin,
|
||||
component: ComponentHasResult<T, U>,
|
||||
initialData?: U
|
||||
): Promise<T | undefined> {
|
||||
const dialog = new SvelteDialog<T, U>(plugin, component, initialData);
|
||||
dialog.open();
|
||||
|
||||
return await dialog.waitForClose();
|
||||
}
|
||||
|
||||
export class SvelteDialogManager implements SvelteDialogManagerBase {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
constructor(plugin: ObsidianLiveSyncPlugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
async open<T, U>(component: ComponentHasResult<T, U>, initialData?: U): Promise<T | undefined> {
|
||||
return await openSvelteDialog<T, U>(this.plugin, component, initialData);
|
||||
}
|
||||
async openWithExplicitCancel<T, U>(component: ComponentHasResult<T, U>, initialData?: U): Promise<T> {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const ret = await openSvelteDialog<T, U>(this.plugin, component, initialData);
|
||||
if (ret !== undefined) {
|
||||
return ret;
|
||||
}
|
||||
if (this.plugin.services.appLifecycle.hasUnloaded()) {
|
||||
throw new Error("Operation cancelled due to app shutdown.");
|
||||
}
|
||||
Logger("Please select 'Cancel' explicitly to cancel this operation.", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
throw new Error("Operation Forcibly cancelled by user.");
|
||||
}
|
||||
}
|
||||
|
||||
export function getObsidianDialogContext<T = any>(): DialogControls<T> {
|
||||
return getDialogContext<T>() as DialogControls<T>;
|
||||
}
|
||||
@@ -16,8 +16,7 @@
|
||||
} from "../../../../lib/src/common/types";
|
||||
|
||||
import { onMount } from "svelte";
|
||||
import type { GuestDialogProps } from "../../../../lib/src/UI/svelteDialog";
|
||||
import { getObsidianDialogContext } from "../ObsidianSvelteDialog";
|
||||
import { getDialogContext, type GuestDialogProps } from "../../../../lib/src/UI/svelteDialog";
|
||||
import { copyTo, pickBucketSyncSettings } from "../../../../lib/src/common/utils";
|
||||
|
||||
const default_setting = pickBucketSyncSettings(DEFAULT_SETTINGS);
|
||||
@@ -39,7 +38,7 @@
|
||||
}
|
||||
});
|
||||
let error = $state("");
|
||||
const context = getObsidianDialogContext();
|
||||
const context = getDialogContext();
|
||||
const isEndpointSecure = $derived.by(() => {
|
||||
return syncSetting.endpoint.trim().toLowerCase().startsWith("https://");
|
||||
});
|
||||
|
||||
@@ -17,9 +17,8 @@
|
||||
} from "../../../../lib/src/common/types";
|
||||
import { isCloudantURI } from "../../../../lib/src/pouchdb/utils_couchdb";
|
||||
|
||||
import { getObsidianDialogContext } from "../ObsidianSvelteDialog";
|
||||
import { onMount } from "svelte";
|
||||
import type { GuestDialogProps } from "../../../../lib/src/UI/svelteDialog";
|
||||
import { getDialogContext, type GuestDialogProps } from "../../../../lib/src/UI/svelteDialog";
|
||||
import { copyTo, pickCouchDBSyncSettings } from "../../../../lib/src/common/utils";
|
||||
import PanelCouchDBCheck from "./PanelCouchDBCheck.svelte";
|
||||
|
||||
@@ -40,7 +39,7 @@
|
||||
});
|
||||
|
||||
let error = $state("");
|
||||
const context = getObsidianDialogContext();
|
||||
const context = getDialogContext();
|
||||
|
||||
function generateSetting() {
|
||||
const connSetting: CouchDBConnection = {
|
||||
|
||||
@@ -22,16 +22,15 @@
|
||||
import { TrysteroReplicator } from "../../../../lib/src/replication/trystero/TrysteroReplicator";
|
||||
import type { ReplicatorHostEnv } from "../../../../lib/src/replication/trystero/types";
|
||||
import { copyTo, pickP2PSyncSettings, type SimpleStore } from "../../../../lib/src/common/utils";
|
||||
import { getObsidianDialogContext } from "../ObsidianSvelteDialog";
|
||||
import { onMount } from "svelte";
|
||||
import type { GuestDialogProps } from "../../../../lib/src/UI/svelteDialog";
|
||||
import { getDialogContext, type GuestDialogProps } from "../../../../lib/src/UI/svelteDialog";
|
||||
import { SETTING_KEY_P2P_DEVICE_NAME } from "../../../../lib/src/common/types";
|
||||
import ExtraItems from "../../../../lib/src/UI/components/ExtraItems.svelte";
|
||||
|
||||
const default_setting = pickP2PSyncSettings(DEFAULT_SETTINGS);
|
||||
let syncSetting = $state<P2PConnectionInfo>({ ...default_setting });
|
||||
|
||||
const context = getObsidianDialogContext();
|
||||
const context = getDialogContext();
|
||||
let error = $state("");
|
||||
const TYPE_CANCELLED = "cancelled";
|
||||
type SettingInfo = P2PConnectionInfo;
|
||||
@@ -104,7 +103,7 @@
|
||||
processReplicatedDocs: async (docs: any[]) => {
|
||||
return;
|
||||
},
|
||||
confirm: context.plugin.confirm,
|
||||
confirm: context.services.confirm,
|
||||
db: dummyPouch,
|
||||
simpleStore: store,
|
||||
deviceName: syncSetting.P2P_DevicePeerName || "unnamed-device",
|
||||
|
||||
@@ -13,8 +13,8 @@ import { versionNumberString2Number } from "../../lib/src/string_and_binary/conv
|
||||
import { cancelAllPeriodicTask, cancelAllTasks } from "octagonal-wheels/concurrency/task";
|
||||
import { stopAllRunningProcessors } from "octagonal-wheels/concurrency/processor";
|
||||
import { AbstractModule } from "../AbstractModule.ts";
|
||||
import { EVENT_PLATFORM_UNLOADED } from "../../lib/src/PlatformAPIs/base/APIBase.ts";
|
||||
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
|
||||
import { EVENT_PLATFORM_UNLOADED } from "@lib/events/coreEvents";
|
||||
import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub.ts";
|
||||
import type { LiveSyncCore } from "../../main.ts";
|
||||
import { initialiseWorkerModule } from "@/lib/src/worker/bgWorker.ts";
|
||||
|
||||
|
||||
111
src/modules/services/ObsidianConfirm.ts
Normal file
111
src/modules/services/ObsidianConfirm.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { type App, type Plugin, Notice } from "@/deps";
|
||||
import { scheduleTask, memoIfNotExist, memoObject, retrieveMemoObject, disposeMemoObject } from "@/common/utils";
|
||||
import { $msg } from "@/lib/src/common/i18n";
|
||||
import type { Confirm } from "@/lib/src/interfaces/Confirm";
|
||||
import type { ObsidianServiceContext } from "@/lib/src/services/implements/obsidian/ObsidianServiceContext";
|
||||
import {
|
||||
askYesNo,
|
||||
askString,
|
||||
confirmWithMessageWithWideButton,
|
||||
askSelectString,
|
||||
confirmWithMessage,
|
||||
} from "../coreObsidian/UILib/dialogs";
|
||||
|
||||
export class ObsidianConfirm<T extends ObsidianServiceContext = ObsidianServiceContext> implements Confirm {
|
||||
private _context: T;
|
||||
get _app(): App {
|
||||
return this._context.app;
|
||||
}
|
||||
get _plugin(): Plugin {
|
||||
return this._context.plugin;
|
||||
}
|
||||
constructor(context: T) {
|
||||
this._context = context;
|
||||
}
|
||||
askYesNo(message: string): Promise<"yes" | "no"> {
|
||||
return askYesNo(this._app, message);
|
||||
}
|
||||
askString(title: string, key: string, placeholder: string, isPassword: boolean = false): Promise<string | false> {
|
||||
return askString(this._app, title, key, placeholder, isPassword);
|
||||
}
|
||||
|
||||
async askYesNoDialog(
|
||||
message: string,
|
||||
opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number } = { title: "Confirmation" }
|
||||
): Promise<"yes" | "no"> {
|
||||
const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleConfirmation");
|
||||
const yesLabel = $msg("moduleInputUIObsidian.optionYes");
|
||||
const noLabel = $msg("moduleInputUIObsidian.optionNo");
|
||||
const defaultOption = opt.defaultOption === "Yes" ? yesLabel : noLabel;
|
||||
const ret = await confirmWithMessageWithWideButton(
|
||||
this._plugin,
|
||||
opt.title || defaultTitle,
|
||||
message,
|
||||
[yesLabel, noLabel],
|
||||
defaultOption,
|
||||
opt.timeout
|
||||
);
|
||||
return ret === yesLabel ? "yes" : "no";
|
||||
}
|
||||
|
||||
askSelectString(message: string, items: string[]): Promise<string> {
|
||||
return askSelectString(this._app, message, items);
|
||||
}
|
||||
|
||||
askSelectStringDialogue<T extends readonly string[]>(
|
||||
message: string,
|
||||
buttons: T,
|
||||
opt: { title?: string; defaultAction: T[number]; timeout?: number }
|
||||
): Promise<T[number] | false> {
|
||||
const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleSelect");
|
||||
return confirmWithMessageWithWideButton(
|
||||
this._plugin,
|
||||
opt.title || defaultTitle,
|
||||
message,
|
||||
buttons,
|
||||
opt.defaultAction,
|
||||
opt.timeout
|
||||
);
|
||||
}
|
||||
|
||||
askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void) {
|
||||
const fragment = createFragment((doc) => {
|
||||
const [beforeText, afterText] = dialogText.split("{HERE}", 2);
|
||||
doc.createEl("span", undefined, (a) => {
|
||||
a.appendText(beforeText);
|
||||
a.appendChild(
|
||||
a.createEl("a", undefined, (anchor) => {
|
||||
anchorCallback(anchor);
|
||||
})
|
||||
);
|
||||
a.appendText(afterText);
|
||||
});
|
||||
});
|
||||
const popupKey = "popup-" + key;
|
||||
scheduleTask(popupKey, 1000, async () => {
|
||||
const popup = await memoIfNotExist(popupKey, () => new Notice(fragment, 0));
|
||||
const isShown = popup?.noticeEl?.isShown();
|
||||
if (!isShown) {
|
||||
memoObject(popupKey, new Notice(fragment, 0));
|
||||
}
|
||||
scheduleTask(popupKey + "-close", 20000, () => {
|
||||
const popup = retrieveMemoObject<Notice>(popupKey);
|
||||
if (!popup) return;
|
||||
if (popup?.noticeEl?.isShown()) {
|
||||
popup.hide();
|
||||
}
|
||||
disposeMemoObject(popupKey);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
confirmWithMessage(
|
||||
title: string,
|
||||
contentMd: string,
|
||||
buttons: string[],
|
||||
defaultAction: (typeof buttons)[number],
|
||||
timeout?: number
|
||||
): Promise<(typeof buttons)[number] | false> {
|
||||
return confirmWithMessage(this._plugin, title, contentMd, buttons, defaultAction, timeout);
|
||||
}
|
||||
}
|
||||
73
src/modules/services/ObsidianServiceHub.ts
Normal file
73
src/modules/services/ObsidianServiceHub.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { InjectableServiceHub } from "@/lib/src/services/implements/injectable/InjectableServiceHub";
|
||||
import { ObsidianServiceContext } from "@/lib/src/services/implements/obsidian/ObsidianServiceContext";
|
||||
import type { ServiceInstances } from "@/lib/src/services/ServiceHub";
|
||||
import type ObsidianLiveSyncPlugin from "@/main";
|
||||
import {
|
||||
ObsidianAPIService,
|
||||
ObsidianAppLifecycleService,
|
||||
ObsidianConflictService,
|
||||
ObsidianDatabaseService,
|
||||
ObsidianFileProcessingService,
|
||||
ObsidianReplicationService,
|
||||
ObsidianReplicatorService,
|
||||
ObsidianRemoteService,
|
||||
ObsidianSettingService,
|
||||
ObsidianTweakValueService,
|
||||
ObsidianVaultService,
|
||||
ObsidianTestService,
|
||||
ObsidianDatabaseEventService,
|
||||
ObsidianPathService,
|
||||
ObsidianConfigService,
|
||||
} from "./ObsidianServices";
|
||||
import { ObsidianUIService } from "./ObsidianUIService";
|
||||
|
||||
// InjectableServiceHub
|
||||
|
||||
export class ObsidianServiceHub extends InjectableServiceHub<ObsidianServiceContext> {
|
||||
constructor(plugin: ObsidianLiveSyncPlugin) {
|
||||
const context = new ObsidianServiceContext(plugin.app, plugin, plugin);
|
||||
|
||||
const API = new ObsidianAPIService(context);
|
||||
const appLifecycle = new ObsidianAppLifecycleService(context);
|
||||
const conflict = new ObsidianConflictService(context);
|
||||
const database = new ObsidianDatabaseService(context);
|
||||
const fileProcessing = new ObsidianFileProcessingService(context);
|
||||
const replication = new ObsidianReplicationService(context);
|
||||
const replicator = new ObsidianReplicatorService(context);
|
||||
const remote = new ObsidianRemoteService(context);
|
||||
const setting = new ObsidianSettingService(context);
|
||||
const tweakValue = new ObsidianTweakValueService(context);
|
||||
const vault = new ObsidianVaultService(context);
|
||||
const test = new ObsidianTestService(context);
|
||||
const databaseEvents = new ObsidianDatabaseEventService(context);
|
||||
const path = new ObsidianPathService(context);
|
||||
const config = new ObsidianConfigService(context, vault);
|
||||
const ui = new ObsidianUIService(context, {
|
||||
appLifecycle,
|
||||
config,
|
||||
replicator,
|
||||
});
|
||||
|
||||
// Using 'satisfies' to ensure all services are provided
|
||||
const serviceInstancesToInit = {
|
||||
appLifecycle: appLifecycle,
|
||||
conflict: conflict,
|
||||
database: database,
|
||||
databaseEvents: databaseEvents,
|
||||
fileProcessing: fileProcessing,
|
||||
replication: replication,
|
||||
replicator: replicator,
|
||||
remote: remote,
|
||||
setting: setting,
|
||||
tweakValue: tweakValue,
|
||||
vault: vault,
|
||||
test: test,
|
||||
ui: ui,
|
||||
path: path,
|
||||
API: API,
|
||||
config: config,
|
||||
} satisfies Required<ServiceInstances<ObsidianServiceContext>>;
|
||||
|
||||
super(context, serviceInstancesToInit);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +1,104 @@
|
||||
import { ServiceContext, type ServiceInstances } from "@/lib/src/services/ServiceHub.ts";
|
||||
import {
|
||||
InjectableAPIService,
|
||||
InjectableAppLifecycleService,
|
||||
InjectableConflictService,
|
||||
InjectableDatabaseEventService,
|
||||
InjectableDatabaseService,
|
||||
InjectableFileProcessingService,
|
||||
InjectablePathService,
|
||||
InjectableRemoteService,
|
||||
InjectableReplicationService,
|
||||
InjectableReplicatorService,
|
||||
InjectableSettingService,
|
||||
InjectableTestService,
|
||||
InjectableTweakValueService,
|
||||
InjectableVaultService,
|
||||
} from "../../lib/src/services/InjectableServices.ts";
|
||||
import { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
|
||||
import { ConfigServiceBrowserCompat } from "../../lib/src/services/Services.ts";
|
||||
import type ObsidianLiveSyncPlugin from "../../main.ts";
|
||||
import { ObsidianUIService } from "./ObsidianUIService.ts";
|
||||
import type { App, Plugin } from "@/deps";
|
||||
|
||||
export class ObsidianServiceContext extends ServiceContext {
|
||||
app: App;
|
||||
plugin: Plugin;
|
||||
liveSyncPlugin: ObsidianLiveSyncPlugin;
|
||||
constructor(app: App, plugin: Plugin, liveSyncPlugin: ObsidianLiveSyncPlugin) {
|
||||
super();
|
||||
this.app = app;
|
||||
this.plugin = plugin;
|
||||
this.liveSyncPlugin = liveSyncPlugin;
|
||||
}
|
||||
}
|
||||
import { InjectableAPIService } from "@lib/services/implements/injectable/InjectableAPIService";
|
||||
import { InjectableAppLifecycleService } from "@lib/services/implements/injectable/InjectableAppLifecycleService";
|
||||
import { InjectableConflictService } from "@lib/services/implements/injectable/InjectableConflictService";
|
||||
import { InjectableDatabaseEventService } from "@lib/services/implements/injectable/InjectableDatabaseEventService";
|
||||
import { InjectableDatabaseService } from "@lib/services/implements/injectable/InjectableDatabaseService";
|
||||
import { InjectableFileProcessingService } from "@lib/services/implements/injectable/InjectableFileProcessingService";
|
||||
import { InjectablePathService } from "@lib/services/implements/injectable/InjectablePathService";
|
||||
import { InjectableRemoteService } from "@lib/services/implements/injectable/InjectableRemoteService";
|
||||
import { InjectableReplicationService } from "@lib/services/implements/injectable/InjectableReplicationService";
|
||||
import { InjectableReplicatorService } from "@lib/services/implements/injectable/InjectableReplicatorService";
|
||||
import { InjectableSettingService } from "@lib/services/implements/injectable/InjectableSettingService";
|
||||
import { InjectableTestService } from "@lib/services/implements/injectable/InjectableTestService";
|
||||
import { InjectableTweakValueService } from "@lib/services/implements/injectable/InjectableTweakValueService";
|
||||
import { InjectableVaultService } from "@lib/services/implements/injectable/InjectableVaultService";
|
||||
import { ConfigServiceBrowserCompat } from "@lib/services/implements/browser/ConfigServiceBrowserCompat";
|
||||
import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext.ts";
|
||||
import { Platform } from "@/deps";
|
||||
import type { SimpleStore } from "@/lib/src/common/utils";
|
||||
import type { IDatabaseService } from "@/lib/src/services/base/IService";
|
||||
import { handlers } from "@/lib/src/services/lib/HandlerUtils";
|
||||
import { ObsHttpHandler } from "../essentialObsidian/APILib/ObsHttpHandler";
|
||||
|
||||
// All Services will be migrated to be based on Plain Services, not Injectable Services.
|
||||
// This is a migration step.
|
||||
|
||||
export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceContext> {
|
||||
_customHandler: ObsHttpHandler | undefined;
|
||||
getCustomFetchHandler(): ObsHttpHandler {
|
||||
if (!this._customHandler) this._customHandler = new ObsHttpHandler(undefined, undefined);
|
||||
return this._customHandler;
|
||||
}
|
||||
|
||||
async showWindow(viewType: string): Promise<void> {
|
||||
const leaves = this.app.workspace.getLeavesOfType(viewType);
|
||||
if (leaves.length == 0) {
|
||||
await this.app.workspace.getLeaf(true).setViewState({
|
||||
type: viewType,
|
||||
active: true,
|
||||
});
|
||||
} else {
|
||||
await leaves[0].setViewState({
|
||||
type: viewType,
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
if (leaves.length > 0) {
|
||||
await this.app.workspace.revealLeaf(leaves[0]);
|
||||
}
|
||||
}
|
||||
|
||||
private get app() {
|
||||
return this.context.app;
|
||||
}
|
||||
|
||||
getPlatform(): string {
|
||||
return "obsidian";
|
||||
if (Platform.isAndroidApp) {
|
||||
return "android-app";
|
||||
} else if (Platform.isIosApp) {
|
||||
return "ios";
|
||||
} else if (Platform.isMacOS) {
|
||||
return "macos";
|
||||
} else if (Platform.isMobileApp) {
|
||||
return "mobile-app";
|
||||
} else if (Platform.isMobile) {
|
||||
return "mobile";
|
||||
} else if (Platform.isSafari) {
|
||||
return "safari";
|
||||
} else if (Platform.isDesktop) {
|
||||
return "desktop";
|
||||
} else if (Platform.isDesktopApp) {
|
||||
return "desktop-app";
|
||||
} else {
|
||||
return "unknown-obsidian";
|
||||
}
|
||||
}
|
||||
override isMobile(): boolean {
|
||||
//@ts-ignore : internal API
|
||||
return this.app.isMobile;
|
||||
}
|
||||
override getAppID(): string {
|
||||
return `${"appId" in this.app ? this.app.appId : ""}`;
|
||||
}
|
||||
override getAppVersion(): string {
|
||||
const navigatorString = globalThis.navigator?.userAgent ?? "";
|
||||
const match = navigatorString.match(/obsidian\/([0-9]+\.[0-9]+\.[0-9]+)/);
|
||||
if (match && match.length >= 2) {
|
||||
return match[1];
|
||||
}
|
||||
return "0.0.0";
|
||||
}
|
||||
|
||||
override getPluginVersion(): string {
|
||||
return this.context.plugin.manifest.version;
|
||||
}
|
||||
}
|
||||
export class ObsidianPathService extends InjectablePathService<ObsidianServiceContext> {}
|
||||
export class ObsidianDatabaseService extends InjectableDatabaseService<ObsidianServiceContext> {}
|
||||
export class ObsidianDatabaseService extends InjectableDatabaseService<ObsidianServiceContext> {
|
||||
openSimpleStore = handlers<IDatabaseService>().binder("openSimpleStore") as (<T>(
|
||||
kind: string
|
||||
) => SimpleStore<T>) & { setHandler: (handler: IDatabaseService["openSimpleStore"], override?: boolean) => void };
|
||||
}
|
||||
export class ObsidianDatabaseEventService extends InjectableDatabaseEventService<ObsidianServiceContext> {}
|
||||
|
||||
// InjectableReplicatorService
|
||||
@@ -66,49 +122,3 @@ export class ObsidianVaultService extends InjectableVaultService<ObsidianService
|
||||
// InjectableTestService
|
||||
export class ObsidianTestService extends InjectableTestService<ObsidianServiceContext> {}
|
||||
export class ObsidianConfigService extends ConfigServiceBrowserCompat<ObsidianServiceContext> {}
|
||||
|
||||
// InjectableServiceHub
|
||||
|
||||
export class ObsidianServiceHub extends InjectableServiceHub<ObsidianServiceContext> {
|
||||
constructor(plugin: ObsidianLiveSyncPlugin) {
|
||||
const context = new ObsidianServiceContext(plugin.app, plugin, plugin);
|
||||
|
||||
const API = new ObsidianAPIService(context);
|
||||
const appLifecycle = new ObsidianAppLifecycleService(context);
|
||||
const conflict = new ObsidianConflictService(context);
|
||||
const database = new ObsidianDatabaseService(context);
|
||||
const fileProcessing = new ObsidianFileProcessingService(context);
|
||||
const replication = new ObsidianReplicationService(context);
|
||||
const replicator = new ObsidianReplicatorService(context);
|
||||
const remote = new ObsidianRemoteService(context);
|
||||
const setting = new ObsidianSettingService(context);
|
||||
const tweakValue = new ObsidianTweakValueService(context);
|
||||
const vault = new ObsidianVaultService(context);
|
||||
const test = new ObsidianTestService(context);
|
||||
const databaseEvents = new ObsidianDatabaseEventService(context);
|
||||
const path = new ObsidianPathService(context);
|
||||
const ui = new ObsidianUIService(context);
|
||||
const config = new ObsidianConfigService(context, vault);
|
||||
// Using 'satisfies' to ensure all services are provided
|
||||
const serviceInstancesToInit = {
|
||||
appLifecycle: appLifecycle,
|
||||
conflict: conflict,
|
||||
database: database,
|
||||
databaseEvents: databaseEvents,
|
||||
fileProcessing: fileProcessing,
|
||||
replication: replication,
|
||||
replicator: replicator,
|
||||
remote: remote,
|
||||
setting: setting,
|
||||
tweakValue: tweakValue,
|
||||
vault: vault,
|
||||
test: test,
|
||||
ui: ui,
|
||||
path: path,
|
||||
API: API,
|
||||
config: config,
|
||||
} satisfies Required<ServiceInstances<ObsidianServiceContext>>;
|
||||
|
||||
super(context, serviceInstancesToInit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,156 +1,30 @@
|
||||
import { UIService } from "../../lib/src/services/Services";
|
||||
import { Notice, type App, type Plugin } from "@/deps";
|
||||
import { SvelteDialogManager } from "../features/SetupWizard/ObsidianSvelteDialog";
|
||||
import DialogueToCopy from "../../lib/src/UI/dialogues/DialogueToCopy.svelte";
|
||||
import type { ObsidianServiceContext } from "./ObsidianServices";
|
||||
import type ObsidianLiveSyncPlugin from "@/main";
|
||||
import type { Confirm } from "@/lib/src/interfaces/Confirm";
|
||||
import {
|
||||
askSelectString,
|
||||
askString,
|
||||
askYesNo,
|
||||
confirmWithMessage,
|
||||
confirmWithMessageWithWideButton,
|
||||
} from "../coreObsidian/UILib/dialogs";
|
||||
import { $msg } from "@/lib/src/common/i18n";
|
||||
import { disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "@/common/utils";
|
||||
export class ObsidianConfirm implements Confirm {
|
||||
private _app: App;
|
||||
private _plugin: Plugin;
|
||||
constructor(app: App, plugin: Plugin) {
|
||||
this._app = app;
|
||||
this._plugin = plugin;
|
||||
}
|
||||
askYesNo(message: string): Promise<"yes" | "no"> {
|
||||
return askYesNo(this._app, message);
|
||||
}
|
||||
askString(title: string, key: string, placeholder: string, isPassword: boolean = false): Promise<string | false> {
|
||||
return askString(this._app, title, key, placeholder, isPassword);
|
||||
}
|
||||
import type { ConfigService } from "@lib/services/base/ConfigService";
|
||||
import type { AppLifecycleService } from "@lib/services/base/AppLifecycleService";
|
||||
import type { ReplicatorService } from "@lib/services/base/ReplicatorService";
|
||||
import { UIService } from "@lib/services//implements/base/UIService";
|
||||
import { ObsidianServiceContext } from "@/lib/src/services/implements/obsidian/ObsidianServiceContext";
|
||||
import { ObsidianSvelteDialogManager } from "./SvelteDialogObsidian";
|
||||
import { ObsidianConfirm } from "./ObsidianConfirm";
|
||||
|
||||
async askYesNoDialog(
|
||||
message: string,
|
||||
opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number } = { title: "Confirmation" }
|
||||
): Promise<"yes" | "no"> {
|
||||
const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleConfirmation");
|
||||
const yesLabel = $msg("moduleInputUIObsidian.optionYes");
|
||||
const noLabel = $msg("moduleInputUIObsidian.optionNo");
|
||||
const defaultOption = opt.defaultOption === "Yes" ? yesLabel : noLabel;
|
||||
const ret = await confirmWithMessageWithWideButton(
|
||||
this._plugin,
|
||||
opt.title || defaultTitle,
|
||||
message,
|
||||
[yesLabel, noLabel],
|
||||
defaultOption,
|
||||
opt.timeout
|
||||
);
|
||||
return ret === yesLabel ? "yes" : "no";
|
||||
}
|
||||
export type ObsidianUIServiceDependencies<T extends ObsidianServiceContext = ObsidianServiceContext> = {
|
||||
appLifecycle: AppLifecycleService<T>;
|
||||
config: ConfigService<T>;
|
||||
replicator: ReplicatorService<T>;
|
||||
};
|
||||
|
||||
askSelectString(message: string, items: string[]): Promise<string> {
|
||||
return askSelectString(this._app, message, items);
|
||||
}
|
||||
|
||||
askSelectStringDialogue<T extends readonly string[]>(
|
||||
message: string,
|
||||
buttons: T,
|
||||
opt: { title?: string; defaultAction: T[number]; timeout?: number }
|
||||
): Promise<T[number] | false> {
|
||||
const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleSelect");
|
||||
return confirmWithMessageWithWideButton(
|
||||
this._plugin,
|
||||
opt.title || defaultTitle,
|
||||
message,
|
||||
buttons,
|
||||
opt.defaultAction,
|
||||
opt.timeout
|
||||
);
|
||||
}
|
||||
|
||||
askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void) {
|
||||
const fragment = createFragment((doc) => {
|
||||
const [beforeText, afterText] = dialogText.split("{HERE}", 2);
|
||||
doc.createEl("span", undefined, (a) => {
|
||||
a.appendText(beforeText);
|
||||
a.appendChild(
|
||||
a.createEl("a", undefined, (anchor) => {
|
||||
anchorCallback(anchor);
|
||||
})
|
||||
);
|
||||
a.appendText(afterText);
|
||||
});
|
||||
});
|
||||
const popupKey = "popup-" + key;
|
||||
scheduleTask(popupKey, 1000, async () => {
|
||||
const popup = await memoIfNotExist(popupKey, () => new Notice(fragment, 0));
|
||||
const isShown = popup?.noticeEl?.isShown();
|
||||
if (!isShown) {
|
||||
memoObject(popupKey, new Notice(fragment, 0));
|
||||
}
|
||||
scheduleTask(popupKey + "-close", 20000, () => {
|
||||
const popup = retrieveMemoObject<Notice>(popupKey);
|
||||
if (!popup) return;
|
||||
if (popup?.noticeEl?.isShown()) {
|
||||
popup.hide();
|
||||
}
|
||||
disposeMemoObject(popupKey);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
confirmWithMessage(
|
||||
title: string,
|
||||
contentMd: string,
|
||||
buttons: string[],
|
||||
defaultAction: (typeof buttons)[number],
|
||||
timeout?: number
|
||||
): Promise<(typeof buttons)[number] | false> {
|
||||
return confirmWithMessage(this._plugin, title, contentMd, buttons, defaultAction, timeout);
|
||||
}
|
||||
}
|
||||
export class ObsidianUIService extends UIService<ObsidianServiceContext> {
|
||||
private _dialogManager: SvelteDialogManager;
|
||||
private _plugin: Plugin;
|
||||
private _liveSyncPlugin: ObsidianLiveSyncPlugin;
|
||||
private _confirmInstance: ObsidianConfirm;
|
||||
get dialogManager() {
|
||||
return this._dialogManager;
|
||||
}
|
||||
constructor(context: ObsidianServiceContext) {
|
||||
super(context);
|
||||
this._liveSyncPlugin = context.liveSyncPlugin;
|
||||
this._dialogManager = new SvelteDialogManager(this._liveSyncPlugin);
|
||||
this._plugin = context.plugin;
|
||||
this._confirmInstance = new ObsidianConfirm(this._plugin.app, this._plugin);
|
||||
}
|
||||
|
||||
async promptCopyToClipboard(title: string, value: string): Promise<boolean> {
|
||||
const param = {
|
||||
title: title,
|
||||
dataToCopy: value,
|
||||
};
|
||||
const result = await this._dialogManager.open(DialogueToCopy, param);
|
||||
if (result !== "ok") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
showMarkdownDialog<T extends string[]>(
|
||||
title: string,
|
||||
contentMD: string,
|
||||
buttons: T,
|
||||
defaultAction?: (typeof buttons)[number]
|
||||
): Promise<(typeof buttons)[number] | false> {
|
||||
// TODO: implement `confirm` to this service
|
||||
return this._liveSyncPlugin.confirm.askSelectStringDialogue(contentMD, buttons, {
|
||||
title,
|
||||
defaultAction: defaultAction ?? buttons[0],
|
||||
timeout: 0,
|
||||
constructor(context: ObsidianServiceContext, dependents: ObsidianUIServiceDependencies<ObsidianServiceContext>) {
|
||||
const obsidianConfirm = new ObsidianConfirm(context);
|
||||
const obsidianSvelteDialogManager = new ObsidianSvelteDialogManager<ObsidianServiceContext>(context, {
|
||||
appLifecycle: dependents.appLifecycle,
|
||||
config: dependents.config,
|
||||
replicator: dependents.replicator,
|
||||
confirm: obsidianConfirm,
|
||||
});
|
||||
super(context, {
|
||||
appLifecycle: dependents.appLifecycle,
|
||||
dialogManager: obsidianSvelteDialogManager,
|
||||
confirm: obsidianConfirm,
|
||||
});
|
||||
}
|
||||
|
||||
get confirm(): Confirm {
|
||||
return this._confirmInstance;
|
||||
}
|
||||
}
|
||||
|
||||
37
src/modules/services/SvelteDialogObsidian.ts
Normal file
37
src/modules/services/SvelteDialogObsidian.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Modal } from "@/deps";
|
||||
|
||||
import {
|
||||
SvelteDialogManagerBase,
|
||||
SvelteDialogMixIn,
|
||||
type ComponentHasResult,
|
||||
type SvelteDialogManagerDependencies,
|
||||
} from "@lib/services/implements/base/SvelteDialog";
|
||||
import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext";
|
||||
|
||||
export const SvelteDialogBase = SvelteDialogMixIn(Modal);
|
||||
export class SvelteDialogObsidian<
|
||||
T,
|
||||
U,
|
||||
C extends ObsidianServiceContext = ObsidianServiceContext,
|
||||
> extends SvelteDialogBase<T, U, C> {
|
||||
constructor(
|
||||
context: C,
|
||||
dependents: SvelteDialogManagerDependencies<C>,
|
||||
component: ComponentHasResult<T, U>,
|
||||
initialData?: U
|
||||
) {
|
||||
super(context.app);
|
||||
this.initDialog(context, dependents, component, initialData);
|
||||
}
|
||||
}
|
||||
|
||||
export class ObsidianSvelteDialogManager<T extends ObsidianServiceContext> extends SvelteDialogManagerBase<T> {
|
||||
override async openSvelteDialog<TT, TU>(
|
||||
component: ComponentHasResult<TT, TU>,
|
||||
initialData?: TU
|
||||
): Promise<TT | undefined> {
|
||||
const dialog = new SvelteDialogObsidian<TT, TU, T>(this.context, this.dependents, component, initialData);
|
||||
dialog.open();
|
||||
return await dialog.waitForClose();
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,10 @@ import ObsidianLiveSyncPlugin from "@/main";
|
||||
import { DEFAULT_SETTINGS, type ObsidianLiveSyncSettings } from "@/lib/src/common/types";
|
||||
import { LOG_LEVEL_VERBOSE, setGlobalLogFunction } from "@lib/common/logger";
|
||||
import { SettingCache } from "./obsidian-mock";
|
||||
import { delay, promiseWithResolvers } from "octagonal-wheels/promises";
|
||||
import { delay, fireAndForget, promiseWithResolvers } from "octagonal-wheels/promises";
|
||||
import { EVENT_PLATFORM_UNLOADED } from "@lib/events/coreEvents";
|
||||
import { EVENT_LAYOUT_READY, eventHub } from "@/common/events";
|
||||
import { EVENT_PLATFORM_UNLOADED } from "@/lib/src/PlatformAPIs/base/APIBase";
|
||||
|
||||
import { env } from "../suite/variables";
|
||||
|
||||
export type LiveSyncHarness = {
|
||||
@@ -79,12 +80,14 @@ export async function generateHarness(
|
||||
await plugin.onload();
|
||||
let isDisposed = false;
|
||||
const waitPromise = promiseWithResolvers<void>();
|
||||
eventHub.once(EVENT_PLATFORM_UNLOADED, async () => {
|
||||
console.log(`Harness for vault '${vaultName}' disposed.`);
|
||||
await delay(100);
|
||||
eventHub.offAll();
|
||||
isDisposed = true;
|
||||
waitPromise.resolve();
|
||||
eventHub.once(EVENT_PLATFORM_UNLOADED, () => {
|
||||
fireAndForget(async () => {
|
||||
console.log(`Harness for vault '${vaultName}' disposed.`);
|
||||
await delay(100);
|
||||
eventHub.offAll();
|
||||
isDisposed = true;
|
||||
waitPromise.resolve();
|
||||
});
|
||||
});
|
||||
eventHub.once(EVENT_LAYOUT_READY, () => {
|
||||
plugin.app.vault.trigger("layout-ready");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
script_dir=$(dirname "$0")
|
||||
webpeer_dir=$script_dir/../../src/lib/apps/webpeer
|
||||
webpeer_dir=$script_dir/../../src/apps/webpeer
|
||||
|
||||
docker run -d --name relay-test -p 4000:8080 scsibug/nostr-rs-relay:latest
|
||||
npm run --prefix $webpeer_dir build
|
||||
|
||||
@@ -24,12 +24,5 @@
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "test/**/*.test.ts"],
|
||||
"exclude": [
|
||||
"pouchdb-browser-webpack",
|
||||
"utils",
|
||||
"src/lib/apps",
|
||||
"src/**/*.test.ts",
|
||||
"**/_test/**",
|
||||
"src/**/*.test.ts"
|
||||
]
|
||||
"exclude": ["pouchdb-browser-webpack", "utils", "src/apps", "src/**/*.test.ts", "**/_test/**", "src/**/*.test.ts"]
|
||||
}
|
||||
|
||||
21
updates.md
21
updates.md
@@ -3,6 +3,21 @@ 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.42
|
||||
|
||||
2nd, February, 2026
|
||||
|
||||
This release is identical to 0.25.41-patched-3, except for the version number.
|
||||
|
||||
### Refactored
|
||||
|
||||
- Now the service context is `protected` instead of `private` in `ServiceBase`.
|
||||
- This change allows derived classes to access the context directly.
|
||||
- Some dynamically bound services have been moved to services for better dependency management.
|
||||
- `WebPeer` has been moved to the main repository from the sub repository `livesync-commonlib` for correct dependency management.
|
||||
- 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
|
||||
@@ -18,16 +33,18 @@ The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsid
|
||||
### 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.
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { defineConfig, mergeConfig } from "vitest/config";
|
||||
import viteConfig from "./vitest.config.common";
|
||||
|
||||
export default defineConfig({});
|
||||
export default mergeConfig(viteConfig, defineConfig({}));
|
||||
|
||||
Reference in New Issue
Block a user