feat(playlists): make YouTube-mirrored playlists editable + syncable

Mirrors (source='youtube') were read-only; per the original two-way design they can
now be edited locally (add/remove/reorder/rename) and pushed back. The read-sync
skips a dirty mirror so it won't clobber unpushed edits; a successful push clears
dirty and lets the mirror refresh. _mark_dirty now covers any YouTube-linked list
(exported local or mirror), and rename marks dirty too. push/delete no longer reject
mirrors. Push reconciles the title as well (cheap playlists.list compare, then
playlists.update only if changed) so a local rename doesn't revert on the next pull.
Frontend: editable = not-built-in (was also excluding mirrors); rows/reorder enabled
for all owned lists; AddToPlaylist popover lists mirrors too (with a YT marker); the
origin chip now reads 'edits sync back'. Trilingual.
This commit is contained in:
npeter83 2026-06-15 21:40:13 +02:00
parent 49d226126c
commit 9acea11010
8 changed files with 58 additions and 26 deletions

View file

@ -2,7 +2,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Check, ListPlus, Plus } from "lucide-react";
import { Check, ListPlus, Plus, Youtube } from "lucide-react";
import { api, type Playlist } from "../lib/api";
// A small popover (portaled to body, so the feed grid can't clip it) for toggling a
@ -118,9 +118,9 @@ export default function AddToPlaylist({
}
}
// YouTube-mirrored playlists are read-only until the write-back phase, so they can't be
// added to here yet.
const lists = (membership.data ?? []).filter((pl) => pl.source !== "youtube");
// All of the user's playlists are add targets, including YouTube-linked ones — adding
// marks them dirty, and a later "Sync to YouTube" pushes the change back.
const lists = membership.data ?? [];
return (
<>
@ -167,9 +167,12 @@ export default function AddToPlaylist({
>
{pl.has_video && <Check className="w-3 h-3" />}
</span>
<span className="truncate">
<span className="truncate flex-1 text-left">
{pl.kind === "watch_later" ? t("playlists.watchLater") : pl.name}
</span>
{(pl.source === "youtube" || pl.yt_playlist_id) && (
<Youtube className="w-3 h-3 shrink-0 text-muted" />
)}
</button>
))}
</div>