|
1 | 1 | <script lang="ts"> |
2 | | - import { onMount } from 'svelte'; |
3 | | - import { Monitor, XCircle, Plus, Loader2, Trash2 } from 'lucide-svelte'; |
| 2 | + import { Monitor, XCircle, Plus, Loader2, Trash2, CheckCircle2 } from 'lucide-svelte'; |
4 | 3 | import type { SyncDetect, SyncDevice } from '$lib/api-types'; |
5 | 4 | import { API_BASE } from '$lib/config'; |
6 | 5 | import DeviceCard from './DeviceCard.svelte'; |
7 | 6 |
|
8 | | - let { detect }: { detect: SyncDetect | null } = $props(); |
| 7 | + let { detect, active = false }: { detect: SyncDetect | null; active?: boolean } = $props(); |
9 | 8 |
|
10 | 9 | let devices = $state<SyncDevice[]>([]); |
11 | 10 | let loading = $state(true); |
|
19 | 18 | let removingDeviceId = $state<string | null>(null); |
20 | 19 | let removeConfirmId = $state<string | null>(null); |
21 | 20 |
|
| 21 | + // Flash message |
| 22 | + let flashMessage = $state<string | null>(null); |
| 23 | + let flashTimeout: ReturnType<typeof setTimeout> | null = null; |
| 24 | +
|
| 25 | + function showFlash(msg: string) { |
| 26 | + flashMessage = msg; |
| 27 | + if (flashTimeout) clearTimeout(flashTimeout); |
| 28 | + flashTimeout = setTimeout(() => (flashMessage = null), 3000); |
| 29 | + } |
| 30 | +
|
22 | 31 | async function loadDevices() { |
23 | 32 | loading = true; |
24 | 33 | error = null; |
|
56 | 65 | method: 'POST', |
57 | 66 | headers: { 'Content-Type': 'application/json' }, |
58 | 67 | body: JSON.stringify({ |
59 | | - device_id: newDeviceId.trim(), |
| 68 | + device_id: newDeviceId.trim().toUpperCase(), |
60 | 69 | name: newDeviceName.trim() || newDeviceId.trim() |
61 | 70 | }) |
62 | 71 | }); |
63 | 72 | if (res.ok) { |
| 73 | + const addedName = newDeviceName.trim() || 'Device'; |
64 | 74 | newDeviceId = ''; |
65 | 75 | newDeviceName = ''; |
66 | 76 | await loadDevices(); |
| 77 | + showFlash(`${addedName} paired successfully`); |
67 | 78 | } else { |
68 | 79 | const body = await res.json().catch(() => ({})); |
69 | 80 | pairError = body?.detail ?? 'Failed to pair device.'; |
|
76 | 87 | } |
77 | 88 |
|
78 | 89 | async function removeDevice(deviceId: string) { |
| 90 | + const deviceName = devices.find((d) => d.device_id === deviceId)?.name ?? 'Device'; |
79 | 91 | removingDeviceId = deviceId; |
80 | 92 | try { |
81 | 93 | const res = await fetch(`${API_BASE}/sync/devices/${encodeURIComponent(deviceId)}`, { |
82 | 94 | method: 'DELETE' |
83 | 95 | }); |
84 | 96 | if (res.ok) { |
85 | 97 | await loadDevices(); |
| 98 | + showFlash(`${deviceName} removed`); |
86 | 99 | } |
87 | 100 | } catch { |
88 | 101 | // ignore |
|
92 | 105 | } |
93 | 106 | } |
94 | 107 |
|
95 | | - onMount(() => { |
96 | | - loadDevices(); |
| 108 | + // Reload when tab becomes active |
| 109 | + $effect(() => { |
| 110 | + if (active) { |
| 111 | + loadDevices(); |
| 112 | + } |
97 | 113 | }); |
98 | 114 | </script> |
99 | 115 |
|
100 | 116 | <div class="p-6 space-y-4"> |
| 117 | + <!-- Flash message --> |
| 118 | + {#if flashMessage} |
| 119 | + <div |
| 120 | + class="flex items-center gap-2 px-4 py-2.5 rounded-[var(--radius-lg)] bg-[var(--success)]/10 border border-[var(--success)]/20 text-xs font-medium text-[var(--success)]" |
| 121 | + > |
| 122 | + <CheckCircle2 size={14} class="shrink-0" /> |
| 123 | + {flashMessage} |
| 124 | + </div> |
| 125 | + {/if} |
| 126 | + |
101 | 127 | {#if loading} |
102 | 128 | <!-- Skeleton --> |
103 | 129 | <div class="space-y-3"> |
|
139 | 165 | {#if !device.is_self} |
140 | 166 | <!-- Remove button overlay --> |
141 | 167 | {#if removeConfirmId === device.device_id} |
142 | | - <div class="absolute top-2 right-2 flex items-center gap-1.5 bg-[var(--bg-subtle)] rounded-md p-1 border border-[var(--border)]"> |
143 | | - <span class="text-xs text-[var(--text-muted)] px-1">Remove?</span> |
| 168 | + <div |
| 169 | + class="absolute top-1.5 right-1.5 flex items-center gap-1.5 bg-[var(--bg-base)] rounded-lg px-2.5 py-1.5 border border-[var(--border)] shadow-md z-10" |
| 170 | + > |
| 171 | + <span class="text-xs text-[var(--text-secondary)]">Remove?</span> |
144 | 172 | <button |
145 | 173 | onclick={() => removeDevice(device.device_id)} |
146 | 174 | disabled={removingDeviceId === device.device_id} |
147 | | - class="px-2 py-0.5 text-xs font-medium rounded bg-[var(--error-subtle)] text-[var(--error)] border border-[var(--error)]/20 hover:bg-[var(--error)]/20 transition-colors disabled:opacity-50" |
| 175 | + class="px-2.5 py-1 text-xs font-medium rounded-md bg-[var(--error)] text-white hover:opacity-90 transition-opacity disabled:opacity-50" |
148 | 176 | > |
149 | 177 | {removingDeviceId === device.device_id ? '...' : 'Yes'} |
150 | 178 | </button> |
151 | 179 | <button |
152 | 180 | onclick={() => (removeConfirmId = null)} |
153 | | - class="px-2 py-0.5 text-xs font-medium rounded border border-[var(--border)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors" |
| 181 | + class="px-2.5 py-1 text-xs font-medium rounded-md border border-[var(--border)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors" |
154 | 182 | > |
155 | 183 | No |
156 | 184 | </button> |
|
186 | 214 | type="text" |
187 | 215 | bind:value={newDeviceId} |
188 | 216 | placeholder="XXXXXXX-XXXXXXX-..." |
189 | | - class="w-full px-3 py-2 text-xs font-mono rounded-[var(--radius)] border border-[var(--border)] bg-[var(--bg-base)] text-[var(--text-primary)] placeholder:text-[var(--text-muted)] focus:outline-none focus:ring-2 focus:ring-[var(--accent)]/40 focus:border-[var(--accent)] transition-colors" |
| 217 | + class="w-full px-3 py-2 text-sm font-mono rounded-[var(--radius)] border border-[var(--border)] bg-[var(--bg-base)] text-[var(--text-primary)] placeholder:text-[var(--text-muted)] focus:outline-none focus:ring-2 focus:ring-[var(--accent)]/40 focus:border-[var(--accent)] transition-colors uppercase" |
190 | 218 | /> |
| 219 | + <p class="text-[11px] text-[var(--text-muted)]"> |
| 220 | + Find this in Syncthing → Actions → Show ID |
| 221 | + </p> |
191 | 222 | </div> |
192 | 223 | <div class="space-y-1.5"> |
193 | 224 | <label for="new-device-name" class="block text-xs font-medium text-[var(--text-secondary)]"> |
|
0 commit comments