feat(P4C.T4): SoundboardSettings — sync hook mount + per-row badges + remote-delete

Mount useSoundboardSync in SoundboardManagerDialog, thread badges map through
SoundboardCategoryGroup/SoundboardRow, render a cloud-state glyph badge inline
with each row's size/mime metadata, and wrap handleDelete to attempt a
best-effort remote delete via deleteRemoteSound before the local deleteSound call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
byGalax
2026-05-16 21:02:26 +02:00
parent 759113b9ce
commit 890d5dc2b7
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { deleteSound as deleteRemoteSound } from '@chat-app/shared/chat';
import { getPttSettings } from '../lib/pttSettings';
import { codeToShortcut } from '../lib/globalShortcut';
import { invalidate as invalidateSoundCache, preload } from '../lib/soundboardPlayback';
@@ -15,6 +16,8 @@ import {
subscribeSoundboardChanges,
updateSound,
} from '../lib/soundboardStorage';
import { supabase } from '../lib/supabase';
import { useSoundboardSync, type SyncBadge } from '../hooks/useSoundboardSync';
import { Modal } from './Modal';
import {
AlertIcon,
@@ -46,6 +49,7 @@ export function SoundboardManagerDialog({ open, onClose }: Props) {
const [busyId, setBusyId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [previewingId, setPreviewingId] = useState<string | null>(null);
const { badges } = useSoundboardSync();
const refresh = useCallback(async () => {
setLoading(true);
@@ -167,6 +171,11 @@ export function SoundboardManagerDialog({ open, onClose }: Props) {
}
setBusyId(id);
try {
try {
await deleteRemoteSound(supabase, id);
} catch (err) {
console.warn('remote sound delete failed (local delete proceeds)', err);
}
await deleteSound(id);
invalidateSoundCache(id);
if (previewingId === id) stopPreview();
@@ -301,6 +310,7 @@ export function SoundboardManagerDialog({ open, onClose }: Props) {
entriesTotal={entries}
busyId={busyId}
previewingId={previewingId}
badges={badges}
onPatch={handlePatch}
onDelete={handleDelete}
onPreview={handlePreview}
@@ -323,6 +333,7 @@ interface GroupProps {
entriesTotal: SoundboardEntry[];
busyId: string | null;
previewingId: string | null;
badges: Map<string, SyncBadge>;
onPatch: (id: string, patch: Parameters<typeof updateSound>[1]) => Promise<void>;
onDelete: (id: string) => Promise<void>;
onPreview: (entry: SoundboardEntry) => Promise<void>;
@@ -335,6 +346,7 @@ function SoundboardCategoryGroup({
entriesTotal,
busyId,
previewingId,
badges,
onPatch,
onDelete,
onPreview,
@@ -370,6 +382,7 @@ function SoundboardCategoryGroup({
isLast={idx === entries.length - 1}
busy={busyId === entry.id}
previewing={previewingId === entry.id}
badge={badges.get(entry.id)}
onPatch={onPatch}
onDelete={onDelete}
onPreview={onPreview}
@@ -392,6 +405,7 @@ interface RowProps {
isLast: boolean;
busy: boolean;
previewing: boolean;
badge: SyncBadge | undefined;
onPatch: (id: string, patch: Parameters<typeof updateSound>[1]) => Promise<void>;
onDelete: (id: string) => Promise<void>;
onPreview: (entry: SoundboardEntry) => Promise<void>;
@@ -406,6 +420,7 @@ function SoundboardRow({
isLast,
busy,
previewing,
badge,
onPatch,
onDelete,
onPreview,
@@ -503,6 +518,13 @@ function SoundboardRow({
)}
<p className="text-[10px] text-fg-muted">
{(entry.size / BYTES_PER_MB).toFixed(2)} MB · {entry.mime}
<span
title={badgeTitle(badge)}
aria-label={badgeTitle(badge)}
className="ml-2 inline-flex items-center text-[10px] font-medium text-fg-muted"
>
{badgeGlyph(badge)}
</span>
</p>
</div>
@@ -623,3 +645,25 @@ function SoundboardRow({
</li>
);
}
// ---------------------------------------------------------------------------
function badgeGlyph(b: SyncBadge | undefined): string {
switch (b) {
case 'uploading': return '↑';
case 'downloading': return '↓';
case 'error': return '⚠';
case 'synced':
default: return '☁';
}
}
function badgeTitle(b: SyncBadge | undefined): string {
switch (b) {
case 'uploading': return 'Hochladen…';
case 'downloading': return 'Wird heruntergeladen…';
case 'error': return 'Synchronisationsfehler';
case 'synced':
default: return 'Synchronisiert';
}
}