diff --git a/frontend/src/components/ChannelDiscovery.tsx b/frontend/src/components/ChannelDiscovery.tsx index 3bf291a1..12ac9f33 100644 --- a/frontend/src/components/ChannelDiscovery.tsx +++ b/frontend/src/components/ChannelDiscovery.tsx @@ -97,6 +97,34 @@ export default function ChannelDiscovery({ ); const columns: Column[] = [ + // Actions first, mirroring the Subscriptions table (where it sits right after Prio) so the two + // tabs' action clusters line up in the same place. + { + key: "actions", + header: t("channels.cols.actions"), + align: "left", + nowrap: true, + width: "120px", + cardLabel: false, + render: (c) => ( + + + + ), + }, { key: "channel", header: t("channels.cols.channel"), @@ -156,32 +184,6 @@ export default function ChannelDiscovery({ ), }, - { - key: "actions", - header: t("channels.cols.actions"), - align: "right", - nowrap: true, - width: "120px", - cardLabel: false, - render: (c) => ( - - - - ), - }, ]; // Card layout paginates like the table (slice the sorted rows; the Pager reports changes). diff --git a/frontend/src/components/ChannelsProvider.tsx b/frontend/src/components/ChannelsProvider.tsx index c7989d5d..e47bfc3f 100644 --- a/frontend/src/components/ChannelsProvider.tsx +++ b/frontend/src/components/ChannelsProvider.tsx @@ -120,9 +120,19 @@ export function ChannelsProvider({ children }: { children: ReactNode }) { setStatusFilterState(f); setAccountRaw(LS.channelFilter, f); }, []); + // The active tab rides in history.state (`_chTab`), the same way the channel page (`_chan`) and the + // live YouTube search (`_yt`) do — so Back steps back through the tabs you actually visited instead + // of jumping straight out to the previous module. const setView = useCallback((v: ChannelsView) => { setViewState(v); setAccountRaw(LS.channelsView, v); + const st = window.history.state || {}; + // Only own an entry while the manager is already on screen. Arriving from elsewhere + // (focusChannel / goToFullHistory) lets setPage push the entry — the stamp effect labels it — so + // one navigation never costs two Back presses. + if (st.sfPage === "channels") { + window.history.pushState({ ...st, sfPage: "channels", _chTab: v }, ""); + } }, []); const setLayout = useCallback((l: ChannelLayout) => { setLayoutState(l); @@ -154,6 +164,27 @@ export function ChannelsProvider({ children }: { children: ReactNode }) { if (page !== "channels") setFocusName(null); }, [page]); + // Label the entry the manager was REACHED on (setPage pushes a bare { sfPage }), so stepping back + // onto it later restores the tab it belonged to rather than leaving whatever tab is current. + useEffect(() => { + if (page !== "channels") return; + const st = window.history.state || {}; + if (st._chTab === undefined) window.history.replaceState({ ...st, _chTab: view }, ""); + }, [page, view]); + + // Back/Forward across tab entries: restore the tab the entry carries. + useEffect(() => { + function onPop(e: PopStateEvent) { + const tab = e.state?._chTab; + if (!tab) return; // an entry outside the manager — leave the tab alone + const next = clampView(tab as string); + setViewState(next); + setAccountRaw(LS.channelsView, next); + } + window.addEventListener("popstate", onPop); + return () => window.removeEventListener("popstate", onPop); + }, []); + const state = useMemo( () => ({ search, statusFilter, view, layout, sort, filtersResetToken, focusName, focusToken }), [search, statusFilter, view, layout, sort, filtersResetToken, focusName, focusToken], diff --git a/frontend/src/components/Pager.tsx b/frontend/src/components/Pager.tsx index 68a96347..ac8eb8c5 100644 --- a/frontend/src/components/Pager.tsx +++ b/frontend/src/components/Pager.tsx @@ -34,9 +34,17 @@ export default function Pager({ // Local text for the editable "jump to page" box (committed on Enter/blur). const [pageInput, setPageInput] = useState(String(safePage + 1)); useEffect(() => setPageInput(String(safePage + 1)), [safePage]); + + // Turning the page swaps the whole list, so put the reader at its top — landing mid-list in a page + // you haven't seen is disorienting. The page scroller is the app's
(the same implicit + // contract BackToTop and VirtualGrid's getScrollParent rely on). + function goToPage(p: number) { + onPageChange(p); + document.querySelector("main")?.scrollTo({ top: 0 }); + } function commitPageInput() { const n = parseInt(pageInput, 10); - if (!isNaN(n)) onPageChange(Math.min(totalPages - 1, Math.max(0, n - 1))); + if (!isNaN(n)) goToPage(Math.min(totalPages - 1, Math.max(0, n - 1))); else setPageInput(String(safePage + 1)); } @@ -46,7 +54,7 @@ export default function Pager({ {totalPages > 1 && (