Skip to content

Commit 76fd922

Browse files
committed
Use icon+text action buttons and add toast feedback on subscriptions, monitors, playlists
- Replace icon-only action buttons with icon+text across all three pages - Subscription check now keeps spinner active until SSE result fires; result shown as toast - Toggle (pause/resume) on subscriptions and monitors shows a delayed toast only if the request takes >350ms - Add success toasts for subscription and monitor creation - Playlists edit/delete buttons in grid and list views now show labels
1 parent bf83192 commit 76fd922

3 files changed

Lines changed: 77 additions & 37 deletions

File tree

src/routes/monitors/+page.svelte

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { onMount } from 'svelte';
33
import { onSSEEvent } from '$lib/stores/sse.svelte';
44
import { showConfirm } from '$lib/stores/modal.svelte';
5-
import { addToast } from '$lib/stores/toast.svelte';
5+
import { addToast, removeToast } from '$lib/stores/toast.svelte';
66
import { csrfFetch } from '$lib/utils/fetch';
77
import Skeleton from '$lib/components/ui/Skeleton.svelte';
88
import EmptyState from '$lib/components/ui/EmptyState.svelte';
@@ -119,6 +119,7 @@
119119
monFormUrl = '';
120120
monFormOptions = { sponsorblock: false, subtitles: false, metadata: false };
121121
showMonitorsForm = false;
122+
addToast('success', 'Monitor added');
122123
await loadMonitors();
123124
} else {
124125
const data = await res.json().catch(() => null);
@@ -130,6 +131,10 @@
130131
}
131132
132133
async function toggleMonitor(id: string, enabled: boolean) {
134+
let toastId: string | null = null;
135+
const timer = setTimeout(() => {
136+
toastId = addToast('info', enabled ? 'Pausing monitor...' : 'Resuming monitor...', 10000);
137+
}, 350);
133138
try {
134139
await csrfFetch(`/api/monitors/${id}`, {
135140
method: 'PATCH',
@@ -138,7 +143,10 @@
138143
});
139144
await loadMonitors();
140145
} catch (e) {
141-
console.error('Failed to toggle monitor:', e);
146+
addToast('error', 'Failed to update monitor');
147+
} finally {
148+
clearTimeout(timer);
149+
if (toastId) removeToast(toastId);
142150
}
143151
}
144152
@@ -365,23 +373,27 @@
365373
{/if}
366374

367375
<div class="actions">
368-
<button class="btn btn-sm btn-icon btn-secondary" onclick={() => startEditMonitor(monitor)} aria-label="Edit" title="Edit">
376+
<button class="btn btn-sm btn-secondary" onclick={() => startEditMonitor(monitor)} aria-label="Edit" title="Edit">
369377
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
378+
Edit
370379
</button>
371380
<button
372-
class="btn btn-sm btn-icon btn-secondary"
381+
class="btn btn-sm btn-secondary"
373382
onclick={() => toggleMonitor(monitor.id, monitor.enabled)}
374383
aria-label={monitor.enabled ? 'Pause' : 'Resume'}
375384
title={monitor.enabled ? 'Pause' : 'Resume'}
376385
>
377386
{#if monitor.enabled}
378387
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
388+
Pause
379389
{:else}
380390
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
391+
Resume
381392
{/if}
382393
</button>
383-
<button class="btn btn-sm btn-icon btn-danger" onclick={() => deleteMonitor(monitor.id)} aria-label="Delete" title="Delete">
394+
<button class="btn btn-sm btn-danger" onclick={() => deleteMonitor(monitor.id)} aria-label="Delete" title="Delete">
384395
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
396+
Delete
385397
</button>
386398
</div>
387399
{/if}

src/routes/playlists/+page.svelte

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -280,11 +280,13 @@
280280
<span class="list-row-date">{formatDate(playlist.updatedAt)}</span>
281281
</button>
282282
<div class="list-row-actions">
283-
<button class="btn btn-sm btn-icon btn-secondary" onclick={(e) => { e.stopPropagation(); startEdit(playlist); }} title="Edit playlist" aria-label="Edit playlist">
283+
<button class="btn btn-sm btn-secondary" onclick={(e) => { e.stopPropagation(); startEdit(playlist); }} title="Edit playlist" aria-label="Edit playlist">
284284
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
285+
Edit
285286
</button>
286-
<button class="btn btn-sm btn-icon btn-danger" onclick={(e) => { e.stopPropagation(); deletePlaylist(playlist); }} title="Delete playlist" aria-label="Delete playlist">
287+
<button class="btn btn-sm btn-danger" onclick={(e) => { e.stopPropagation(); deletePlaylist(playlist); }} title="Delete playlist" aria-label="Delete playlist">
287288
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
289+
Delete
288290
</button>
289291
</div>
290292
</div>
@@ -317,16 +319,18 @@
317319
</div>
318320
</button>
319321
<div class="card-actions">
320-
<button class="btn btn-sm btn-icon btn-secondary" onclick={(e) => { e.stopPropagation(); startEdit(playlist); }} title="Edit playlist" aria-label="Edit playlist">
322+
<button class="btn btn-sm btn-secondary" onclick={(e) => { e.stopPropagation(); startEdit(playlist); }} title="Edit playlist" aria-label="Edit playlist">
321323
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
322324
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
323325
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
324326
</svg>
327+
Edit
325328
</button>
326-
<button class="btn btn-sm btn-icon btn-danger" onclick={(e) => { e.stopPropagation(); deletePlaylist(playlist); }} title="Delete playlist" aria-label="Delete playlist">
329+
<button class="btn btn-sm btn-danger" onclick={(e) => { e.stopPropagation(); deletePlaylist(playlist); }} title="Delete playlist" aria-label="Delete playlist">
327330
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
328331
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
329332
</svg>
333+
Delete
330334
</button>
331335
</div>
332336
</div>

src/routes/subscriptions/+page.svelte

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { onMount } from 'svelte';
33
import { onSSEEvent } from '$lib/stores/sse.svelte';
44
import { showConfirm } from '$lib/stores/modal.svelte';
5-
import { addToast } from '$lib/stores/toast.svelte';
5+
import { addToast, removeToast } from '$lib/stores/toast.svelte';
66
import { csrfFetch, safeFetchJson, isFetchError, type FetchError } from '$lib/utils/fetch';
77
import Skeleton from '$lib/components/ui/Skeleton.svelte';
88
import EmptyState from '$lib/components/ui/EmptyState.svelte';
@@ -26,7 +26,8 @@
2626
let subsError = $state<FetchError | null>(null);
2727
let profilesError = $state<FetchError | null>(null);
2828
let checkingNow = $state<Set<string>>(new Set());
29-
let checkResult = $state<{ id: string; message: string } | null>(null);
29+
const checkTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
30+
const checkPendingToastDismiss = new Map<string, () => void>();
3031
let showSubsForm = $state(false);
3132
let subFormUrl = $state('');
3233
let subFormProfileId = $state('');
@@ -95,14 +96,15 @@
9596
loadSubscriptions();
9697
9798
const unsubChecked = onSSEEvent('subscription:checked', ({ id, name, newVideos }) => {
98-
const message =
99-
newVideos > 0
100-
? `Found ${newVideos} new video${newVideos > 1 ? 's' : ''} for ${name}`
101-
: `No new videos for ${name}`;
102-
checkResult = { id, message };
103-
setTimeout(() => {
104-
if (checkResult?.id === id) checkResult = null;
105-
}, 5000);
99+
const safetyTimeout = checkTimeouts.get(id);
100+
if (safetyTimeout) { clearTimeout(safetyTimeout); checkTimeouts.delete(id); }
101+
const dismiss = checkPendingToastDismiss.get(id);
102+
if (dismiss) { dismiss(); checkPendingToastDismiss.delete(id); }
103+
checkingNow = new Set([...checkingNow].filter((x) => x !== id));
104+
const message = newVideos > 0
105+
? `Found ${newVideos} new video${newVideos > 1 ? 's' : ''} for ${name}`
106+
: `No new videos for ${name}`;
107+
addToast(newVideos > 0 ? 'success' : 'info', message);
106108
loadSubscriptions();
107109
});
108110
const unsubBackfill = onSSEEvent('subscription:backfill', ({ name, totalVideos, newVideos }) => {
@@ -196,6 +198,7 @@
196198
subFormSaveToLibrary = libraryConfigured;
197199
subFormOptions = { sponsorblock: false, subtitles: false, metadata: false };
198200
showSubsForm = false;
201+
addToast('success', 'Subscription added');
199202
await loadSubscriptions();
200203
} else {
201204
const data = await res.json().catch(() => null);
@@ -207,6 +210,10 @@
207210
}
208211
209212
async function toggleSubscription(id: string, enabled: boolean) {
213+
let toastId: string | null = null;
214+
const timer = setTimeout(() => {
215+
toastId = addToast('info', enabled ? 'Pausing subscription...' : 'Resuming subscription...', 10000);
216+
}, 350);
210217
try {
211218
await csrfFetch(`/api/subscriptions/${id}`, {
212219
method: 'PATCH',
@@ -215,8 +222,10 @@
215222
});
216223
await loadSubscriptions();
217224
} catch (e) {
218-
console.error('Failed to toggle subscription:', e);
219225
addToast('error', 'Failed to update subscription');
226+
} finally {
227+
clearTimeout(timer);
228+
if (toastId) removeToast(toastId);
220229
}
221230
}
222231
@@ -239,12 +248,30 @@
239248
async function checkNow(id: string) {
240249
if (checkingNow.has(id)) return;
241250
checkingNow = new Set([...checkingNow, id]);
251+
252+
let toastId: string | null = null;
253+
const toastTimer = setTimeout(() => {
254+
toastId = addToast('info', 'Checking for new videos...', 30000);
255+
}, 350);
256+
const dismissToast = () => { clearTimeout(toastTimer); if (toastId) removeToast(toastId); };
257+
checkPendingToastDismiss.set(id, dismissToast);
258+
259+
const safetyTimeout = setTimeout(() => {
260+
dismissToast();
261+
checkPendingToastDismiss.delete(id);
262+
checkingNow = new Set([...checkingNow].filter((x) => x !== id));
263+
}, 60000);
264+
checkTimeouts.set(id, safetyTimeout);
265+
242266
try {
243267
await csrfFetch(`/api/subscriptions/${id}/check`, { method: 'POST' });
244268
} catch (e) {
245-
console.error('Failed to check subscription:', e);
246-
} finally {
269+
clearTimeout(safetyTimeout);
270+
checkTimeouts.delete(id);
271+
dismissToast();
272+
checkPendingToastDismiss.delete(id);
247273
checkingNow = new Set([...checkingNow].filter((x) => x !== id));
274+
addToast('error', 'Failed to start check');
248275
}
249276
}
250277
@@ -548,49 +575,52 @@
548575
</p>
549576
{/if}
550577

551-
{#if checkResult && checkResult.id === sub.id}
552-
<p class="check-result">{checkResult.message}</p>
553-
{/if}
554-
555578
<div class="actions">
556579
<button
557-
class="btn btn-sm btn-icon btn-primary"
580+
class="btn btn-sm btn-primary"
558581
onclick={() => checkNow(sub.id)}
559582
disabled={checkingNow.has(sub.id)}
560583
aria-label="Check now"
561584
title="Check now"
562585
>
563586
{#if checkingNow.has(sub.id)}
564587
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="spin"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
588+
Checking
565589
{:else}
566590
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
591+
Check
567592
{/if}
568593
</button>
569-
<button class="btn btn-sm btn-icon btn-secondary" onclick={() => startEditSub(sub)} aria-label="Edit" title="Edit">
594+
<button class="btn btn-sm btn-secondary" onclick={() => startEditSub(sub)} aria-label="Edit" title="Edit">
570595
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
596+
Edit
571597
</button>
572598
<button
573-
class="btn btn-sm btn-icon btn-secondary"
599+
class="btn btn-sm btn-secondary"
574600
onclick={() => toggleSubscription(sub.id, sub.enabled)}
575601
aria-label={sub.enabled ? 'Pause' : 'Resume'}
576602
title={sub.enabled ? 'Pause' : 'Resume'}
577603
>
578604
{#if sub.enabled}
579605
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
606+
Pause
580607
{:else}
581608
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
609+
Resume
582610
{/if}
583611
</button>
584612
<button
585-
class="btn btn-sm btn-icon btn-secondary"
613+
class="btn btn-sm btn-secondary"
586614
onclick={() => (showBackfillMenu = showBackfillMenu === sub.id ? null : sub.id)}
587615
aria-label={showBackfillMenu === sub.id ? 'Close backfill' : 'Backfill'}
588616
title={showBackfillMenu === sub.id ? 'Close backfill' : 'Backfill'}
589617
>
590618
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
619+
{showBackfillMenu === sub.id ? 'Close' : 'Backfill'}
591620
</button>
592-
<button class="btn btn-sm btn-icon btn-danger" onclick={() => deleteSubscription(sub.id)} aria-label="Delete" title="Delete">
621+
<button class="btn btn-sm btn-danger" onclick={() => deleteSubscription(sub.id)} aria-label="Delete" title="Delete">
593622
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
623+
Delete
594624
</button>
595625
</div>
596626

@@ -850,12 +880,6 @@
850880
color: var(--color-text-secondary);
851881
}
852882
853-
.check-result {
854-
font-size: 0.85rem;
855-
color: var(--color-accent-primary);
856-
margin: var(--spacing-xs) 0 0;
857-
}
858-
859883
.form-error {
860884
color: var(--color-status-error, #ef4444);
861885
font-size: 0.85rem;

0 commit comments

Comments
 (0)