From 857b414631743c6ed3ac0d7ccebf56aab0166b4c Mon Sep 17 00:00:00 2001 From: mattsigal Date: Sat, 6 Jun 2026 10:11:06 -0700 Subject: [PATCH 1/3] feat(library): add horizontal scroll direction option to library display settings - Add LibraryScrollDirection enum (vertical/horizontal) to preference_constants.dart - Add libraryScrollDirection() per-library preference factory to user_preferences.dart (key: library_scroll_dir_, default: vertical) - Wire _scrollDirection field, getter, constructor init, and setScrollDirection() into LibraryBrowseViewModel - Add 'Scroll Direction' section as first option in the library Display Settings cog, above Image Type, with Vertical/Horizontal radio tiles matching existing style - Split _buildGrid() into _buildVerticalGrid() (existing behavior) and _buildHorizontalGrid() (new Axis.horizontal CustomScrollView with auto row count calculated from available height) - Add scrollDirection, scrollDirectionVertical, scrollDirectionHorizontal l10n keys to app_en.arb; auto-generated all locale stubs --- .../viewmodels/library_browse_view_model.dart | 16 ++ lib/l10n/app_en.arb | 12 ++ lib/l10n/app_localizations.dart | 18 ++ lib/l10n/app_localizations_af.dart | 9 + lib/l10n/app_localizations_ar.dart | 9 + lib/l10n/app_localizations_be.dart | 9 + lib/l10n/app_localizations_bg.dart | 9 + lib/l10n/app_localizations_bn.dart | 9 + lib/l10n/app_localizations_ca.dart | 9 + lib/l10n/app_localizations_cs.dart | 9 + lib/l10n/app_localizations_cy.dart | 9 + lib/l10n/app_localizations_da.dart | 9 + lib/l10n/app_localizations_de.dart | 9 + lib/l10n/app_localizations_el.dart | 9 + lib/l10n/app_localizations_en.dart | 9 + lib/l10n/app_localizations_eo.dart | 9 + lib/l10n/app_localizations_es.dart | 9 + lib/l10n/app_localizations_et.dart | 9 + lib/l10n/app_localizations_fa.dart | 9 + lib/l10n/app_localizations_fi.dart | 9 + lib/l10n/app_localizations_fr.dart | 9 + lib/l10n/app_localizations_gl.dart | 9 + lib/l10n/app_localizations_he.dart | 9 + lib/l10n/app_localizations_hi.dart | 9 + lib/l10n/app_localizations_hr.dart | 9 + lib/l10n/app_localizations_hu.dart | 9 + lib/l10n/app_localizations_id.dart | 9 + lib/l10n/app_localizations_it.dart | 9 + lib/l10n/app_localizations_ja.dart | 9 + lib/l10n/app_localizations_kk.dart | 9 + lib/l10n/app_localizations_kn.dart | 9 + lib/l10n/app_localizations_ko.dart | 9 + lib/l10n/app_localizations_lt.dart | 9 + lib/l10n/app_localizations_lv.dart | 9 + lib/l10n/app_localizations_mk.dart | 9 + lib/l10n/app_localizations_ml.dart | 9 + lib/l10n/app_localizations_mn.dart | 9 + lib/l10n/app_localizations_nb.dart | 9 + lib/l10n/app_localizations_nl.dart | 9 + lib/l10n/app_localizations_pa.dart | 9 + lib/l10n/app_localizations_pl.dart | 9 + lib/l10n/app_localizations_pt.dart | 9 + lib/l10n/app_localizations_ro.dart | 9 + lib/l10n/app_localizations_ru.dart | 9 + lib/l10n/app_localizations_si.dart | 9 + lib/l10n/app_localizations_sk.dart | 9 + lib/l10n/app_localizations_sl.dart | 9 + lib/l10n/app_localizations_sq.dart | 9 + lib/l10n/app_localizations_sr.dart | 9 + lib/l10n/app_localizations_sv.dart | 9 + lib/l10n/app_localizations_sw.dart | 9 + lib/l10n/app_localizations_ta.dart | 9 + lib/l10n/app_localizations_te.dart | 9 + lib/l10n/app_localizations_th.dart | 9 + lib/l10n/app_localizations_tl.dart | 9 + lib/l10n/app_localizations_tr.dart | 9 + lib/l10n/app_localizations_ug.dart | 9 + lib/l10n/app_localizations_uk.dart | 9 + lib/l10n/app_localizations_vi.dart | 9 + lib/l10n/app_localizations_yue.dart | 9 + lib/l10n/app_localizations_zh.dart | 9 + lib/preference/preference_constants.dart | 5 + lib/preference/user_preferences.dart | 8 + .../screens/browse/library_browse_screen.dart | 176 +++++++++++++++++- 64 files changed, 756 insertions(+), 1 deletion(-) diff --git a/lib/data/viewmodels/library_browse_view_model.dart b/lib/data/viewmodels/library_browse_view_model.dart index 7ef5897ac..b6f5d0022 100644 --- a/lib/data/viewmodels/library_browse_view_model.dart +++ b/lib/data/viewmodels/library_browse_view_model.dart @@ -84,6 +84,9 @@ class LibraryBrowseViewModel extends ChangeNotifier { late PosterSize _posterSize; PosterSize get posterSize => _posterSize; + late LibraryScrollDirection _scrollDirection; + LibraryScrollDirection get scrollDirection => _scrollDirection; + String? _errorMessage; String? get errorMessage => _errorMessage; @@ -167,6 +170,9 @@ class LibraryBrowseViewModel extends ChangeNotifier { _letterFilter = ''; _imageType = _prefs.get(UserPreferences.libraryImageType(_imagePrefKey)); _posterSize = _readScopedPosterSize(); + _scrollDirection = _prefs.get( + UserPreferences.libraryScrollDirection(_imagePrefKey), + ); } String get _prefKey => genreId ?? libraryId; @@ -643,6 +649,16 @@ class LibraryBrowseViewModel extends ChangeNotifier { notifyListeners(); } + Future setScrollDirection(LibraryScrollDirection value) async { + if (_scrollDirection == value) return; + _scrollDirection = value; + await _prefs.set( + UserPreferences.libraryScrollDirection(_imagePrefKey), + value, + ); + notifyListeners(); + } + bool get isSeriesLibrary => _collectionType == 'tvshows' || (includeItemTypes != null && includeItemTypes!.contains('Series')); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b255eb5f9..97d8af4fe 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -472,6 +472,18 @@ "@extraLarge": { "description": "Poster size option" }, + "scrollDirection": "Scroll Direction", + "@scrollDirection": { + "description": "Section header for scroll direction selection in library display settings" + }, + "scrollDirectionVertical": "Vertical", + "@scrollDirectionVertical": { + "description": "Library scroll direction option: vertical grid (default)" + }, + "scrollDirectionHorizontal": "Horizontal", + "@scrollDirectionHorizontal": { + "description": "Library scroll direction option: horizontal grid for ultrawide displays" + }, "libraryGenresTitle": "{name} \u2014 Genres", "@libraryGenresTitle": { "description": "Header title showing library name with genres suffix", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 0902045e8..1f55917e9 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -857,6 +857,24 @@ abstract class AppLocalizations { /// **'Extra Large'** String get extraLarge; + /// Section header for scroll direction selection in library display settings + /// + /// In en, this message translates to: + /// **'Scroll Direction'** + String get scrollDirection; + + /// Library scroll direction option: vertical grid (default) + /// + /// In en, this message translates to: + /// **'Vertical'** + String get scrollDirectionVertical; + + /// Library scroll direction option: horizontal grid for ultrawide displays + /// + /// In en, this message translates to: + /// **'Horizontal'** + String get scrollDirectionHorizontal; + /// Header title showing library name with genres suffix /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_af.dart b/lib/l10n/app_localizations_af.dart index 475cbe572..6a25c75f9 100644 --- a/lib/l10n/app_localizations_af.dart +++ b/lib/l10n/app_localizations_af.dart @@ -359,6 +359,15 @@ class AppLocalizationsAf extends AppLocalizations { @override String get extraLarge => 'Ekstra groot'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 14a55ffda..d8a7aa41e 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -355,6 +355,15 @@ class AppLocalizationsAr extends AppLocalizations { @override String get extraLarge => 'كبير جدا'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — الأنواع'; diff --git a/lib/l10n/app_localizations_be.dart b/lib/l10n/app_localizations_be.dart index 63dbb19e3..beffb6968 100644 --- a/lib/l10n/app_localizations_be.dart +++ b/lib/l10n/app_localizations_be.dart @@ -359,6 +359,15 @@ class AppLocalizationsBe extends AppLocalizations { @override String get extraLarge => 'Вельмі вялікі'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Жанры'; diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 716dc3cc9..4fb63d5d9 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -358,6 +358,15 @@ class AppLocalizationsBg extends AppLocalizations { @override String get extraLarge => 'Изключително голям'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Жанрове'; diff --git a/lib/l10n/app_localizations_bn.dart b/lib/l10n/app_localizations_bn.dart index 9b0e684d7..e062ac44e 100644 --- a/lib/l10n/app_localizations_bn.dart +++ b/lib/l10n/app_localizations_bn.dart @@ -358,6 +358,15 @@ class AppLocalizationsBn extends AppLocalizations { @override String get extraLarge => 'অতিরিক্ত বড়'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — ধরন'; diff --git a/lib/l10n/app_localizations_ca.dart b/lib/l10n/app_localizations_ca.dart index 73aafcc69..ed13fe4ce 100644 --- a/lib/l10n/app_localizations_ca.dart +++ b/lib/l10n/app_localizations_ca.dart @@ -364,6 +364,15 @@ class AppLocalizationsCa extends AppLocalizations { @override String get extraLarge => 'Extragran'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Gèneres'; diff --git a/lib/l10n/app_localizations_cs.dart b/lib/l10n/app_localizations_cs.dart index ced726323..818dbf1b2 100644 --- a/lib/l10n/app_localizations_cs.dart +++ b/lib/l10n/app_localizations_cs.dart @@ -360,6 +360,15 @@ class AppLocalizationsCs extends AppLocalizations { @override String get extraLarge => 'Extra velké'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Žánry'; diff --git a/lib/l10n/app_localizations_cy.dart b/lib/l10n/app_localizations_cy.dart index 8daa96c85..328eebd96 100644 --- a/lib/l10n/app_localizations_cy.dart +++ b/lib/l10n/app_localizations_cy.dart @@ -360,6 +360,15 @@ class AppLocalizationsCy extends AppLocalizations { @override String get extraLarge => 'Mawr Ychwanegol'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_da.dart b/lib/l10n/app_localizations_da.dart index 6f8a50a21..c2873b46e 100644 --- a/lib/l10n/app_localizations_da.dart +++ b/lib/l10n/app_localizations_da.dart @@ -359,6 +359,15 @@ class AppLocalizationsDa extends AppLocalizations { @override String get extraLarge => 'Ekstra stor'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genrer'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index d4bc17d7b..22af07e25 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -362,6 +362,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String get extraLarge => 'Sehr groß'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name – Genres'; diff --git a/lib/l10n/app_localizations_el.dart b/lib/l10n/app_localizations_el.dart index 629d0858e..1e5fba09b 100644 --- a/lib/l10n/app_localizations_el.dart +++ b/lib/l10n/app_localizations_el.dart @@ -363,6 +363,15 @@ class AppLocalizationsEl extends AppLocalizations { @override String get extraLarge => 'Πολύ μεγάλο'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Είδη'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 7445e2bb6..614ec4010 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -359,6 +359,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get extraLarge => 'Extra Large'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_eo.dart b/lib/l10n/app_localizations_eo.dart index 9db0922ca..f425503e9 100644 --- a/lib/l10n/app_localizations_eo.dart +++ b/lib/l10n/app_localizations_eo.dart @@ -358,6 +358,15 @@ class AppLocalizationsEo extends AppLocalizations { @override String get extraLarge => 'Ekstra Granda'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Ĝenroj'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index cf1705a76..93ef87b8f 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -361,6 +361,15 @@ class AppLocalizationsEs extends AppLocalizations { @override String get extraLarge => 'Muy grande'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Géneros'; diff --git a/lib/l10n/app_localizations_et.dart b/lib/l10n/app_localizations_et.dart index 32c8039e4..522536061 100644 --- a/lib/l10n/app_localizations_et.dart +++ b/lib/l10n/app_localizations_et.dart @@ -360,6 +360,15 @@ class AppLocalizationsEt extends AppLocalizations { @override String get extraLarge => 'Eriti suur'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Žanrid'; diff --git a/lib/l10n/app_localizations_fa.dart b/lib/l10n/app_localizations_fa.dart index ebdd80116..3b617aa22 100644 --- a/lib/l10n/app_localizations_fa.dart +++ b/lib/l10n/app_localizations_fa.dart @@ -357,6 +357,15 @@ class AppLocalizationsFa extends AppLocalizations { @override String get extraLarge => 'فوق العاده بزرگ'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name - ​​ژانرها'; diff --git a/lib/l10n/app_localizations_fi.dart b/lib/l10n/app_localizations_fi.dart index 56ba6f4b8..0a44e98ae 100644 --- a/lib/l10n/app_localizations_fi.dart +++ b/lib/l10n/app_localizations_fi.dart @@ -361,6 +361,15 @@ class AppLocalizationsFi extends AppLocalizations { @override String get extraLarge => 'Erittäin suuri'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genret'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index f9a27f723..e770195cc 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -360,6 +360,15 @@ class AppLocalizationsFr extends AppLocalizations { @override String get extraLarge => 'Très grand'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_gl.dart b/lib/l10n/app_localizations_gl.dart index 344339a4e..f2667e230 100644 --- a/lib/l10n/app_localizations_gl.dart +++ b/lib/l10n/app_localizations_gl.dart @@ -363,6 +363,15 @@ class AppLocalizationsGl extends AppLocalizations { @override String get extraLarge => 'Extra grande'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Xéneros'; diff --git a/lib/l10n/app_localizations_he.dart b/lib/l10n/app_localizations_he.dart index 8fab4358f..d43b420a1 100644 --- a/lib/l10n/app_localizations_he.dart +++ b/lib/l10n/app_localizations_he.dart @@ -354,6 +354,15 @@ class AppLocalizationsHe extends AppLocalizations { @override String get extraLarge => 'גדול במיוחד'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 86fff2818..d1082dd3f 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -359,6 +359,15 @@ class AppLocalizationsHi extends AppLocalizations { @override String get extraLarge => 'एक्स्ट्रा लार्ज'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_hr.dart b/lib/l10n/app_localizations_hr.dart index 2764ee1d4..8b2d34123 100644 --- a/lib/l10n/app_localizations_hr.dart +++ b/lib/l10n/app_localizations_hr.dart @@ -359,6 +359,15 @@ class AppLocalizationsHr extends AppLocalizations { @override String get extraLarge => 'Iznimno velik'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 79d7dc085..22691a500 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -359,6 +359,15 @@ class AppLocalizationsHu extends AppLocalizations { @override String get extraLarge => 'Extra nagy'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 1c916073b..90243cff6 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -359,6 +359,15 @@ class AppLocalizationsId extends AppLocalizations { @override String get extraLarge => 'Ekstra besar'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index ebbd58bf9..94b6044e2 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -360,6 +360,15 @@ class AppLocalizationsIt extends AppLocalizations { @override String get extraLarge => 'Extra Grande'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 0905453a7..ecdba016d 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -350,6 +350,15 @@ class AppLocalizationsJa extends AppLocalizations { @override String get extraLarge => '特大'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_kk.dart b/lib/l10n/app_localizations_kk.dart index f1d95c344..08abf30a3 100644 --- a/lib/l10n/app_localizations_kk.dart +++ b/lib/l10n/app_localizations_kk.dart @@ -360,6 +360,15 @@ class AppLocalizationsKk extends AppLocalizations { @override String get extraLarge => 'Өте үлкен'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_kn.dart b/lib/l10n/app_localizations_kn.dart index 4675385db..411e066bc 100644 --- a/lib/l10n/app_localizations_kn.dart +++ b/lib/l10n/app_localizations_kn.dart @@ -359,6 +359,15 @@ class AppLocalizationsKn extends AppLocalizations { @override String get extraLarge => 'ಹೆಚ್ಚುವರಿ ದೊಡ್ಡದು'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 6a42055fe..e683cd898 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -350,6 +350,15 @@ class AppLocalizationsKo extends AppLocalizations { @override String get extraLarge => '특대'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_lt.dart b/lib/l10n/app_localizations_lt.dart index 200e84a22..2808702da 100644 --- a/lib/l10n/app_localizations_lt.dart +++ b/lib/l10n/app_localizations_lt.dart @@ -358,6 +358,15 @@ class AppLocalizationsLt extends AppLocalizations { @override String get extraLarge => 'Itin didelis'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_lv.dart b/lib/l10n/app_localizations_lv.dart index 9d0b3f9a4..9e67c1ff9 100644 --- a/lib/l10n/app_localizations_lv.dart +++ b/lib/l10n/app_localizations_lv.dart @@ -360,6 +360,15 @@ class AppLocalizationsLv extends AppLocalizations { @override String get extraLarge => 'Īpaši liels'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Žanri'; diff --git a/lib/l10n/app_localizations_mk.dart b/lib/l10n/app_localizations_mk.dart index 9ab4c8420..c4733bee5 100644 --- a/lib/l10n/app_localizations_mk.dart +++ b/lib/l10n/app_localizations_mk.dart @@ -359,6 +359,15 @@ class AppLocalizationsMk extends AppLocalizations { @override String get extraLarge => 'Екстра голем'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_ml.dart b/lib/l10n/app_localizations_ml.dart index d5fd353b7..255e6fcf5 100644 --- a/lib/l10n/app_localizations_ml.dart +++ b/lib/l10n/app_localizations_ml.dart @@ -361,6 +361,15 @@ class AppLocalizationsMl extends AppLocalizations { @override String get extraLarge => 'എക്സ്ട്രാ ലാർജ്'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_mn.dart b/lib/l10n/app_localizations_mn.dart index b3079d983..a7cc1b84c 100644 --- a/lib/l10n/app_localizations_mn.dart +++ b/lib/l10n/app_localizations_mn.dart @@ -360,6 +360,15 @@ class AppLocalizationsMn extends AppLocalizations { @override String get extraLarge => 'Хэт том'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Төрөл'; diff --git a/lib/l10n/app_localizations_nb.dart b/lib/l10n/app_localizations_nb.dart index a3d79124c..b595fbd84 100644 --- a/lib/l10n/app_localizations_nb.dart +++ b/lib/l10n/app_localizations_nb.dart @@ -358,6 +358,15 @@ class AppLocalizationsNb extends AppLocalizations { @override String get extraLarge => 'Ekstra stor'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 49cbadaca..d4bb16d4a 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -361,6 +361,15 @@ class AppLocalizationsNl extends AppLocalizations { @override String get extraLarge => 'Extra groot'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_pa.dart b/lib/l10n/app_localizations_pa.dart index bb8fdf050..9de4c5da5 100644 --- a/lib/l10n/app_localizations_pa.dart +++ b/lib/l10n/app_localizations_pa.dart @@ -359,6 +359,15 @@ class AppLocalizationsPa extends AppLocalizations { @override String get extraLarge => 'ਵਾਧੂ ਵੱਡਾ'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 4b86687a3..d92f1b207 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -360,6 +360,15 @@ class AppLocalizationsPl extends AppLocalizations { @override String get extraLarge => 'Bardzo duży'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 9f4d7c054..d8eae19b4 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -359,6 +359,15 @@ class AppLocalizationsPt extends AppLocalizations { @override String get extraLarge => 'Extra Grande'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Gêneros'; diff --git a/lib/l10n/app_localizations_ro.dart b/lib/l10n/app_localizations_ro.dart index 223d3b66a..279c3c549 100644 --- a/lib/l10n/app_localizations_ro.dart +++ b/lib/l10n/app_localizations_ro.dart @@ -359,6 +359,15 @@ class AppLocalizationsRo extends AppLocalizations { @override String get extraLarge => 'Extra Large'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genuri'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index d6e6d52de..cbdc355a6 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -359,6 +359,15 @@ class AppLocalizationsRu extends AppLocalizations { @override String get extraLarge => 'Очень большой'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_si.dart b/lib/l10n/app_localizations_si.dart index 447bd00b1..935608108 100644 --- a/lib/l10n/app_localizations_si.dart +++ b/lib/l10n/app_localizations_si.dart @@ -358,6 +358,15 @@ class AppLocalizationsSi extends AppLocalizations { @override String get extraLarge => 'අති විශාල'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index b406c9db9..c87d3713d 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -359,6 +359,15 @@ class AppLocalizationsSk extends AppLocalizations { @override String get extraLarge => 'Extra veľké'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 27863d4e5..42563883f 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -362,6 +362,15 @@ class AppLocalizationsSl extends AppLocalizations { @override String get extraLarge => 'Zelo velik'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_sq.dart b/lib/l10n/app_localizations_sq.dart index 197ec0c05..1cdc1ff68 100644 --- a/lib/l10n/app_localizations_sq.dart +++ b/lib/l10n/app_localizations_sq.dart @@ -362,6 +362,15 @@ class AppLocalizationsSq extends AppLocalizations { @override String get extraLarge => 'Extra Large'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_sr.dart b/lib/l10n/app_localizations_sr.dart index bf77189e3..ad0cac611 100644 --- a/lib/l10n/app_localizations_sr.dart +++ b/lib/l10n/app_localizations_sr.dart @@ -359,6 +359,15 @@ class AppLocalizationsSr extends AppLocalizations { @override String get extraLarge => 'Ектра Ларге'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index fa44205b4..e1b80bf18 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -357,6 +357,15 @@ class AppLocalizationsSv extends AppLocalizations { @override String get extraLarge => 'Extra large'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_sw.dart b/lib/l10n/app_localizations_sw.dart index 87a4f6d34..799e1e9a8 100644 --- a/lib/l10n/app_localizations_sw.dart +++ b/lib/l10n/app_localizations_sw.dart @@ -363,6 +363,15 @@ class AppLocalizationsSw extends AppLocalizations { @override String get extraLarge => 'Kubwa Zaidi'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_ta.dart b/lib/l10n/app_localizations_ta.dart index 8634ce0cc..a7e42a64a 100644 --- a/lib/l10n/app_localizations_ta.dart +++ b/lib/l10n/app_localizations_ta.dart @@ -359,6 +359,15 @@ class AppLocalizationsTa extends AppLocalizations { @override String get extraLarge => 'கூடுதல் பெரியது'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_te.dart b/lib/l10n/app_localizations_te.dart index 8e26e2019..c871b6933 100644 --- a/lib/l10n/app_localizations_te.dart +++ b/lib/l10n/app_localizations_te.dart @@ -359,6 +359,15 @@ class AppLocalizationsTe extends AppLocalizations { @override String get extraLarge => 'అదనపు పెద్దది'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_th.dart b/lib/l10n/app_localizations_th.dart index e805499ba..a85675b5b 100644 --- a/lib/l10n/app_localizations_th.dart +++ b/lib/l10n/app_localizations_th.dart @@ -359,6 +359,15 @@ class AppLocalizationsTh extends AppLocalizations { @override String get extraLarge => 'ใหญ่พิเศษ'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — ประเภท'; diff --git a/lib/l10n/app_localizations_tl.dart b/lib/l10n/app_localizations_tl.dart index 7e4478b2e..52f5f24e0 100644 --- a/lib/l10n/app_localizations_tl.dart +++ b/lib/l10n/app_localizations_tl.dart @@ -361,6 +361,15 @@ class AppLocalizationsTl extends AppLocalizations { @override String get extraLarge => 'Extra Large'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Mga Genre'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 60e02340e..9b5f4fc94 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -358,6 +358,15 @@ class AppLocalizationsTr extends AppLocalizations { @override String get extraLarge => 'Ekstra Büyük'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_ug.dart b/lib/l10n/app_localizations_ug.dart index afa6854ce..cfe13118f 100644 --- a/lib/l10n/app_localizations_ug.dart +++ b/lib/l10n/app_localizations_ug.dart @@ -359,6 +359,15 @@ class AppLocalizationsUg extends AppLocalizations { @override String get extraLarge => 'قوشۇمچە چوڭ'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 834072931..93413cca6 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -359,6 +359,15 @@ class AppLocalizationsUk extends AppLocalizations { @override String get extraLarge => 'Дуже великий'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_vi.dart b/lib/l10n/app_localizations_vi.dart index 7a8b42149..97d247687 100644 --- a/lib/l10n/app_localizations_vi.dart +++ b/lib/l10n/app_localizations_vi.dart @@ -362,6 +362,15 @@ class AppLocalizationsVi extends AppLocalizations { @override String get extraLarge => 'Cực lớn'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Thể loại'; diff --git a/lib/l10n/app_localizations_yue.dart b/lib/l10n/app_localizations_yue.dart index 68258ad1e..edf123439 100644 --- a/lib/l10n/app_localizations_yue.dart +++ b/lib/l10n/app_localizations_yue.dart @@ -347,6 +347,15 @@ class AppLocalizationsYue extends AppLocalizations { @override String get extraLarge => '特大號'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 66776669e..e94322f3d 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -347,6 +347,15 @@ class AppLocalizationsZh extends AppLocalizations { @override String get extraLarge => '特大号'; + @override + String get scrollDirection => 'Scroll Direction'; + + @override + String get scrollDirectionVertical => 'Vertical'; + + @override + String get scrollDirectionHorizontal => 'Horizontal'; + @override String libraryGenresTitle(String name) { return '$name — Genres'; diff --git a/lib/preference/preference_constants.dart b/lib/preference/preference_constants.dart index 7724eb4cf..6df14048b 100644 --- a/lib/preference/preference_constants.dart +++ b/lib/preference/preference_constants.dart @@ -274,6 +274,11 @@ enum SeriesStatusFilter { ended, } +enum LibraryScrollDirection { + vertical, + horizontal, +} + enum FavoriteTypeFilter { all, movie, diff --git a/lib/preference/user_preferences.dart b/lib/preference/user_preferences.dart index 3952f155c..a20c996a3 100644 --- a/lib/preference/user_preferences.dart +++ b/lib/preference/user_preferences.dart @@ -1137,6 +1137,14 @@ class UserPreferences extends ChangeNotifier { values: ImageType.values, ); + static EnumPreference libraryScrollDirection( + String libraryId, + ) => EnumPreference( + key: 'library_scroll_dir_$libraryId', + defaultValue: LibraryScrollDirection.vertical, + values: LibraryScrollDirection.values, + ); + static final allGenresImageType = EnumPreference( key: 'all_genres_image_type', defaultValue: ImageType.thumb, diff --git a/lib/ui/screens/browse/library_browse_screen.dart b/lib/ui/screens/browse/library_browse_screen.dart index 388ff4906..ceac57a9c 100644 --- a/lib/ui/screens/browse/library_browse_screen.dart +++ b/lib/ui/screens/browse/library_browse_screen.dart @@ -3956,6 +3956,13 @@ class _LibraryBrowseScreenState extends State ); } + if (_vm.scrollDirection == LibraryScrollDirection.horizontal) { + return _buildHorizontalGrid(); + } + return _buildVerticalGrid(); + } + + Widget _buildVerticalGrid() { final cardWidth = _cardWidth(); final spacing = _vm.isBookLibrary ? 16.0 : 12.0; final watchedBehavior = _prefs.get( @@ -4045,7 +4052,6 @@ class _LibraryBrowseScreenState extends State (index % crossAxisCount) == crossAxisCount - 1; final isLastItem = index == _vm.items.length - 1; if (isLastColumn || isLastItem) { - // Keep focus in the current grid row at the right edge. return KeyEventResult.handled; } } @@ -4095,6 +4101,123 @@ class _LibraryBrowseScreenState extends State ); } + Widget _buildHorizontalGrid() { + final cardWidth = _cardWidth(); + final spacing = 12.0; + final watchedBehavior = _prefs.get(UserPreferences.watchedIndicatorBehavior); + + return LayoutBuilder( + builder: (context, constraints) { + final gridPadding = _horizontalPadding; + final ar = _gridBaseAspectRatio(); + final hasSubtitles = _vm.items.any( + (item) => (_cardSubtitle(item)?.isNotEmpty ?? false), + ); + final textHeight = hasSubtitles ? 42.0 : 24.0; + // Cell height in the horizontal view = card width + text + final cellHeight = cardWidth / ar + textHeight; + // How many rows fit in the available vertical height + final rowCount = + ((constraints.maxHeight - gridPadding * 2 + spacing) / + (cellHeight + spacing)) + .floor() + .clamp(1, 10); + + final availableHeight = + constraints.maxHeight - gridPadding * 2; + final actualCellHeight = + (availableHeight - (rowCount - 1) * spacing) / rowCount; + final actualCellWidth = (actualCellHeight - textHeight) * ar; + final childAspectRatio = actualCellWidth / actualCellHeight; + + return CustomScrollView( + controller: _scrollController, + scrollDirection: Axis.horizontal, + slivers: [ + SliverPadding( + padding: EdgeInsets.fromLTRB(gridPadding, gridPadding, 16, gridPadding), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: rowCount, + mainAxisSpacing: spacing, + crossAxisSpacing: spacing, + childAspectRatio: childAspectRatio, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + final item = _vm.items[index]; + final itemAspectRatio = _itemAspectRatio(item); + final focusColor = Color( + _prefs.get(UserPreferences.focusColor).colorValue, + ); + final isNeon = + ThemeRegistry.active.id == ThemeRegistry.neonPulseId; + return MediaCard( + title: item.name, + subtitle: _cardSubtitle(item), + imageUrl: _imageUrl(item), + width: double.infinity, + aspectRatio: itemAspectRatio, + focusColor: focusColor, + focusNode: getGridItemFocusNode(index), + cardFocusExpansion: _prefs.get( + UserPreferences.cardFocusExpansion, + ), + suppressFocusGlow: isNeon, + isPlayed: item.isPlayed, + isFavorite: item.isFavorite, + unplayedCount: item.unplayedItemCount, + playedPercentage: _displayPlayedPercentage(item), + watchedBehavior: watchedBehavior, + itemType: item.type, + onFocus: () => _onItemFocused(item), + onHoverStart: () => _onItemFocused(item), + onHoverEnd: () => _vm.setFocusedItem(null), + onKeyEvent: (_, event) { + if (!_vm.hasMore && !_vm.loadingMore) { + return KeyEventResult.ignored; + } + if (!event.isActionable || + !event.logicalKey.isRightKey) { + return KeyEventResult.ignored; + } + // Last item in its column — trigger load more + final col = index ~/ rowCount; + final isLastCol = + (col + 1) * rowCount >= _vm.items.length; + if (!isLastCol) return KeyEventResult.ignored; + _vm.loadMore(); + return KeyEventResult.handled; + }, + onLongPress: () => showContextMenu( + context, + item, + onChanged: () => setState(() {}), + ), + onTap: () => _onItemTap(item), + ); + }, + childCount: _vm.items.length, + ), + ), + ), + if (_vm.loadingMore) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Center( + child: CircularProgressIndicator( + color: _vm.isBookLibrary ? _bookAccent : _jellyfinBlue, + ), + ), + ), + ), + ], + ); + }, + ); + } + Widget _buildBookGrid() { final baseItems = _vm.items.where((item) => !_vm.isNavigableFolder(item)); final tabFiltered = baseItems.where( @@ -5145,7 +5268,29 @@ class _SettingsDialogState extends State<_SettingsDialog> { ), ), Divider(color: dividerColor), + Padding( + padding: const EdgeInsets.fromLTRB(24, 12, 24, 4), + child: Text( + l10n.scrollDirection, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: sectionColor, + ), + ), + ), + _scrollDirectionRadioTile( + vm, + LibraryScrollDirection.vertical, + l10n.scrollDirectionVertical, + ), + _scrollDirectionRadioTile( + vm, + LibraryScrollDirection.horizontal, + l10n.scrollDirectionHorizontal, + ), if (!vm.isPlaylistBrowse) ...[ + Divider(color: dividerColor), Padding( padding: const EdgeInsets.fromLTRB(24, 12, 24, 4), child: Text( @@ -5179,6 +5324,35 @@ class _SettingsDialogState extends State<_SettingsDialog> { ); } + Widget _scrollDirectionRadioTile( + LibraryBrowseViewModel vm, + LibraryScrollDirection direction, + String label, + ) { + final selected = vm.scrollDirection == direction; + final accent = vm.isBookLibrary ? _bookAccent : _jellyfinBlue; + final onSurface = AppColorScheme.onSurface; + return InkWell( + onTap: () => vm.setScrollDirection(direction), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + child: Row( + children: [ + _radioCircle(selected, accent), + const SizedBox(width: 12), + Text( + label, + style: TextStyle( + fontSize: 15, + color: selected ? onSurface : onSurface.withValues(alpha: 0.72), + ), + ), + ], + ), + ), + ); + } + Widget _settingsRadioTile(LibraryBrowseViewModel vm, ImageType type) { final selected = vm.imageType == type; final accent = vm.isBookLibrary ? _bookAccent : _jellyfinBlue; From 71fc2e0e335c9327e9fe4c80a90b06b8fb136f14 Mon Sep 17 00:00:00 2001 From: mattsigal Date: Sat, 6 Jun 2026 10:29:45 -0700 Subject: [PATCH 2/3] fix(library): polish horizontal scroll mode - card sizing, scroll wheel, chevrons, padding - Fix #1 (card sizing): use _cardWidth() as target cell HEIGHT instead of image width so poster-size preference scales correctly for all aspect ratios (Poster/Thumb/Banner) - Fix #2 (scroll wheel): wrap horizontal CustomScrollView in Listener that redirects vertical PointerScrollEvent.scrollDelta.dy to horizontal scroll offset - Fix #3 (chevrons): overlay left/right _HorizontalScrollChevron buttons on desktop (non-TV) matching home-screen row pattern; fade out when at scroll boundary - Fix #4 (leading padding): ClampingScrollPhysics() prevents rubber-banding past offset 0 so SliverPadding leading gap is preserved on scroll-back - Add missing package:flutter/gestures.dart import for PointerScrollEvent --- .../screens/browse/library_browse_screen.dart | 281 +++++++++++++----- 1 file changed, 200 insertions(+), 81 deletions(-) diff --git a/lib/ui/screens/browse/library_browse_screen.dart b/lib/ui/screens/browse/library_browse_screen.dart index ceac57a9c..df6372588 100644 --- a/lib/ui/screens/browse/library_browse_screen.dart +++ b/lib/ui/screens/browse/library_browse_screen.dart @@ -6,6 +6,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; @@ -4102,7 +4103,6 @@ class _LibraryBrowseScreenState extends State } Widget _buildHorizontalGrid() { - final cardWidth = _cardWidth(); final spacing = 12.0; final watchedBehavior = _prefs.get(UserPreferences.watchedIndicatorBehavior); @@ -4114,110 +4114,229 @@ class _LibraryBrowseScreenState extends State (item) => (_cardSubtitle(item)?.isNotEmpty ?? false), ); final textHeight = hasSubtitles ? 42.0 : 24.0; - // Cell height in the horizontal view = card width + text - final cellHeight = cardWidth / ar + textHeight; - // How many rows fit in the available vertical height + + // Fix #1 — use _cardWidth() as the TARGET CELL HEIGHT so that the + // poster-size preference controls card size regardless of aspect ratio. + // (Previously it was used as image width, causing posters to fill the + // entire viewport height because their tall aspect ratio produced 1 row.) + final targetCellHeight = _cardWidth(); final rowCount = ((constraints.maxHeight - gridPadding * 2 + spacing) / - (cellHeight + spacing)) + (targetCellHeight + spacing)) .floor() .clamp(1, 10); - final availableHeight = - constraints.maxHeight - gridPadding * 2; + final availableHeight = constraints.maxHeight - gridPadding * 2; final actualCellHeight = (availableHeight - (rowCount - 1) * spacing) / rowCount; final actualCellWidth = (actualCellHeight - textHeight) * ar; final childAspectRatio = actualCellWidth / actualCellHeight; - return CustomScrollView( - controller: _scrollController, - scrollDirection: Axis.horizontal, - slivers: [ - SliverPadding( - padding: EdgeInsets.fromLTRB(gridPadding, gridPadding, 16, gridPadding), - sliver: SliverGrid( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: rowCount, - mainAxisSpacing: spacing, - crossAxisSpacing: spacing, - childAspectRatio: childAspectRatio, + final showDesktopChevrons = + PlatformDetection.useDesktopUi && !PlatformDetection.isTV; + + // Fix #2 — redirect vertical mouse-wheel delta to horizontal scroll. + // Fix #4 — ClampingScrollPhysics prevents the view from rubber-banding + // past position 0 which caused the leading padding to vanish. + final scrollView = Listener( + onPointerSignal: (signal) { + if (signal is PointerScrollEvent) { + final pos = _scrollController.position; + final newOffset = (_scrollController.offset + signal.scrollDelta.dy) + .clamp(pos.minScrollExtent, pos.maxScrollExtent); + _scrollController.jumpTo(newOffset); + } + }, + child: CustomScrollView( + controller: _scrollController, + scrollDirection: Axis.horizontal, + physics: const ClampingScrollPhysics(), + slivers: [ + SliverPadding( + // Fix #4 — explicit leading padding preserved by Clamping physics. + padding: EdgeInsets.fromLTRB( + gridPadding, gridPadding, 16, gridPadding, ), - delegate: SliverChildBuilderDelegate( - (context, index) { - final item = _vm.items[index]; - final itemAspectRatio = _itemAspectRatio(item); - final focusColor = Color( - _prefs.get(UserPreferences.focusColor).colorValue, - ); - final isNeon = - ThemeRegistry.active.id == ThemeRegistry.neonPulseId; - return MediaCard( - title: item.name, - subtitle: _cardSubtitle(item), - imageUrl: _imageUrl(item), - width: double.infinity, - aspectRatio: itemAspectRatio, - focusColor: focusColor, - focusNode: getGridItemFocusNode(index), - cardFocusExpansion: _prefs.get( - UserPreferences.cardFocusExpansion, - ), - suppressFocusGlow: isNeon, - isPlayed: item.isPlayed, - isFavorite: item.isFavorite, - unplayedCount: item.unplayedItemCount, - playedPercentage: _displayPlayedPercentage(item), - watchedBehavior: watchedBehavior, - itemType: item.type, - onFocus: () => _onItemFocused(item), - onHoverStart: () => _onItemFocused(item), - onHoverEnd: () => _vm.setFocusedItem(null), - onKeyEvent: (_, event) { - if (!_vm.hasMore && !_vm.loadingMore) { - return KeyEventResult.ignored; - } - if (!event.isActionable || - !event.logicalKey.isRightKey) { - return KeyEventResult.ignored; - } - // Last item in its column — trigger load more - final col = index ~/ rowCount; - final isLastCol = - (col + 1) * rowCount >= _vm.items.length; - if (!isLastCol) return KeyEventResult.ignored; - _vm.loadMore(); - return KeyEventResult.handled; - }, - onLongPress: () => showContextMenu( - context, - item, - onChanged: () => setState(() {}), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: rowCount, + mainAxisSpacing: spacing, + crossAxisSpacing: spacing, + childAspectRatio: childAspectRatio, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + final item = _vm.items[index]; + final itemAspectRatio = _itemAspectRatio(item); + final focusColor = Color( + _prefs.get(UserPreferences.focusColor).colorValue, + ); + final isNeon = + ThemeRegistry.active.id == ThemeRegistry.neonPulseId; + return MediaCard( + title: item.name, + subtitle: _cardSubtitle(item), + imageUrl: _imageUrl(item), + width: double.infinity, + aspectRatio: itemAspectRatio, + focusColor: focusColor, + focusNode: getGridItemFocusNode(index), + cardFocusExpansion: _prefs.get( + UserPreferences.cardFocusExpansion, + ), + suppressFocusGlow: isNeon, + isPlayed: item.isPlayed, + isFavorite: item.isFavorite, + unplayedCount: item.unplayedItemCount, + playedPercentage: _displayPlayedPercentage(item), + watchedBehavior: watchedBehavior, + itemType: item.type, + onFocus: () => _onItemFocused(item), + onHoverStart: () => _onItemFocused(item), + onHoverEnd: () => _vm.setFocusedItem(null), + onKeyEvent: (_, event) { + if (!_vm.hasMore && !_vm.loadingMore) { + return KeyEventResult.ignored; + } + if (!event.isActionable || + !event.logicalKey.isRightKey) { + return KeyEventResult.ignored; + } + final col = index ~/ rowCount; + final isLastCol = + (col + 1) * rowCount >= _vm.items.length; + if (!isLastCol) return KeyEventResult.ignored; + _vm.loadMore(); + return KeyEventResult.handled; + }, + onLongPress: () => showContextMenu( + context, + item, + onChanged: () => setState(() {}), + ), + onTap: () => _onItemTap(item), + ); + }, + childCount: _vm.items.length, + ), + ), + ), + if (_vm.loadingMore) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Center( + child: CircularProgressIndicator( + color: _vm.isBookLibrary ? _bookAccent : _jellyfinBlue, ), - onTap: () => _onItemTap(item), - ); - }, - childCount: _vm.items.length, + ), + ), + ), + ], + ), + ); + + // Fix #3 — overlay left/right chevron buttons on desktop (non-TV), + // matching the home-screen row pattern. + if (!showDesktopChevrons) return scrollView; + + const chevronScrollStep = 480.0; + return Stack( + children: [ + scrollView, + // Left chevron + Positioned( + left: 0, + top: 0, + bottom: 0, + child: Focus( + canRequestFocus: false, + skipTraversal: true, + descendantsAreFocusable: false, + child: Center( + child: AnimatedBuilder( + animation: _scrollController, + builder: (context, _) { + final canScrollLeft = _scrollController.hasClients && + _scrollController.offset > 0; + return Opacity( + opacity: canScrollLeft ? 1.0 : 0.0, + child: _HorizontalScrollChevron( + icon: Icons.chevron_left_rounded, + onPressed: canScrollLeft + ? () => _scrollController.animateTo( + (_scrollController.offset - chevronScrollStep) + .clamp(0, double.infinity), + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ) + : null, + ), + ); + }, + ), ), ), ), - if (_vm.loadingMore) - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Center( - child: CircularProgressIndicator( - color: _vm.isBookLibrary ? _bookAccent : _jellyfinBlue, - ), + // Right chevron + Positioned( + right: 0, + top: 0, + bottom: 0, + child: Focus( + canRequestFocus: false, + skipTraversal: true, + descendantsAreFocusable: false, + child: Center( + child: AnimatedBuilder( + animation: _scrollController, + builder: (context, _) { + final canScrollRight = _scrollController.hasClients && + _scrollController.offset < + _scrollController.position.maxScrollExtent; + return Opacity( + opacity: canScrollRight ? 1.0 : 0.0, + child: _HorizontalScrollChevron( + icon: Icons.chevron_right_rounded, + onPressed: canScrollRight + ? () => _scrollController.animateTo( + _scrollController.offset + chevronScrollStep, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ) + : null, + ), + ); + }, ), ), ), + ), ], ); }, ); } + // Reusable chevron button for the horizontal library grid. + // Wrapped in a semi-transparent pill so it's visible over any poster art. + static Widget _HorizontalScrollChevron({ + required IconData icon, + required VoidCallback? onPressed, + }) => + Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(20), + ), + child: IconButton( + icon: Icon(icon, color: Colors.white), + onPressed: onPressed, + visualDensity: VisualDensity.compact, + ), + ); + Widget _buildBookGrid() { final baseItems = _vm.items.where((item) => !_vm.isNavigableFolder(item)); final tabFiltered = baseItems.where( From 809bed709fb3e2080fe764648661e5eec218e2e3 Mon Sep 17 00:00:00 2001 From: mattsigal Date: Sat, 6 Jun 2026 11:07:00 -0700 Subject: [PATCH 3/3] fix(library): fix horizontal grid poster sizing, gray overlay, and chevron style - Poster sizing: use _cardWidth()/ar as target image height so cards match vertical-mode dimensions exactly; fix childAspectRatio to height/width (correct formula for horizontal SliverGrid where cross=height, main=width) - Gray overlay: remove Stack/Positioned/Opacity chevron overlay from inside _buildHorizontalGrid() which was causing the translucent gray tint - Chevrons: move to _buildBody() as a simple right-aligned Row with Icons.chevron_left/chevron_right + VisualDensity.compact, matching the home-screen _buildTitledRow pattern exactly; shown only when horizontal mode is active on desktop (non-TV) --- .../screens/browse/library_browse_screen.dart | 221 +++++++----------- 1 file changed, 87 insertions(+), 134 deletions(-) diff --git a/lib/ui/screens/browse/library_browse_screen.dart b/lib/ui/screens/browse/library_browse_screen.dart index df6372588..27b6458bb 100644 --- a/lib/ui/screens/browse/library_browse_screen.dart +++ b/lib/ui/screens/browse/library_browse_screen.dart @@ -3926,28 +3926,85 @@ class _LibraryBrowseScreenState extends State Widget _buildBody() { final spinnerColor = _vm.isBookLibrary ? _bookAccent : _jellyfinBlue; - return switch (_vm.state) { - LibraryBrowseState.loading => Center( - child: CircularProgressIndicator(color: spinnerColor), - ), - LibraryBrowseState.error => Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - _vm.errorMessage ?? AppLocalizations.of(context).failedToLoadLibrary, - style: TextStyle( - color: _vm.isBookLibrary ? const Color(0xFFF4E6D5) : Colors.white, - ), + final showHorizChevrons = + _vm.scrollDirection == LibraryScrollDirection.horizontal && + PlatformDetection.useDesktopUi && + !PlatformDetection.isTV; + return Column( + children: [ + if (showHorizChevrons) + // Chevron row matching home-screen style — sits just below the toolbar, + // flush with the right edge at _horizontalPadding inset. + Padding( + padding: EdgeInsets.fromLTRB(_horizontalPadding, 0, _horizontalPadding, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Focus( + canRequestFocus: false, + skipTraversal: true, + descendantsAreFocusable: false, + child: IconButton( + icon: const Icon(Icons.chevron_left), + onPressed: () { + if (_scrollController.hasClients) { + _scrollController.animateTo( + (_scrollController.offset - 480).clamp(0, double.infinity), + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + }, + visualDensity: VisualDensity.compact, + ), + ), + Focus( + canRequestFocus: false, + skipTraversal: true, + descendantsAreFocusable: false, + child: IconButton( + icon: const Icon(Icons.chevron_right), + onPressed: () { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.offset + 480, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + }, + visualDensity: VisualDensity.compact, + ), + ), + ], ), - const SizedBox(height: 16), - ElevatedButton(onPressed: _vm.load, child: Text(AppLocalizations.of(context).retry)), - ], + ), + Expanded( + child: switch (_vm.state) { + LibraryBrowseState.loading => Center( + child: CircularProgressIndicator(color: spinnerColor), + ), + LibraryBrowseState.error => Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _vm.errorMessage ?? AppLocalizations.of(context).failedToLoadLibrary, + style: TextStyle( + color: _vm.isBookLibrary ? const Color(0xFFF4E6D5) : Colors.white, + ), + ), + const SizedBox(height: 16), + ElevatedButton(onPressed: _vm.load, child: Text(AppLocalizations.of(context).retry)), + ], + ), + ), + LibraryBrowseState.ready => + _vm.isBookLibrary ? _buildBookGrid() : _buildGrid(), + }, ), - ), - LibraryBrowseState.ready => - _vm.isBookLibrary ? _buildBookGrid() : _buildGrid(), - }; + ], + ); } Widget _buildGrid() { @@ -4115,11 +4172,9 @@ class _LibraryBrowseScreenState extends State ); final textHeight = hasSubtitles ? 42.0 : 24.0; - // Fix #1 — use _cardWidth() as the TARGET CELL HEIGHT so that the - // poster-size preference controls card size regardless of aspect ratio. - // (Previously it was used as image width, causing posters to fill the - // entire viewport height because their tall aspect ratio produced 1 row.) - final targetCellHeight = _cardWidth(); + final targetImageHeight = _cardWidth() / ar; + final targetCellHeight = targetImageHeight + textHeight; + final rowCount = ((constraints.maxHeight - gridPadding * 2 + spacing) / (targetCellHeight + spacing)) @@ -4129,21 +4184,18 @@ class _LibraryBrowseScreenState extends State final availableHeight = constraints.maxHeight - gridPadding * 2; final actualCellHeight = (availableHeight - (rowCount - 1) * spacing) / rowCount; - final actualCellWidth = (actualCellHeight - textHeight) * ar; - final childAspectRatio = actualCellWidth / actualCellHeight; + final actualImageHeight = actualCellHeight - textHeight; + final actualCellWidth = actualImageHeight * ar; - final showDesktopChevrons = - PlatformDetection.useDesktopUi && !PlatformDetection.isTV; + final childAspectRatio = actualCellHeight / actualCellWidth; - // Fix #2 — redirect vertical mouse-wheel delta to horizontal scroll. - // Fix #4 — ClampingScrollPhysics prevents the view from rubber-banding - // past position 0 which caused the leading padding to vanish. - final scrollView = Listener( + return Listener( onPointerSignal: (signal) { if (signal is PointerScrollEvent) { final pos = _scrollController.position; - final newOffset = (_scrollController.offset + signal.scrollDelta.dy) - .clamp(pos.minScrollExtent, pos.maxScrollExtent); + final newOffset = + (_scrollController.offset + signal.scrollDelta.dy) + .clamp(pos.minScrollExtent, pos.maxScrollExtent); _scrollController.jumpTo(newOffset); } }, @@ -4153,7 +4205,6 @@ class _LibraryBrowseScreenState extends State physics: const ClampingScrollPhysics(), slivers: [ SliverPadding( - // Fix #4 — explicit leading padding preserved by Clamping physics. padding: EdgeInsets.fromLTRB( gridPadding, gridPadding, 16, gridPadding, ), @@ -4235,108 +4286,10 @@ class _LibraryBrowseScreenState extends State ], ), ); - - // Fix #3 — overlay left/right chevron buttons on desktop (non-TV), - // matching the home-screen row pattern. - if (!showDesktopChevrons) return scrollView; - - const chevronScrollStep = 480.0; - return Stack( - children: [ - scrollView, - // Left chevron - Positioned( - left: 0, - top: 0, - bottom: 0, - child: Focus( - canRequestFocus: false, - skipTraversal: true, - descendantsAreFocusable: false, - child: Center( - child: AnimatedBuilder( - animation: _scrollController, - builder: (context, _) { - final canScrollLeft = _scrollController.hasClients && - _scrollController.offset > 0; - return Opacity( - opacity: canScrollLeft ? 1.0 : 0.0, - child: _HorizontalScrollChevron( - icon: Icons.chevron_left_rounded, - onPressed: canScrollLeft - ? () => _scrollController.animateTo( - (_scrollController.offset - chevronScrollStep) - .clamp(0, double.infinity), - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ) - : null, - ), - ); - }, - ), - ), - ), - ), - // Right chevron - Positioned( - right: 0, - top: 0, - bottom: 0, - child: Focus( - canRequestFocus: false, - skipTraversal: true, - descendantsAreFocusable: false, - child: Center( - child: AnimatedBuilder( - animation: _scrollController, - builder: (context, _) { - final canScrollRight = _scrollController.hasClients && - _scrollController.offset < - _scrollController.position.maxScrollExtent; - return Opacity( - opacity: canScrollRight ? 1.0 : 0.0, - child: _HorizontalScrollChevron( - icon: Icons.chevron_right_rounded, - onPressed: canScrollRight - ? () => _scrollController.animateTo( - _scrollController.offset + chevronScrollStep, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ) - : null, - ), - ); - }, - ), - ), - ), - ), - ], - ); }, ); } - // Reusable chevron button for the horizontal library grid. - // Wrapped in a semi-transparent pill so it's visible over any poster art. - static Widget _HorizontalScrollChevron({ - required IconData icon, - required VoidCallback? onPressed, - }) => - Container( - margin: const EdgeInsets.symmetric(horizontal: 4), - decoration: BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.circular(20), - ), - child: IconButton( - icon: Icon(icon, color: Colors.white), - onPressed: onPressed, - visualDensity: VisualDensity.compact, - ), - ); - Widget _buildBookGrid() { final baseItems = _vm.items.where((item) => !_vm.isNavigableFolder(item)); final tabFiltered = baseItems.where(