polish(playlists): compact header actions + open-on-YouTube link

The detail header overflowed the items column. Make the secondary actions icon-only
with tooltips (Sync/Export, Reset to YouTube, Delete) keeping only Play all labeled;
replace the long 'Unsynced changes' / 'Synced from YouTube' chips with a compact
accent 'Unsynced' dot (tooltip explains). Add an open-on-YouTube external-link button
next to the name + edit pencil (tooltip), shown for any YouTube-linked playlist.
This commit is contained in:
npeter83 2026-06-15 22:40:21 +02:00
parent 02a90f4c21
commit 2dfafd4484
4 changed files with 51 additions and 31 deletions

View file

@ -20,6 +20,7 @@ import {
ArrowDown,
ArrowUp,
Check,
ExternalLink,
GripVertical,
ListPlus,
Pencil,
@ -320,9 +321,8 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) {
}, [sortedPlaylists]);
const builtin = detail?.kind === "watch_later";
// YouTube-mirrored playlists can be edited locally and synced back (the read-sync skips
// a dirty mirror so it won't clobber unpushed edits); the chip just marks their origin.
const mirrored = detail?.source === "youtube";
// YouTube-mirrored playlists can be edited locally and synced back (the read-sync skips a
// dirty mirror so it won't clobber unpushed edits); the YT-link button marks their origin.
const editable = !builtin;
const linked = editable && !!detail?.yt_playlist_id;
const [syncing, setSyncing] = useState(false);
@ -651,7 +651,7 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) {
className="text-lg font-semibold bg-card border border-border rounded-md px-2 py-0.5 outline-none focus:border-accent"
/>
) : (
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 min-w-0">
<h2 className="text-lg font-semibold truncate">{plName(detail)}</h2>
{editable && (
<button
@ -660,17 +660,30 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) {
setRenaming(true);
}}
title={t("playlists.rename")}
className="text-muted hover:text-fg"
aria-label={t("playlists.rename")}
className="shrink-0 text-muted hover:text-fg"
>
<Pencil className="w-3.5 h-3.5" />
</button>
)}
{detail.yt_playlist_id && (
<a
href={`https://www.youtube.com/playlist?list=${detail.yt_playlist_id}`}
target="_blank"
rel="noreferrer noopener"
title={t("playlists.openOnYoutube")}
aria-label={t("playlists.openOnYoutube")}
className="shrink-0 text-muted hover:text-accent"
>
<ExternalLink className="w-3.5 h-3.5" />
</a>
)}
</div>
)}
<div className="text-xs text-muted mt-0.5">
{t("playlists.itemCount", { count: items.length })}
</div>
<div className="flex gap-2 mt-3 flex-wrap">
<div className="flex gap-1.5 mt-3 flex-wrap items-center">
<button
onClick={() => items.length && setPlayingIndex(0)}
disabled={!items.length}
@ -683,7 +696,8 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) {
onClick={pushToYoutube}
disabled={pushing}
title={linked ? t("playlists.pushToYoutube") : t("playlists.exportToYoutube")}
className={`inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-lg border transition disabled:opacity-40 ${
aria-label={linked ? t("playlists.pushToYoutube") : t("playlists.exportToYoutube")}
className={`inline-flex items-center justify-center p-2 rounded-lg border transition disabled:opacity-40 ${
detail.dirty
? "border-accent text-accent hover:bg-accent/10"
: "border-border text-muted hover:text-fg hover:border-accent"
@ -696,37 +710,37 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) {
) : (
<Upload className="w-3.5 h-3.5" />
)}
{linked ? t("playlists.pushToYoutube") : t("playlists.exportToYoutube")}
</button>
)}
{linked && detail.dirty && (
<button
onClick={revertToYoutube}
disabled={reverting}
title={t("playlists.revert")}
aria-label={t("playlists.revert")}
className="inline-flex items-center justify-center p-2 rounded-lg border border-border text-muted hover:text-fg hover:border-accent transition disabled:opacity-40"
>
<RotateCcw className={`w-3.5 h-3.5 ${reverting ? "animate-spin" : ""}`} />
</button>
)}
{editable && (
<button
onClick={deletePlaylist}
className="inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-lg border border-border text-muted hover:text-fg hover:border-accent transition"
title={t("playlists.delete")}
aria-label={t("playlists.delete")}
className="inline-flex items-center justify-center p-2 rounded-lg border border-border text-muted hover:text-red-400 hover:border-red-400/50 transition"
>
<Trash2 className="w-3.5 h-3.5" /> {t("playlists.delete")}
<Trash2 className="w-3.5 h-3.5" />
</button>
)}
{linked && detail.dirty && (
<>
<button
onClick={revertToYoutube}
disabled={reverting}
title={t("playlists.revert")}
className="inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-lg border border-border text-muted hover:text-fg hover:border-accent transition disabled:opacity-40"
<span
title={t("playlists.unsyncedHint")}
className="inline-flex items-center gap-1.5 text-[11px] text-accent ml-1"
>
<RotateCcw className={`w-3.5 h-3.5 ${reverting ? "animate-spin" : ""}`} />
{t("playlists.revert")}
</button>
<span className="inline-flex items-center gap-1.5 text-[11px] px-2.5 py-1.5 rounded-lg text-accent">
<span className="w-1.5 h-1.5 rounded-full bg-accent" />
{t("playlists.unsynced")}
</span>
</>
)}
{mirrored && (
<span className="inline-flex items-center gap-1.5 text-[11px] px-2.5 py-1.5 rounded-lg text-muted">
<Youtube className="w-3.5 h-3.5" /> {t("playlists.ytReadonly")}
</span>
)}
</div>
</div>

View file

@ -19,7 +19,9 @@
"addToPlaylist": "Zur Wiedergabeliste hinzufügen",
"exportToYoutube": "Zu YouTube exportieren",
"pushToYoutube": "Mit YouTube synchronisieren",
"unsynced": "Nicht synchronisierte Änderungen",
"unsynced": "Nicht synchronisiert",
"unsyncedHint": "Du hast lokale Änderungen, die noch nicht mit YouTube synchronisiert sind.",
"openOnYoutube": "Auf YouTube öffnen",
"revert": "Von YouTube zurücksetzen",
"revertTitle": "Lokale Änderungen verwerfen?",
"revertMsg": "Dies lädt die Wiedergabeliste von YouTube neu und verwirft deine nicht synchronisierten lokalen Änderungen (Reihenfolge, Hinzufügungen, Entfernungen, Umbenennung). Fortfahren?",

View file

@ -19,7 +19,9 @@
"addToPlaylist": "Add to playlist",
"exportToYoutube": "Export to YouTube",
"pushToYoutube": "Sync to YouTube",
"unsynced": "Unsynced changes",
"unsynced": "Unsynced",
"unsyncedHint": "You have local changes not yet synced to YouTube.",
"openOnYoutube": "Open on YouTube",
"revert": "Reset to YouTube",
"revertTitle": "Discard local changes?",
"revertMsg": "This reloads the playlist from YouTube and discards your unsynced local changes (order, additions, removals, rename). Continue?",

View file

@ -19,7 +19,9 @@
"addToPlaylist": "Hozzáadás listához",
"exportToYoutube": "Exportálás YouTube-ra",
"pushToYoutube": "Szinkron YouTube-ra",
"unsynced": "Nem szinkronizált változások",
"unsynced": "Nincs szinkronizálva",
"unsyncedHint": "Vannak helyi módosításaid, amelyek még nincsenek a YouTube-ra szinkronizálva.",
"openOnYoutube": "Megnyitás YouTube-on",
"revert": "Visszaállítás YouTube-ról",
"revertTitle": "Eldobod a helyi módosításokat?",
"revertMsg": "Ez újratölti a listát a YouTube-ról, és eldobja a nem szinkronizált helyi módosításaidat (sorrend, hozzáadás, eltávolítás, átnevezés). Folytatod?",