From f2b90449841373d9b0fdd70449f3e94c9dd0116d Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 26 Aug 2026 16:45:43 +0200 Subject: feat(video): add an All/Movies/Series filter to the toolbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A segmented control to the left of the search box, defaulting to "All". Applied before the text filter — a title match within a type nobody asked to see still isn't shown. Resets to "All" on group change, matching the text filter's own reset. --- .../src/meshbay_hub/static/locales/de.js | 3 ++ .../src/meshbay_hub/static/locales/en.js | 3 ++ .../src/meshbay_hub/static/locales/es.js | 3 ++ .../src/meshbay_hub/static/locales/fr.js | 3 ++ .../src/meshbay_hub/static/locales/it.js | 3 ++ .../src/meshbay_hub/static/locales/ja.js | 3 ++ .../src/meshbay_hub/static/locales/nl.js | 3 ++ .../src/meshbay_hub/static/locales/pl.js | 3 ++ .../src/meshbay_hub/static/locales/pt-BR.js | 3 ++ .../src/meshbay_hub/static/locales/zh-CN.js | 3 ++ .../meshbay-hub/src/meshbay_hub/static/style.css | 32 ++++++++++++++++++++++ .../src/meshbay_hub/static/video-app.js | 28 +++++++++++++++---- 12 files changed, 85 insertions(+), 5 deletions(-) (limited to 'packages/meshbay-hub/src') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index b12f3f4..d30fb93 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -161,6 +161,9 @@ export default { 'video.err_mse': 'Codec wird für das Streaming nicht unterstützt: {codec}', 'video.mode_poster': 'Poster', 'video.mode_flat': 'Flache Liste', + 'video.filter_all': 'All', + 'video.filter_movies': 'Movies', + 'video.filter_series': 'Series', 'video.empty': 'Keine Videos gefunden.', 'video.no_root_configured': 'Für diese Gruppe ist noch kein Videos-Stammordner festgelegt — ein Operator kann in den Einstellungen einen auswählen.', 'music.no_root_configured': 'Für diese Gruppe ist noch kein Musik-Stammordner festgelegt — ein Operator kann in den Einstellungen einen auswählen.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index 54f47e9..e9ef2cc 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -159,6 +159,9 @@ export default { 'video.err_mse': 'Codec not supported for streaming: {codec}', 'video.mode_poster': 'Posters', 'video.mode_flat': 'Flat list', + 'video.filter_all': 'All', + 'video.filter_movies': 'Movies', + 'video.filter_series': 'Series', 'video.empty': 'No videos found.', 'video.no_root_configured': 'No Videos root folder is set for this group yet — an operator can choose one in Settings.', 'music.no_root_configured': 'No Music root folder is set for this group yet — an operator can choose one in Settings.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index 03d1d62..6a92e01 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -159,6 +159,9 @@ export default { 'video.err_mse': 'Códec no compatible con la reproducción en continuo: {codec}', 'video.mode_poster': 'Pósteres', 'video.mode_flat': 'Lista plana', + 'video.filter_all': 'All', + 'video.filter_movies': 'Movies', + 'video.filter_series': 'Series', 'video.empty': 'No se encontraron vídeos.', 'video.no_root_configured': 'Aún no se ha definido una carpeta raíz de Vídeos para este grupo — un operador puede elegir una en Configuración.', 'music.no_root_configured': 'Aún no se ha definido una carpeta raíz de Música para este grupo — un operador puede elegir una en Configuración.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index 48289d6..48b10fb 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -160,6 +160,9 @@ export default { 'video.err_mse': 'Codec non pris en charge pour la diffusion : {codec}', 'video.mode_poster': 'Affiches', 'video.mode_flat': 'Liste à plat', + 'video.filter_all': 'Tous', + 'video.filter_movies': 'Films', + 'video.filter_series': 'Séries', 'video.empty': 'Aucune vidéo trouvée.', 'video.no_root_configured': "Aucun dossier racine des Vidéos n'est encore défini pour ce groupe — un opérateur peut en choisir un dans les Paramètres.", 'music.no_root_configured': 'Aucun dossier racine de Musique n\'est encore défini pour ce groupe — un opérateur peut en choisir un dans les Paramètres.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index f73da13..50aee7c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -160,6 +160,9 @@ export default { 'video.err_mse': 'Codec non supportato per lo streaming: {codec}', 'video.mode_poster': 'Locandine', 'video.mode_flat': 'Elenco semplice', + 'video.filter_all': 'All', + 'video.filter_movies': 'Movies', + 'video.filter_series': 'Series', 'video.empty': 'Nessun video trovato.', 'video.no_root_configured': 'Per questo gruppo non è ancora impostata una cartella radice di Video — un operatore può sceglierne una nelle Impostazioni.', 'music.no_root_configured': 'Per questo gruppo non è ancora impostata una cartella radice di Musica — un operatore può sceglierne una nelle Impostazioni.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index 0b04cca..f0b7e4b 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -157,6 +157,9 @@ export default { 'video.err_mse': 'ストリーミング再生に対応していないコーデックです:{codec}', 'video.mode_poster': 'ポスター表示', 'video.mode_flat': 'フラット表示', + 'video.filter_all': 'All', + 'video.filter_movies': 'Movies', + 'video.filter_series': 'Series', 'video.empty': '動画が見つかりません。', 'video.no_root_configured': 'このグループにはまだ動画のルートフォルダが設定されていません — 操作者が設定画面で選択できます。', 'music.no_root_configured': 'このグループにはまだ音楽のルートフォルダが設定されていません — 操作者が設定画面で選択できます。', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index ef8a60b..8f3178a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -161,6 +161,9 @@ export default { 'video.err_mse': 'Codec wordt niet ondersteund voor streamen: {codec}', 'video.mode_poster': 'Posters', 'video.mode_flat': 'Platte lijst', + 'video.filter_all': 'All', + 'video.filter_movies': 'Movies', + 'video.filter_series': 'Series', 'video.empty': "Geen video's gevonden.", 'video.no_root_configured': "Er is nog geen hoofdmap voor Video's ingesteld voor deze groep — een operator kan er een kiezen bij Instellingen.", 'music.no_root_configured': 'Er is nog geen hoofdmap voor Muziek ingesteld voor deze groep — een operator kan er een kiezen bij Instellingen.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index 63ef1f7..73db56f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -166,6 +166,9 @@ export default { 'video.err_mse': 'Kodek nieobsługiwany przy odtwarzaniu strumieniowym: {codec}', 'video.mode_poster': 'Plakaty', 'video.mode_flat': 'Lista płaska', + 'video.filter_all': 'All', + 'video.filter_movies': 'Movies', + 'video.filter_series': 'Series', 'video.empty': 'Nie znaleziono żadnych filmów.', 'video.no_root_configured': 'Dla tej grupy nie wybrano jeszcze katalogu głównego Wideo — operator może go wybrać w Ustawieniach.', 'music.no_root_configured': 'Dla tej grupy nie wybrano jeszcze katalogu głównego Muzyki — operator może go wybrać w Ustawieniach.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index 71f2829..1fc70dd 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -161,6 +161,9 @@ export default { 'video.err_mse': 'Codec sem suporte para transmissão: {codec}', 'video.mode_poster': 'Pôsteres', 'video.mode_flat': 'Lista simples', + 'video.filter_all': 'All', + 'video.filter_movies': 'Movies', + 'video.filter_series': 'Series', 'video.empty': 'Nenhum vídeo encontrado.', 'video.no_root_configured': 'Ainda não há uma pasta raiz de Vídeos definida para este grupo — um operador pode escolher uma em Configurações.', 'music.no_root_configured': 'Ainda não há uma pasta raiz de Música definida para este grupo — um operador pode escolher uma em Configurações.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index d88817f..e67eed8 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -154,6 +154,9 @@ export default { 'video.err_mse': '该编解码器不支持流式播放:{codec}', 'video.mode_poster': '海报视图', 'video.mode_flat': '平铺列表', + 'video.filter_all': 'All', + 'video.filter_movies': 'Movies', + 'video.filter_series': 'Series', 'video.empty': '未找到视频。', 'video.no_root_configured': '此群组尚未设置视频根目录 — 操作员可以在设置中选择一个。', 'music.no_root_configured': '此群组尚未设置音乐根目录 — 操作员可以在设置中选择一个。', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index f426f8d..61404a9 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -2563,6 +2563,38 @@ a.transfer-name { } .video-toolbar .tb-search { margin-left: auto; } +/* All/Movies/Series — a segmented control, not three separate .tb-btn: the + three are mutually exclusive (unlike the view-mode buttons, which read + fine standalone), and grouping them under one shared border is what says + so at a glance. */ +.tb-typefilter { + display: inline-flex; + height: 32px; + border: 1px solid var(--border); + border-radius: 6px; + overflow: hidden; + flex-shrink: 0; +} +.tb-typefilter button { + display: inline-flex; + align-items: center; + height: 100%; + padding: 0 12px; + border: none; + background: var(--bg-surface); + color: var(--text-dim); + font-size: 0.83em; + white-space: nowrap; + cursor: pointer; + transition: background 0.12s, color 0.12s; +} +.tb-typefilter button + button { border-left: 1px solid var(--border); } +.tb-typefilter button:hover { color: var(--accent); } +.tb-typefilter button.active { + background: var(--accent); + color: var(--accent-text); +} + /* Mode A — poster grid */ .video-grid { diff --git a/packages/meshbay-hub/src/meshbay_hub/static/video-app.js b/packages/meshbay-hub/src/meshbay_hub/static/video-app.js index ce38b0e..dd120da 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/video-app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/video-app.js @@ -758,10 +758,14 @@ function VideoApp({ }) { const [mode, setMode] = useState(loadViewMode); const [filter, setFilter] = useState(''); + // Which of movies/shows to show at all — independent of the text filter + // below, and applied first: a title match within a type nobody asked to + // see is still not what "Movies only" means. + const [typeFilter, setTypeFilter] = useState('all'); const tmdbEnabled = tmdbConfig ? tmdbConfig.enabled : true; useEffect(() => { setMode(loadViewMode()); }, [groupId]); - useEffect(() => { setFilter(''); }, [groupId]); + useEffect(() => { setFilter(''); setTypeFilter('all'); }, [groupId]); const setModeAndSave = (m) => { setMode(m); saveViewMode(m); }; @@ -769,10 +773,10 @@ function VideoApp({ () => groupVideoEntries(entries, videoRoot), [entries, videoRoot]); const needle = filter.trim().toLowerCase(); - const filteredMovies = useMemo(() => (!needle ? movies : movies.filter( - (e) => (e.display_title || e.name).toLowerCase().includes(needle))), [movies, needle]); - const filteredShows = useMemo(() => (!needle ? shows : shows.filter( - (s) => s.title.toLowerCase().includes(needle))), [shows, needle]); + const filteredMovies = useMemo(() => (typeFilter === 'series' ? [] : !needle ? movies : movies.filter( + (e) => (e.display_title || e.name).toLowerCase().includes(needle))), [movies, needle, typeFilter]); + const filteredShows = useMemo(() => (typeFilter === 'movies' ? [] : !needle ? shows : shows.filter( + (s) => s.title.toLowerCase().includes(needle))), [shows, needle, typeFilter]); return html` ${(status === 'discovering' || status === 'connecting' || status === 'fetching') && html` @@ -794,6 +798,20 @@ function VideoApp({ onClick=${() => setModeAndSave('flat')}> ${t('video.mode_flat')} +
+ + + +