mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-14 05:12:30 +00:00
Enhance P2P synchronization features and UI improvements
This commit is contained in:
@@ -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.
|
- **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`).
|
- **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.*
|
*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).
|
- **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.
|
- **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.
|
- **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)
|
## 4. Replication Dialogue
|
||||||
If you want to synchronise manually, click the **🔄 (Replicate)** button next to a peer in the device list. This opens the **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**:
|
Two actions are available per peer:
|
||||||
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.
|
|
||||||
|
|
||||||
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).
|
- **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.
|
- **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.
|
- **Security:** All data remains end-to-end encrypted. Even the signalling relay never sees your actual notes.
|
||||||
|
|||||||
@@ -57,16 +57,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSyncAndClose(peerId: string) {
|
async function handleSyncAndClose(peerId: string) {
|
||||||
try {
|
fireAndForget(async () => {
|
||||||
syncingPeerId = peerId;
|
try {
|
||||||
Logger(`Starting sync and close with ${peerId}`, logLevel);
|
Logger(`Starting sync with ${peerId}`, logLevel);
|
||||||
await onSyncAndClose(peerId);
|
await onSync(peerId);
|
||||||
Logger(`Sync and close completed with ${peerId}`, logLevel);
|
Logger(`Sync completed with ${peerId}`, logLevel);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger(`Error during sync and close: ${e instanceof Error ? e.message : String(e)}`, logLevel);
|
Logger(`Error during sync: ${e instanceof Error ? e.message : String(e)}`, logLevel);
|
||||||
} finally {
|
}
|
||||||
syncingPeerId = null;
|
});
|
||||||
}
|
onClose();
|
||||||
}
|
}
|
||||||
async function disconnect(){
|
async function disconnect(){
|
||||||
try {
|
try {
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="peers-section">
|
<div class="peers-section">
|
||||||
<h3>Available Devices</h3>
|
<h3>Available Peers</h3>
|
||||||
{#if serverInfo && serverInfo.knownAdvertisements.length > 0}
|
{#if serverInfo && serverInfo.knownAdvertisements.length > 0}
|
||||||
<div class="peers-list">
|
<div class="peers-list">
|
||||||
{#each serverInfo.knownAdvertisements as peer (peer.peerId)}
|
{#each serverInfo.knownAdvertisements as peer (peer.peerId)}
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
disabled={syncingPeerId !== null}
|
disabled={syncingPeerId !== null}
|
||||||
onclick={() => handleSyncAndClose(peer.peerId)}
|
onclick={() => handleSyncAndClose(peer.peerId)}
|
||||||
>
|
>
|
||||||
{syncingPeerId === peer.peerId ? "Syncing..." : "Sync & Close"}
|
Start Sync & Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -181,6 +181,7 @@
|
|||||||
.peer-item {
|
.peer-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
background-color: var(--background-secondary);
|
background-color: var(--background-secondary);
|
||||||
@@ -234,9 +235,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.peer-actions {
|
.peer-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
|
|||||||
@@ -12,17 +12,32 @@
|
|||||||
import type { P2PReplicatorStatus, P2PReplicationReport } from "@/lib/src/replication/trystero/TrysteroReplicator";
|
import type { P2PReplicatorStatus, P2PReplicationReport } from "@/lib/src/replication/trystero/TrysteroReplicator";
|
||||||
import { delay, fireAndForget } from "octagonal-wheels/promises";
|
import { delay, fireAndForget } from "octagonal-wheels/promises";
|
||||||
import P2PServerStatusCard from "./P2PServerStatusCard.svelte";
|
import P2PServerStatusCard from "./P2PServerStatusCard.svelte";
|
||||||
|
import { EVENT_SETTING_SAVED } from "@lib/events/coreEvents";
|
||||||
|
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
liveSyncReplicator: LiveSyncTrysteroReplicator;
|
liveSyncReplicator: LiveSyncTrysteroReplicator;
|
||||||
|
core: LiveSyncBaseCore;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { liveSyncReplicator }: Props = $props();
|
let { liveSyncReplicator, core }: Props = $props();
|
||||||
let serverInfo = $state<P2PServerInfo | undefined>(undefined);
|
let serverInfo = $state<P2PServerInfo | undefined>(undefined);
|
||||||
let replicatorInfo = $state<P2PReplicatorStatus | undefined>(undefined);
|
let replicatorInfo = $state<P2PReplicatorStatus | undefined>(undefined);
|
||||||
let decidingPeerId = $state<string | null>(null);
|
let decidingPeerId = $state<string | null>(null);
|
||||||
let communicatingUntil = $state<Record<string, number>>({});
|
let communicatingUntil = $state<Record<string, number>>({});
|
||||||
const COMMUNICATION_HOLD_MS = 2500;
|
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) {
|
function markCommunicating(peerId: string) {
|
||||||
const expiry = Date.now() + COMMUNICATION_HOLD_MS;
|
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 () => {
|
fireAndForget(async () => {
|
||||||
await delay(100);
|
await delay(100);
|
||||||
await requestServerStatus();
|
await requestServerStatus();
|
||||||
@@ -69,6 +88,7 @@
|
|||||||
unsubscribe();
|
unsubscribe();
|
||||||
unsubscribeReplicatorStatus();
|
unsubscribeReplicatorStatus();
|
||||||
unsubscribeReplicatorProgress();
|
unsubscribeReplicatorProgress();
|
||||||
|
unsubscribeSettings();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,6 +165,22 @@
|
|||||||
const isHeldCommunicating = (communicatingUntil[peerId] ?? 0) > Date.now();
|
const isHeldCommunicating = (communicatingUntil[peerId] ?? 0) > Date.now();
|
||||||
return isLiveCommunicating || isHeldCommunicating;
|
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);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p2p-container">
|
<div class="p2p-container">
|
||||||
@@ -207,8 +243,17 @@
|
|||||||
>
|
>
|
||||||
{isWatching(peer.peerId) ? '👁' : '👁🗨'}
|
{isWatching(peer.peerId) ? '👁' : '👁🗨'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div> <div class="decision-row watch-row">
|
||||||
{:else}
|
<span class="decision-label">SYNC</span>
|
||||||
|
<button
|
||||||
|
class="emoji-button {isSyncTarget(peer.name) ? 'is-watching' : ''}"
|
||||||
|
title={isSyncTarget(peer.name) ? 'Sync target \u2014 click to remove' : 'Set as sync target'}
|
||||||
|
aria-label={isSyncTarget(peer.name) ? 'Remove sync target' : 'Set sync target'}
|
||||||
|
onclick={() => toggleSyncTarget(peer)}
|
||||||
|
>
|
||||||
|
{isSyncTarget(peer.name) ? '🔄' : '🔁'}
|
||||||
|
</button>
|
||||||
|
</div> {:else}
|
||||||
<div class="decision-status">
|
<div class="decision-status">
|
||||||
<span class="badge status-chip {getAcceptanceStatusClass(peer)}">
|
<span class="badge status-chip {getAcceptanceStatusClass(peer)}">
|
||||||
{getAcceptanceStatus(peer)}
|
{getAcceptanceStatus(peer)}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export class P2PServerStatusPaneView extends SvelteItemView {
|
|||||||
target,
|
target,
|
||||||
props: {
|
props: {
|
||||||
liveSyncReplicator: this._p2pResult.replicator,
|
liveSyncReplicator: this._p2pResult.replicator,
|
||||||
|
core: this.core,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
Submodule src/lib updated: bf060df091...4ce9136a43
@@ -48,6 +48,8 @@
|
|||||||
bind:value={userType}
|
bind:value={userType}
|
||||||
>
|
>
|
||||||
This is an advanced option for users who do not have a URI or who wish to configure detailed settings.
|
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 <strong>P2P (Peer-to-Peer) synchronisation</strong>
|
||||||
|
instead of a CouchDB/S3 server — P2P requires no server setup at all.
|
||||||
</Option>
|
</Option>
|
||||||
</Options>
|
</Options>
|
||||||
</Instruction>
|
</Instruction>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from "@/features/P2PSync/P2PReplicator/P2PServerStatusPaneView";
|
} from "@/features/P2PSync/P2PReplicator/P2PServerStatusPaneView";
|
||||||
import type { LiveSyncCore } from "@/main";
|
import type { LiveSyncCore } from "@/main";
|
||||||
import type { WorkspaceLeaf } from "@/deps";
|
import type { WorkspaceLeaf } from "@/deps";
|
||||||
|
import { REMOTE_P2P } from "@lib/common/models/setting.const";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ServiceFeature: P2P Replicator lifecycle management.
|
* ServiceFeature: P2P Replicator lifecycle management.
|
||||||
@@ -105,6 +106,41 @@ export function useP2PReplicatorUI(
|
|||||||
void openStatusPane();
|
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", () => {
|
// api.addRibbonIcon("waypoints", "P2P Replicator", () => {
|
||||||
// void openPane();
|
// void openPane();
|
||||||
|
|||||||
Reference in New Issue
Block a user