video discover: give the Browse panel its own language filter (self-contained search)

The Browse-all grid silently inherited the global rail language preference, so users
couldn't ad-hoc browse foreign cinema without changing their homepage prefs. Added a
language chipset to the Browse panel (auto-wired via the generic chip handler -> state.sel.lang)
and the grid now always sends lang= : a real code filters, 'any' opts OUT of the rail
preference entirely. Route treats lang=any as 'no language filter'. Added a 'Browse the full
catalog' eyebrow so the panel reads as a self-contained search, parallel to 'Across Discover'.

Hide-owned and the saved 'My services' pref stay single/global by design (they apply page-wide);
the Browse panel's provider chips remain its own grid filter.
This commit is contained in:
BoulderBadgeDad 2026-06-23 09:26:22 -07:00
parent dffc5c2e5d
commit 73395b9668
4 changed files with 28 additions and 4 deletions

View file

@ -282,13 +282,19 @@ def register_routes(bp):
if providers and "," in providers:
providers = providers.replace(",", "|") # TMDB with_watch_providers OR-join
sort = request.args.get("sort") or "popularity.desc"
lang = (request.args.get("lang") or "").strip() or None # explicit (foreign rail)
lang = (request.args.get("lang") or "").strip().lower() or None # explicit (foreign rail / browse)
# The Browse-all grid sends `lang=any` to opt OUT of the rail language preference
# entirely (ad-hoc search shows every language); a real code (en/ko/…) filters to it.
any_lang = lang in ("any", "all")
if any_lang:
lang = None
hide_owned = (request.args.get("hide_owned") or "") in ("1", "true", "yes")
# Preferred original-languages (multi) for GENERAL/curated rails — so the feeds
# aren't flooded with foreign titles (e.g. Bollywood in Popular/Trending). A rail
# with an explicit `lang` (a dedicated foreign rail) bypasses this. Default 'en'.
# with an explicit `lang` (a dedicated foreign rail) or `lang=any` (browse) bypasses
# this. Default 'en'.
prefer_langs = None
if not lang:
if not lang and not any_lang:
try:
from . import get_video_db
raw = get_video_db().get_setting("discover_languages", "en") or "en"

View file

@ -1072,6 +1072,7 @@
</div>
<div class="vdsc-browse">
<div class="vdsc-browse-label">Browse the full catalog</div>
<div class="vdsc-browse-top">
<div class="vdsc-seg" data-vdsc-seg="kind" role="group" aria-label="Type">
<button class="vdsc-seg-btn vdsc-seg-btn--on" type="button" data-val="movie">Movies</button>
@ -1097,6 +1098,17 @@
<button class="vdsc-chip" type="button" data-val="531" style="--c: 14, 116, 240;">Paramount+</button>
<button class="vdsc-chip" type="button" data-val="386" style="--c: 147, 112, 219;">Peacock</button>
</div>
<div class="vdsc-chips" data-vdsc-chipset="lang" aria-label="Language">
<button class="vdsc-chip vdsc-chip--reset vdsc-chip--on" type="button" data-val="">Any language</button>
<button class="vdsc-chip" type="button" data-val="en">English</button>
<button class="vdsc-chip" type="button" data-val="ko">Korean</button>
<button class="vdsc-chip" type="button" data-val="ja">Japanese</button>
<button class="vdsc-chip" type="button" data-val="es">Spanish</button>
<button class="vdsc-chip" type="button" data-val="fr">French</button>
<button class="vdsc-chip" type="button" data-val="hi">Hindi</button>
<button class="vdsc-chip" type="button" data-val="de">German</button>
<button class="vdsc-chip" type="button" data-val="it">Italian</button>
</div>
<div class="vdsc-browse-bottom">
<div class="vdsc-chips vdsc-chips--era" data-vdsc-chipset="decade" aria-label="Decade">
<button class="vdsc-chip vdsc-chip--reset vdsc-chip--on" type="button" data-val="">Any era</button>

View file

@ -23,7 +23,7 @@
io: null,
hero: { items: [], idx: 0, timer: null },
cat: { title: '', q: '', page: 1, paginates: true, busy: false, hasMore: false },
sel: { kind: 'movie', genre: '', decade: '', providers: '', sort: 'popularity.desc' }, // Browse panel
sel: { kind: 'movie', genre: '', decade: '', providers: '', lang: '', sort: 'popularity.desc' }, // Browse panel
};
var AUTO = (typeof IntersectionObserver !== 'undefined'); // infinite-scroll capable
var sentinelVisible = false;
@ -587,6 +587,10 @@
if (s.genre) { q.push('genre=' + s.genre); var gn = genreName(s.kind, s.genre); if (gn) bits.push(gn); }
if (s.providers) { q.push('providers=' + s.providers); var pn = activeChipText('providers'); if (pn) bits.push('on ' + pn); }
if (s.decade) { q.push('decade=' + s.decade); bits.push(s.decade + 's'); }
// Browse-all is a self-contained search: always send a language so it never silently
// inherits the rail language preference. 'any' = show every language; a code filters.
q.push('lang=' + (s.lang || 'any'));
if (s.lang) { var ln = activeChipText('lang'); if (ln) bits.push(ln); }
openCategory(bits.join(' · '), q.join('&'));
}
function genreName(kind, id) {

View file

@ -2372,6 +2372,8 @@ body[data-side="video"] #soulsync-toggle { display: none; }
color: rgba(255, 255, 255, 0.32); margin-right: 2px; }
.vdsc-global .vdsc-toggle { margin-left: 0; }
.vdsc-global .vdsc-langs { margin-left: 0; }
.vdsc-browse-label { font-size: 10.5px; font-weight: 800; letter-spacing: 0.6px; text-transform: uppercase;
color: rgba(255, 255, 255, 0.32); margin: 0 0 10px 2px; }
/* ── 'Not interested' card button ──────────────────────────────────────────── */
.vsr-card { position: relative; }