From 9a90256a8a9dca35bd2b1c627884d26c1211df15 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sat, 16 May 2026 23:50:08 +0900 Subject: [PATCH] Enhance P2P synchronization features and UI improvements --- docs/p2p_sync_updates_2026.md | 29 +++++++---- .../P2POpenReplicationPane.svelte | 27 +++++----- .../P2PReplicator/P2PServerStatusPane.svelte | 51 +++++++++++++++++-- .../P2PReplicator/P2PServerStatusPaneView.ts | 1 + src/lib | 2 +- .../dialogs/SelectMethodNewUser.svelte | 2 + src/serviceFeatures/useP2PReplicatorUI.ts | 36 +++++++++++++ 7 files changed, 122 insertions(+), 26 deletions(-) diff --git a/docs/p2p_sync_updates_2026.md b/docs/p2p_sync_updates_2026.md index 13acf46..34dc9de 100644 --- a/docs/p2p_sync_updates_2026.md +++ b/docs/p2p_sync_updates_2026.md @@ -13,7 +13,7 @@ This opens the **P2P Setup** dialogue where you can configure the essentials: - **Passphrase:** Your encryption key. Ensure all your devices use the exact same passphrase. - **Device Name:** A recognisable name for the current device (e.g., `iphone-16`). -Once you have saved the settings, return to the **P2P Status Pane** and click the **Connect** button to join the network. +Once you have saved the settings, return to the **P2P Status Pane** and click the **Connect** button to join the network. *Tip: You can also toggle **Auto Connect** in the setup dialogue to automatically join the network whenever Obsidian starts.* @@ -23,19 +23,30 @@ The status pane in the right sidebar provides granular control over your synchro - **Signalling Status:** Shows if you are connected to the relay (🟢 Online). - **Live-push (Broadcast):** Toggle "Broadcast changes" to notify other peers whenever you make an edit. - **Watch:** Enable "Watch" on specific peers to automatically pull changes when they broadcast. This creates a "LiveSync-like" experience. +- **Sync (🔄/🔁):** Mark specific peers as **sync targets**. Peers marked here will be included when you run the **"P2P: Sync with targets"** command (see section 5). Click the button next to a peer to toggle it on (🔄, highlighted) or off (🔁). This setting is persisted in your configuration. -## 4. Enhanced Replication Dialogue (Bidirectional Sync) -If you want to synchronise manually, click the **🔄 (Replicate)** button next to a peer in the device list. This opens the **Replication Dialogue**. +## 4. Replication Dialogue +If you want to synchronise with a specific peer manually, use the **Replication** command or button. This opens the **Replication Dialogue** listing available devices. -Inside the dialogue, you can still see the **Server Status** at the top, so you will know if you are still connected while performing manual synchronisations. +Inside the dialogue, the **Server Status** card at the top confirms you are still connected while performing the sync. -When you trigger a synchronisation this way, the system now performs a **Bidirectional Synchronisation**: -1. **Pull:** It first fetches changes from the peer. -2. **Push:** If the pull is successful, it immediately pushes your local changes to that peer. +Two actions are available per peer: -This "one-click" approach ensures both devices are perfectly in synchronisation without manual back-and-forth. +- **Sync** — Starts a bidirectional synchronisation (Pull then Push) and keeps the dialogue open so you can monitor progress or sync with additional peers. +- **Start Sync & Close** — Starts the same bidirectional sync in the background and **immediately closes the dialogue**, so you can continue working without waiting. -## 5. Technical Improvements in 2026 +## 5. Syncing with Registered Targets via Command Palette + +You can now trigger a synchronisation with all your pre-registered target peers in one step, without opening any UI. + +1. Open the **Command Palette** (`Ctrl/Cmd + P`). +2. Run **"P2P: Sync with targets"**. + +This command synchronises with every peer whose **SYNC** toggle is enabled in the **Detected Peers** list. If no targets are registered, or if the P2P server is not running, the command will notify you accordingly. + +*Tip: Pair this command with a hotkey for a quick, keyboard-driven sync workflow.* + +## 6. Technical Improvements in 2026 - **Decoupled Architecture:** The UI is now strictly separated from the core logic, making the plugin more stable across different platforms (Mobile, Desktop, and Web). - **Svelte 5 UI:** The interface has been rebuilt for better responsiveness and clearer status indicators. - **Security:** All data remains end-to-end encrypted. Even the signalling relay never sees your actual notes. diff --git a/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte b/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte index 8247b67..c745891 100644 --- a/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte +++ b/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte @@ -57,16 +57,16 @@ } async function handleSyncAndClose(peerId: string) { - try { - syncingPeerId = peerId; - Logger(`Starting sync and close with ${peerId}`, logLevel); - await onSyncAndClose(peerId); - Logger(`Sync and close completed with ${peerId}`, logLevel); - } catch (e) { - Logger(`Error during sync and close: ${e instanceof Error ? e.message : String(e)}`, logLevel); - } finally { - syncingPeerId = null; - } + fireAndForget(async () => { + try { + Logger(`Starting sync with ${peerId}`, logLevel); + await onSync(peerId); + Logger(`Sync completed with ${peerId}`, logLevel); + } catch (e) { + Logger(`Error during sync: ${e instanceof Error ? e.message : String(e)}`, logLevel); + } + }); + onClose(); } async function disconnect(){ try { @@ -103,7 +103,7 @@ />
-

Available Devices

+

Available Peers

{#if serverInfo && serverInfo.knownAdvertisements.length > 0}
{#each serverInfo.knownAdvertisements as peer (peer.peerId)} @@ -133,7 +133,7 @@ disabled={syncingPeerId !== null} onclick={() => handleSyncAndClose(peer.peerId)} > - {syncingPeerId === peer.peerId ? "Syncing..." : "Sync & Close"} + Start Sync & Close
@@ -181,6 +181,7 @@ .peer-item { display: flex; justify-content: space-between; + flex-wrap: wrap; align-items: center; padding: 0.75rem; background-color: var(--background-secondary); @@ -234,9 +235,9 @@ } .peer-actions { + flex-wrap: wrap; display: flex; gap: 0.5rem; - flex-shrink: 0; } .btn { diff --git a/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte b/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte index 2952146..fc148f8 100644 --- a/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte +++ b/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte @@ -12,17 +12,32 @@ import type { P2PReplicatorStatus, P2PReplicationReport } from "@/lib/src/replication/trystero/TrysteroReplicator"; import { delay, fireAndForget } from "octagonal-wheels/promises"; import P2PServerStatusCard from "./P2PServerStatusCard.svelte"; + import { EVENT_SETTING_SAVED } from "@lib/events/coreEvents"; + import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore"; interface Props { liveSyncReplicator: LiveSyncTrysteroReplicator; + core: LiveSyncBaseCore; } - let { liveSyncReplicator }: Props = $props(); + let { liveSyncReplicator, core }: Props = $props(); let serverInfo = $state(undefined); let replicatorInfo = $state(undefined); let decidingPeerId = $state(null); let communicatingUntil = $state>({}); const COMMUNICATION_HOLD_MS = 2500; + let syncOnReplicationSetting = $state( + core.services.setting.currentSettings()?.P2P_SyncOnReplication ?? "" + ); + + function addToList(item: string, list: string): string { + const items = list.split(",").map((e) => e.trim()).filter((e) => e); + if (!items.includes(item)) items.push(item); + return items.join(","); + } + function removeFromList(item: string, list: string): string { + return list.split(",").map((e) => e.trim()).filter((e) => e && e !== item).join(","); + } function markCommunicating(peerId: string) { const expiry = Date.now() + COMMUNICATION_HOLD_MS; @@ -60,6 +75,10 @@ } }); + const unsubscribeSettings = eventHub.onEvent(EVENT_SETTING_SAVED, (settings) => { + syncOnReplicationSetting = settings?.P2P_SyncOnReplication ?? ""; + }); + fireAndForget(async () => { await delay(100); await requestServerStatus(); @@ -69,6 +88,7 @@ unsubscribe(); unsubscribeReplicatorStatus(); unsubscribeReplicatorProgress(); + unsubscribeSettings(); }; }); @@ -145,6 +165,22 @@ const isHeldCommunicating = (communicatingUntil[peerId] ?? 0) > Date.now(); return isLiveCommunicating || isHeldCommunicating; } + + function isSyncTarget(peerName: string) { + return syncOnReplicationSetting + .split(",") + .map((e) => e.trim()) + .filter((e) => e) + .includes(peerName); + } + + async function toggleSyncTarget(peer: P2PServerInfo["knownAdvertisements"][number]) { + const currentValue = core.services.setting.currentSettings()?.P2P_SyncOnReplication ?? ""; + const newValue = isSyncTarget(peer.name) + ? removeFromList(peer.name, currentValue) + : addToList(peer.name, currentValue); + await core.services.setting.applyPartial({ P2P_SyncOnReplication: newValue }, true); + }
@@ -207,8 +243,17 @@ > {isWatching(peer.peerId) ? '👁' : '👁‍🗨'} -
- {:else} +
+ SYNC + +
{:else}
{getAcceptanceStatus(peer)} diff --git a/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts b/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts index 99d2347..a182b8a 100644 --- a/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts +++ b/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts @@ -36,6 +36,7 @@ export class P2PServerStatusPaneView extends SvelteItemView { target, props: { liveSyncReplicator: this._p2pResult.replicator, + core: this.core, }, }); } diff --git a/src/lib b/src/lib index bf060df..4ce9136 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit bf060df09183f2992829f0e8ec19bb6e389c1613 +Subproject commit 4ce9136a434fe540ee25ee48568a510703a31b1a diff --git a/src/modules/features/SetupWizard/dialogs/SelectMethodNewUser.svelte b/src/modules/features/SetupWizard/dialogs/SelectMethodNewUser.svelte index 2478521..b4dd9ea 100644 --- a/src/modules/features/SetupWizard/dialogs/SelectMethodNewUser.svelte +++ b/src/modules/features/SetupWizard/dialogs/SelectMethodNewUser.svelte @@ -48,6 +48,8 @@ bind:value={userType} > This is an advanced option for users who do not have a URI or who wish to configure detailed settings. + You can also select this option if you intend to use P2P (Peer-to-Peer) synchronisation + instead of a CouchDB/S3 server — P2P requires no server setup at all. diff --git a/src/serviceFeatures/useP2PReplicatorUI.ts b/src/serviceFeatures/useP2PReplicatorUI.ts index 59ef277..3e3882a 100644 --- a/src/serviceFeatures/useP2PReplicatorUI.ts +++ b/src/serviceFeatures/useP2PReplicatorUI.ts @@ -10,6 +10,7 @@ import { } from "@/features/P2PSync/P2PReplicator/P2PServerStatusPaneView"; import type { LiveSyncCore } from "@/main"; import type { WorkspaceLeaf } from "@/deps"; +import { REMOTE_P2P } from "@lib/common/models/setting.const"; /** * ServiceFeature: P2P Replicator lifecycle management. @@ -105,6 +106,41 @@ export function useP2PReplicatorUI( void openStatusPane(); }, }); + host.services.API.addCommand({ + id: "replicate-now-by-p2p-default-peer", + name: "Replicate P2P to default peer", + checkCallback: (isChecking: boolean) => { + const settings = host.services.setting.currentSettings(); + if (isChecking) { + if (settings.remoteType == REMOTE_P2P) return false; + return replicator.replicator?.server?.isServing ?? false; + } + void replicator.replicator?.openReplication(settings, false, true, false); + }, + }); + host.services.API.addCommand({ + id: "replicate-now-by-p2p", + name: "Replicate now by P2P", + checkCallback: (isChecking: boolean) => { + const settings = host.services.setting.currentSettings(); + if (isChecking) { + if (settings.remoteType == REMOTE_P2P) return false; + return replicator.replicator?.server?.isServing ?? false; + } + void replicator.replicator?.openReplication(settings, false, true, false); + }, + }); + + host.services.API.addCommand({ + id: "p2p-sync-targets", + name: "P2P: Sync with targets", + checkCallback: (isChecking: boolean) => { + if (isChecking) { + return replicator.replicator?.server?.isServing ?? false; + } + void replicator.replicator?.replicateFromCommand(true); + }, + }); // api.addRibbonIcon("waypoints", "P2P Replicator", () => { // void openPane();