mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-08-11 14:15:46 +00:00
Continues the source-key migration inside the application boundary introduced in 1.0.0, where LiveSync owns its catalogue and consumes Commonlib as a published package. - Replace hardcoded user-visible strings in the Obsidian UI (Setup Wizard dialogues, P2P panes, Customisation Sync panes, Global History, the JSON conflict pane and the remote-configuration menu) with `$msg` calls, keeping the English source string as the key. - Wire up strings whose translations already existed in the catalogue but were still rendered as literals, for example the whole Intro dialogue. - Add the new entries to `src/common/messagesYAML/en.yaml` and `es.yaml`, then regenerate `messagesJson/` and `combinedMessages.prod.ts` through the documented `i18n:bake` pipeline. - No Commonlib gitlink or catalogue is involved; every change is LiveSync-owned. Regenerating the catalogue also normalises three pre-existing entries each in `ko.json` and `zh.json`, where the committed JSON kept a trailing space before a newline that YAML cannot represent. Verified with tsc-check, tsc-check:apps, svelte-check and lint. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
509 lines
20 KiB
Svelte
509 lines
20 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from "svelte";
|
|
import { AutoAccepting, DEFAULT_SETTINGS, type P2PSyncSetting } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
|
import {
|
|
AcceptedStatus,
|
|
ConnectionStatus,
|
|
type PeerStatus,
|
|
} from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorPaneCommon";
|
|
import type { P2PReplicatorPaneHost } from "./P2PReplicatorPaneHost";
|
|
import PeerStatusRow from "@/features/P2PSync/P2PReplicator/PeerStatusRow.svelte";
|
|
import { EVENT_LAYOUT_READY } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
|
|
import {
|
|
type PeerInfo,
|
|
type P2PServerInfo,
|
|
EVENT_SERVER_STATUS,
|
|
EVENT_REQUEST_STATUS,
|
|
EVENT_P2P_REPLICATOR_STATUS,
|
|
} from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicatorP2PServer";
|
|
import type { P2PReplicatorStatus } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicator";
|
|
import { $msg as _msg } from "@/common/translation";
|
|
import { SETTING_KEY_P2P_DEVICE_NAME } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
|
import { generateP2PRoomId } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
|
|
|
interface Props {
|
|
host: P2PReplicatorPaneHost;
|
|
}
|
|
|
|
let { host }: Props = $props();
|
|
let services = $derived(host.services);
|
|
let events = $derived(services.context.events);
|
|
const currentSettings = () => services.setting.currentSettings() as P2PSyncSetting;
|
|
const currentReplicator = () => host.p2p.replicator;
|
|
const initialSettings = { ...currentSettings() } as P2PSyncSetting;
|
|
|
|
let settings = $state<P2PSyncSetting>(initialSettings);
|
|
|
|
let deviceName = $state<string>("");
|
|
|
|
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>("");
|
|
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 = {
|
|
// ...currentSettings(),
|
|
// 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,
|
|
// };
|
|
await services.setting.applyPartial(
|
|
{
|
|
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,
|
|
},
|
|
true
|
|
);
|
|
services.config.setSmallConfig(SETTING_KEY_P2P_DEVICE_NAME, eDeviceName);
|
|
deviceName = eDeviceName;
|
|
}
|
|
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: P2PSyncSetting, force: boolean) => {
|
|
if (force) {
|
|
const initDeviceName =
|
|
services.config.getSmallConfig(SETTING_KEY_P2P_DEVICE_NAME) ?? services.vault.getVaultName();
|
|
deviceName = initDeviceName;
|
|
eDeviceName = initDeviceName;
|
|
}
|
|
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 = events.onEvent("setting-saved", async (d) => {
|
|
applyLoadSettings(d, false);
|
|
closeServer();
|
|
});
|
|
const rx = events.onEvent(EVENT_LAYOUT_READY, () => {
|
|
applyLoadSettings(currentSettings(), true);
|
|
});
|
|
const r2 = events.onEvent(EVENT_SERVER_STATUS, (status) => {
|
|
serverInfo = status;
|
|
advertisements = status?.knownAdvertisements ?? [];
|
|
});
|
|
const r3 = events.onEvent(EVENT_P2P_REPLICATOR_STATUS, (status) => {
|
|
replicatorInfo = status;
|
|
});
|
|
applyLoadSettings(currentSettings(), true);
|
|
events.emitEvent(EVENT_REQUEST_STATUS);
|
|
return () => {
|
|
r();
|
|
rx();
|
|
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 chooseRandom() {
|
|
eRoomId = generateP2PRoomId();
|
|
}
|
|
|
|
async function openServer() {
|
|
await currentReplicator().open();
|
|
}
|
|
async function closeServer() {
|
|
await currentReplicator().close();
|
|
}
|
|
function startBroadcasting() {
|
|
currentReplicator().enableBroadcastChanges();
|
|
}
|
|
function stopBroadcasting() {
|
|
currentReplicator().disableBroadcastChanges();
|
|
}
|
|
|
|
const initialDialogStatusKey = `p2p-dialog-status`;
|
|
const getDialogStatus = () => {
|
|
try {
|
|
const initialDialogStatus = JSON.parse(services.config.getSmallConfig(initialDialogStatusKey) ?? "{}") as {
|
|
notice?: boolean;
|
|
setting?: boolean;
|
|
};
|
|
return initialDialogStatus;
|
|
} catch {
|
|
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,
|
|
};
|
|
services.config.setSmallConfig(initialDialogStatusKey, JSON.stringify(dialogStatus));
|
|
});
|
|
let isObsidian = $derived.by(() => {
|
|
return services.API.getPlatform() === "obsidian";
|
|
});
|
|
</script>
|
|
|
|
<article>
|
|
<h1>{_msg("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>{_msg("Connection Settings")}</h2>
|
|
{#if isObsidian}
|
|
{_msg("You can configure in the Obsidian Plugin Settings.")}
|
|
{:else}
|
|
<details bind:open={isSettingOpened}>
|
|
<summary>{eRelay}</summary>
|
|
<table class="settings">
|
|
<tbody>
|
|
<tr>
|
|
<th>{_msg("Enable P2P Replicator")}</th>
|
|
<td>
|
|
<label class={{ "is-dirty": isP2PEnabledModified }}>
|
|
<input type="checkbox" bind:checked={eP2PEnabled} />
|
|
</label>
|
|
</td>
|
|
</tr><tr>
|
|
<th>{_msg("Relay settings")}</th>
|
|
<td>
|
|
<label class={{ "is-dirty": isRelayModified }}>
|
|
<input
|
|
type="text"
|
|
placeholder="wss://exp-relay.vrtmrz.net, wss://xxxxx"
|
|
bind:value={eRelay}
|
|
autocomplete="off"
|
|
/>
|
|
<button onclick={() => useDefaultRelay()}>{_msg("Use vrtmrz's relay")}</button>
|
|
</label>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th>{_msg("Room ID")}</th>
|
|
<td>
|
|
<label class={{ "is-dirty": isRoomIdModified }}>
|
|
<input
|
|
type="text"
|
|
placeholder="anything-you-like"
|
|
bind:value={eRoomId}
|
|
autocomplete="off"
|
|
spellcheck="false"
|
|
autocorrect="off"
|
|
/>
|
|
<button onclick={() => chooseRandom()}>{_msg("Use Random Number")}</button>
|
|
</label>
|
|
<span>
|
|
<small
|
|
>{_msg(
|
|
"This can isolate your connections between devices. Use the same Room ID for the same devices."
|
|
)}</small
|
|
>
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th>{_msg("Password")}</th>
|
|
<td>
|
|
<label class={{ "is-dirty": isPasswordModified }}>
|
|
<input type="password" placeholder="password" bind:value={ePassword} />
|
|
</label>
|
|
<span>
|
|
<small>
|
|
{_msg("This password is used to encrypt the connection. Use something long enough.")}
|
|
</small>
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th>{_msg("This device name")}</th>
|
|
<td>
|
|
<label class={{ "is-dirty": isDeviceNameModified }}>
|
|
<input
|
|
type="text"
|
|
placeholder="iphone-16"
|
|
bind:value={eDeviceName}
|
|
autocomplete="off"
|
|
/>
|
|
</label>
|
|
<span>
|
|
<small>
|
|
{_msg(
|
|
'Device name to identify the device. Please use shorter one for the stable peer detection, i.e., "iphone-16" or "macbook-2021".'
|
|
)}
|
|
</small>
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th>{_msg("Auto Connect")}</th>
|
|
<td>
|
|
<label class={{ "is-dirty": isAutoStartModified }}>
|
|
<input type="checkbox" bind:checked={eAutoStart} />
|
|
</label>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th>{_msg("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}
|
|
>{_msg("Save and Apply")}</button
|
|
>
|
|
<button disabled={!isAnyModified} class="button" onclick={revert}>{_msg("Revert changes")}</button>
|
|
</details>
|
|
{/if}
|
|
|
|
<div>
|
|
<h2>{_msg("Signaling Server Connection")}</h2>
|
|
<div>
|
|
{#if !isConnected}
|
|
<p>{_msg("No Connection")}</p>
|
|
{:else}
|
|
<p>{_msg("Connected to Signaling Server (as Peer ID: ${peerId})", { peerId: serverPeerId })}</p>
|
|
{/if}
|
|
</div>
|
|
<div>
|
|
{#if !isConnected}
|
|
<button onclick={openServer}>{_msg("Connect")}</button>
|
|
{:else}
|
|
<button onclick={closeServer}>{_msg("Disconnect")}</button>
|
|
{#if replicatorInfo?.isBroadcasting !== undefined}
|
|
{#if replicatorInfo?.isBroadcasting}
|
|
<button onclick={stopBroadcasting}>{_msg("Stop Broadcasting")}</button>
|
|
{:else}
|
|
<button onclick={startBroadcasting}>{_msg("Start Broadcasting")}</button>
|
|
{/if}
|
|
{/if}
|
|
<details>
|
|
<summary>{_msg("Broadcasting?")}</summary>
|
|
<p>
|
|
<small>
|
|
{_msg(
|
|
"If you want to use `LiveSync`, you should broadcast changes. All `watching` peers which detects this will start the replication for fetching."
|
|
)}
|
|
<br />
|
|
{_msg("However, This should not be enabled if you want to increase your secrecy more.")}
|
|
</small>
|
|
</p>
|
|
</details>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h2>{_msg("Peers")}</h2>
|
|
<table class="peers">
|
|
<thead>
|
|
<tr>
|
|
<th>{_msg("Name")}</th>
|
|
<th>{_msg("Action")}</th>
|
|
<th>{_msg("Command")}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each peers as peer}
|
|
<PeerStatusRow peerStatus={peer} p2p={host.p2p} showPeerMenu={host.showPeerMenu}></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>
|