Improved

- New Translation: `es` (Spanish) by @zeedif (Thank you so much)!
- Now all of messages can be selectable and copyable, also on the iPhone, iPad, and Android devices. Now we can copy or share the messages easily.

New Feature

- Peer-to-Peer Synchronisation has been implemented!

Fixed

- No longer memory or resource leaks when the plug-in is disabled.
- Now deleted chunks are correctly detected on conflict resolution, and we are guided to resurrect them.
- Hanging issue during the initial synchronisation has been fixed.
- Some unnecessary logs have been removed.
- Now all modal dialogues are correctly closed when the plug-in is disabled.

Refactor

- Several interfaces have been moved to the separated library.
- Translations have been moved to each language file, and during the build, they are merged into one file.
- Non-mobile friendly code has been removed and replaced with the safer code.
- Started writing Platform impedance-matching-layer.
- Svelte has been updated to v5.
- Some function have got more robust type definitions.
- Terser optimisation has slightly improved.
- During the build, analysis meta-file of the bundled codes will be generated.
This commit is contained in:
vorotamoroz
2025-02-13 12:48:00 +00:00
parent 45ceca8bb6
commit 1cd1465f2c
39 changed files with 9209 additions and 632 deletions

1
.gitignore vendored
View File

@@ -10,6 +10,7 @@ package-lock.json
main.js main.js
main_org.js main_org.js
*.js.map *.js.map
meta.json
meta-*.json meta-*.json

View File

@@ -10,6 +10,7 @@ import fs from "node:fs";
import { minify } from "terser"; import { minify } from "terser";
import inlineWorkerPlugin from "esbuild-plugin-inline-worker"; import inlineWorkerPlugin from "esbuild-plugin-inline-worker";
import { terserOption } from "./terser.config.mjs"; import { terserOption } from "./terser.config.mjs";
import path from "node:path";
const prod = process.argv[2] === "production"; const prod = process.argv[2] === "production";
const keepTest = true; //!prod; const keepTest = true; //!prod;
@@ -18,6 +19,46 @@ const manifestJson = JSON.parse(fs.readFileSync("./manifest.json") + "");
const packageJson = JSON.parse(fs.readFileSync("./package.json") + ""); const packageJson = JSON.parse(fs.readFileSync("./package.json") + "");
const updateInfo = JSON.stringify(fs.readFileSync("./updates.md") + ""); const updateInfo = JSON.stringify(fs.readFileSync("./updates.md") + "");
const moduleAliasPlugin = {
name: "module-alias",
setup(build) {
build.onResolve({ filter: /.(dev)(.ts|)$/ }, (args) => {
// console.log(args.path);
if (prod) {
let prodTs = args.path.replace(".dev", ".prod");
const statFile = prodTs.endsWith(".ts") ? prodTs : prodTs + ".ts";
const realPath = path.join(args.resolveDir, statFile);
console.log(`Checking ${statFile}`);
if (fs.existsSync(realPath)) {
console.log(`Replaced ${args.path} with ${prodTs}`);
return {
path: realPath,
namespace: "file",
};
}
}
return null;
});
build.onResolve({ filter: /.(platform)(.ts|)$/ }, (args) => {
// console.log(args.path);
if (prod) {
let prodTs = args.path.replace(".platform", ".obsidian");
const statFile = prodTs.endsWith(".ts") ? prodTs : prodTs + ".ts";
const realPath = path.join(args.resolveDir, statFile);
console.log(`Checking ${statFile}`);
if (fs.existsSync(realPath)) {
console.log(`Replaced ${args.path} with ${prodTs}`);
return {
path: realPath,
namespace: "file",
};
}
}
return null;
});
},
};
/** @type esbuild.Plugin[] */ /** @type esbuild.Plugin[] */
const plugins = [ const plugins = [
{ {
@@ -26,7 +67,17 @@ const plugins = [
let count = 0; let count = 0;
build.onEnd(async (result) => { build.onEnd(async (result) => {
if (count++ === 0) { if (count++ === 0) {
console.log("first build:", result); console.log("first build:");
if (prod) {
console.log("MetaFile:");
if (result.metafile) {
fs.writeFileSync("meta.json", JSON.stringify(result.metafile));
let text = await esbuild.analyzeMetafile(result.metafile, {
verbose: true,
});
// console.log(text);
}
}
} else { } else {
console.log("subsequent build:"); console.log("subsequent build:");
} }
@@ -95,6 +146,7 @@ const context = await esbuild.context({
dropLabels: prod && !keepTest ? ["TEST", "DEV"] : [], dropLabels: prod && !keepTest ? ["TEST", "DEV"] : [],
// keepNames: true, // keepNames: true,
plugins: [ plugins: [
moduleAliasPlugin,
inlineWorkerPlugin({ inlineWorkerPlugin({
external: externals, external: externals,
treeShaking: true, treeShaking: true,

7601
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,8 +5,9 @@
"main": "main.js", "main": "main.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"bakei18n": "npx tsx ./src/lib/_tools/bakei18n.ts",
"dev": "node esbuild.config.mjs", "dev": "node esbuild.config.mjs",
"build": "node esbuild.config.mjs production", "build": "npm run bakei18n && node esbuild.config.mjs production",
"buildDev": "node esbuild.config.mjs dev", "buildDev": "node esbuild.config.mjs dev",
"lint": "eslint src", "lint": "eslint src",
"svelte-check": "svelte-check --tsconfig ./tsconfig.json", "svelte-check": "svelte-check --tsconfig ./tsconfig.json",
@@ -55,11 +56,12 @@
"pouchdb-replication": "^9.0.0", "pouchdb-replication": "^9.0.0",
"pouchdb-utils": "^9.0.0", "pouchdb-utils": "^9.0.0",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"svelte": "^4.2.19", "svelte": "^5.19.7",
"svelte-preprocess": "^6.0.2", "svelte-preprocess": "^6.0.3",
"terser": "^5.37.0", "terser": "^5.37.0",
"transform-pouch": "^2.0.0", "transform-pouch": "^2.0.0",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"tsx": "^4.19.2",
"typescript": "^5.7.3" "typescript": "^5.7.3"
}, },
"dependencies": { "dependencies": {
@@ -72,8 +74,9 @@
"fflate": "^0.8.2", "fflate": "^0.8.2",
"idb": "^8.0.2", "idb": "^8.0.2",
"minimatch": "^10.0.1", "minimatch": "^10.0.1",
"octagonal-wheels": "^0.1.22", "octagonal-wheels": "^0.1.23",
"svelte-check": "^4.1.4", "svelte-check": "^4.1.4",
"trystero": "^0.20.0",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2" "xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
} }
} }

View File

@@ -0,0 +1,28 @@
import { ItemView } from "obsidian";
import { type mount, unmount } from "svelte";
export abstract class SvelteItemView extends ItemView {
abstract instantiateComponent(target: HTMLElement): ReturnType<typeof mount> | Promise<ReturnType<typeof mount>>;
component?: ReturnType<typeof mount>;
async onOpen() {
await super.onOpen();
this.contentEl.empty();
await this._dismountComponent();
this.component = await this.instantiateComponent(this.contentEl);
return;
}
async _dismountComponent() {
if (this.component) {
await unmount(this.component);
this.component = undefined;
}
}
async onClose() {
await super.onClose();
if (this.component) {
await unmount(this.component);
this.component = undefined;
}
return;
}
}

View File

@@ -10,6 +10,8 @@ export const EVENT_FILE_RENAMED = "file-renamed";
export const EVENT_FILE_SAVED = "file-saved"; export const EVENT_FILE_SAVED = "file-saved";
export const EVENT_LEAF_ACTIVE_CHANGED = "leaf-active-changed"; export const EVENT_LEAF_ACTIVE_CHANGED = "leaf-active-changed";
export const EVENT_DATABASE_REBUILT = "database-rebuilt";
export const EVENT_LOG_ADDED = "log-added"; export const EVENT_LOG_ADDED = "log-added";
export const EVENT_REQUEST_OPEN_SETTINGS = "request-open-settings"; export const EVENT_REQUEST_OPEN_SETTINGS = "request-open-settings";
@@ -21,6 +23,9 @@ export const EVENT_REQUEST_RELOAD_SETTING_TAB = "reload-setting-tab";
export const EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG = "request-open-plugin-sync-dialog"; export const EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG = "request-open-plugin-sync-dialog";
export const EVENT_REQUEST_OPEN_P2P = "request-open-p2p";
export const EVENT_REQUEST_CLOSE_P2P = "request-close-p2p";
// export const EVENT_FILE_CHANGED = "file-changed"; // export const EVENT_FILE_CHANGED = "file-changed";
declare global { declare global {
@@ -43,6 +48,9 @@ declare global {
[EVENT_REQUEST_OPEN_SETTING_WIZARD]: undefined; [EVENT_REQUEST_OPEN_SETTING_WIZARD]: undefined;
[EVENT_FILE_RENAMED]: { newPath: FilePathWithPrefix; old: FilePathWithPrefix }; [EVENT_FILE_RENAMED]: { newPath: FilePathWithPrefix; old: FilePathWithPrefix };
[EVENT_LEAF_ACTIVE_CHANGED]: undefined; [EVENT_LEAF_ACTIVE_CHANGED]: undefined;
[EVENT_REQUEST_OPEN_P2P]: undefined;
[EVENT_REQUEST_CLOSE_P2P]: undefined;
[EVENT_DATABASE_REBUILT]: undefined;
} }
} }

View File

@@ -1,5 +1,10 @@
<script lang="ts"> <script lang="ts">
import { ConfigSync, PluginDataExDisplayV2, type IPluginDataExDisplay, type PluginDataExFile } from "./CmdConfigSync.ts"; import {
ConfigSync,
PluginDataExDisplayV2,
type IPluginDataExDisplay,
type PluginDataExFile,
} from "./CmdConfigSync.ts";
import { Logger } from "../../lib/src/common/logger"; import { Logger } from "../../lib/src/common/logger";
import { type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "../../lib/src/common/types"; import { type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "../../lib/src/common/types";
import { getDocData, timeDeltaToHumanReadable, unique } from "../../lib/src/common/utils"; import { getDocData, timeDeltaToHumanReadable, unique } from "../../lib/src/common/utils";
@@ -15,7 +20,11 @@
export let applyAllPluse = 0; export let applyAllPluse = 0;
export let applyData: (data: IPluginDataExDisplay) => Promise<boolean>; export let applyData: (data: IPluginDataExDisplay) => Promise<boolean>;
export let compareData: (dataA: IPluginDataExDisplay, dataB: IPluginDataExDisplay, compareEach?: boolean) => Promise<boolean>; export let compareData: (
dataA: IPluginDataExDisplay,
dataB: IPluginDataExDisplay,
compareEach?: boolean
) => Promise<boolean>;
export let deleteData: (data: IPluginDataExDisplay) => Promise<boolean>; export let deleteData: (data: IPluginDataExDisplay) => Promise<boolean>;
export let hidden: boolean; export let hidden: boolean;
export let plugin: ObsidianLiveSyncPlugin; export let plugin: ObsidianLiveSyncPlugin;
@@ -151,7 +160,11 @@
canCompare = result.canCompare; canCompare = result.canCompare;
pickToCompare = false; pickToCompare = false;
if (canCompare) { if (canCompare) {
if (local?.files.length == remote?.files.length && local?.files.length == 1 && local?.files[0].filename == remote?.files[0].filename) { if (
local?.files.length == remote?.files.length &&
local?.files.length == 1 &&
local?.files[0].filename == remote?.files[0].filename
) {
pickToCompare = false; pickToCompare = false;
} else { } else {
pickToCompare = true; pickToCompare = true;
@@ -250,7 +263,11 @@
const selectedItem = list.find((e) => e.term == selected); const selectedItem = list.find((e) => e.term == selected);
await compareItems(local, selectedItem); await compareItems(local, selectedItem);
} }
async function compareItems(local: IPluginDataExDisplay | undefined, remote: IPluginDataExDisplay | undefined, filename?: string) { async function compareItems(
local: IPluginDataExDisplay | undefined,
remote: IPluginDataExDisplay | undefined,
filename?: string
) {
if (local && remote) { if (local && remote) {
if (!filename) { if (!filename) {
if (await compareData(local, remote)) { if (await compareData(local, remote)) {
@@ -258,8 +275,10 @@
} }
return; return;
} else { } else {
const localCopy = local instanceof PluginDataExDisplayV2 ? new PluginDataExDisplayV2(local) : { ...local }; const localCopy =
const remoteCopy = remote instanceof PluginDataExDisplayV2 ? new PluginDataExDisplayV2(remote) : { ...remote }; local instanceof PluginDataExDisplayV2 ? new PluginDataExDisplayV2(local) : { ...local };
const remoteCopy =
remote instanceof PluginDataExDisplayV2 ? new PluginDataExDisplayV2(remote) : { ...remote };
localCopy.files = localCopy.files.filter((e) => e.filename == filename); localCopy.files = localCopy.files.filter((e) => e.filename == filename);
remoteCopy.files = remoteCopy.files.filter((e) => e.filename == filename); remoteCopy.files = remoteCopy.files.filter((e) => e.filename == filename);
if (await compareData(localCopy, remoteCopy, true)) { if (await compareData(localCopy, remoteCopy, true)) {
@@ -329,7 +348,7 @@
</script> </script>
{#if terms.length > 0} {#if terms.length > 0}
<span class="spacer" /> <span class="spacer"></span>
{#if !hidden} {#if !hidden}
<span class="chip-wrap"> <span class="chip-wrap">
<span class="chip modified">{freshness}</span> <span class="chip modified">{freshness}</span>
@@ -351,12 +370,15 @@
<button on:click={compareSelected}>⮂</button> <button on:click={compareSelected}>⮂</button>
{/if} {/if}
{:else} {:else}
<button disabled /> <!-- svelte-ignore a11y_consider_explicit_label -->
<button disabled></button>
{/if} {/if}
<button on:click={applySelected}>✓</button> <button on:click={applySelected}>✓</button>
{:else} {:else}
<button disabled /> <!-- svelte-ignore a11y_consider_explicit_label -->
<button disabled /> <button disabled></button>
<!-- svelte-ignore a11y_consider_explicit_label -->
<button disabled></button>
{/if} {/if}
{#if isMaintenanceMode} {#if isMaintenanceMode}
{#if selected != ""} {#if selected != ""}
@@ -367,10 +389,12 @@
{/if} {/if}
{/if} {/if}
{:else} {:else}
<span class="spacer" /> <span class="spacer"></span>
<span class="message even">All the same or non-existent</span> <span class="message even">All the same or non-existent</span>
<button disabled /> <!-- svelte-ignore a11y_consider_explicit_label -->
<button disabled /> <button disabled></button>
<!-- svelte-ignore a11y_consider_explicit_label -->
<button disabled></button>
{/if} {/if}
<style> <style>

View File

@@ -1,9 +1,10 @@
import { mount, unmount } from "svelte";
import { App, Modal } from "../../deps.ts"; import { App, Modal } from "../../deps.ts";
import ObsidianLiveSyncPlugin from "../../main.ts"; import ObsidianLiveSyncPlugin from "../../main.ts";
import PluginPane from "./PluginPane.svelte"; import PluginPane from "./PluginPane.svelte";
export class PluginDialogModal extends Modal { export class PluginDialogModal extends Modal {
plugin: ObsidianLiveSyncPlugin; plugin: ObsidianLiveSyncPlugin;
component: PluginPane | undefined; component: ReturnType<typeof mount> | undefined;
isOpened() { isOpened() {
return this.component != undefined; return this.component != undefined;
} }
@@ -20,7 +21,7 @@ export class PluginDialogModal extends Modal {
this.contentEl.style.flexDirection = "column"; this.contentEl.style.flexDirection = "column";
this.titleEl.setText("Customization Sync (Beta3)"); this.titleEl.setText("Customization Sync (Beta3)");
if (!this.component) { if (!this.component) {
this.component = new PluginPane({ this.component = mount(PluginPane, {
target: contentEl, target: contentEl,
props: { plugin: this.plugin }, props: { plugin: this.plugin },
}); });
@@ -29,7 +30,7 @@ export class PluginDialogModal extends Modal {
onClose() { onClose() {
if (this.component) { if (this.component) {
this.component.$destroy(); void unmount(this.component);
this.component = undefined; this.component = undefined;
} }
} }

View File

@@ -542,10 +542,12 @@
padding-right: 4px; padding-right: 4px;
flex-wrap: wrap; flex-wrap: wrap;
} }
.filerow.hideeven:has(.even),
.labelrow.hideeven:has(.even) { .filerow.hideeven:has(:global(.even)),
.labelrow.hideeven:has(:global(.even)) {
display: none; display: none;
} }
.noterow { .noterow {
min-height: 2em; min-height: 2em;
display: flex; display: flex;

View File

@@ -136,7 +136,9 @@
{#if selectedObj != false} {#if selectedObj != false}
<div class="op-scrollable json-source"> <div class="op-scrollable json-source">
{#each diffs as diff} {#each diffs as diff}
<span class={diff[0] == DIFF_DELETE ? "deleted" : diff[0] == DIFF_INSERT ? "added" : "normal"}>{diff[1]}</span> <span class={diff[0] == DIFF_DELETE ? "deleted" : diff[0] == DIFF_INSERT ? "added" : "normal"}
>{diff[1]}</span
>
{/each} {/each}
</div> </div>
{:else} {:else}
@@ -145,6 +147,7 @@
<div class="infos"> <div class="infos">
<table> <table>
<tbody>
<tr> <tr>
<th>{nameA}</th> <th>{nameA}</th>
<td <td
@@ -169,6 +172,7 @@
{docBContent.length} letters {docBContent.length} letters
</td> </td>
</tr> </tr>
</tbody>
</table> </table>
</div> </div>
@@ -203,6 +207,7 @@
overflow-y: scroll; overflow-y: scroll;
max-height: 60vh; max-height: 60vh;
user-select: text; user-select: text;
-webkit-user-select: text;
} }
.json-source { .json-source {
white-space: pre; white-space: pre;

View File

@@ -0,0 +1,222 @@
import type { IObsidianModule } from "../../modules/AbstractObsidianModule";
import { P2PReplicatorPaneView, VIEW_TYPE_P2P } from "./P2PReplicator/P2PReplicatorPaneView.ts";
import { TrysteroReplicator } from "../../lib/src/replication/trystero/TrysteroReplicator.ts";
import {
AutoAccepting,
DEFAULT_SETTINGS,
LOG_LEVEL_INFO,
LOG_LEVEL_NOTICE,
LOG_LEVEL_VERBOSE,
REMOTE_P2P,
type EntryDoc,
type RemoteDBSettings,
} from "../../lib/src/common/types.ts";
import { LiveSyncCommands } from "../LiveSyncCommands.ts";
import {
LiveSyncTrysteroReplicator,
setReplicatorFunc,
} from "../../lib/src/replication/trystero/LiveSyncTrysteroReplicator.ts";
import {
EVENT_DATABASE_REBUILT,
EVENT_PLUGIN_UNLOADED,
EVENT_REQUEST_OPEN_P2P,
eventHub,
} from "../../common/events.ts";
import {
EVENT_ADVERTISEMENT_RECEIVED,
EVENT_DEVICE_LEAVED,
EVENT_P2P_REQUEST_FORCE_OPEN,
EVENT_REQUEST_STATUS,
} from "../../lib/src/replication/trystero/TrysteroReplicatorP2PServer.ts";
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator.ts";
import { Logger } from "octagonal-wheels/common/logger";
import { $msg } from "../../lib/src/common/i18n.ts";
export class P2PReplicator extends LiveSyncCommands implements IObsidianModule {
$anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator> {
const settings = { ...this.settings, ...settingOverride };
if (settings.remoteType == REMOTE_P2P) {
return Promise.resolve(new LiveSyncTrysteroReplicator(this.plugin));
}
return undefined!;
}
_replicatorInstance?: TrysteroReplicator;
onunload(): void {
setReplicatorFunc(() => undefined);
void this.close();
}
onload(): void | Promise<void> {
setReplicatorFunc(() => this._replicatorInstance);
eventHub.onEvent(EVENT_ADVERTISEMENT_RECEIVED, (peerId) => this._replicatorInstance?.onNewPeer(peerId));
eventHub.onEvent(EVENT_DEVICE_LEAVED, (info) => this._replicatorInstance?.onPeerLeaved(info));
eventHub.onEvent(EVENT_REQUEST_STATUS, () => {
this._replicatorInstance?.requestStatus();
});
eventHub.onEvent(EVENT_P2P_REQUEST_FORCE_OPEN, () => {
void this.open();
});
eventHub.onEvent(EVENT_REQUEST_OPEN_P2P, () => {
void this.openPane();
});
eventHub.onEvent(EVENT_DATABASE_REBUILT, async () => {
await this.initialiseP2PReplicator();
});
eventHub.onEvent(EVENT_PLUGIN_UNLOADED, () => {
void this.close();
});
// throw new Error("Method not implemented.");
}
async $everyOnInitializeDatabase(): Promise<boolean> {
await this.initialiseP2PReplicator();
return Promise.resolve(true);
}
async $allSuspendExtraSync() {
this.plugin.settings.P2P_Enabled = false;
this.plugin.settings.P2P_AutoAccepting = AutoAccepting.NONE;
this.plugin.settings.P2P_AutoBroadcast = false;
this.plugin.settings.P2P_AutoStart = false;
this.plugin.settings.P2P_AutoSyncPeers = "";
this.plugin.settings.P2P_AutoWatchPeers = "";
return await Promise.resolve(true);
}
async $everyOnLoadStart() {
return await Promise.resolve();
}
async openPane() {
await this.plugin.$$showView(VIEW_TYPE_P2P);
}
async $everyOnloadStart(): Promise<boolean> {
this.plugin.registerView(VIEW_TYPE_P2P, (leaf) => new P2PReplicatorPaneView(leaf, this.plugin));
this.plugin.addCommand({
id: "open-p2p-replicator",
name: "P2P Sync : Open P2P Replicator",
callback: async () => {
await this.openPane();
},
});
this.plugin.addCommand({
id: "p2p-establish-connection",
name: "P2P Sync : Connect to the Signalling Server",
checkCallback: (isChecking) => {
if (isChecking) {
return !(this._replicatorInstance?.server?.isServing ?? false);
}
void this.open();
},
});
this.plugin.addCommand({
id: "p2p-close-connection",
name: "P2P Sync : Disconnect from the Signalling Server",
checkCallback: (isChecking) => {
if (isChecking) {
return this._replicatorInstance?.server?.isServing ?? false;
}
Logger(`Closing P2P Connection`, LOG_LEVEL_NOTICE);
void this.close();
},
});
this.plugin.addCommand({
id: "replicate-now-by-p2p",
name: "Replicate now by P2P",
checkCallback: (isChecking) => {
if (isChecking) {
if (this.settings.remoteType == REMOTE_P2P) return false;
if (!this._replicatorInstance?.server?.isServing) return false;
return true;
}
void this._replicatorInstance?.replicateFromCommand(false);
},
});
this.plugin
.addRibbonIcon("waypoints", "P2P Replicator", async () => {
await this.openPane();
})
.addClass("livesync-ribbon-replicate-p2p");
return await Promise.resolve(true);
}
$everyAfterResumeProcess(): Promise<boolean> {
if (this.settings.P2P_Enabled && this.settings.P2P_AutoStart) {
setTimeout(() => void this.open(), 100);
}
return Promise.resolve(true);
}
async open() {
if (!this.settings.P2P_Enabled) {
this._notice($msg("P2P.NotEnabled"));
return;
}
if (!this._replicatorInstance) {
await this.initialiseP2PReplicator();
}
await this._replicatorInstance?.open();
}
async close() {
await this._replicatorInstance?.close();
this._replicatorInstance = undefined;
}
getConfig(key: string) {
const vaultName = this.plugin.$$getVaultName();
const dbKey = `${vaultName}-${key}`;
return localStorage.getItem(dbKey);
}
setConfig(key: string, value: string) {
const vaultName = this.plugin.$$getVaultName();
const dbKey = `${vaultName}-${key}`;
localStorage.setItem(dbKey, value);
}
async initialiseP2PReplicator(): Promise<TrysteroReplicator> {
const getPlugin = () => this.plugin;
try {
// const plugin = this.plugin;
if (this._replicatorInstance) {
await this._replicatorInstance.close();
this._replicatorInstance = undefined;
}
if (!this.settings.P2P_AppID) {
this.settings.P2P_AppID = DEFAULT_SETTINGS.P2P_AppID;
}
const initialDeviceName = this.getConfig("p2p_device_name") || this.plugin.$$getDeviceAndVaultName();
const env = {
get db() {
return getPlugin().localDatabase.localDatabase;
},
get confirm() {
return getPlugin().confirm;
},
get deviceName() {
return initialDeviceName;
},
platform: "wip",
get settings() {
return getPlugin().settings;
},
async processReplicatedDocs(docs: EntryDoc[]): Promise<void> {
return await getPlugin().$$parseReplicationResult(
docs as PouchDB.Core.ExistingDocument<EntryDoc>[]
);
},
simpleStore: getPlugin().$$getSimpleStore("p2p-sync"),
};
this._replicatorInstance = new TrysteroReplicator(env);
// p2p_replicator.set(this.p2pReplicator);
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;
}
}
}

View File

@@ -0,0 +1,466 @@
<script lang="ts">
import { onMount, setContext } from "svelte";
import {
AutoAccepting,
DEFAULT_SETTINGS,
type ObsidianLiveSyncSettings,
type P2PSyncSetting,
} from "../../../lib/src/common/types";
import { type P2PReplicator } from "../CmdP2PSync";
import { AcceptedStatus, ConnectionStatus, type PeerStatus } from "./P2PReplicatorPaneView";
import PeerStatusRow from "../P2PReplicator/PeerStatusRow.svelte";
import { EVENT_LAYOUT_READY, eventHub } from "../../../common/events";
import type ObsidianLiveSyncPlugin from "../../../main";
import {
type PeerInfo,
type P2PServerInfo,
EVENT_SERVER_STATUS,
EVENT_REQUEST_STATUS,
EVENT_P2P_REPLICATOR_STATUS,
} from "../../../lib/src/replication/trystero/TrysteroReplicatorP2PServer";
import { type P2PReplicatorStatus } from "../../../lib/src/replication/trystero/TrysteroReplicator";
import { $msg as _msg } from "../../../lib/src/common/i18n";
interface Props {
plugin: ObsidianLiveSyncPlugin;
}
let { plugin }: Props = $props();
const cmdSync = plugin.getAddOn<P2PReplicator>("P2PReplicator")!;
setContext("getReplicator", () => cmdSync);
const initialSettings = { ...plugin.settings };
let settings = $state<P2PSyncSetting>(initialSettings);
// const vaultName = plugin.$$getVaultName();
// const dbKey = `${vaultName}-p2p-device-name`;
const initialDeviceName = cmdSync.getConfig("p2p_device_name") ?? plugin.$$getVaultName();
let deviceName = $state<string>(initialDeviceName);
let eP2PEnabled = $state<boolean>(initialSettings.P2P_Enabled);
let eRelay = $state<string>(initialSettings.P2P_relays);
let eRoomId = $state<string>(initialSettings.P2P_roomID);
let ePassword = $state<string>(initialSettings.P2P_passphrase);
let eAppId = $state<string>(initialSettings.P2P_AppID);
let eDeviceName = $state<string>(initialDeviceName);
let eAutoAccept = $state<boolean>(initialSettings.P2P_AutoAccepting == AutoAccepting.ALL);
let eAutoStart = $state<boolean>(initialSettings.P2P_AutoStart);
let eAutoBroadcast = $state<boolean>(initialSettings.P2P_AutoBroadcast);
const isP2PEnabledModified = $derived.by(() => eP2PEnabled !== settings.P2P_Enabled);
const isRelayModified = $derived.by(() => eRelay !== settings.P2P_relays);
const isRoomIdModified = $derived.by(() => eRoomId !== settings.P2P_roomID);
const isPasswordModified = $derived.by(() => ePassword !== settings.P2P_passphrase);
const isAppIdModified = $derived.by(() => eAppId !== settings.P2P_AppID);
const isDeviceNameModified = $derived.by(() => eDeviceName !== deviceName);
const isAutoAcceptModified = $derived.by(() => eAutoAccept !== (settings.P2P_AutoAccepting == AutoAccepting.ALL));
const isAutoStartModified = $derived.by(() => eAutoStart !== settings.P2P_AutoStart);
const isAutoBroadcastModified = $derived.by(() => eAutoBroadcast !== settings.P2P_AutoBroadcast);
const isAnyModified = $derived.by(
() =>
isP2PEnabledModified ||
isRelayModified ||
isRoomIdModified ||
isPasswordModified ||
isAppIdModified ||
isDeviceNameModified ||
isAutoAcceptModified ||
isAutoStartModified ||
isAutoBroadcastModified
);
async function saveAndApply() {
const newSettings = {
...plugin.settings,
P2P_Enabled: eP2PEnabled,
P2P_relays: eRelay,
P2P_roomID: eRoomId,
P2P_passphrase: ePassword,
P2P_AppID: eAppId,
P2P_AutoAccepting: eAutoAccept ? AutoAccepting.ALL : AutoAccepting.NONE,
P2P_AutoStart: eAutoStart,
P2P_AutoBroadcast: eAutoBroadcast,
};
plugin.settings = newSettings;
cmdSync.setConfig("p2p_device_name", eDeviceName);
deviceName = eDeviceName;
await plugin.saveSettings();
}
async function revert() {
eP2PEnabled = settings.P2P_Enabled;
eRelay = settings.P2P_relays;
eRoomId = settings.P2P_roomID;
ePassword = settings.P2P_passphrase;
eAppId = settings.P2P_AppID;
eAutoAccept = settings.P2P_AutoAccepting == AutoAccepting.ALL;
eAutoStart = settings.P2P_AutoStart;
eAutoBroadcast = settings.P2P_AutoBroadcast;
}
let serverInfo = $state<P2PServerInfo | undefined>(undefined);
let replicatorInfo = $state<P2PReplicatorStatus | undefined>(undefined);
const applyLoadSettings = (d: ObsidianLiveSyncSettings, force: boolean) => {
const { P2P_relays, P2P_roomID, P2P_passphrase, P2P_AppID, P2P_AutoAccepting } = d;
if (force || !isP2PEnabledModified) eP2PEnabled = d.P2P_Enabled;
if (force || !isRelayModified) eRelay = P2P_relays;
if (force || !isRoomIdModified) eRoomId = P2P_roomID;
if (force || !isPasswordModified) ePassword = P2P_passphrase;
if (force || !isAppIdModified) eAppId = P2P_AppID;
const newAutoAccept = P2P_AutoAccepting === AutoAccepting.ALL;
if (force || !isAutoAcceptModified) eAutoAccept = newAutoAccept;
if (force || !isAutoStartModified) eAutoStart = d.P2P_AutoStart;
if (force || !isAutoBroadcastModified) eAutoBroadcast = d.P2P_AutoBroadcast;
settings = d;
};
onMount(() => {
const r = eventHub.onEvent("setting-saved", async (d) => {
applyLoadSettings(d, false);
closeServer();
});
const rx = eventHub.onEvent(EVENT_LAYOUT_READY, () => {
applyLoadSettings(plugin.settings, true);
});
const r2 = eventHub.onEvent(EVENT_SERVER_STATUS, (status) => {
serverInfo = status;
advertisements = status?.knownAdvertisements ?? [];
});
const r3 = eventHub.onEvent(EVENT_P2P_REPLICATOR_STATUS, (status) => {
replicatorInfo = status;
});
eventHub.emitEvent(EVENT_REQUEST_STATUS);
return () => {
r();
r2();
r3();
};
});
let isConnected = $derived.by(() => {
return serverInfo?.isConnected ?? false;
});
let serverPeerId = $derived.by(() => {
return serverInfo?.serverPeerId ?? "";
});
let advertisements = $state<PeerInfo[]>([]);
let autoSyncPeers = $derived.by(() =>
settings.P2P_AutoSyncPeers.split(",")
.map((e) => e.trim())
.filter((e) => e)
);
let autoWatchPeers = $derived.by(() =>
settings.P2P_AutoWatchPeers.split(",")
.map((e) => e.trim())
.filter((e) => e)
);
let syncOnCommand = $derived.by(() =>
settings.P2P_SyncOnReplication.split(",")
.map((e) => e.trim())
.filter((e) => e)
);
const peers = $derived.by(() =>
advertisements.map((ad) => {
let accepted: AcceptedStatus;
const isTemporaryAccepted = ad.isTemporaryAccepted;
if (isTemporaryAccepted === undefined) {
if (ad.isAccepted === undefined) {
accepted = AcceptedStatus.UNKNOWN;
} else {
accepted = ad.isAccepted ? AcceptedStatus.ACCEPTED : AcceptedStatus.DENIED;
}
} else if (isTemporaryAccepted === true) {
accepted = AcceptedStatus.ACCEPTED_IN_SESSION;
} else {
accepted = AcceptedStatus.DENIED_IN_SESSION;
}
const isFetching = replicatorInfo?.replicatingFrom.indexOf(ad.peerId) !== -1;
const isSending = replicatorInfo?.replicatingTo.indexOf(ad.peerId) !== -1;
const isWatching = replicatorInfo?.watchingPeers.indexOf(ad.peerId) !== -1;
const syncOnStart = autoSyncPeers.indexOf(ad.name) !== -1;
const watchOnStart = autoWatchPeers.indexOf(ad.name) !== -1;
const syncOnReplicationCommand = syncOnCommand.indexOf(ad.name) !== -1;
const st: PeerStatus = {
name: ad.name,
peerId: ad.peerId,
accepted: accepted,
status: ad.isAccepted ? ConnectionStatus.CONNECTED : ConnectionStatus.DISCONNECTED,
isSending: isSending,
isFetching: isFetching,
isWatching: isWatching,
syncOnConnect: syncOnStart,
watchOnConnect: watchOnStart,
syncOnReplicationCommand: syncOnReplicationCommand,
};
return st;
})
);
function useDefaultRelay() {
eRelay = DEFAULT_SETTINGS.P2P_relays;
}
function _generateRandom() {
return (Math.floor(Math.random() * 1000) + 1000).toString().substring(1);
}
function generateRandom(length: number) {
let buf = "";
while (buf.length < length) {
buf += "-" + _generateRandom();
}
return buf.substring(1, length);
}
function chooseRandom() {
eRoomId = generateRandom(12) + "-" + Math.random().toString(36).substring(2, 5);
}
async function openServer() {
await cmdSync.open();
}
async function closeServer() {
await cmdSync.close();
}
function startBroadcasting() {
cmdSync._replicatorInstance?.enableBroadcastChanges();
}
function stopBroadcasting() {
cmdSync._replicatorInstance?.disableBroadcastChanges();
}
const initialDialogStatusKey = `p2p-dialog-status`;
const getDialogStatus = () => {
try {
const initialDialogStatus = JSON.parse(cmdSync.getConfig(initialDialogStatusKey) ?? "{}") as {
notice?: boolean;
setting?: boolean;
};
return initialDialogStatus;
} catch (e) {
return {};
}
};
const initialDialogStatus = getDialogStatus();
let isNoticeOpened = $state<boolean>(initialDialogStatus.notice ?? true);
let isSettingOpened = $state<boolean>(initialDialogStatus.setting ?? true);
$effect(() => {
const dialogStatus = {
notice: isNoticeOpened,
setting: isSettingOpened,
};
cmdSync.setConfig(initialDialogStatusKey, JSON.stringify(dialogStatus));
});
</script>
<article>
<h1>Peer to Peer Replicator</h1>
<details bind:open={isNoticeOpened}>
<summary>{_msg("P2P.Note.Summary")}</summary>
<p class="important">{_msg("P2P.Note.important_note")}</p>
<p class="important-sub">
{_msg("P2P.Note.important_note_sub")}
</p>
{#each _msg("P2P.Note.description").split("\n\n") as paragraph}
<p>{paragraph}</p>
{/each}
</details>
<h2>Connection Settings</h2>
<details bind:open={isSettingOpened}>
<summary>{eRelay}</summary>
<table class="settings">
<tbody>
<tr>
<th> Enable P2P Replicator </th>
<td>
<label class={{ "is-dirty": isP2PEnabledModified }}>
<input type="checkbox" bind:checked={eP2PEnabled} />
</label>
</td>
</tr><tr>
<th> Relay settings </th>
<td>
<label class={{ "is-dirty": isRelayModified }}>
<input
type="text"
placeholder="wss://exp-relay.vrtmrz.net, wss://xxxxx"
bind:value={eRelay}
/>
<button onclick={() => useDefaultRelay()}> Use vrtmrz's relay </button>
</label>
</td>
</tr>
<tr>
<th> Room ID </th>
<td>
<label class={{ "is-dirty": isRoomIdModified }}>
<input type="text" placeholder="anything-you-like" bind:value={eRoomId} />
<button onclick={() => chooseRandom()}> Use Random Number </button>
</label>
<span>
<small>
This can isolate your connections between devices. Use the same Room ID for the same
devices.</small
>
</span>
</td>
</tr>
<tr>
<th> Password </th>
<td>
<label class={{ "is-dirty": isPasswordModified }}>
<input type="password" placeholder="password" bind:value={ePassword} />
</label>
<span>
<small> This password is used to encrypt the connection. Use something long enough. </small>
</span>
</td>
</tr>
<tr>
<th> This device name </th>
<td>
<label class={{ "is-dirty": isDeviceNameModified }}>
<input type="text" placeholder="iphone-16" bind:value={eDeviceName} />
</label>
</td>
</tr>
<tr>
<th> Auto Connect </th>
<td>
<label class={{ "is-dirty": isAutoStartModified }}>
<input type="checkbox" bind:checked={eAutoStart} />
</label>
</td>
</tr>
<tr>
<th> Start change-broadcasting on Connect </th>
<td>
<label class={{ "is-dirty": isAutoBroadcastModified }}>
<input type="checkbox" bind:checked={eAutoBroadcast} />
</label>
</td>
</tr>
<!-- <tr>
<th> Auto Accepting </th>
<td>
<label class={{ "is-dirty": isAutoAcceptModified }}>
<input type="checkbox" bind:checked={eAutoAccept} />
</label>
</td>
</tr> -->
</tbody>
</table>
<button disabled={!isAnyModified} class="button mod-cta" onclick={saveAndApply}>Save and Apply</button>
<button disabled={!isAnyModified} class="button" onclick={revert}>Revert changes</button>
</details>
<div>
<h2>Signaling Server Connection</h2>
<div>
{#if !isConnected}
<p>No Connection</p>
{:else}
<p>Connected to Signaling Server (as Peer ID: {serverPeerId})</p>
{/if}
</div>
<div>
{#if !isConnected}
<button onclick={openServer}>Connect</button>
{:else}
<button onclick={closeServer}>Disconnect</button>
{#if replicatorInfo?.isBroadcasting !== undefined}
{#if replicatorInfo?.isBroadcasting}
<button onclick={stopBroadcasting}>Stop Broadcasting</button>
{:else}
<button onclick={startBroadcasting}>Start Broadcasting</button>
{/if}
{/if}
<details>
<summary>Broadcasting?</summary>
<p>
<small>
If you want to use `LiveSync`, you should broadcast changes. All `watching` peers which
detects this will start the replication for fetching. <br />
However, This should not be enabled if you want to increase your secrecy more.
</small>
</p>
</details>
{/if}
</div>
</div>
<div>
<h2>Peers</h2>
<table class="peers">
<thead>
<tr>
<th>Name</th>
<th>Action</th>
<th>Command</th>
</tr>
</thead>
<tbody>
{#each peers as peer}
<PeerStatusRow peerStatus={peer}></PeerStatusRow>
{/each}
</tbody>
</table>
</div>
</article>
<style>
article {
max-width: 100%;
}
article p {
user-select: text;
-webkit-user-select: text;
}
h2 {
margin-top: var(--size-4-1);
margin-bottom: var(--size-4-1);
padding-bottom: var(--size-4-1);
border-bottom: 1px solid var(--background-modifier-border);
}
label.is-dirty {
background-color: var(--background-modifier-error);
}
input {
background-color: transparent;
}
th {
/* display: flex;
justify-content: center;
align-items: center; */
min-height: var(--input-height);
}
td {
min-height: var(--input-height);
}
td > label {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
min-height: var(--input-height);
}
td > label > * {
margin: auto var(--size-4-1);
}
table.peers {
width: 100%;
}
.important {
color: var(--text-error);
font-size: 1.2em;
font-weight: bold;
}
.important-sub {
color: var(--text-warning);
}
.settings label {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
}
</style>

View File

@@ -0,0 +1,65 @@
import { WorkspaceLeaf } from "obsidian";
import ReplicatorPaneComponent from "./P2PReplicatorPane.svelte";
import type ObsidianLiveSyncPlugin from "../../../main.ts";
import { mount } from "svelte";
import { SvelteItemView } from "../../../common/SvelteItemView.ts";
export const VIEW_TYPE_P2P = "p2p-replicator";
export enum AcceptedStatus {
UNKNOWN = "Unknown",
ACCEPTED = "Accepted",
DENIED = "Denied",
ACCEPTED_IN_SESSION = "Accepted in session",
DENIED_IN_SESSION = "Denied in session",
}
export enum ConnectionStatus {
CONNECTED = "Connected",
CONNECTED_LIVE = "Connected(live)",
DISCONNECTED = "Disconnected",
}
export type PeerStatus = {
name: string;
peerId: string;
syncOnConnect: boolean;
watchOnConnect: boolean;
syncOnReplicationCommand: boolean;
accepted: AcceptedStatus;
status: ConnectionStatus;
isFetching: boolean;
isSending: boolean;
isWatching: boolean;
};
export class P2PReplicatorPaneView extends SvelteItemView {
plugin: ObsidianLiveSyncPlugin;
icon = "waypoints";
title: string = "";
navigation = false;
getIcon(): string {
return "waypoints";
}
constructor(leaf: WorkspaceLeaf, plugin: ObsidianLiveSyncPlugin) {
super(leaf);
this.plugin = plugin;
}
getViewType() {
return VIEW_TYPE_P2P;
}
getDisplayText() {
return "Peer-to-Peer Replicator";
}
instantiateComponent(target: HTMLElement) {
return mount(ReplicatorPaneComponent, {
target: target,
props: {
plugin: this.plugin,
},
});
}
}

View File

@@ -0,0 +1,402 @@
<script lang="ts">
import { getContext } from "svelte";
import { AcceptedStatus, ConnectionStatus, type PeerStatus } from "./P2PReplicatorPaneView";
import { Menu, Setting } from "obsidian";
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, Logger } from "octagonal-wheels/common/logger";
import type { P2PReplicator } from "../CmdP2PSync";
import { unique } from "../../../lib/src/common/utils";
import { REMOTE_P2P } from "src/lib/src/common/types";
interface Props {
peerStatus: PeerStatus;
}
let { peerStatus }: Props = $props();
let peer = $derived(peerStatus);
function select<T extends string | number | symbol, U>(d: T, cond: Record<T, U>): U;
function select<T extends string | number | symbol, U, V>(d: T, cond: Record<T, U>, def: V): U | V;
function select<T extends string | number | symbol, U>(d: T, cond: Record<T, U>, def?: U): U | undefined {
return d in cond ? cond[d] : def;
}
let statusChips = $derived.by(() =>
[
peer.isWatching ? ["WATCHING"] : [],
peer.isFetching ? ["FETCHING"] : [],
peer.isSending ? ["SENDING"] : [],
].flat()
);
let acceptedStatusChip = $derived.by(() =>
select(
peer.accepted.toString(),
{
[AcceptedStatus.ACCEPTED]: "ACCEPTED",
[AcceptedStatus.ACCEPTED_IN_SESSION]: "ACCEPTED (in session)",
[AcceptedStatus.DENIED_IN_SESSION]: "DENIED (in session)",
[AcceptedStatus.DENIED]: "DENIED",
[AcceptedStatus.UNKNOWN]: "NEW",
},
""
)
);
const classList = {
["SENDING"]: "connected",
["FETCHING"]: "connected",
["WATCHING"]: "connected-live",
["WAITING"]: "waiting",
["ACCEPTED"]: "accepted",
["DENIED"]: "denied",
["NEW"]: "unknown",
};
let isAccepted = $derived.by(
() => peer.accepted === AcceptedStatus.ACCEPTED || peer.accepted === AcceptedStatus.ACCEPTED_IN_SESSION
);
let isDenied = $derived.by(
() => peer.accepted === AcceptedStatus.DENIED || peer.accepted === AcceptedStatus.DENIED_IN_SESSION
);
let isNew = $derived.by(() => peer.accepted === AcceptedStatus.UNKNOWN);
function makeDecision(isAccepted: boolean, isTemporary: boolean) {
cmdReplicator._replicatorInstance?.server?.makeDecision({
peerId: peer.peerId,
name: peer.name,
decision: isAccepted,
isTemporary: isTemporary,
});
}
function revokeDecision() {
cmdReplicator._replicatorInstance?.server?.revokeDecision({
peerId: peer.peerId,
name: peer.name,
});
}
const cmdReplicator = getContext<() => P2PReplicator>("getReplicator")();
const replicator = cmdReplicator._replicatorInstance!;
const peerAttrLabels = $derived.by(() => {
const attrs = [];
if (peer.syncOnConnect) {
attrs.push("✔ SYNC");
}
if (peer.watchOnConnect) {
attrs.push("✔ WATCH");
}
if (peer.syncOnReplicationCommand) {
attrs.push("✔ SELECT");
}
return attrs;
});
function startWatching() {
replicator.watchPeer(peer.peerId);
}
function stopWatching() {
replicator.unwatchPeer(peer.peerId);
}
function replicateFrom() {
replicator.replicateFrom(peer.peerId);
}
function replicateTo() {
replicator.requestSynchroniseToPeer(peer.peerId);
}
function sync() {
replicator.sync(peer.peerId, false);
}
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(",");
}
function moreMenu(evt: MouseEvent) {
const m = new Menu()
.addItem((item) => item.setTitle("📥 Only Fetch").onClick(() => replicateFrom()))
.addItem((item) => item.setTitle("📤 Only Send").onClick(() => replicateTo()))
.addSeparator()
.addItem((item) => {
item.setTitle("🔧 Get Configuration").onClick(async () => {
Logger(
`Requesting remote config for ${peer.name}. Please input the passphrase on the remote device`,
LOG_LEVEL_NOTICE
);
const remoteConfig = await 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 replicator.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 replicator.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;
}
}
}
cmdReplicator.plugin.settings = remoteConfig;
await cmdReplicator.plugin.saveSettings();
// await cmdReplicator.setConfig("rebuildFrom", peer.name);
if (yn === DROP) {
await cmdReplicator.plugin.rebuilder.scheduleFetch();
} else {
await cmdReplicator.plugin.$$scheduleAppReload();
}
} else {
Logger(`Cancelled\nRemote config for ${peer.name} is not applied`, LOG_LEVEL_NOTICE);
}
} else {
Logger(`Cannot retrieve remote config for ${peer.peerId}`);
}
});
})
.addSeparator()
.addItem((item) => {
const mark = peer.syncOnConnect ? "checkmark" : null;
item.setTitle("Toggle Sync on connect")
.onClick(async () => {
// TODO: Fix to prevent writing to settings directly
if (peer.syncOnConnect) {
cmdReplicator.settings.P2P_AutoSyncPeers = removeFromList(
peer.name,
cmdReplicator.settings.P2P_AutoSyncPeers
);
await cmdReplicator.plugin.saveSettings();
} else {
cmdReplicator.settings.P2P_AutoSyncPeers = addToList(
peer.name,
cmdReplicator.settings.P2P_AutoSyncPeers
);
await cmdReplicator.plugin.saveSettings();
}
})
.setIcon(mark);
})
.addItem((item) => {
const mark = peer.watchOnConnect ? "checkmark" : null;
item.setTitle("Toggle Watch on connect")
.onClick(async () => {
// TODO: Fix to prevent writing to settings directly
if (peer.watchOnConnect) {
cmdReplicator.settings.P2P_AutoWatchPeers = removeFromList(
peer.name,
cmdReplicator.settings.P2P_AutoWatchPeers
);
await cmdReplicator.plugin.saveSettings();
} else {
cmdReplicator.settings.P2P_AutoWatchPeers = addToList(
peer.name,
cmdReplicator.settings.P2P_AutoWatchPeers
);
await cmdReplicator.plugin.saveSettings();
}
})
.setIcon(mark);
})
.addItem((item) => {
const mark = peer.syncOnReplicationCommand ? "checkmark" : null;
item.setTitle("Toggle Sync on `Replicate now` command")
.onClick(async () => {
// TODO: Fix to prevent writing to settings directly
if (peer.syncOnReplicationCommand) {
cmdReplicator.settings.P2P_SyncOnReplication = removeFromList(
peer.name,
cmdReplicator.settings.P2P_SyncOnReplication
);
await cmdReplicator.plugin.saveSettings();
} else {
cmdReplicator.settings.P2P_SyncOnReplication = addToList(
peer.name,
cmdReplicator.settings.P2P_SyncOnReplication
);
await cmdReplicator.plugin.saveSettings();
}
})
.setIcon(mark);
});
m.showAtPosition({ x: evt.x, y: evt.y });
}
</script>
<tr>
<td>
<div class="info">
<div class="row name">
<span class="peername">{peer.name}</span>
</div>
<div class="row peer-id">
<span class="peerid">({peer.peerId})</span>
</div>
</div>
<div class="status-chips">
<div class="row">
<span class="chip {select(acceptedStatusChip, classList)}">{acceptedStatusChip}</span>
</div>
{#if isAccepted}
<div class="row">
{#each statusChips as chip}
<span class="chip {select(chip, classList)}">{chip}</span>
{/each}
</div>
{/if}
<div class="row">
{#each peerAttrLabels as attr}
<span class="chip attr">{attr}</span>
{/each}
</div>
</div>
</td>
<td>
<div class="buttons">
<div class="row">
{#if isNew}
{#if !isAccepted}
<button class="button" onclick={() => makeDecision(true, true)}>Accept in session</button>
<button class="button mod-cta" onclick={() => makeDecision(true, false)}>Accept</button>
{/if}
{#if !isDenied}
<button class="button" onclick={() => makeDecision(false, true)}>Deny in session</button>
<button class="button mod-warning" onclick={() => makeDecision(false, false)}>Deny</button>
{/if}
{:else}
<button class="button mod-warning" onclick={() => revokeDecision()}>Revoke</button>
{/if}
</div>
</div>
</td>
<td>
{#if isAccepted}
<div class="buttons">
<div class="row">
<button class="button" onclick={sync} disabled={peer.isSending || peer.isFetching}>🔄</button>
<!-- <button class="button" onclick={replicateFrom} disabled={peer.isFetching}>📥</button>
<button class="button" onclick={replicateTo} disabled={peer.isSending}>📤</button> -->
{#if peer.isWatching}
<button class="button" onclick={stopWatching}>Stop ⚡</button>
{:else}
<button class="button" onclick={startWatching} title="live"></button>
{/if}
<button class="button" onclick={moreMenu}>...</button>
</div>
</div>
{/if}
</td>
</tr>
<style>
tr:nth-child(odd) {
background-color: var(--background-primary-alt);
}
.info {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: var(--size-4-1) var(--size-4-1);
}
.peer-id {
font-size: 0.8em;
}
.status-chips {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
/* min-width: 10em; */
}
.buttons {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.buttons .row {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
/* padding: var(--size-4-1) var(--size-4-1); */
}
.chip {
display: inline-block;
padding: 4px 8px;
margin: 4px;
border-radius: 4px;
font-size: 0.75em;
font-weight: bold;
background-color: var(--tag-background);
border: var(--tag-border-width) solid var(--tag-border-color);
}
.chip.connected {
background-color: var(--background-modifier-success);
color: var(--text-normal);
}
.chip.connected-live {
background-color: var(--background-modifier-success);
border-color: var(--background-modifier-success);
color: var(--text-normal);
}
.chip.accepted {
background-color: var(--background-modifier-success);
color: var(--text-normal);
}
.chip.waiting {
background-color: var(--background-secondary);
}
.chip.unknown {
background-color: var(--background-primary);
color: var(--text-warning);
}
.chip.denied {
background-color: var(--background-modifier-error);
color: var(--text-error);
}
.chip.attr {
background-color: var(--background-secondary);
}
.button {
margin: var(--size-4-1);
}
.button.affirmative {
background-color: var(--interactive-accent);
color: var(--text-normal);
}
.button.affirmative:hover {
background-color: var(--interactive-accent-hover);
}
.button.negative {
background-color: var(--background-modifier-error);
color: var(--text-error);
}
.button.negative:hover {
background-color: var(--background-modifier-error-hover);
}
</style>

Submodule src/lib updated: d94133d964...b8b05a2146

View File

@@ -51,7 +51,7 @@ import { ModuleRedFlag } from "./modules/coreFeatures/ModuleRedFlag.ts";
import { ModuleObsidianMenu } from "./modules/essentialObsidian/ModuleObsidianMenu.ts"; import { ModuleObsidianMenu } from "./modules/essentialObsidian/ModuleObsidianMenu.ts";
import { ModuleSetupObsidian } from "./modules/features/ModuleSetupObsidian.ts"; import { ModuleSetupObsidian } from "./modules/features/ModuleSetupObsidian.ts";
import type { StorageAccess } from "./modules/interfaces/StorageAccess.ts"; import type { StorageAccess } from "./modules/interfaces/StorageAccess.ts";
import type { Confirm } from "./modules/interfaces/Confirm.ts"; import type { Confirm } from "./lib/src/interfaces/Confirm.ts";
import type { Rebuilder } from "./modules/interfaces/DatabaseRebuilder.ts"; import type { Rebuilder } from "./modules/interfaces/DatabaseRebuilder.ts";
import type { DatabaseFileAccess } from "./modules/interfaces/DatabaseFileAccess.ts"; import type { DatabaseFileAccess } from "./modules/interfaces/DatabaseFileAccess.ts";
import { ModuleDatabaseFileAccess } from "./modules/core/ModuleDatabaseFileAccess.ts"; import { ModuleDatabaseFileAccess } from "./modules/core/ModuleDatabaseFileAccess.ts";
@@ -82,6 +82,7 @@ import { ModuleReplicateTest } from "./modules/extras/ModuleReplicateTest.ts";
import { ModuleLiveSyncMain } from "./modules/main/ModuleLiveSyncMain.ts"; import { ModuleLiveSyncMain } from "./modules/main/ModuleLiveSyncMain.ts";
import { ModuleExtraSyncObsidian } from "./modules/extraFeaturesObsidian/ModuleExtraSyncObsidian.ts"; import { ModuleExtraSyncObsidian } from "./modules/extraFeaturesObsidian/ModuleExtraSyncObsidian.ts";
import { LocalDatabaseMaintenance } from "./features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts"; import { LocalDatabaseMaintenance } from "./features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts";
import { P2PReplicator } from "./features/P2PSync/CmdP2PSync.ts";
function throwShouldBeOverridden(): never { function throwShouldBeOverridden(): never {
throw new Error("This function should be overridden by the module."); throw new Error("This function should be overridden by the module.");
@@ -118,7 +119,12 @@ export default class ObsidianLiveSyncPlugin
} }
// Keep order to display the dialogue in order. // Keep order to display the dialogue in order.
addOns = [new ConfigSync(this), new HiddenFileSync(this), new LocalDatabaseMaintenance(this)] as LiveSyncCommands[]; addOns = [
new ConfigSync(this),
new HiddenFileSync(this),
new LocalDatabaseMaintenance(this),
new P2PReplicator(this),
] as LiveSyncCommands[];
modules = [ modules = [
new ModuleLiveSyncMain(this), new ModuleLiveSyncMain(this),
@@ -399,6 +405,7 @@ export default class ObsidianLiveSyncPlugin
6. localDatabase.onunload 6. localDatabase.onunload
7. replicator.closeReplication 7. replicator.closeReplication
8. localDatabase.close 8. localDatabase.close
9. (event) EVENT_PLATFORM_UNLOADED
*/ */
@@ -538,8 +545,10 @@ export default class ObsidianLiveSyncPlugin
$everyAfterResumeProcess(): Promise<boolean> { $everyAfterResumeProcess(): Promise<boolean> {
return InterceptiveEvery; return InterceptiveEvery;
} }
$$checkAndAskResolvingMismatchedTweaks(preferred: Partial<TweakValues>): Promise<[TweakValues | boolean, boolean]> {
$$askResolvingMismatchedTweaks(): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> { throwShouldBeOverridden();
}
$$askResolvingMismatchedTweaks(preferredSource: TweakValues): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> {
throwShouldBeOverridden(); throwShouldBeOverridden();
} }
@@ -549,6 +558,12 @@ export default class ObsidianLiveSyncPlugin
throwShouldBeOverridden(); throwShouldBeOverridden();
} }
$$askUseRemoteConfiguration(
trialSetting: RemoteDBSettings,
preferred: TweakValues
): Promise<{ result: false | TweakValues; requireFetch: boolean }> {
throwShouldBeOverridden();
}
$everyBeforeReplicate(showMessage: boolean): Promise<boolean> { $everyBeforeReplicate(showMessage: boolean): Promise<boolean> {
return InterceptiveEvery; return InterceptiveEvery;
} }

View File

@@ -12,6 +12,7 @@ import type { Rebuilder } from "../interfaces/DatabaseRebuilder.ts";
import type { ICoreModule } from "../ModuleTypes.ts"; import type { ICoreModule } from "../ModuleTypes.ts";
import type { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator.ts"; import type { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator.ts";
import { fetchAllUsedChunks } from "../../lib/src/pouchdb/utils_couchdb.ts"; import { fetchAllUsedChunks } from "../../lib/src/pouchdb/utils_couchdb.ts";
import { EVENT_DATABASE_REBUILT, eventHub } from "src/common/events.ts";
export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebuilder { export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebuilder {
$everyOnload(): Promise<boolean> { $everyOnload(): Promise<boolean> {
@@ -214,6 +215,7 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
const suffix = (await this.core.$anyGetAppId()) || ""; const suffix = (await this.core.$anyGetAppId()) || "";
this.core.settings.additionalSuffixOfDatabaseName = suffix; this.core.settings.additionalSuffixOfDatabaseName = suffix;
await this.core.$$resetLocalDatabase(); await this.core.$$resetLocalDatabase();
eventHub.emitEvent(EVENT_DATABASE_REBUILT);
} }
async fetchRemoteChunks() { async fetchRemoteChunks() {
if ( if (

View File

@@ -84,8 +84,8 @@ export class ModuleReplicator extends AbstractModule implements ICoreModule {
//<-- Here could be an module. //<-- Here could be an module.
const ret = await this.core.replicator.openReplication(this.settings, false, showMessage, false); const ret = await this.core.replicator.openReplication(this.settings, false, showMessage, false);
if (!ret) { if (!ret) {
if (this.core.replicator.tweakSettingsMismatched) { if (this.core.replicator.tweakSettingsMismatched && this.core.replicator.preferredTweakValue) {
await this.core.$$askResolvingMismatchedTweaks(); await this.core.$$askResolvingMismatchedTweaks(this.core.replicator.preferredTweakValue);
} else { } else {
if (this.core.replicator?.remoteLockedAndDeviceNotAccepted) { if (this.core.replicator?.remoteLockedAndDeviceNotAccepted) {
if (this.core.replicator.remoteCleaned && this.settings.useIndexedDBAdapter) { if (this.core.replicator.remoteCleaned && this.settings.useIndexedDBAdapter) {
@@ -220,6 +220,11 @@ Or if you are sure know what had been happened, we can unlock the database from
const ids = [...new Set((await this.core.kvDB.get<string[]>(kvDBKey)) ?? [])]; const ids = [...new Set((await this.core.kvDB.get<string[]>(kvDBKey)) ?? [])];
const batchSize = 100; const batchSize = 100;
const chunkedIds = arrayToChunkedArray(ids, batchSize); const chunkedIds = arrayToChunkedArray(ids, batchSize);
// suspendParseReplicationResult is true, so we have to resume it if it is suspended.
if (this.replicationResultProcessor.isSuspended) {
this.replicationResultProcessor.resume();
}
for await (const idsBatch of chunkedIds) { for await (const idsBatch of chunkedIds) {
const ret = await this.localDatabase.allDocsRaw<EntryDoc>({ const ret = await this.localDatabase.allDocsRaw<EntryDoc>({
keys: idsBatch, keys: idsBatch,
@@ -233,8 +238,11 @@ Or if you are sure know what had been happened, we can unlock the database from
Logger(JSON.stringify(errors), LOG_LEVEL_VERBOSE); Logger(JSON.stringify(errors), LOG_LEVEL_VERBOSE);
} }
this.replicationResultProcessor.enqueueAll(docs); this.replicationResultProcessor.enqueueAll(docs);
await this.replicationResultProcessor.waitForAllProcessed();
} }
if (this.replicationResultProcessor.isSuspended) {
this.replicationResultProcessor.resume();
}
await this.replicationResultProcessor.waitForAllProcessed();
} }
replicationResultProcessor = new QueueProcessor( replicationResultProcessor = new QueueProcessor(

View File

@@ -1,5 +1,5 @@
import { fireAndForget } from "octagonal-wheels/promises"; import { fireAndForget } from "octagonal-wheels/promises";
import { REMOTE_MINIO, type RemoteDBSettings } from "../../lib/src/common/types"; import { REMOTE_MINIO, REMOTE_P2P, type RemoteDBSettings } from "../../lib/src/common/types";
import { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator"; import { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator";
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator"; import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
import { AbstractModule } from "../AbstractModule"; import { AbstractModule } from "../AbstractModule";
@@ -9,13 +9,13 @@ export class ModuleReplicatorCouchDB extends AbstractModule implements ICoreModu
$anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator> { $anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator> {
const settings = { ...this.settings, ...settingOverride }; const settings = { ...this.settings, ...settingOverride };
// If new remote types were added, add them here. Do not use `REMOTE_COUCHDB` directly for the safety valve. // If new remote types were added, add them here. Do not use `REMOTE_COUCHDB` directly for the safety valve.
if (settings.remoteType == REMOTE_MINIO) { if (settings.remoteType == REMOTE_MINIO || settings.remoteType == REMOTE_P2P) {
return undefined!; return undefined!;
} }
return Promise.resolve(new LiveSyncCouchDBReplicator(this.core)); return Promise.resolve(new LiveSyncCouchDBReplicator(this.core));
} }
$everyAfterResumeProcess(): Promise<boolean> { $everyAfterResumeProcess(): Promise<boolean> {
if (this.settings.remoteType != REMOTE_MINIO) { if (this.settings.remoteType != REMOTE_MINIO && this.settings.remoteType != REMOTE_P2P) {
// If LiveSync enabled, open replication // If LiveSync enabled, open replication
if (this.settings.liveSync) { if (this.settings.liveSync) {
fireAndForget(() => this.core.replicator.openReplication(this.settings, true, false, false)); fireAndForget(() => this.core.replicator.openReplication(this.settings, true, false, false));

View File

@@ -0,0 +1,30 @@
import { REMOTE_P2P, type RemoteDBSettings } from "../../lib/src/common/types";
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
import { AbstractModule } from "../AbstractModule";
import type { ICoreModule } from "../ModuleTypes";
import { LiveSyncTrysteroReplicator } from "../../lib/src/replication/trystero/LiveSyncTrysteroReplicator";
export class ModuleReplicatorP2P extends AbstractModule implements ICoreModule {
$anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator> {
const settings = { ...this.settings, ...settingOverride };
if (settings.remoteType == REMOTE_P2P) {
return Promise.resolve(new LiveSyncTrysteroReplicator(this.core));
}
return undefined!;
}
$everyAfterResumeProcess(): Promise<boolean> {
if (this.settings.remoteType == REMOTE_P2P) {
// // If LiveSync enabled, open replication
// if (this.settings.liveSync) {
// fireAndForget(() => this.core.replicator.openReplication(this.settings, true, false, false));
// }
// // If sync on start enabled, open replication
// if (!this.settings.liveSync && this.settings.syncOnStart) {
// // Possibly ok as if only share the result
// fireAndForget(() => this.core.replicator.openReplication(this.settings, false, false, false));
// }
}
return Promise.resolve(true);
}
}

View File

@@ -192,7 +192,7 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul
} }
return diff; return diff;
}); });
console.warn(mTimeAndRev); // console.warn(mTimeAndRev);
this._log( this._log(
`Resolving conflict by newest: ${filename} (Newest: ${new Date(mTimeAndRev[0][0]).toLocaleString()}) (${mTimeAndRev.length} revisions exists)` `Resolving conflict by newest: ${filename} (Newest: ${new Date(mTimeAndRev[0][0]).toLocaleString()}) (${mTimeAndRev.length} revisions exists)`
); );

View File

@@ -13,18 +13,18 @@ import type { ICoreModule } from "../ModuleTypes.ts";
export class ModuleResolvingMismatchedTweaks extends AbstractModule implements ICoreModule { export class ModuleResolvingMismatchedTweaks extends AbstractModule implements ICoreModule {
async $anyAfterConnectCheckFailed(): Promise<boolean | "CHECKAGAIN" | undefined> { async $anyAfterConnectCheckFailed(): Promise<boolean | "CHECKAGAIN" | undefined> {
if (!this.core.replicator.tweakSettingsMismatched) return false; if (!this.core.replicator.tweakSettingsMismatched && !this.core.replicator.preferredTweakValue) return false;
const ret = await this.core.$$askResolvingMismatchedTweaks(); const preferred = this.core.replicator.preferredTweakValue;
if (!preferred) return false;
const ret = await this.core.$$askResolvingMismatchedTweaks(preferred);
if (ret == "OK") return false; if (ret == "OK") return false;
if (ret == "CHECKAGAIN") return "CHECKAGAIN"; if (ret == "CHECKAGAIN") return "CHECKAGAIN";
if (ret == "IGNORE") return true; if (ret == "IGNORE") return true;
} }
async $$askResolvingMismatchedTweaks(): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> { async $$checkAndAskResolvingMismatchedTweaks(
if (!this.core.replicator.tweakSettingsMismatched) { preferred: Partial<TweakValues>
return "OK"; ): Promise<[TweakValues | boolean, boolean]> {
}
const preferred = extractObject(TweakValuesShouldMatchedTemplate, this.core.replicator.preferredTweakValue!);
const mine = extractObject(TweakValuesShouldMatchedTemplate, this.settings); const mine = extractObject(TweakValuesShouldMatchedTemplate, this.settings);
const items = Object.entries(TweakValuesShouldMatchedTemplate); const items = Object.entries(TweakValuesShouldMatchedTemplate);
let rebuildRequired = false; let rebuildRequired = false;
@@ -85,8 +85,22 @@ Please select which one you want to use.
CHOICE_DISMISS, CHOICE_DISMISS,
60 60
); );
if (!retKey) return "IGNORE"; if (!retKey) return [false, false];
const conf = CHOICES[retKey]; return [CHOICES[retKey], rebuildRequired];
}
async $$askResolvingMismatchedTweaks(): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> {
if (!this.core.replicator.tweakSettingsMismatched) {
return "OK";
}
const tweaks = this.core.replicator.preferredTweakValue;
if (!tweaks) {
return "IGNORE";
}
const preferred = extractObject(TweakValuesShouldMatchedTemplate, tweaks);
const [conf, rebuildRequired] = await this.core.$$checkAndAskResolvingMismatchedTweaks(preferred);
if (!conf) return "IGNORE";
if (conf === true) { if (conf === true) {
await this.core.replicator.setPreferredRemoteTweakSettings(this.settings); await this.core.replicator.setPreferredRemoteTweakSettings(this.settings);
@@ -119,6 +133,20 @@ Please select which one you want to use.
if (await replicator.tryConnectRemote(trialSetting)) { if (await replicator.tryConnectRemote(trialSetting)) {
const preferred = await replicator.getRemotePreferredTweakValues(trialSetting); const preferred = await replicator.getRemotePreferredTweakValues(trialSetting);
if (preferred) { if (preferred) {
return await this.$$askUseRemoteConfiguration(trialSetting, preferred);
} else {
this._log("Failed to get the preferred tweak values from the remote server.", LOG_LEVEL_NOTICE);
}
return { result: false, requireFetch: false };
} else {
this._log("Failed to connect to the remote server.", LOG_LEVEL_NOTICE);
return { result: false, requireFetch: false };
}
}
async $$askUseRemoteConfiguration(
trialSetting: RemoteDBSettings,
preferred: TweakValues
): Promise<{ result: false | TweakValues; requireFetch: boolean }> {
const items = Object.entries(TweakValuesShouldMatchedTemplate); const items = Object.entries(TweakValuesShouldMatchedTemplate);
let rebuildRequired = false; let rebuildRequired = false;
// Making tables: // Making tables:
@@ -141,10 +169,7 @@ Please select which one you want to use.
} }
if (differenceCount === 0) { if (differenceCount === 0) {
this._log( this._log("The settings in the remote database are the same as the local database.", LOG_LEVEL_NOTICE);
"The settings in the remote database are the same as the local database.",
LOG_LEVEL_NOTICE
);
return { result: false, requireFetch: false }; return { result: false, requireFetch: false };
} }
const additionalMessage = const additionalMessage =
@@ -184,13 +209,6 @@ ${additionalMessage}`;
if (retKey === CHOICE_USE_REMOTE) { if (retKey === CHOICE_USE_REMOTE) {
return { result: { ...trialSetting, ...preferred }, requireFetch: rebuildRequired }; return { result: { ...trialSetting, ...preferred }, requireFetch: rebuildRequired };
} }
} else {
this._log("Failed to get the preferred tweak values from the remote server.", LOG_LEVEL_NOTICE);
}
return { result: false, requireFetch: false };
} else {
this._log("Failed to connect to the remote server.", LOG_LEVEL_NOTICE);
return { result: false, requireFetch: false }; return { result: false, requireFetch: false };
} }
} }
}

View File

@@ -10,7 +10,8 @@ import {
confirmWithMessageWithWideButton, confirmWithMessageWithWideButton,
} from "./UILib/dialogs.ts"; } from "./UILib/dialogs.ts";
import { Notice } from "../../deps.ts"; import { Notice } from "../../deps.ts";
import type { Confirm } from "../interfaces/Confirm.ts"; import type { Confirm } from "../../lib/src/interfaces/Confirm.ts";
import { setConfirmInstance } from "../../lib/src/PlatformAPIs/obsidian/Confirm.ts";
import { $msg } from "src/lib/src/common/i18n.ts"; import { $msg } from "src/lib/src/common/i18n.ts";
// This module cannot be a common module because it depends on Obsidian's API. // This module cannot be a common module because it depends on Obsidian's API.
@@ -19,20 +20,20 @@ import { $msg } from "src/lib/src/common/i18n.ts";
export class ModuleInputUIObsidian extends AbstractObsidianModule implements IObsidianModule, Confirm { export class ModuleInputUIObsidian extends AbstractObsidianModule implements IObsidianModule, Confirm {
$everyOnload(): Promise<boolean> { $everyOnload(): Promise<boolean> {
this.core.confirm = this; this.core.confirm = this;
setConfirmInstance(this);
return Promise.resolve(true); return Promise.resolve(true);
} }
askYesNo(message: string): Promise<"yes" | "no"> { askYesNo(message: string): Promise<"yes" | "no"> {
return askYesNo(this.app, message); return askYesNo(this.app, message);
} }
askString(title: string, key: string, placeholder: string, isPassword: boolean = false): Promise<string | false> { askString(title: string, key: string, placeholder: string, isPassword: boolean = false): Promise<string | false> {
return askString(this.app, title, key, placeholder, isPassword); return askString(this.app, title, key, placeholder, isPassword);
} }
async askYesNoDialog( async askYesNoDialog(
message: string, message: string,
opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number } = {} opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number } = { title: "Confirmation" }
): Promise<"yes" | "no"> { ): Promise<"yes" | "no"> {
const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleConfirmation"); const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleConfirmation");
const yesLabel = $msg("moduleInputUIObsidian.optionYes"); const yesLabel = $msg("moduleInputUIObsidian.optionYes");
@@ -53,11 +54,11 @@ export class ModuleInputUIObsidian extends AbstractObsidianModule implements IOb
return askSelectString(this.app, message, items); return askSelectString(this.app, message, items);
} }
askSelectStringDialogue( askSelectStringDialogue<T extends readonly string[]>(
message: string, message: string,
buttons: string[], buttons: T,
opt: { title?: string; defaultAction: (typeof buttons)[number]; timeout?: number } opt: { title?: string; defaultAction: T[number]; timeout?: number }
): Promise<(typeof buttons)[number] | false> { ): Promise<T[number] | false> {
const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleSelect"); const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleSelect");
return confirmWithMessageWithWideButton( return confirmWithMessageWithWideButton(
this.plugin, this.plugin,

View File

@@ -1,25 +1,20 @@
import { ButtonComponent } from "obsidian"; import { ButtonComponent } from "obsidian";
import { App, FuzzySuggestModal, MarkdownRenderer, Modal, Plugin, Setting } from "../../../deps.ts"; import { App, FuzzySuggestModal, MarkdownRenderer, Modal, Plugin, Setting } from "../../../deps.ts";
import { EVENT_PLUGIN_UNLOADED, eventHub } from "../../../common/events.ts"; import { EVENT_PLUGIN_UNLOADED, eventHub } from "../../../common/events.ts";
import { delay } from "octagonal-wheels/promises";
class AutoClosableModal extends Modal { class AutoClosableModal extends Modal {
removeEvent: (() => void) | undefined; _closeByUnload() {
eventHub.off(EVENT_PLUGIN_UNLOADED, this._closeByUnload);
this.close();
}
constructor(app: App) { constructor(app: App) {
super(app); super(app);
this.removeEvent = eventHub.onEvent(EVENT_PLUGIN_UNLOADED, async () => { this._closeByUnload = this._closeByUnload.bind(this);
await delay(100); eventHub.once(EVENT_PLUGIN_UNLOADED, this._closeByUnload);
if (!this.removeEvent) return;
this.close();
this.removeEvent = undefined;
});
} }
onClose() { onClose() {
if (this.removeEvent) { eventHub.off(EVENT_PLUGIN_UNLOADED, this._closeByUnload);
this.removeEvent();
this.removeEvent = undefined;
}
} }
} }
@@ -135,11 +130,11 @@ export class PopoverSelectString extends FuzzySuggestModal<string> {
} }
} }
export class MessageBox extends AutoClosableModal { export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
plugin: Plugin; plugin: Plugin;
title: string; title: string;
contentMd: string; contentMd: string;
buttons: string[]; buttons: T;
result: string | false = false; result: string | false = false;
isManuallyClosed = false; isManuallyClosed = false;
defaultAction: string | undefined; defaultAction: string | undefined;
@@ -154,11 +149,11 @@ export class MessageBox extends AutoClosableModal {
plugin: Plugin, plugin: Plugin,
title: string, title: string,
contentMd: string, contentMd: string,
buttons: string[], buttons: T,
defaultAction: (typeof buttons)[number], defaultAction: T[number],
timeout: number | undefined, timeout: number | undefined,
wideButton: boolean, wideButton: boolean,
onSubmit: (result: (typeof buttons)[number] | false) => void onSubmit: (result: T[number] | false) => void
) { ) {
super(plugin.app); super(plugin.app);
this.plugin = plugin; this.plugin = plugin;
@@ -194,6 +189,7 @@ export class MessageBox extends AutoClosableModal {
this.titleEl.setText(this.title); this.titleEl.setText(this.title);
const div = contentEl.createDiv(); const div = contentEl.createDiv();
div.style.userSelect = "text"; div.style.userSelect = "text";
div.style["webkitUserSelect"] = "text";
void MarkdownRenderer.render(this.plugin.app, this.contentMd, div, "/", this.plugin); void MarkdownRenderer.render(this.plugin.app, this.contentMd, div, "/", this.plugin);
const buttonSetting = new Setting(contentEl); const buttonSetting = new Setting(contentEl);
const labelWrapper = contentEl.createDiv(); const labelWrapper = contentEl.createDiv();
@@ -262,14 +258,14 @@ export class MessageBox extends AutoClosableModal {
} }
} }
export function confirmWithMessage( export function confirmWithMessage<T extends readonly string[]>(
plugin: Plugin, plugin: Plugin,
title: string, title: string,
contentMd: string, contentMd: string,
buttons: string[], buttons: T,
defaultAction: (typeof buttons)[number], defaultAction: T[number],
timeout?: number timeout?: number
): Promise<(typeof buttons)[number] | false> { ): Promise<T[number] | false> {
return new Promise((res) => { return new Promise((res) => {
const dialog = new MessageBox(plugin, title, contentMd, buttons, defaultAction, timeout, false, (result) => const dialog = new MessageBox(plugin, title, contentMd, buttons, defaultAction, timeout, false, (result) =>
res(result) res(result)
@@ -277,14 +273,14 @@ export function confirmWithMessage(
dialog.open(); dialog.open();
}); });
} }
export function confirmWithMessageWithWideButton( export function confirmWithMessageWithWideButton<T extends readonly string[]>(
plugin: Plugin, plugin: Plugin,
title: string, title: string,
contentMd: string, contentMd: string,
buttons: string[], buttons: T,
defaultAction: (typeof buttons)[number], defaultAction: T[number],
timeout?: number timeout?: number
): Promise<(typeof buttons)[number] | false> { ): Promise<T[number] | false> {
return new Promise((res) => { return new Promise((res) => {
const dialog = new MessageBox(plugin, title, contentMd, buttons, defaultAction, timeout, true, (result) => const dialog = new MessageBox(plugin, title, contentMd, buttons, defaultAction, timeout, true, (result) =>
res(result) res(result)

View File

@@ -1,6 +1,7 @@
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger.js"; import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger.js";
import { SETTING_VERSION_SUPPORT_CASE_INSENSITIVE } from "../../lib/src/common/types.js"; import { SETTING_VERSION_SUPPORT_CASE_INSENSITIVE } from "../../lib/src/common/types.js";
import { import {
EVENT_REQUEST_OPEN_P2P,
EVENT_REQUEST_OPEN_SETTING_WIZARD, EVENT_REQUEST_OPEN_SETTING_WIZARD,
EVENT_REQUEST_OPEN_SETTINGS, EVENT_REQUEST_OPEN_SETTINGS,
EVENT_REQUEST_OPEN_SETUP_URI, EVENT_REQUEST_OPEN_SETUP_URI,
@@ -194,10 +195,11 @@ export class ModuleMigration extends AbstractModule implements ICoreModule {
async askAgainForSetupURI() { async askAgainForSetupURI() {
const message = $msg("moduleMigration.msgRecommendSetupUri", { URI_DOC: $msg("moduleMigration.docUri") }); const message = $msg("moduleMigration.msgRecommendSetupUri", { URI_DOC: $msg("moduleMigration.docUri") });
const USE_MINIMAL = $msg("moduleMigration.optionSetupWizard"); const USE_MINIMAL = $msg("moduleMigration.optionSetupWizard");
const USE_P2P = $msg("moduleMigration.optionSetupViaP2P");
const USE_SETUP = $msg("moduleMigration.optionManualSetup"); const USE_SETUP = $msg("moduleMigration.optionManualSetup");
const NEXT = $msg("moduleMigration.optionRemindNextLaunch"); const NEXT = $msg("moduleMigration.optionRemindNextLaunch");
const ret = await this.core.confirm.askSelectStringDialogue(message, [USE_MINIMAL, USE_SETUP, NEXT], { const ret = await this.core.confirm.askSelectStringDialogue(message, [USE_MINIMAL, USE_SETUP, USE_P2P, NEXT], {
title: $msg("moduleMigration.titleRecommendSetupUri"), title: $msg("moduleMigration.titleRecommendSetupUri"),
defaultAction: USE_MINIMAL, defaultAction: USE_MINIMAL,
}); });
@@ -205,6 +207,10 @@ export class ModuleMigration extends AbstractModule implements ICoreModule {
eventHub.emitEvent(EVENT_REQUEST_OPEN_SETTING_WIZARD); eventHub.emitEvent(EVENT_REQUEST_OPEN_SETTING_WIZARD);
return false; return false;
} }
if (ret === USE_P2P) {
eventHub.emitEvent(EVENT_REQUEST_OPEN_P2P);
return false;
}
if (ret === USE_SETUP) { if (ret === USE_SETUP) {
eventHub.emitEvent(EVENT_REQUEST_OPEN_SETTINGS); eventHub.emitEvent(EVENT_REQUEST_OPEN_SETTINGS);
return false; return false;

View File

@@ -124,7 +124,7 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid
}); });
} }
if (leaves.length > 0) { if (leaves.length > 0) {
this.app.workspace.revealLeaf(leaves[0]); await this.app.workspace.revealLeaf(leaves[0]);
} }
} }
} }

View File

@@ -2,7 +2,7 @@ import { delay } from "octagonal-wheels/promises";
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts"; import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger"; import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
import { eventHub } from "../../common/events"; import { eventHub } from "../../common/events";
import { webcrypto } from "crypto"; import { getWebCrypto } from "../../lib/src/mods.ts";
import { uint8ArrayToHexString } from "octagonal-wheels/binary/hex"; import { uint8ArrayToHexString } from "octagonal-wheels/binary/hex";
import { parseYaml, requestUrl, stringifyYaml } from "obsidian"; import { parseYaml, requestUrl, stringifyYaml } from "obsidian";
import type { FilePath } from "../../lib/src/common/types.ts"; import type { FilePath } from "../../lib/src/common/types.ts";
@@ -162,6 +162,7 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
async _dumpFileList(outFile?: string) { async _dumpFileList(outFile?: string) {
const files = this.core.storageAccess.getFiles(); const files = this.core.storageAccess.getFiles();
const out = [] as any[]; const out = [] as any[];
const webcrypto = await getWebCrypto();
for (const file of files) { for (const file of files) {
if (!(await this.core.$$isTargetFile(file.path))) { if (!(await this.core.$$isTargetFile(file.path))) {
continue; continue;
@@ -202,7 +203,8 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
.map((e) => new RegExp(e, "i")); .map((e) => new RegExp(e, "i"));
const out = [] as any[]; const out = [] as any[];
const files = await this.core.storageAccess.getFilesIncludeHidden("", undefined, ignorePatterns); const files = await this.core.storageAccess.getFilesIncludeHidden("", undefined, ignorePatterns);
console.dir(files); // console.dir(files);
const webcrypto = await getWebCrypto();
for (const file of files) { for (const file of files) {
// if (!await this.core.$$isTargetFile(file)) { // if (!await this.core.$$isTargetFile(file)) {
// continue; // continue;

View File

@@ -66,7 +66,10 @@
for (const revInfo of reversedRevs) { for (const revInfo of reversedRevs) {
if (revInfo.status == "available") { if (revInfo.status == "available") {
const doc = (!isPlain && showDiffInfo) || (checkStorageDiff && revInfo.rev == docA._rev) ? await db.getDBEntry(path, { rev: revInfo.rev }, false, false, true) : await db.getDBEntryMeta(path, { rev: revInfo.rev }, true); const doc =
(!isPlain && showDiffInfo) || (checkStorageDiff && revInfo.rev == docA._rev)
? await db.getDBEntry(path, { rev: revInfo.rev }, false, false, true)
: await db.getDBEntryMeta(path, { rev: revInfo.rev }, true);
if (doc === false) continue; if (doc === false) continue;
const rev = revInfo.rev; const rev = revInfo.rev;
@@ -94,7 +97,10 @@
[DIFF_EQUAL]: 0, [DIFF_EQUAL]: 0,
[DIFF_INSERT]: 0, [DIFF_INSERT]: 0,
} as { [key: number]: number }; } as { [key: number]: number };
const px = diff.reduce((p, c) => ({ ...p, [c[0]]: (p[c[0]] ?? 0) + c[1].length }), pxInit); const px = diff.reduce(
(p, c) => ({ ...p, [c[0]]: (p[c[0]] ?? 0) + c[1].length }),
pxInit
);
diffDetail = `-${px[DIFF_DELETE]}, +${px[DIFF_INSERT]}`; diffDetail = `-${px[DIFF_DELETE]}, +${px[DIFF_INSERT]}`;
} }
} }
@@ -104,9 +110,13 @@
} }
if (rev == docA._rev) { if (rev == docA._rev) {
if (checkStorageDiff) { if (checkStorageDiff) {
const isExist = await plugin.storageAccess.isExistsIncludeHidden(stripAllPrefixes(getPath(docA))); const isExist = await plugin.storageAccess.isExistsIncludeHidden(
stripAllPrefixes(getPath(docA))
);
if (isExist) { if (isExist) {
const data = await plugin.storageAccess.readHiddenFileBinary(stripAllPrefixes(getPath(docA))); const data = await plugin.storageAccess.readHiddenFileBinary(
stripAllPrefixes(getPath(docA))
);
const d = readAsBlob(doc); const d = readAsBlob(doc);
const result = await isDocContentSame(data, d); const result = await isDocContentSame(data, d);
if (result) { if (result) {
@@ -187,19 +197,28 @@
<div class="globalhistory"> <div class="globalhistory">
<h1>Vault history</h1> <h1>Vault history</h1>
<div class="control"> <div class="control">
<div class="row"><label for="">From:</label><input type="date" bind:value={dispDateFrom} disabled={loading} /></div> <div class="row">
<label for="">From:</label><input type="date" bind:value={dispDateFrom} disabled={loading} />
</div>
<div class="row"><label for="">To:</label><input type="date" bind:value={dispDateTo} disabled={loading} /></div> <div class="row"><label for="">To:</label><input type="date" bind:value={dispDateTo} disabled={loading} /></div>
<div class="row"> <div class="row">
<label for="">Info:</label> <label for="">Info:</label>
<label><input type="checkbox" bind:checked={showDiffInfo} disabled={loading} /><span>Diff</span></label> <label><input type="checkbox" bind:checked={showDiffInfo} disabled={loading} /><span>Diff</span></label>
<label><input type="checkbox" bind:checked={showChunkCorrected} disabled={loading} /><span>Chunks</span></label> <label
<label><input type="checkbox" bind:checked={checkStorageDiff} disabled={loading} /><span>File integrity</span></label> ><input type="checkbox" bind:checked={showChunkCorrected} disabled={loading} /><span>Chunks</span
></label
>
<label
><input type="checkbox" bind:checked={checkStorageDiff} disabled={loading} /><span>File integrity</span
></label
>
</div> </div>
</div> </div>
{#if loading} {#if loading}
<div class="">Gathering information...</div> <div class="">Gathering information...</div>
{/if} {/if}
<table> <table>
<tbody>
<tr> <tr>
<th> Date </th> <th> Date </th>
<th> Path </th> <th> Path </th>
@@ -212,7 +231,7 @@
<tr> <tr>
<td colspan="5" class="more"> <td colspan="5" class="more">
{#if loading} {#if loading}
<div class="" /> <div class=""></div>
{:else} {:else}
<div><button on:click={() => nextWeek()}>+1 week</button></div> <div><button on:click={() => nextWeek()}>+1 week</button></div>
{/if} {/if}
@@ -257,12 +276,13 @@
<tr> <tr>
<td colspan="5" class="more"> <td colspan="5" class="more">
{#if loading} {#if loading}
<div class="" /> <div class=""></div>
{:else} {:else}
<div><button on:click={() => prevWeek()}>+1 week</button></div> <div><button on:click={() => prevWeek()}>+1 week</button></div>
{/if} {/if}
</td> </td>
</tr> </tr>
</tbody>
</table> </table>
</div> </div>

View File

@@ -1,10 +1,20 @@
import { ItemView, WorkspaceLeaf } from "../../../deps.ts"; import { WorkspaceLeaf } from "../../../deps.ts";
import GlobalHistoryComponent from "./GlobalHistory.svelte"; import GlobalHistoryComponent from "./GlobalHistory.svelte";
import type ObsidianLiveSyncPlugin from "../../../main.ts"; import type ObsidianLiveSyncPlugin from "../../../main.ts";
import { SvelteItemView } from "../../../common/SvelteItemView.ts";
import { mount } from "svelte";
export const VIEW_TYPE_GLOBAL_HISTORY = "global-history"; export const VIEW_TYPE_GLOBAL_HISTORY = "global-history";
export class GlobalHistoryView extends ItemView { export class GlobalHistoryView extends SvelteItemView {
component?: GlobalHistoryComponent; instantiateComponent(target: HTMLElement) {
return mount(GlobalHistoryComponent, {
target: target,
props: {
plugin: this.plugin,
},
});
}
plugin: ObsidianLiveSyncPlugin; plugin: ObsidianLiveSyncPlugin;
icon = "clock"; icon = "clock";
title: string = ""; title: string = "";
@@ -26,19 +36,4 @@ export class GlobalHistoryView extends ItemView {
getDisplayText() { getDisplayText() {
return "Vault history"; return "Vault history";
} }
async onOpen() {
this.component = new GlobalHistoryComponent({
target: this.contentEl,
props: {
plugin: this.plugin,
},
});
await Promise.resolve();
}
async onClose() {
this.component?.$destroy();
await Promise.resolve();
}
} }

View File

@@ -6,10 +6,16 @@
import { $msg as msg, currentLang as lang } from "../../../lib/src/common/i18n.ts"; import { $msg as msg, currentLang as lang } from "../../../lib/src/common/i18n.ts";
let unsubscribe: () => void; let unsubscribe: () => void;
let messages = [] as string[]; let messages = $state([] as string[]);
let wrapRight = false; let wrapRight = $state(false);
let autoScroll = true; let autoScroll = $state(true);
let suspended = false; let suspended = $state(false);
type Props = {
close: () => void;
};
let { close }: Props = $props();
// export let close: () => void;
function updateLog(logs: ReactiveInstance<string[]>) { function updateLog(logs: ReactiveInstance<string[]>) {
const e = logs.value; const e = logs.value;
if (!suspended) { if (!suspended) {
@@ -29,6 +35,9 @@
if (unsubscribe) unsubscribe(); if (unsubscribe) unsubscribe();
}); });
let scroll: HTMLDivElement; let scroll: HTMLDivElement;
function closeDialogue() {
close();
}
</script> </script>
<div class="logpane"> <div class="logpane">
@@ -47,6 +56,8 @@
<input type="checkbox" bind:checked={suspended} /> <input type="checkbox" bind:checked={suspended} />
<span>{msg("logPane.pause", {}, lang)}</span> <span>{msg("logPane.pause", {}, lang)}</span>
</label> </label>
<span class="spacer"></span>
<button onclick={() => closeDialogue()}>Close</button>
</div> </div>
</div> </div>
<div class="log" bind:this={scroll}> <div class="log" bind:this={scroll}>
@@ -68,6 +79,7 @@
.log { .log {
overflow-y: scroll; overflow-y: scroll;
user-select: text; user-select: text;
-webkit-user-select: text;
padding-bottom: 2em; padding-bottom: 2em;
} }
.log > pre { .log > pre {

View File

@@ -1,15 +1,27 @@
import { ItemView, WorkspaceLeaf } from "obsidian"; import { WorkspaceLeaf } from "obsidian";
import LogPaneComponent from "./LogPane.svelte"; import LogPaneComponent from "./LogPane.svelte";
import type ObsidianLiveSyncPlugin from "../../../main.ts"; import type ObsidianLiveSyncPlugin from "../../../main.ts";
import { SvelteItemView } from "../../../common/SvelteItemView.ts";
import { $msg } from "src/lib/src/common/i18n.ts"; import { $msg } from "src/lib/src/common/i18n.ts";
import { mount } from "svelte";
export const VIEW_TYPE_LOG = "log-log"; export const VIEW_TYPE_LOG = "log-log";
//Log view //Log view
export class LogPaneView extends ItemView { export class LogPaneView extends SvelteItemView {
component?: LogPaneComponent; instantiateComponent(target: HTMLElement) {
return mount(LogPaneComponent, {
target: target,
props: {
close: () => {
this.leaf.detach();
},
},
});
}
plugin: ObsidianLiveSyncPlugin; plugin: ObsidianLiveSyncPlugin;
icon = "view-log"; icon = "view-log";
title: string = ""; title: string = "";
navigation = true; navigation = false;
getIcon(): string { getIcon(): string {
return "view-log"; return "view-log";
@@ -28,17 +40,4 @@ export class LogPaneView extends ItemView {
// TODO: This function is not reactive and does not update the title based on the current language // TODO: This function is not reactive and does not update the title based on the current language
return $msg("logPane.title"); return $msg("logPane.title");
} }
async onOpen() {
this.component = new LogPaneComponent({
target: this.contentEl,
props: {},
});
await Promise.resolve();
}
async onClose() {
this.component?.$destroy();
await Promise.resolve();
}
} }

View File

@@ -27,6 +27,14 @@ import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
import { LogPaneView, VIEW_TYPE_LOG } from "./Log/LogPaneView.ts"; import { LogPaneView, VIEW_TYPE_LOG } from "./Log/LogPaneView.ts";
import { serialized } from "octagonal-wheels/concurrency/lock"; import { serialized } from "octagonal-wheels/concurrency/lock";
import { $msg } from "src/lib/src/common/i18n.ts"; import { $msg } from "src/lib/src/common/i18n.ts";
import type { P2PReplicationProgress } from "../../lib/src/replication/trystero/TrysteroReplicator.ts";
import {
EVENT_ADVERTISEMENT_RECEIVED,
EVENT_DEVICE_LEAVED,
EVENT_P2P_CONNECTED,
EVENT_P2P_DISCONNECTED,
EVENT_P2P_REPLICATOR_PROGRESS,
} from "src/lib/src/replication/trystero/TrysteroReplicatorP2PServer.ts";
// This module cannot be a core module because it depends on the Obsidian UI. // This module cannot be a core module because it depends on the Obsidian UI.
@@ -168,10 +176,12 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
const queued = queueCountLabel(); const queued = queueCountLabel();
const waiting = waitingLabel(); const waiting = waitingLabel();
const networkActivity = requestingStatLabel(); const networkActivity = requestingStatLabel();
const p2p = this.p2pReplicationLine.value;
return { return {
message: `${networkActivity}Sync: ${w}${sent}${pushLast}${arrived}${pullLast}${waiting}${queued}`, message: `${networkActivity}Sync: ${w}${sent}${pushLast}${arrived}${pullLast}${waiting}${queued}${p2p == "" ? "" : "\n" + p2p}`,
}; };
}); });
const statusBarLabels = reactive(() => { const statusBarLabels = reactive(() => {
const scheduleMessage = this.core.$$isReloadingScheduled() const scheduleMessage = this.core.$$isReloadingScheduled()
? `WARNING! RESTARTING OBSIDIAN IS SCHEDULED!\n` ? `WARNING! RESTARTING OBSIDIAN IS SCHEDULED!\n`
@@ -193,9 +203,90 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
statusBarLabels.onChanged((label) => applyToDisplay(label.value)); statusBarLabels.onChanged((label) => applyToDisplay(label.value));
} }
p2pReplicationResult = new Map<string, P2PReplicationProgress>();
updateP2PReplicationLine() {
const p2pReplicationResultX = [...this.p2pReplicationResult.values()].sort((a, b) =>
a.peerId.localeCompare(b.peerId)
);
const renderProgress = (current: number, max: number) => {
if (current == max) return `${current}`;
return `${current} (${max})`;
};
const line = p2pReplicationResultX
.map(
(e) =>
`${e.fetching.isActive || e.sending.isActive ? "⚡" : "💤"} ${e.peerName}${renderProgress(e.sending.current, e.sending.max)}${renderProgress(e.fetching.current, e.fetching.max)} `
)
.join("\n");
this.p2pReplicationLine.value = line;
}
// p2pReplicationResultX = reactiveSource([] as P2PReplicationProgress[]);
p2pReplicationLine = reactiveSource("");
$everyOnload(): Promise<boolean> { $everyOnload(): Promise<boolean> {
eventHub.onEvent(EVENT_LEAF_ACTIVE_CHANGED, () => this.onActiveLeafChange()); eventHub.onEvent(EVENT_LEAF_ACTIVE_CHANGED, () => this.onActiveLeafChange());
eventHub.onceEvent(EVENT_LAYOUT_READY, () => this.onActiveLeafChange()); eventHub.onceEvent(EVENT_LAYOUT_READY, () => this.onActiveLeafChange());
eventHub.onEvent(EVENT_ADVERTISEMENT_RECEIVED, (data) => {
this.p2pReplicationResult.set(data.peerId, {
peerId: data.peerId,
peerName: data.name,
fetching: {
current: 0,
max: 0,
isActive: false,
},
sending: {
current: 0,
max: 0,
isActive: false,
},
});
this.updateP2PReplicationLine();
});
eventHub.onEvent(EVENT_P2P_CONNECTED, () => {
this.p2pReplicationResult.clear();
this.updateP2PReplicationLine();
});
eventHub.onEvent(EVENT_P2P_DISCONNECTED, () => {
this.p2pReplicationResult.clear();
this.updateP2PReplicationLine();
});
eventHub.onEvent(EVENT_DEVICE_LEAVED, (peerId) => {
this.p2pReplicationResult.delete(peerId);
this.updateP2PReplicationLine();
});
eventHub.onEvent(EVENT_P2P_REPLICATOR_PROGRESS, (data) => {
const prev = this.p2pReplicationResult.get(data.peerId) || {
peerId: data.peerId,
peerName: data.peerName,
fetching: {
current: 0,
max: 0,
isActive: false,
},
sending: {
current: 0,
max: 0,
isActive: false,
},
};
if ("fetching" in data) {
if (data.fetching.isActive) {
prev.fetching = data.fetching;
} else {
prev.fetching.isActive = false;
}
}
if ("sending" in data) {
if (data.sending.isActive) {
prev.sending = data.sending;
} else {
prev.sending.isActive = false;
}
}
this.p2pReplicationResult.set(data.peerId, prev);
this.updateP2PReplicationLine();
});
return Promise.resolve(true); return Promise.resolve(true);
} }
adjustStatusDivPosition() { adjustStatusDivPosition() {

View File

@@ -25,6 +25,7 @@ import {
LEVEL_EDGE_CASE, LEVEL_EDGE_CASE,
type MetaEntry, type MetaEntry,
type FilePath, type FilePath,
REMOTE_P2P,
} from "../../../lib/src/common/types.ts"; } from "../../../lib/src/common/types.ts";
import { import {
createBlob, createBlob,
@@ -78,6 +79,7 @@ import { ICHeader, ICXHeader, PSCHeader } from "../../../common/types.ts";
import { HiddenFileSync } from "../../../features/HiddenFileSync/CmdHiddenFileSync.ts"; import { HiddenFileSync } from "../../../features/HiddenFileSync/CmdHiddenFileSync.ts";
import { EVENT_REQUEST_SHOW_HISTORY } from "../../../common/obsidianEvents.ts"; import { EVENT_REQUEST_SHOW_HISTORY } from "../../../common/obsidianEvents.ts";
import { LocalDatabaseMaintenance } from "../../../features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts"; import { LocalDatabaseMaintenance } from "../../../features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts";
import { mount } from "svelte";
export type OnUpdateResult = { export type OnUpdateResult = {
visibility?: boolean; visibility?: boolean;
@@ -811,6 +813,12 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
return false; return false;
}; };
const enableOnlySyncDisabled = enableOnly(() => !isAnySyncEnabled()); const enableOnlySyncDisabled = enableOnly(() => !isAnySyncEnabled());
const onlyOnP2POrCouchDB = () =>
({
visibility:
this.isConfiguredAs("remoteType", REMOTE_P2P) || this.isConfiguredAs("remoteType", REMOTE_COUCHDB),
}) as OnUpdateResult;
const onlyOnCouchDB = () => const onlyOnCouchDB = () =>
({ ({
visibility: this.isConfiguredAs("remoteType", REMOTE_COUCHDB), visibility: this.isConfiguredAs("remoteType", REMOTE_COUCHDB),
@@ -819,7 +827,16 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
({ ({
visibility: this.isConfiguredAs("remoteType", REMOTE_MINIO), visibility: this.isConfiguredAs("remoteType", REMOTE_MINIO),
}) as OnUpdateResult; }) as OnUpdateResult;
const onlyOnOnlyP2P = () =>
({
visibility: this.isConfiguredAs("remoteType", REMOTE_P2P),
}) as OnUpdateResult;
const onlyOnCouchDBOrMinIO = () =>
({
visibility:
this.isConfiguredAs("remoteType", REMOTE_COUCHDB) ||
this.isConfiguredAs("remoteType", REMOTE_MINIO),
}) as OnUpdateResult;
// E2EE Function // E2EE Function
const checkWorkingPassphrase = async (): Promise<boolean> => { const checkWorkingPassphrase = async (): Promise<boolean> => {
if (this.editingSettings.remoteType == REMOTE_MINIO) return true; if (this.editingSettings.remoteType == REMOTE_MINIO) return true;
@@ -1364,10 +1381,35 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
options: { options: {
[REMOTE_COUCHDB]: $msg("obsidianLiveSyncSettingTab.optionCouchDB"), [REMOTE_COUCHDB]: $msg("obsidianLiveSyncSettingTab.optionCouchDB"),
[REMOTE_MINIO]: $msg("obsidianLiveSyncSettingTab.optionMinioS3R2"), [REMOTE_MINIO]: $msg("obsidianLiveSyncSettingTab.optionMinioS3R2"),
[REMOTE_P2P]: "Only Peer-to-Peer",
}, },
onUpdate: enableOnlySyncDisabled, onUpdate: enableOnlySyncDisabled,
}); });
void addPanel(paneEl, "Peer-to-Peer", undefined, onlyOnOnlyP2P).then((paneEl) => {
const syncWarnP2P = this.createEl(paneEl, "div", {
text: "",
});
const p2pMessage = `This feature is a Work In Progress, and configurable on \`P2P Replicator\` Pane.
The pane also can be launched by \`P2P Replicator\` command from the Command Palette.
`;
void MarkdownRenderer.render(this.plugin.app, p2pMessage, syncWarnP2P, "/", this.plugin);
syncWarnP2P.addClass("op-warn-info");
new Setting(paneEl)
.setName("Apply Settings")
.setClass("wizardHidden")
.addApplyButton(["remoteType"]);
// .addOnUpdate(onlyOnMinIO);
// new Setting(paneEl).addButton((button) =>
// button
// .setButtonText("Open P2P Replicator")
// .onClick(() => {
// const addOn = this.plugin.getAddOn<P2PReplicator>(P2PReplicator.name);
// void addOn?.openPane();
// this.closeSetting();
// })
// );
});
void addPanel( void addPanel(
paneEl, paneEl,
$msg("obsidianLiveSyncSettingTab.titleMinioS3R2"), $msg("obsidianLiveSyncSettingTab.titleMinioS3R2"),
@@ -1523,7 +1565,12 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.addOnUpdate(onlyOnCouchDB); .addOnUpdate(onlyOnCouchDB);
}); });
}); });
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleNotification")).then((paneEl) => { void addPanel(
paneEl,
$msg("obsidianLiveSyncSettingTab.titleNotification"),
() => {},
onlyOnCouchDB
).then((paneEl) => {
paneEl.addClass("wizardHidden"); paneEl.addClass("wizardHidden");
new Setting(paneEl) new Setting(paneEl)
.autoWireNumeric("notifyThresholdOfRemoteStorageSize", {}) .autoWireNumeric("notifyThresholdOfRemoteStorageSize", {})
@@ -2001,7 +2048,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
"(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files." "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files."
) )
.setClass("wizardHidden"); .setClass("wizardHidden");
new MultipleRegExpControl({ mount(MultipleRegExpControl, {
target: syncFilesSetting.controlEl, target: syncFilesSetting.controlEl,
props: { props: {
patterns: this.editingSettings.syncOnlyRegEx.split("|[]|"), patterns: this.editingSettings.syncOnlyRegEx.split("|[]|"),
@@ -2024,7 +2071,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
) )
.setClass("wizardHidden"); .setClass("wizardHidden");
new MultipleRegExpControl({ mount(MultipleRegExpControl, {
target: nonSyncFilesSetting.controlEl, target: nonSyncFilesSetting.controlEl,
props: { props: {
patterns: this.editingSettings.syncIgnoreRegEx.split("|[]|"), patterns: this.editingSettings.syncIgnoreRegEx.split("|[]|"),
@@ -2057,7 +2104,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.filter((x) => x != ""); .filter((x) => x != "");
const patSetting = new Setting(paneEl).setName("Ignore patterns").setClass("wizardHidden").setDesc(""); const patSetting = new Setting(paneEl).setName("Ignore patterns").setClass("wizardHidden").setDesc("");
new MultipleRegExpControl({ mount(MultipleRegExpControl, {
target: patSetting.controlEl, target: patSetting.controlEl,
props: { props: {
patterns: pat, patterns: pat,
@@ -2233,9 +2280,14 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
pluginConfig.encryptedCouchDBConnection = REDACTED; pluginConfig.encryptedCouchDBConnection = REDACTED;
pluginConfig.accessKey = REDACTED; pluginConfig.accessKey = REDACTED;
pluginConfig.secretKey = REDACTED; pluginConfig.secretKey = REDACTED;
pluginConfig.region = `${REDACTED}(${pluginConfig.region.length} letters)`; const redact = (source: string) => `${REDACTED}(${source.length} letters)`;
pluginConfig.bucket = `${REDACTED}(${pluginConfig.bucket.length} letters)`; pluginConfig.region = redact(pluginConfig.region);
pluginConfig.bucket = redact(pluginConfig.bucket);
pluginConfig.pluginSyncExtendedSetting = {}; pluginConfig.pluginSyncExtendedSetting = {};
pluginConfig.P2P_AppID = redact(pluginConfig.P2P_AppID);
pluginConfig.P2P_passphrase = redact(pluginConfig.P2P_passphrase);
pluginConfig.P2P_roomID = redact(pluginConfig.P2P_roomID);
pluginConfig.P2P_relays = redact(pluginConfig.P2P_relays);
const endpoint = pluginConfig.endpoint; const endpoint = pluginConfig.endpoint;
if (endpoint == "") { if (endpoint == "") {
pluginConfig.endpoint = "Not configured or AWS"; pluginConfig.endpoint = "Not configured or AWS";
@@ -2918,7 +2970,8 @@ ${stringifyYaml(pluginConfig)}`;
.onClick(async () => { .onClick(async () => {
await this.plugin.$$markRemoteLocked(); await this.plugin.$$markRemoteLocked();
}) })
); )
.addOnUpdate(onlyOnCouchDBOrMinIO);
new Setting(paneEl) new Setting(paneEl)
.setName("Emergency restart") .setName("Emergency restart")
@@ -2935,7 +2988,7 @@ ${stringifyYaml(pluginConfig)}`;
); );
}); });
void addPanel(paneEl, "Syncing").then((paneEl) => { void addPanel(paneEl, "Syncing", () => {}, onlyOnCouchDBOrMinIO).then((paneEl) => {
new Setting(paneEl) new Setting(paneEl)
.setName("Resend") .setName("Resend")
.setDesc("Resend all chunks to the remote.") .setDesc("Resend all chunks to the remote.")
@@ -2951,6 +3004,7 @@ ${stringifyYaml(pluginConfig)}`;
}) })
) )
.addOnUpdate(onlyOnCouchDB); .addOnUpdate(onlyOnCouchDB);
new Setting(paneEl) new Setting(paneEl)
.setName("Reset journal received history") .setName("Reset journal received history")
.setDesc( .setDesc(
@@ -2994,7 +3048,7 @@ ${stringifyYaml(pluginConfig)}`;
) )
.addOnUpdate(onlyOnMinIO); .addOnUpdate(onlyOnMinIO);
}); });
void addPanel(paneEl, "Garbage Collection (Beta)", (e) => e, onlyOnCouchDB).then((paneEl) => { void addPanel(paneEl, "Garbage Collection (Beta)", (e) => e, onlyOnP2POrCouchDB).then((paneEl) => {
new Setting(paneEl) new Setting(paneEl)
.setName("Remove all orphaned chunks") .setName("Remove all orphaned chunks")
.setDesc("Remove all orphaned chunks from the local database.") .setDesc("Remove all orphaned chunks from the local database.")
@@ -3080,7 +3134,7 @@ ${stringifyYaml(pluginConfig)}`;
.addOnUpdate(onlyOnCouchDB); .addOnUpdate(onlyOnCouchDB);
}); });
void addPanel(paneEl, "Total Overhaul").then((paneEl) => { void addPanel(paneEl, "Total Overhaul", () => {}, onlyOnCouchDBOrMinIO).then((paneEl) => {
new Setting(paneEl) new Setting(paneEl)
.setName("Rebuild everything") .setName("Rebuild everything")
.setDesc("Rebuild local and remote database with local files.") .setDesc("Rebuild local and remote database with local files.")
@@ -3104,7 +3158,8 @@ ${stringifyYaml(pluginConfig)}`;
}) })
); );
}); });
void addPanel(paneEl, "Rebuilding Operations (Remote Only)").then((paneEl) => { void addPanel(paneEl, "Rebuilding Operations (Remote Only)", () => {}, onlyOnCouchDBOrMinIO).then(
(paneEl) => {
new Setting(paneEl) new Setting(paneEl)
.setName("Perform cleanup") .setName("Perform cleanup")
.setDesc( .setDesc(
@@ -3141,7 +3196,9 @@ ${stringifyYaml(pluginConfig)}`;
new Setting(paneEl) new Setting(paneEl)
.setName("Reset all journal counter") .setName("Reset all journal counter")
.setDesc("Initialise all journal history, On the next sync, every item will be received and sent.") .setDesc(
"Initialise all journal history, On the next sync, every item will be received and sent."
)
.addButton((button) => .addButton((button) =>
button button
.setButtonText("Reset all") .setButtonText("Reset all")
@@ -3191,7 +3248,8 @@ ${stringifyYaml(pluginConfig)}`;
}) })
) )
.addOnUpdate(onlyOnMinIO); .addOnUpdate(onlyOnMinIO);
}); }
);
void addPanel(paneEl, "Reset").then((paneEl) => { void addPanel(paneEl, "Reset").then((paneEl) => {
new Setting(paneEl) new Setting(paneEl)

View File

@@ -1,27 +0,0 @@
export interface Confirm {
askYesNo(message: string): Promise<"yes" | "no">;
askString(title: string, key: string, placeholder: string, isPassword?: boolean): Promise<string | false>;
askYesNoDialog(
message: string,
opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number }
): Promise<"yes" | "no">;
askSelectString(message: string, items: string[]): Promise<string>;
askSelectStringDialogue(
message: string,
buttons: string[],
opt: { title?: string; defaultAction: (typeof buttons)[number]; timeout?: number }
): Promise<(typeof buttons)[number] | false>;
askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void): void;
confirmWithMessage(
title: string,
contentMd: string,
buttons: string[],
defaultAction: (typeof buttons)[number],
timeout?: number
): Promise<(typeof buttons)[number] | false>;
}

View File

@@ -14,6 +14,7 @@ import { cancelAllPeriodicTask, cancelAllTasks } from "octagonal-wheels/concurre
import { stopAllRunningProcessors } from "octagonal-wheels/concurrency/processor"; import { stopAllRunningProcessors } from "octagonal-wheels/concurrency/processor";
import { AbstractModule } from "../AbstractModule.ts"; import { AbstractModule } from "../AbstractModule.ts";
import type { ICoreModule } from "../ModuleTypes.ts"; import type { ICoreModule } from "../ModuleTypes.ts";
import { EVENT_PLATFORM_UNLOADED } from "../../lib/src/PlatformAPIs/APIBase.ts";
export class ModuleLiveSyncMain extends AbstractModule implements ICoreModule { export class ModuleLiveSyncMain extends AbstractModule implements ICoreModule {
async $$onLiveSyncReady() { async $$onLiveSyncReady() {
@@ -139,6 +140,8 @@ export class ModuleLiveSyncMain extends AbstractModule implements ICoreModule {
} }
await this.localDatabase.close(); await this.localDatabase.close();
} }
eventHub.emitEvent(EVENT_PLATFORM_UNLOADED);
eventHub.offAll();
this._log($msg("moduleLiveSyncMain.logUnloadingPlugin")); this._log($msg("moduleLiveSyncMain.logUnloadingPlugin"));
} }

View File

@@ -17,6 +17,7 @@
/* min-height: 280px; */ /* min-height: 280px; */
max-height: 280px; max-height: 280px;
user-select: text; user-select: text;
-webkit-user-select: text;
} }
.op-pre { .op-pre {

View File

@@ -5,6 +5,9 @@ if you want to view the source, please visit the github repository of this plugi
`; `;
const prod = process.argv[2] === "production"; const prod = process.argv[2] === "production";
/***
* @type import("terser").MinifyOptions
*/
const terserOption = { const terserOption = {
sourceMap: !prod sourceMap: !prod
? { ? {
@@ -28,7 +31,6 @@ const terserOption = {
evaluate: true, evaluate: true,
dead_code: true, dead_code: true,
// directives: true, // directives: true,
// conditionals: true,
inline: 3, inline: 3,
join_vars: true, join_vars: true,
loops: true, loops: true,
@@ -38,12 +40,25 @@ const terserOption = {
arrows: true, arrows: true,
collapse_vars: true, collapse_vars: true,
comparisons: true, comparisons: true,
//@ts-ignore
lhs_constants: true, lhs_constants: true,
hoist_props: true, hoist_props: true,
side_effects: true, side_effects: true,
ecma: 2018, ecma: 2018,
// hoist_vars: true,
// hoist_funs: true,
if_return: true, if_return: true,
// unsafe_math: true,
unused: true, unused: true,
// --
typeofs: true,
properties: true,
module: true,
booleans: true,
conditionals: true,
hoist_funs: true,
hoist_vars: true,
// toplevel: "vars",
}, },
mangle: false, mangle: false,

View File

@@ -7,7 +7,7 @@
"target": "ES2018", "target": "ES2018",
"allowJs": true, "allowJs": true,
"noImplicitAny": true, "noImplicitAny": true,
"moduleResolution": "node", "moduleResolution": "bundler",
"types": ["svelte", "node"], "types": ["svelte", "node"],
// "importsNotUsedAsValues": "error", // "importsNotUsedAsValues": "error",
"importHelpers": false, "importHelpers": false,

View File

@@ -14,6 +14,44 @@ Thank you, and I hope your troubles will be resolved!
--- ---
## 0.24.11
Peer-to-peer synchronisation has been implemented!
Until now, I have not provided a synchronisation server. More people may not even know that I have shut down the test server. I confess that this is a bit repetitive, but I confess it is a cautionary tale. This is out of a sense of self-discipline that someone has occurred who could see your data. Even if the 'someone' is me. I should not be unaware of its superiority, even though well-meaning and am a servant of all.
However, now I can provide you with a signalling server. Because, to the best of my knowledge, it is only the network that is connected to your device.
Also, this signalling server is just a Nostr relay, not my implementation. You can run your implementation, which you consider trustworthy, on a trustworthy server. You do not even have to trust me. Mate, it is great, isn't it? For your information, strfry is running on my signalling server.
### Improved
- New Translation: `es` (Spanish) by @zeedif (Thank you so much)!
- Now all of messages can be selectable and copyable, also on the iPhone, iPad, and Android devices. Now we can copy or share the messages easily.
-
### New Feature
- Peer-to-Peer Synchronisation has been implemented!
- This feature is still in early beta, and it is recommended to use it with caution.
- However, it is a significant step towards the self-hosting concept. It is now possible to synchronise your data without using any remote database or storage. It is a direct connection between your devices.
- Note: We should keep the device online to synchronise the data. It is not a background synchronisation. Also it needs a signalling server to establish the connection. But, the signalling server is used only for establishing the connection, and it does not store any data.
-
### Fixed
- No longer memory or resource leaks when the plug-in is disabled.
- Now deleted chunks are correctly detected on conflict resolution, and we are guided to resurrect them.
- Hanging issue during the initial synchronisation has been fixed.
- Some unnecessary logs have been removed.
### Refactor
- Several interfaces have been moved to the separated library.
- Translations have been moved to each language file, and during the build, they are merged into one file.
- Non-mobile friendly code has been removed and replaced with the safer code.
- (Now a days, mostly server-side engine can use webcrypto, so it will be rewritten in the future more).
- Started writing Platform impedance-matching-layer.
- Svelte has been updated to v5.
## 0.24.10 ## 0.24.10
### Fixed ### Fixed