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:
parent
49d226126c
commit
9acea11010
8 changed files with 58 additions and 26 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -161,9 +161,10 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) {
|
|||
const plName = (p: { kind: string; name: string }) =>
|
||||
p.kind === "watch_later" ? t("playlists.watchLater") : p.name;
|
||||
const builtin = detail?.kind === "watch_later";
|
||||
// YouTube-mirrored playlists are read-only here until the write-back phase.
|
||||
// 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";
|
||||
const editable = !builtin && !mirrored;
|
||||
const editable = !builtin;
|
||||
const linked = editable && !!detail?.yt_playlist_id;
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [pushing, setPushing] = useState(false);
|
||||
|
|
@ -253,7 +254,7 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) {
|
|||
|
||||
async function onDragEnd(e: DragEndEvent) {
|
||||
const { active: a, over } = e;
|
||||
if (!over || a.id === over.id || selectedId == null || mirrored) return;
|
||||
if (!over || a.id === over.id || selectedId == null) return;
|
||||
const oldIndex = items.findIndex((v) => v.id === a.id);
|
||||
const newIndex = items.findIndex((v) => v.id === over.id);
|
||||
if (oldIndex < 0 || newIndex < 0) return;
|
||||
|
|
@ -513,7 +514,6 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) {
|
|||
key={v.id}
|
||||
video={v}
|
||||
index={i}
|
||||
readOnly={mirrored}
|
||||
onPlay={() => setPlayingIndex(i)}
|
||||
onRemove={() => removeItem(v.id)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"syncYoutube": "Von YouTube synchronisieren",
|
||||
"syncedToast": "{{count}} Wiedergabelisten von YouTube synchronisiert",
|
||||
"syncFailed": "Synchronisierung von YouTube fehlgeschlagen",
|
||||
"ytReadonly": "Von YouTube synchronisiert — vorerst schreibgeschützt",
|
||||
"ytReadonly": "Von YouTube synchronisiert — Änderungen werden zurücksynchronisiert",
|
||||
"noneYet": "Noch keine Wiedergabelisten",
|
||||
"newPlaylist": "Neue Liste…",
|
||||
"itemCount_one": "{{count}} Video",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"syncYoutube": "Sync from YouTube",
|
||||
"syncedToast": "Synced {{count}} playlists from YouTube",
|
||||
"syncFailed": "Couldn't sync from YouTube",
|
||||
"ytReadonly": "Synced from YouTube — read-only for now",
|
||||
"ytReadonly": "Synced from YouTube — edits sync back",
|
||||
"noneYet": "No playlists yet",
|
||||
"newPlaylist": "New playlist…",
|
||||
"itemCount_one": "{{count}} video",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"syncYoutube": "Szinkron YouTube-ról",
|
||||
"syncedToast": "{{count}} lista szinkronizálva a YouTube-ról",
|
||||
"syncFailed": "Nem sikerült a YouTube-szinkron",
|
||||
"ytReadonly": "YouTube-ról szinkronizálva — egyelőre csak olvasható",
|
||||
"ytReadonly": "YouTube-ról szinkronizálva — a módosítások visszaszinkronizálhatók",
|
||||
"noneYet": "Még nincs lejátszási lista",
|
||||
"newPlaylist": "Új lista…",
|
||||
"itemCount_one": "{{count}} videó",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue