mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-26 19:13:20 +00:00
397 lines
14 KiB
Svelte
397 lines
14 KiB
Svelte
<script lang="ts">
|
|
import { onDestroy, onMount } from "svelte";
|
|
import type { GuestDialogProps } from "@/modules/services/LiveSyncUI/svelteDialog";
|
|
import DialogHeader from "@/modules/services/LiveSyncUI/components/DialogHeader.svelte";
|
|
import InfoNote from "@/modules/services/LiveSyncUI/components/InfoNote.svelte";
|
|
import { $msg as translate } from "@/common/translation";
|
|
import type {
|
|
JoinedDeviceRow,
|
|
JoinedDevicesDialogOptions,
|
|
JoinedDevicesView,
|
|
} from "@/serviceFeatures/joinedDevicesDialogTypes";
|
|
|
|
const { setResult, getInitialData }: GuestDialogProps<"close", JoinedDevicesDialogOptions> = $props();
|
|
let options = $state<JoinedDevicesDialogOptions>();
|
|
let view = $state<JoinedDevicesView>();
|
|
let isRefreshing = $state(false);
|
|
let confirmingId = $state<string>();
|
|
let forgettingId = $state<string>();
|
|
let statusMessage = $state("");
|
|
let errorMessage = $state("");
|
|
let disposed = false;
|
|
const busy = $derived(isRefreshing || forgettingId !== undefined);
|
|
const title = translate("Joined devices");
|
|
|
|
onMount(() => {
|
|
options = getInitialData?.();
|
|
if (!options) {
|
|
errorMessage = translate(
|
|
"The device information could not be loaded. Close this dialogue and open Joined devices again if the remote configuration has changed."
|
|
);
|
|
return;
|
|
}
|
|
void refreshView();
|
|
});
|
|
|
|
onDestroy(() => {
|
|
disposed = true;
|
|
options?.cancel();
|
|
});
|
|
|
|
async function refreshView(afterForget = false) {
|
|
const dialogOptions = options;
|
|
if (!dialogOptions || disposed || isRefreshing || (forgettingId !== undefined && !afterForget)) return;
|
|
|
|
isRefreshing = true;
|
|
errorMessage = "";
|
|
if (!afterForget) {
|
|
statusMessage = "";
|
|
confirmingId = undefined;
|
|
}
|
|
try {
|
|
const refreshedView = await dialogOptions.refresh();
|
|
if (disposed) return;
|
|
view = refreshedView;
|
|
} catch {
|
|
if (!disposed) {
|
|
errorMessage = translate(
|
|
"The device information could not be loaded. Close this dialogue and open Joined devices again if the remote configuration has changed."
|
|
);
|
|
}
|
|
} finally {
|
|
if (!disposed) isRefreshing = false;
|
|
}
|
|
}
|
|
|
|
function toggleConfirmation(device: JoinedDeviceRow) {
|
|
if (busy || device.self) return;
|
|
confirmingId = confirmingId === device.id ? undefined : device.id;
|
|
errorMessage = "";
|
|
statusMessage = "";
|
|
}
|
|
|
|
async function forgetDevice(device: JoinedDeviceRow) {
|
|
const dialogOptions = options;
|
|
if (!dialogOptions || disposed || busy || device.self || confirmingId !== device.id) return;
|
|
|
|
forgettingId = device.id;
|
|
confirmingId = undefined;
|
|
errorMessage = "";
|
|
statusMessage = "";
|
|
try {
|
|
const result = await dialogOptions.forget(device.id);
|
|
if (disposed) return;
|
|
|
|
if (result === "forgotten") {
|
|
statusMessage = translate("The device record was forgotten.");
|
|
await refreshView(true);
|
|
} else if (result === "not-found") {
|
|
statusMessage = translate("The device record was already absent.");
|
|
await refreshView(true);
|
|
} else if (result === "conflict") {
|
|
errorMessage = translate(
|
|
"The device list changed while you were trying to forget this device. Refresh the list before trying again."
|
|
);
|
|
} else {
|
|
errorMessage = translate(
|
|
"This remote does not support conditional updates, so the device record could not be removed."
|
|
);
|
|
}
|
|
} catch {
|
|
if (!disposed) {
|
|
errorMessage = translate(
|
|
"Joined devices could not be updated. Refresh the list and try again. If the remote configuration has changed, close this dialogue and open Joined devices again."
|
|
);
|
|
}
|
|
} finally {
|
|
if (!disposed) forgettingId = undefined;
|
|
}
|
|
}
|
|
|
|
function formatLastConnected(timestamp: number | undefined) {
|
|
if (timestamp === undefined || !Number.isFinite(timestamp) || timestamp <= 0) return translate("Not recorded");
|
|
const date = new Date(timestamp);
|
|
if (Number.isNaN(date.getTime())) return translate("Not recorded");
|
|
return date.toLocaleString();
|
|
}
|
|
</script>
|
|
|
|
<DialogHeader {title} />
|
|
<div class="joined-devices-dialog">
|
|
<p>
|
|
{translate("This list shows device information recorded by the remote. It does not show whether a device is online now.")}
|
|
</p>
|
|
|
|
<InfoNote
|
|
message="Synchronisation between different plug-in versions is generally safe unless the connection is explicitly blocked. However, it may be less efficient or use more storage, and P2P synchronisation may be unavailable."
|
|
/>
|
|
|
|
{#if view}
|
|
{#if !view.milestonePresent}
|
|
<p class="state-message">{translate("This remote does not currently have a record of joined devices.")}</p>
|
|
{/if}
|
|
{#if view.cleaned}
|
|
<p class="state-message">
|
|
{translate(
|
|
"This remote has been cleaned. Use the existing recovery process to join devices again as needed."
|
|
)}
|
|
</p>
|
|
{/if}
|
|
{#if view.locked}
|
|
<p class="state-message">
|
|
{translate(
|
|
"This remote is locked. Use the existing recovery process for devices that need to join again."
|
|
)}
|
|
</p>
|
|
{/if}
|
|
{/if}
|
|
|
|
{#if errorMessage}
|
|
<p class="error-message" role="alert">{errorMessage}</p>
|
|
{/if}
|
|
{#if statusMessage}
|
|
<p class="status-message" role="status">{statusMessage}</p>
|
|
{/if}
|
|
{#if isRefreshing && !view}
|
|
<p role="status">{translate("Loading joined devices…")}</p>
|
|
{/if}
|
|
|
|
{#if view}
|
|
{#if view.devices.length > 0}
|
|
<ul class="device-list">
|
|
{#each view.devices as device (device.id)}
|
|
<li class="device-card">
|
|
<div class="device-heading">
|
|
<div class="device-identity">
|
|
{#if device.self}
|
|
<span class="device-badge">{translate("This device")}</span>
|
|
{/if}
|
|
<code class="device-id">{device.id}</code>
|
|
</div>
|
|
<div class="device-badges">
|
|
<span class="device-badge">
|
|
{device.accepted ? translate("Accepted") : translate("Not accepted")}
|
|
</span>
|
|
{#if device.lockedOut}
|
|
<span class="device-badge device-badge-warning">{translate("Locked out")}</span>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
{#if device.self && device.lockedOut}
|
|
<p class="state-message">
|
|
{translate(
|
|
"This device has been locked out. Use the existing recovery process to join again."
|
|
)}
|
|
</p>
|
|
{/if}
|
|
|
|
<dl class="device-details">
|
|
<div>
|
|
<dt>{translate("Platform")}</dt>
|
|
<dd>{device.platform || translate("Unknown platform")}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>{translate("Plug-in version")}</dt>
|
|
<dd>{device.pluginVersion || translate("Not recorded")}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>{translate("App version")}</dt>
|
|
<dd>{device.appVersion || translate("Not recorded")}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>{translate("Last recorded")}</dt>
|
|
<dd>{formatLastConnected(device.lastConnected)}</dd>
|
|
</div>
|
|
</dl>
|
|
|
|
{#if !device.self}
|
|
{#if confirmingId === device.id}
|
|
<div class="forget-confirmation">
|
|
<p><strong>{translate("Forget this device record?")}</strong></p>
|
|
<p>
|
|
{translate("This removes only this device's record from the remote.")}
|
|
</p>
|
|
<p>
|
|
{translate(
|
|
"Forgetting the record does not delete Vault files or revoke credentials."
|
|
)}
|
|
</p>
|
|
<p>
|
|
{view.locked
|
|
? translate(
|
|
"This remote is locked. If the device returns, use the existing recovery process to join it again."
|
|
)
|
|
: translate("This remote is unlocked. A device that returns can register again.")}
|
|
</p>
|
|
<div class="device-actions">
|
|
<button disabled={busy} onclick={() => toggleConfirmation(device)}>
|
|
{translate("Cancel")}
|
|
</button>
|
|
<button
|
|
class="mod-warning"
|
|
disabled={busy}
|
|
onclick={() => void forgetDevice(device)}
|
|
>
|
|
{translate("Forget device")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<div class="device-actions">
|
|
<button disabled={busy} onclick={() => toggleConfirmation(device)}>
|
|
{forgettingId === device.id
|
|
? translate("Forgetting…")
|
|
: translate("Forget device")}
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
{:else if !isRefreshing}
|
|
<p>{translate("No joined devices are recorded on this remote.")}</p>
|
|
{/if}
|
|
{/if}
|
|
|
|
<div class="dialog-controls">
|
|
<button disabled={busy} onclick={() => void refreshView()}>{translate("Refresh")}</button>
|
|
<button onclick={() => setResult("close")}>{translate("Close")}</button>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.joined-devices-dialog {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--size-4-2);
|
|
min-width: 0;
|
|
max-width: 100%;
|
|
}
|
|
|
|
p {
|
|
margin: 0;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.state-message,
|
|
.forget-confirmation {
|
|
border-left: 3px solid var(--interactive-accent);
|
|
padding: var(--size-4-2);
|
|
background: var(--background-secondary);
|
|
}
|
|
|
|
.error-message {
|
|
color: var(--text-error);
|
|
}
|
|
|
|
.device-list {
|
|
display: grid;
|
|
gap: var(--size-4-2);
|
|
list-style: none;
|
|
margin: 0;
|
|
min-width: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.device-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--size-4-2);
|
|
min-width: 0;
|
|
overflow-wrap: anywhere;
|
|
border: 1px solid var(--background-modifier-border);
|
|
border-radius: var(--radius-m);
|
|
padding: var(--size-4-3);
|
|
}
|
|
|
|
.device-heading {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: var(--size-4-2);
|
|
min-width: 0;
|
|
}
|
|
|
|
.device-identity,
|
|
.device-badges {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: var(--size-4-2);
|
|
min-width: 0;
|
|
}
|
|
|
|
.device-id {
|
|
display: block;
|
|
min-width: 0;
|
|
max-width: 100%;
|
|
overflow-wrap: anywhere;
|
|
white-space: normal;
|
|
}
|
|
|
|
.device-badge {
|
|
border-radius: var(--radius-s);
|
|
background: var(--background-secondary);
|
|
padding: 0 var(--size-4-2);
|
|
font-size: var(--font-ui-smaller);
|
|
}
|
|
|
|
.device-badge-warning {
|
|
color: var(--text-warning);
|
|
}
|
|
|
|
.device-details {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
|
|
gap: var(--size-4-2) var(--size-4-4);
|
|
margin: 0;
|
|
min-width: 0;
|
|
}
|
|
|
|
.device-details > div {
|
|
min-width: 0;
|
|
}
|
|
|
|
dt {
|
|
color: var(--text-muted);
|
|
font-size: var(--font-ui-smaller);
|
|
}
|
|
|
|
dd {
|
|
margin: 0;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.forget-confirmation {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--size-4-2);
|
|
min-width: 0;
|
|
}
|
|
|
|
.device-actions,
|
|
.dialog-controls {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: flex-end;
|
|
gap: var(--size-4-2);
|
|
}
|
|
|
|
button {
|
|
min-height: 44px;
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.device-card {
|
|
padding: var(--size-4-2);
|
|
}
|
|
|
|
.device-details {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
}
|
|
}
|
|
</style>
|