diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 4e890b5..ff3dfcf 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -39,6 +39,7 @@ const DEFAULT_FILTERS: FeedFilters = {
tagMode: "or",
q: "",
sort: "newest",
+ scope: "my",
includeNormal: true,
includeShorts: false,
includeLive: false,
diff --git a/frontend/src/components/Feed.tsx b/frontend/src/components/Feed.tsx
index 09a05cf..7380bb0 100644
--- a/frontend/src/components/Feed.tsx
+++ b/frontend/src/components/Feed.tsx
@@ -148,7 +148,9 @@ export default function Feed({
if (query.isLoading) return
{t("feed.loading")}
;
if (query.isError) return {t("feed.loadError")}
;
if (items.length === 0) {
- if (!canRead)
+ // No YouTube connection and looking at their own (empty) subscriptions: offer both
+ // connecting YouTube and just browsing the shared library (no read scope needed).
+ if (!canRead && filters.scope === "my")
return (
@@ -160,6 +162,12 @@ export default function Feed({
>
{t("feed.setUp")}
+ setFilters({ ...filters, scope: "all" })}
+ className="block mx-auto mt-3 text-sm text-muted hover:text-accent transition"
+ >
+ {t("feed.browseShared")}
+
);
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx
index 7474091..15dc8d0 100644
--- a/frontend/src/components/Header.tsx
+++ b/frontend/src/components/Header.tsx
@@ -1,6 +1,6 @@
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
-import { BarChart3, Home, Info, LogOut, Search, Settings, Shield, Tv } from "lucide-react";
+import { BarChart3, Home, Info, Library, LogOut, Search, Settings, Shield, Tv, User } from "lucide-react";
import type { FeedFilters, Me } from "../lib/api";
import type { Page } from "../lib/urlState";
import { type LangCode } from "../i18n";
@@ -49,6 +49,35 @@ export default function Header({
+ {page === "feed" && (
+
+ {(["my", "all"] as const).map((s) => (
+ setFilters({ ...filters, scope: s })}
+ title={t("header.scope." + s + "Tip")}
+ aria-pressed={filters.scope === s}
+ className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-full transition ${
+ filters.scope === s
+ ? "bg-accent text-accent-fg"
+ : "text-muted hover:text-fg"
+ }`}
+ >
+ {s === "my" ? (
+
+ ) : (
+
+ )}
+ {t("header.scope." + s)}
+
+ ))}
+
+ )}
+
{page === "feed" ? (
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index 6e37c9a..3ab6c97 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -60,8 +60,9 @@ const DATE_PRESETS: { days: number; key: string }[] = [
{ days: 365, key: "1year" },
];
-// Filter values owned by the sidebar (everything except the header search `q`).
-const DEFAULT_SIDEBAR_FILTERS: Omit = {
+// Filter values owned by the sidebar (everything except the header search `q` and the
+// `scope` mode, both preserved across a "Clear all").
+const DEFAULT_SIDEBAR_FILTERS: Omit = {
tags: [],
tagMode: "or",
sort: "newest",
@@ -170,7 +171,7 @@ export default function Sidebar({
function clearAll() {
setCustomDates(false);
- setFilters({ ...DEFAULT_SIDEBAR_FILTERS, q: filters.q });
+ setFilters({ ...DEFAULT_SIDEBAR_FILTERS, q: filters.q, scope: filters.scope });
}
const available: Record = {
diff --git a/frontend/src/i18n/locales/de/feed.json b/frontend/src/i18n/locales/de/feed.json
index 07e9185..3a46ab8 100644
--- a/frontend/src/i18n/locales/de/feed.json
+++ b/frontend/src/i18n/locales/de/feed.json
@@ -4,6 +4,7 @@
"emptyTitle": "Dein Feed ist leer",
"emptyBody": "Verbinde dein YouTube-Konto, um deine Abos zu importieren und deinen Feed aufzubauen.",
"setUp": "Meinen Feed einrichten",
+ "browseShared": "Oder einfach die gemeinsame Bibliothek durchsuchen",
"noMatches": "Keine Videos entsprechen diesen Filtern.",
"videoCount_one": "{{formattedCount}} Video",
"videoCount_other": "{{formattedCount}} Videos",
diff --git a/frontend/src/i18n/locales/de/header.json b/frontend/src/i18n/locales/de/header.json
index 4465368..b634f4f 100644
--- a/frontend/src/i18n/locales/de/header.json
+++ b/frontend/src/i18n/locales/de/header.json
@@ -3,6 +3,13 @@
"searchPlaceholder": "Deine Abos durchsuchen…",
"channelManager": "Kanalverwaltung",
"usageStats": "Nutzung & Statistik",
+ "scope": {
+ "label": "Feed-Quelle",
+ "my": "Meine",
+ "myTip": "Nur Videos aus deinen eigenen Abos",
+ "all": "Bibliothek",
+ "allTip": "Alle Videos im gemeinsamen Katalog"
+ },
"account": {
"admin": "Admin",
"feed": "Feed",
diff --git a/frontend/src/i18n/locales/en/feed.json b/frontend/src/i18n/locales/en/feed.json
index 571247c..8e30dda 100644
--- a/frontend/src/i18n/locales/en/feed.json
+++ b/frontend/src/i18n/locales/en/feed.json
@@ -4,6 +4,7 @@
"emptyTitle": "Your feed is empty",
"emptyBody": "Connect your YouTube account to import your subscriptions and build your feed.",
"setUp": "Set up my feed",
+ "browseShared": "Or just browse the shared library",
"noMatches": "No videos match these filters.",
"videoCount_one": "{{formattedCount}} video",
"videoCount_other": "{{formattedCount}} videos",
diff --git a/frontend/src/i18n/locales/en/header.json b/frontend/src/i18n/locales/en/header.json
index 7cbe22d..d01e782 100644
--- a/frontend/src/i18n/locales/en/header.json
+++ b/frontend/src/i18n/locales/en/header.json
@@ -3,6 +3,13 @@
"searchPlaceholder": "Search your subscriptions…",
"channelManager": "Channel manager",
"usageStats": "Usage & stats",
+ "scope": {
+ "label": "Feed source",
+ "my": "Mine",
+ "myTip": "Only videos from your own subscriptions",
+ "all": "Library",
+ "allTip": "Every video in the shared catalog"
+ },
"account": {
"admin": "Admin",
"feed": "Feed",
diff --git a/frontend/src/i18n/locales/hu/feed.json b/frontend/src/i18n/locales/hu/feed.json
index 0c30c5e..2995492 100644
--- a/frontend/src/i18n/locales/hu/feed.json
+++ b/frontend/src/i18n/locales/hu/feed.json
@@ -4,6 +4,7 @@
"emptyTitle": "A hírfolyamod üres",
"emptyBody": "Kösd össze a YouTube-fiókodat, hogy importáld a feliratkozásaidat, és felépítsd a hírfolyamodat.",
"setUp": "Hírfolyam beállítása",
+ "browseShared": "Vagy csak böngészd a közös könyvtárat",
"noMatches": "Egy videó sem felel meg ezeknek a szűrőknek.",
"videoCount_one": "{{formattedCount}} videó",
"videoCount_other": "{{formattedCount}} videó",
diff --git a/frontend/src/i18n/locales/hu/header.json b/frontend/src/i18n/locales/hu/header.json
index 44dcb04..535fad8 100644
--- a/frontend/src/i18n/locales/hu/header.json
+++ b/frontend/src/i18n/locales/hu/header.json
@@ -3,6 +3,13 @@
"searchPlaceholder": "Keresés a feliratkozásaid között…",
"channelManager": "Csatornakezelő",
"usageStats": "Használat és statisztika",
+ "scope": {
+ "label": "Hírfolyam forrása",
+ "my": "Sajátom",
+ "myTip": "Csak a saját feliratkozásaid videói",
+ "all": "Könyvtár",
+ "allTip": "A közös könyvtár összes videója"
+ },
"account": {
"admin": "Adminisztrátor",
"feed": "Hírfolyam",
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 95a9c43..e813069 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -76,6 +76,8 @@ export interface FeedFilters {
// Re-roll token for the "shuffle" sort; bumped by the reshuffle button so the
// feed re-queries with a fresh order. Ignored by every other sort.
seed?: number;
+ // "my" = only your subscriptions (default); "all" = the whole shared catalog.
+ scope: "my" | "all";
includeNormal: boolean;
includeShorts: boolean;
includeLive: boolean;
@@ -157,6 +159,7 @@ function feedQuery(f: FeedFilters, offset: number, limit: number): string {
if (f.q) p.set("q", f.q);
p.set("sort", f.sort);
if (f.sort === "shuffle" && f.seed) p.set("seed", String(f.seed));
+ p.set("scope", f.scope);
p.set("show_normal", String(f.includeNormal));
p.set("include_shorts", String(f.includeShorts));
p.set("include_live", String(f.includeLive));