feat(playlists): reset a playlist to its YouTube state (discard local edits)
The in-session undo/redo is lost when switching playlists, leaving no way to undo
local edits to a YouTube-linked list afterwards. Add a per-playlist 'Reset to YouTube'
action (shown when a linked playlist is dirty) that force-repulls that single playlist
from YouTube, replacing its items/order/name and clearing dirty — even though the bulk
read-sync skips dirty playlists. Backend: repull_playlist() + POST /api/playlists/
{id}/revert-youtube (read-scope gated). Confirm dialog (destructive). Trilingual.
This commit is contained in:
parent
4efeb2fd43
commit
a8a3734496
7 changed files with 120 additions and 4 deletions
|
|
@ -27,6 +27,7 @@ import {
|
|||
Play,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
RotateCcw,
|
||||
Trash2,
|
||||
Upload,
|
||||
X,
|
||||
|
|
@ -326,6 +327,28 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) {
|
|||
const linked = editable && !!detail?.yt_playlist_id;
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [pushing, setPushing] = useState(false);
|
||||
const [reverting, setReverting] = useState(false);
|
||||
|
||||
async function revertToYoutube() {
|
||||
if (selectedId == null || !detail || reverting) return;
|
||||
const ok = await confirm({
|
||||
title: t("playlists.revertTitle"),
|
||||
message: t("playlists.revertMsg"),
|
||||
confirmLabel: t("playlists.revertConfirm"),
|
||||
danger: true,
|
||||
});
|
||||
if (!ok) return;
|
||||
setReverting(true);
|
||||
try {
|
||||
await api.revertPlaylist(selectedId);
|
||||
refreshAll();
|
||||
notify({ level: "success", message: t("playlists.revertDone") });
|
||||
} catch {
|
||||
notify({ level: "warning", message: t("playlists.revertFailed") });
|
||||
} finally {
|
||||
setReverting(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function pushToYoutube() {
|
||||
if (selectedId == null || !detail || pushing) return;
|
||||
|
|
@ -685,9 +708,20 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) {
|
|||
</button>
|
||||
)}
|
||||
{linked && detail.dirty && (
|
||||
<span className="inline-flex items-center gap-1.5 text-[11px] px-2.5 py-1.5 rounded-lg text-accent">
|
||||
{t("playlists.unsynced")}
|
||||
</span>
|
||||
<>
|
||||
<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"
|
||||
>
|
||||
<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">
|
||||
{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">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue