From 459fc93e98f23e326c2fa77fe86ba74c2bae77f0 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Tue, 15 Sep 2026 17:01:06 +0200 Subject: feat(hub): Videos and Music list their cards a page at a time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous/next arrows in the pinned toolbar, on group pages and in Search. Page size is an account preference (Settings → Defaults), 50 by default, 10 to 200 in steps of 10. Co-Authored-By: Claude Opus 5 --- packages/meshbay-hub/src/meshbay_hub/api/users.py | 1 + .../src/meshbay_hub/static/group-page.js | 2 +- .../meshbay-hub/src/meshbay_hub/static/icon.js | 2 + .../src/meshbay_hub/static/locales/de.js | 5 + .../src/meshbay_hub/static/locales/en.js | 5 + .../src/meshbay_hub/static/locales/es.js | 5 + .../src/meshbay_hub/static/locales/fr.js | 5 + .../src/meshbay_hub/static/locales/it.js | 5 + .../src/meshbay_hub/static/locales/ja.js | 5 + .../src/meshbay_hub/static/locales/nl.js | 5 + .../src/meshbay_hub/static/locales/pl.js | 5 + .../src/meshbay_hub/static/locales/pt-BR.js | 5 + .../src/meshbay_hub/static/locales/zh-CN.js | 5 + .../src/meshbay_hub/static/music-app.js | 44 ++++-- .../meshbay-hub/src/meshbay_hub/static/pager.js | 89 +++++++++++ .../src/meshbay_hub/static/search-page.js | 2 + .../src/meshbay_hub/static/settings-page.js | 28 ++++ .../meshbay-hub/src/meshbay_hub/static/style.css | 18 +++ .../src/meshbay_hub/static/video-app.js | 165 ++++++++++++--------- .../tests/harness/sticky_header_probe.py | 12 +- packages/meshbay-hub/tests/test_hook_ordering.py | 2 +- packages/meshbay-hub/tests/test_media_pager.py | 71 +++++++++ packages/meshbay-hub/tests/test_sticky_header.py | 13 ++ .../meshbay-hub/tests/test_transport_contracts.py | 2 +- 24 files changed, 419 insertions(+), 82 deletions(-) create mode 100644 packages/meshbay-hub/src/meshbay_hub/static/pager.js create mode 100644 packages/meshbay-hub/tests/test_media_pager.py (limited to 'packages') diff --git a/packages/meshbay-hub/src/meshbay_hub/api/users.py b/packages/meshbay-hub/src/meshbay_hub/api/users.py index a74f6af..991a4f7 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/users.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/users.py @@ -1198,6 +1198,7 @@ ALLOWED_PREF_KEYS = frozenset([ "notifications_disabled", "default_tab", "music_keep_screen_on", + "media_page_size", ]) # `default_tab:`, which is what the SPA writes (group-page.js). The diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js index 51e39dc..4de33b9 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js @@ -760,7 +760,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // wire too, so nothing anywhere carries them. ...perAppDirectories, tmdbConfig, - musicbrainzConfig, onPlayQueue, + musicbrainzConfig, onPlayQueue, userPrefs, }; return html` diff --git a/packages/meshbay-hub/src/meshbay_hub/static/icon.js b/packages/meshbay-hub/src/meshbay_hub/static/icon.js index 90d9194..f45d339 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/icon.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/icon.js @@ -64,6 +64,8 @@ const ICON_PATHS = { 'M14.5 6.5l3 3'], check: ['M4.5 12.5l5 5 10-11'], chevron: ['M6 9.5l6 6 6-6'], + 'chevron-left': ['M14.5 6l-6 6 6 6'], + 'chevron-right': ['M9.5 6l6 6-6 6'], close: ['M6 6l12 12M18 6L6 18'], chat: ['M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z'], folder: ['M3.5 6.6a1 1 0 0 1 1-1h4.2l2 2.4h7.8a1 1 0 0 1 1 1v9.4a1 1 0 0 1-1 1h-14a1 1 0 0 1-1-1z'], 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 6e5afa6..c705588 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -256,6 +256,9 @@ export default { 'video.filter_all': 'All', 'video.filter_movies': 'Movies', 'video.filter_series': 'Series', + 'pager.range': '{from}–{to} von {total}', + 'pager.previous': 'Vorherige Seite', + 'pager.next': 'Nächste Seite', '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.', @@ -365,6 +368,8 @@ export default { 'settings.defaults': 'Standardwerte', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', + 'settings.media_page_size': 'Einträge pro Seite in Videos und Musik', + 'settings.media_page_size_hint': 'Wie viele Poster oder Alben auf einmal angezeigt werden; die Pfeile in der Werkzeugleiste blättern weiter.', 'settings.music_keep_screen_on': 'Keep screen on during music playback', 'settings.music_keep_screen_on_hint': 'Prevents the phone from locking on its own while a track is playing.', 'settings.node_pins': 'Node-Identitäten', 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 7ffbc28..4f0fff2 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -254,6 +254,9 @@ export default { 'video.filter_all': 'All', 'video.filter_movies': 'Movies', 'video.filter_series': 'Series', + 'pager.range': '{from}–{to} of {total}', + 'pager.previous': 'Previous page', + 'pager.next': 'Next page', '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.', @@ -362,6 +365,8 @@ export default { 'settings.defaults': 'Defaults', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', + 'settings.media_page_size': 'Items per page in Videos and Music', + 'settings.media_page_size_hint': 'How many posters or albums are shown at once; the arrows in the toolbar move to the next batch.', 'settings.music_keep_screen_on': 'Keep screen on during music playback', 'settings.music_keep_screen_on_hint': 'Prevents the phone from locking on its own while a track is playing.', 'settings.node_pins': 'Node identities', 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 e675e10..fa0b0b2 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -254,6 +254,9 @@ export default { 'video.filter_all': 'All', 'video.filter_movies': 'Movies', 'video.filter_series': 'Series', + 'pager.range': '{from}–{to} de {total}', + 'pager.previous': 'Página anterior', + 'pager.next': 'Página siguiente', '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.', @@ -363,6 +366,8 @@ export default { 'settings.defaults': 'Valores predeterminados', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', + 'settings.media_page_size': 'Elementos por página en Vídeos y Música', + 'settings.media_page_size_hint': 'Cuántos pósters o álbumes se muestran a la vez; las flechas de la barra de herramientas pasan al siguiente grupo.', 'settings.music_keep_screen_on': 'Keep screen on during music playback', 'settings.music_keep_screen_on_hint': 'Prevents the phone from locking on its own while a track is playing.', 'settings.node_pins': 'Identidades de los nodes', 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 cc0892d..b4145ce 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -255,6 +255,9 @@ export default { 'video.filter_all': 'Tous', 'video.filter_movies': 'Films', 'video.filter_series': 'Séries', + 'pager.range': '{from}–{to} sur {total}', + 'pager.previous': 'Page précédente', + 'pager.next': 'Page suivante', '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.', @@ -364,6 +367,8 @@ export default { 'settings.defaults': 'Valeurs par défaut', 'settings.default_tab': 'Onglet par défaut', 'settings.default_tab_hint': 'L\'onglet qui s\'ouvre en premier quand vous entrez dans un groupe.', + 'settings.media_page_size': 'Éléments par page dans Vidéos et Musique', + 'settings.media_page_size_hint': 'Nombre d\'affiches ou d\'albums affichés à la fois ; les flèches de la barre d\'outils passent à la tranche suivante.', 'settings.music_keep_screen_on': 'Garder l\'écran allumé pendant l\'écoute', 'settings.music_keep_screen_on_hint': 'Empêche le téléphone de se verrouiller automatiquement pendant qu\'un morceau joue.', 'settings.node_pins': 'Identités des nodes', 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 7ccaff4..673f0cc 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -255,6 +255,9 @@ export default { 'video.filter_all': 'All', 'video.filter_movies': 'Movies', 'video.filter_series': 'Series', + 'pager.range': '{from}–{to} di {total}', + 'pager.previous': 'Pagina precedente', + 'pager.next': 'Pagina successiva', '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.', @@ -364,6 +367,8 @@ export default { 'settings.defaults': 'Valori predefiniti', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', + 'settings.media_page_size': 'Elementi per pagina in Video e Musica', + 'settings.media_page_size_hint': 'Quanti poster o album vengono mostrati alla volta; le frecce nella barra degli strumenti passano al gruppo successivo.', 'settings.music_keep_screen_on': 'Keep screen on during music playback', 'settings.music_keep_screen_on_hint': 'Prevents the phone from locking on its own while a track is playing.', 'settings.node_pins': 'Identità dei node', 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 aaec812..c36f2c9 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -252,6 +252,9 @@ export default { 'video.filter_all': 'All', 'video.filter_movies': 'Movies', 'video.filter_series': 'Series', + 'pager.range': '{from}–{to} / {total}', + 'pager.previous': '前のページ', + 'pager.next': '次のページ', 'video.empty': '動画が見つかりません。', 'video.no_root_configured': 'このグループにはまだ動画のルートフォルダが設定されていません — 操作者が設定画面で選択できます。', 'music.no_root_configured': 'このグループにはまだ音楽のルートフォルダが設定されていません — 操作者が設定画面で選択できます。', @@ -361,6 +364,8 @@ export default { 'settings.defaults': 'デフォルト', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', + 'settings.media_page_size': 'ビデオと音楽の1ページあたりの表示数', + 'settings.media_page_size_hint': '一度に表示するポスターやアルバムの数。ツールバーの矢印で次のまとまりに移動します。', 'settings.music_keep_screen_on': 'Keep screen on during music playback', 'settings.music_keep_screen_on_hint': 'Prevents the phone from locking on its own while a track is playing.', 'settings.node_pins': 'node の識別情報', 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 3049cef..180c1bb 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -256,6 +256,9 @@ export default { 'video.filter_all': 'All', 'video.filter_movies': 'Movies', 'video.filter_series': 'Series', + 'pager.range': '{from}–{to} van {total}', + 'pager.previous': 'Vorige pagina', + 'pager.next': 'Volgende pagina', '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.', @@ -365,6 +368,8 @@ export default { 'settings.defaults': 'Standaardwaarden', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', + 'settings.media_page_size': 'Items per pagina in Video\'s en Muziek', + 'settings.media_page_size_hint': 'Hoeveel posters of albums tegelijk worden getoond; de pijlen in de werkbalk gaan naar de volgende reeks.', 'settings.music_keep_screen_on': 'Keep screen on during music playback', 'settings.music_keep_screen_on_hint': 'Prevents the phone from locking on its own while a track is playing.', 'settings.node_pins': 'Node-identiteiten', 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 8922d0a..180cdbe 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -261,6 +261,9 @@ export default { 'video.filter_all': 'All', 'video.filter_movies': 'Movies', 'video.filter_series': 'Series', + 'pager.range': '{from}–{to} z {total}', + 'pager.previous': 'Poprzednia strona', + 'pager.next': 'Następna strona', '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.', @@ -376,6 +379,8 @@ export default { 'settings.defaults': 'Wartości domyślne', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', + 'settings.media_page_size': 'Elementy na stronę w Wideo i Muzyce', + 'settings.media_page_size_hint': 'Ile plakatów lub albumów jest wyświetlanych naraz; strzałki na pasku narzędzi przechodzą do kolejnej porcji.', 'settings.music_keep_screen_on': 'Keep screen on during music playback', 'settings.music_keep_screen_on_hint': 'Prevents the phone from locking on its own while a track is playing.', 'settings.node_pins': 'Tożsamości nodes', 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 460a902..2ae26b2 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 @@ -256,6 +256,9 @@ export default { 'video.filter_all': 'All', 'video.filter_movies': 'Movies', 'video.filter_series': 'Series', + 'pager.range': '{from}–{to} de {total}', + 'pager.previous': 'Página anterior', + 'pager.next': 'Próxima página', '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.', @@ -365,6 +368,8 @@ export default { 'settings.defaults': 'Padrões', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', + 'settings.media_page_size': 'Itens por página em Vídeos e Música', + 'settings.media_page_size_hint': 'Quantos pôsteres ou álbuns são exibidos de uma vez; as setas na barra de ferramentas passam para o próximo lote.', 'settings.music_keep_screen_on': 'Keep screen on during music playback', 'settings.music_keep_screen_on_hint': 'Prevents the phone from locking on its own while a track is playing.', 'settings.node_pins': 'Identidades dos nodes', 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 b8ab4ef..6c0ec37 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 @@ -249,6 +249,9 @@ export default { 'video.filter_all': 'All', 'video.filter_movies': 'Movies', 'video.filter_series': 'Series', + 'pager.range': '{from}–{to} / 共 {total}', + 'pager.previous': '上一页', + 'pager.next': '下一页', 'video.empty': '未找到视频。', 'video.no_root_configured': '此群组尚未设置视频根目录 — 操作员可以在设置中选择一个。', 'music.no_root_configured': '此群组尚未设置音乐根目录 — 操作员可以在设置中选择一个。', @@ -357,6 +360,8 @@ export default { 'settings.defaults': '默认值', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', + 'settings.media_page_size': '视频和音乐每页显示数量', + 'settings.media_page_size_hint': '一次显示多少张海报或专辑;工具栏中的箭头可切换到下一批。', 'settings.music_keep_screen_on': 'Keep screen on during music playback', 'settings.music_keep_screen_on_hint': 'Prevents the phone from locking on its own while a track is playing.', 'settings.node_pins': 'node 身份', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/music-app.js b/packages/meshbay-hub/src/meshbay_hub/static/music-app.js index f622b5d..60a7b00 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/music-app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/music-app.js @@ -6,6 +6,7 @@ import { Icon } from './icon.js'; import { MediaThumb, LazyTile } from './video-app.js'; import { formatTime } from './music-player.js'; import { SourceTag } from './group-name.js'; +import { usePager, Pager, pageSizeFrom } from './pager.js'; // -- Music -------------------------------------------------------------------- // @@ -324,8 +325,16 @@ function MusicDetailModal({ album, transportRef, gekRef, musicbrainzEnabled, onC // -- Mode A: album grid ----------------------------------------------------- -function AlbumGrid({ artists, transportRef, gekRef, musicbrainzEnabled, onPlayQueue }) { +// `units` is one page of `{ artist, album }`. An artist whose albums straddle +// two pages gets its heading on both. +function AlbumGrid({ units, transportRef, gekRef, musicbrainzEnabled, onPlayQueue }) { const [detail, setDetail] = useState(null); // the album object + const artists = []; + for (const u of units) { + const prev = artists[artists.length - 1]; + if (prev && prev.artist === u.artist) prev.albums.push(u.album); + else artists.push({ artist: u.artist, albums: [u.album] }); + } return html` ${artists.map((a) => html` @@ -435,12 +444,8 @@ function FlatArtistFolder({ artist, onPlayQueue }) { `; } -function FlatList({ tracks, artists, onPlayQueue }) { - const items = [ - ...tracks.map((tr) => ({ key: tr.display_title || tr.name, kind: 'track', track: tr })), - ...artists.map((a) => ({ key: a.artist, kind: 'artist', artist: a })), - ].sort((a, b) => a.key.localeCompare(b.key)); - +// `items` is one page of rows, already sorted by MusicApp. +function FlatList({ items, onPlayQueue }) { return html`
${items.map((it) => it.kind === 'track' @@ -456,7 +461,7 @@ function FlatList({ tracks, artists, onPlayQueue }) { function MusicApp({ groupId, transportRef, gekRef, status, entries, availableEntries, musicDirectories, musicbrainzConfig, onPlayQueue, - hideFilter, + hideFilter, userPrefs, pageResetKey, }) { const [mode, setMode] = useState(loadViewMode); const [filter, setFilter] = useState(''); @@ -488,6 +493,24 @@ function MusicApp({ const filteredTracks = useMemo(() => (!needle ? tracks : tracks.filter( (tr) => (tr.display_title || tr.name).toLowerCase().includes(needle))), [tracks, needle]); + // What this mode draws, in drawing order: albums under their artists in the + // grid, and in the flat list loose tracks and artist folders sorted together. + const units = useMemo(() => { + if (mode === 'grid') { + return filteredArtists.flatMap((a) => a.albums.map((album) => ({ artist: a.artist, album }))); + } + return [ + ...filteredTracks.map((tr) => ({ key: tr.display_title || tr.name, kind: 'track', track: tr })), + ...filteredArtists.map((a) => ({ key: a.artist, kind: 'artist', artist: a })), + ].sort((a, b) => a.key.localeCompare(b.key)); + }, [mode, filteredArtists, filteredTracks]); + + const pager = usePager(units.length, pageSizeFrom(userPrefs), + `${groupId}|${mode}|${needle}|${pageResetKey || ''}`); + const pageUnits = useMemo(() => { + return units.slice(pager.start, pager.end); + }, [units, pager.start, pager.end]); + const empty = albums.length === 0 && tracks.length === 0; return html` @@ -510,6 +533,7 @@ function MusicApp({ onClick=${() => setModeAndSave('flat')}> ${t('music.mode_flat')} + <${Pager} pager=${pager} /> ${!hideFilter && html`

${t('settings.default_tab_hint')}

+
+ ${t('settings.media_page_size')} + +
+

${t('settings.media_page_size_hint')}

${t('settings.music_keep_screen_on')}