From 7afaba0547a9f88b95590994af2858754ed30dac Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sat, 19 Sep 2026 17:58:40 +0200 Subject: [PATCH] feat(playlists): say which lists YouTube's API cannot give us MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The module shows fewer lists than youtube.com, and without a word about it that read as a Siftlode bug (the owner's ask, 2026-09-03). Owner's pick from the S7 mockups (2026-09-19): one quiet line at the foot of the rail, "Missing a list? Why?", whose Why? opens three facts — YouTube's own Watch later and History are closed to its API (the Watch later list here is Siftlode's own), only lists you own sync, and an account with no YouTube channel has no playlists at all. --- .../PlaylistLimitsHint.dom.test.tsx | 46 +++++++++++++++++ .../src/components/PlaylistLimitsHint.tsx | 49 +++++++++++++++++++ frontend/src/components/PlaylistsRail.tsx | 2 + frontend/src/i18n/locales/en/playlists.json | 10 +++- frontend/src/i18n/locales/hu/playlists.json | 10 +++- 5 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/PlaylistLimitsHint.dom.test.tsx create mode 100644 frontend/src/components/PlaylistLimitsHint.tsx diff --git a/frontend/src/components/PlaylistLimitsHint.dom.test.tsx b/frontend/src/components/PlaylistLimitsHint.dom.test.tsx new file mode 100644 index 00000000..092ad38b --- /dev/null +++ b/frontend/src/components/PlaylistLimitsHint.dom.test.tsx @@ -0,0 +1,46 @@ +// @vitest-environment jsdom +// +// The playlists module shows fewer lists than youtube.com, and said nothing about why, so it read as +// a Siftlode bug (debt burn A3). One quiet line at the foot of the rail; "Why?" opens the three facts. +import { describe, it, expect, vi, afterEach, beforeAll } from "vitest"; +import { render, screen, fireEvent, cleanup } from "@testing-library/react"; +import { FloatingTree } from "@floating-ui/react"; + +vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (k: string) => k }) })); + +import PlaylistLimitsHint from "./PlaylistLimitsHint"; + +// Same floating-ui recipe as ContextMenu.dom.test.tsx: stub the observers, and query with +// `hidden: true` — floating-ui never positions in jsdom, so the surface stays visibility:hidden. +beforeAll(() => { + class Stub { + observe() {} + unobserve() {} + disconnect() {} + takeRecords() { + return []; + } + } + globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver; + globalThis.IntersectionObserver ??= Stub as unknown as typeof IntersectionObserver; +}); +afterEach(cleanup); + +describe("PlaylistLimitsHint", () => { + it("keeps the facts behind Why?, and opens them as a named dialog", async () => { + render( + + + , + ); + expect(screen.getByText("playlists.limits.hint")).toBeTruthy(); + expect(screen.queryByRole("dialog", { hidden: true })).toBeNull(); + + fireEvent.click(screen.getByRole("button", { name: "playlists.limits.why" })); + const dialog = await screen.findByRole("dialog", { hidden: true }); + // Read off the attribute: jsdom computes no name for a visibility:hidden element. + expect(dialog.getAttribute("aria-label")).toBe("playlists.limits.title"); + for (const fact of ["closed", "ownOnly", "noChannel"]) + expect(dialog.textContent).toContain(`playlists.limits.${fact}`); + }); +}); diff --git a/frontend/src/components/PlaylistLimitsHint.tsx b/frontend/src/components/PlaylistLimitsHint.tsx new file mode 100644 index 00000000..e9682ce3 --- /dev/null +++ b/frontend/src/components/PlaylistLimitsHint.tsx @@ -0,0 +1,49 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Info } from "lucide-react"; + +import { PopoverSurface } from "./ui/AnchoredPopover"; +import { useAnchoredPopover } from "./ui/anchoredPopoverHooks"; + +// The playlists module shows fewer lists than youtube.com does, and without a word about it that +// reads as Siftlode being broken (the owner's ask, 2026-09-03: say it is YouTube's API, up front). +// Three facts, behind one quiet line at the foot of the rail — the owner's pick from the S7 mockups +// (2026-09-19): always there to find again, never in the way. +export default function PlaylistLimitsHint() { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const pop = useAnchoredPopover({ + open, + onOpenChange: setOpen, + mode: "dialog", + role: "dialog", + placement: "right-end", + offset: 8, + }); + return ( +
+
+ ); +} diff --git a/frontend/src/components/PlaylistsRail.tsx b/frontend/src/components/PlaylistsRail.tsx index ad6ce147..10c32ba5 100644 --- a/frontend/src/components/PlaylistsRail.tsx +++ b/frontend/src/components/PlaylistsRail.tsx @@ -22,6 +22,7 @@ import { usePlaylists, usePlaylistsActions } from "./playlistsContext"; import { useMe } from "../lib/useMe"; import { playlistName } from "../lib/playlistName"; import { usePlaylistActions } from "./usePlaylistActions"; +import PlaylistLimitsHint from "./PlaylistLimitsHint"; type PlSort = { key: "custom" | "name" | "count" | "duration" | "recent"; @@ -382,6 +383,7 @@ export default function PlaylistsRail({ )} + ), }, diff --git a/frontend/src/i18n/locales/en/playlists.json b/frontend/src/i18n/locales/en/playlists.json index d4483693..67276bfc 100644 --- a/frontend/src/i18n/locales/en/playlists.json +++ b/frontend/src/i18n/locales/en/playlists.json @@ -118,5 +118,13 @@ "viewDiffers": "Temporary view ({{view}}) — the playlist's saved order is unchanged. Save it to keep this order.", "viewMatches": "This view ({{view}}) already matches the playlist's saved order, so there is nothing to save.", "viewFiltered": "Filtered view — {{shown}} of {{total}} shown. The order can't be saved while a filter is on.", - "addFailed": "Couldn't add it to the playlist." + "addFailed": "Couldn't add it to the playlist.", + "limits": { + "hint": "Missing a list?", + "why": "Why?", + "title": "What YouTube lets Siftlode read", + "closed": "YouTube's own Watch later and History have been closed to its API since 2016, so no app can read them. The Watch later list here is Siftlode's own.", + "ownOnly": "Only playlists you own sync. Lists you saved from other channels are not returned.", + "noChannel": "A Google account with no YouTube channel has no playlists at all, as far as the API is concerned." + } } diff --git a/frontend/src/i18n/locales/hu/playlists.json b/frontend/src/i18n/locales/hu/playlists.json index 8b6c104d..45be5d2c 100644 --- a/frontend/src/i18n/locales/hu/playlists.json +++ b/frontend/src/i18n/locales/hu/playlists.json @@ -118,5 +118,13 @@ "viewDiffers": "Ideiglenes nézet ({{view}}) — a lista mentett sorrendje változatlan. Mentsd, ha ez maradjon.", "viewMatches": "Ez a nézet ({{view}}) megegyezik a lista mentett sorrendjével, így nincs mit menteni.", "viewFiltered": "Szűrt nézet — {{total}} elemből {{shown}} látszik. Szűrés közben a sorrend nem menthető.", - "addFailed": "Nem sikerült hozzáadni a lejátszási listához." + "addFailed": "Nem sikerült hozzáadni a lejátszási listához.", + "limits": { + "hint": "Hiányzik egy lista?", + "why": "Miért?", + "title": "Amit a YouTube kiad a Siftlode-nak", + "closed": "A YouTube saját „Megnézem később” listája és az Előzmények 2016 óta zárva vannak az API előtt, így egyik alkalmazás sem olvashatja őket. Az itteni „Megnézem később” lista a Siftlode saját listája.", + "ownOnly": "Csak a saját listáid szinkronizálódnak. A más csatornáktól elmentett listákat az API nem adja vissza.", + "noChannel": "Ha a Google-fiókodhoz nem tartozik YouTube-csatorna, az API szerint egy listád sincs." + } }