From 62015a4e2da4fc580a2e9b44c334b8d3eda1ad89 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 12 Jul 2026 19:38:50 +0700 Subject: [PATCH] refactor(screens): migrate remaining screens to shared header and selection components - album/playlist/library-folder/track-metadata adopt CollapsingHeaderScrollMixin; artist/library-folder adopt SelectionModeMixin - HeaderCircleButton and HeaderMetaRow replace per-screen copies in album and playlist headers; dead _tallHeader flag removed - SelectionBottomBar gains optional subtitle labels and backs the playlist selection bar in queue tab --- lib/screens/album_screen.dart | 162 +++---------- lib/screens/artist_screen.dart | 102 ++++---- lib/screens/library_tracks_folder_screen.dart | 150 ++++-------- lib/screens/playlist_screen.dart | 96 ++------ lib/screens/queue_tab_selection.dart | 218 ++++++------------ lib/screens/track_metadata_screen.dart | 37 ++- lib/widgets/album_detail_header.dart | 39 ++++ lib/widgets/selection_bottom_bar.dart | 14 +- 8 files changed, 259 insertions(+), 559 deletions(-) diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index a9db1296..101e17c5 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -11,6 +11,7 @@ import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/utils/image_cache_utils.dart'; import 'package:spotiflac_android/utils/string_utils.dart'; import 'package:spotiflac_android/utils/cover_art_utils.dart'; +import 'package:spotiflac_android/screens/collapsing_header_scroll_mixin.dart'; import 'package:spotiflac_android/widgets/error_card.dart'; import 'package:spotiflac_android/widgets/album_detail_header.dart'; import 'package:spotiflac_android/utils/nav_bar_inset.dart'; @@ -64,20 +65,17 @@ class AlbumScreen extends ConsumerStatefulWidget { ConsumerState createState() => _AlbumScreenState(); } -class _AlbumScreenState extends ConsumerState { +class _AlbumScreenState extends ConsumerState + with CollapsingHeaderScrollMixin { List? _tracks; bool _isLoading = false; String? _error; - bool _showTitleInAppBar = false; String? _artistId; String? _albumType; int? _albumTotalTracks; String? _headerVideoUrl; String? _headerImageUrl; List _audioTraits = const []; - bool _tallHeader = false; - final ScrollController _scrollController = ScrollController(); - String _effectiveMetadataProviderIdFromAlbumId() { if (widget.extensionId != null && widget.extensionId!.isNotEmpty) { @@ -89,13 +87,10 @@ class _AlbumScreenState extends ConsumerState { ); } - @override void initState() { super.initState(); - _scrollController.addListener(_onScroll); - WidgetsBinding.instance.addPostFrameCallback((_) { final providerId = _effectiveMetadataProviderIdFromAlbumId(); ref @@ -129,30 +124,6 @@ class _AlbumScreenState extends ConsumerState { } } - @override - void dispose() { - _scrollController.removeListener(_onScroll); - _scrollController.dispose(); - super.dispose(); - } - - void _onScroll() { - final expandedHeight = _calculateExpandedHeight(context, tall: _tallHeader); - final shouldShow = - _scrollController.offset > (expandedHeight - kToolbarHeight - 20); - if (shouldShow != _showTitleInAppBar) { - setState(() => _showTitleInAppBar = shouldShow); - } - } - - double _calculateExpandedHeight(BuildContext context, {bool tall = false}) { - final mediaSize = MediaQuery.sizeOf(context); - if (tall) { - return (mediaSize.height * 0.68).clamp(440.0, 660.0); - } - return (mediaSize.height * 0.6).clamp(400.0, 580.0); - } - Future _fetchTracks() async { setState(() => _isLoading = true); try { @@ -258,25 +229,6 @@ class _AlbumScreenState extends ConsumerState { return stripPrefixedResourceId(widget.albumId); } - Widget _metaInlineItem(IconData? icon, String label) { - const textStyle = TextStyle( - color: Colors.white, - fontSize: 13, - fontWeight: FontWeight.w500, - ); - if (icon == null) { - return Text(label, style: textStyle); - } - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 15, color: Colors.white), - const SizedBox(width: 4), - Text(label, style: textStyle), - ], - ); - } - List _audioTraitInline() { final traits = _audioTraits .map((t) => t.toLowerCase().trim()) @@ -288,15 +240,15 @@ class _AlbumScreenState extends ConsumerState { final items = []; if (has(['atmos', 'dolby_atmos', 'dolby-atmos'])) { - items.add(_metaInlineItem(Icons.surround_sound, 'Dolby Atmos')); + items.add(HeaderMetaItem('Dolby Atmos', icon: Icons.surround_sound)); } else if (has(['spatial'])) { - items.add(_metaInlineItem(Icons.surround_sound, 'Spatial Audio')); + items.add(HeaderMetaItem('Spatial Audio', icon: Icons.surround_sound)); } if (has(['hi-res-lossless', 'hi_res_lossless', 'hires-lossless'])) { - items.add(_metaInlineItem(Icons.graphic_eq, 'Hi-Res Lossless')); + items.add(HeaderMetaItem('Hi-Res Lossless', icon: Icons.graphic_eq)); } else if (has(['lossless'])) { - items.add(_metaInlineItem(Icons.graphic_eq, 'Lossless')); + items.add(HeaderMetaItem('Lossless', icon: Icons.graphic_eq)); } return items; @@ -305,38 +257,13 @@ class _AlbumScreenState extends ConsumerState { Widget _buildHeaderMeta(BuildContext context, String? releaseDate) { final items = []; - void add(Widget widget) { - if (items.isNotEmpty) { - items.add( - const Padding( - padding: EdgeInsets.symmetric(horizontal: 6), - child: Text( - '•', - style: TextStyle(color: Colors.white70, fontSize: 12), - ), - ), - ); - } - items.add(widget); - } - final year = _releaseYear(releaseDate); - if (year != null) { - add(_metaInlineItem(null, year)); - } - for (final trait in _audioTraitInline()) { - add(trait); - } + if (year != null) items.add(HeaderMetaItem(year)); + items.addAll(_audioTraitInline()); return ConstrainedBox( constraints: const BoxConstraints(minHeight: 20), - child: Wrap( - alignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 0, - runSpacing: 4, - children: items, - ), + child: HeaderMetaRow(items: items), ); } @@ -397,7 +324,7 @@ class _AlbumScreenState extends ConsumerState { return Scaffold( backgroundColor: pageBackgroundColor, body: CustomScrollView( - controller: _scrollController, + controller: scrollController, slivers: [ _buildAppBar(context, colorScheme, pageBackgroundColor), if (_isLoading) @@ -444,8 +371,7 @@ class _AlbumScreenState extends ConsumerState { Uri.tryParse(motionUrl)?.hasAuthority == true; final coverThumbUrl = widget.coverUrl ?? _headerImageUrl; final showSquareCover = !hasMotion; - _tallHeader = false; - final expandedHeight = _calculateExpandedHeight(context); + final expandedHeight = calculateExpandedHeight(context); final cacheWidth = coverCacheWidthForViewport(context); final headerBgUrl = _headerImageUrl ?? widget.headerImageUrl ?? widget.coverUrl; @@ -470,7 +396,7 @@ class _AlbumScreenState extends ConsumerState { return AlbumDetailHeader( title: widget.albumName, expandedHeight: expandedHeight, - showTitleInAppBar: _showTitleInAppBar, + showTitleInAppBar: showTitleInAppBar, backgroundColor: pageBackgroundColor, background: hasMotion ? MotionHeaderBanner(videoUrl: motionUrl, fallback: headerBgImage) @@ -656,54 +582,25 @@ class _AlbumScreenState extends ConsumerState { tracks.isNotEmpty && tracks.every((t) => collectionsState.isLoved(t)); - return Container( - width: 48, - height: 48, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.white.withValues(alpha: 0.15), - border: Border.all( - color: Colors.white.withValues(alpha: 0.3), - width: 1, - ), - ), - child: IconButton( - onPressed: tracks == null || tracks.isEmpty - ? null - : () => _loveAll(tracks), - icon: Icon( - allLoved ? Icons.favorite : Icons.favorite_border, - size: 22, - color: allLoved ? Colors.redAccent : Colors.white, - ), - tooltip: allLoved - ? context.l10n.trackOptionRemoveFromLoved - : context.l10n.tooltipLoveAll, - padding: EdgeInsets.zero, - ), + return HeaderCircleButton( + icon: allLoved ? Icons.favorite : Icons.favorite_border, + iconColor: allLoved ? Colors.redAccent : Colors.white, + tooltip: allLoved + ? context.l10n.trackOptionRemoveFromLoved + : context.l10n.tooltipLoveAll, + onPressed: tracks == null || tracks.isEmpty + ? null + : () => _loveAll(tracks), ); } Widget _buildAddToPlaylistButton(BuildContext context) { - return Container( - width: 48, - height: 48, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.white.withValues(alpha: 0.15), - border: Border.all( - color: Colors.white.withValues(alpha: 0.3), - width: 1, - ), - ), - child: IconButton( - onPressed: _tracks == null || _tracks!.isEmpty - ? null - : () => showAddTracksToPlaylistSheet(context, ref, _tracks!), - icon: const Icon(Icons.add, size: 22, color: Colors.white), - tooltip: context.l10n.tooltipAddToPlaylist, - padding: EdgeInsets.zero, - ), + return HeaderCircleButton( + icon: Icons.add, + tooltip: context.l10n.tooltipAddToPlaylist, + onPressed: _tracks == null || _tracks!.isEmpty + ? null + : () => showAddTracksToPlaylistSheet(context, ref, _tracks!), ); } @@ -728,5 +625,6 @@ class _AlbumScreenState extends ConsumerState { ); } - Future _loveAll(List tracks) => loveAllTracks(context, ref, tracks); + Future _loveAll(List tracks) => + loveAllTracks(context, ref, tracks); } diff --git a/lib/screens/artist_screen.dart b/lib/screens/artist_screen.dart index 0cc23221..99122dd4 100644 --- a/lib/screens/artist_screen.dart +++ b/lib/screens/artist_screen.dart @@ -25,6 +25,7 @@ import 'package:spotiflac_android/widgets/error_card.dart'; import 'package:spotiflac_android/widgets/in_library_badge.dart'; import 'package:spotiflac_android/widgets/track_collection_quick_actions.dart'; import 'package:spotiflac_android/widgets/animation_utils.dart'; +import 'package:spotiflac_android/screens/selection_mode_mixin.dart'; import 'package:spotiflac_android/utils/clickable_metadata.dart'; import 'package:spotiflac_android/widgets/cached_cover_image.dart'; import 'package:spotiflac_android/widgets/motion_header_banner.dart'; @@ -104,7 +105,8 @@ class ArtistScreen extends ConsumerStatefulWidget { ConsumerState createState() => _ArtistScreenState(); } -class _ArtistScreenState extends ConsumerState { +class _ArtistScreenState extends ConsumerState + with SelectionModeMixin { bool _isLoadingDiscography = false; List? _albums; List? _releases; @@ -119,8 +121,6 @@ class _ArtistScreenState extends ConsumerState { final PageController _popularPageController = PageController(); int _popularCurrentPage = 0; - bool _isSelectionMode = false; - final Set _selectedAlbumIds = {}; bool _isFetchingDiscography = false; List? _albumBucketSource; List _albumsOnlyBucket = const []; @@ -162,7 +162,6 @@ class _ArtistScreenState extends ConsumerState { return _directMetadataProviderId(); } - String _effectiveMetadataProviderIdFromArtistId() { if (widget.extensionId != null && widget.extensionId!.isNotEmpty) { return widget.extensionId!; @@ -173,7 +172,6 @@ class _ArtistScreenState extends ConsumerState { ); } - String? _directMetadataProviderId() { final providerId = _effectiveMetadataProviderIdFromArtistId(); return providerId.isEmpty ? null : providerId; @@ -464,10 +462,10 @@ class _ArtistScreenState extends ConsumerState { !_isLoadingDiscography && _error == null && albums.isNotEmpty; return PopScope( - canPop: !_isSelectionMode, + canPop: !isSelectionMode, onPopInvokedWithResult: (didPop, result) { - if (!didPop && _isSelectionMode) { - _exitSelectionMode(); + if (!didPop && isSelectionMode) { + exitSelectionMode(); } }, child: Scaffold( @@ -500,7 +498,10 @@ class _ArtistScreenState extends ConsumerState { SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), - child: ErrorCard(error: _error!, colorScheme: colorScheme), + child: ErrorCard( + error: _error!, + colorScheme: colorScheme, + ), ), ), if (!_isLoadingDiscography && _error == null) ...[ @@ -543,12 +544,12 @@ class _ArtistScreenState extends ConsumerState { ), ], SliverToBoxAdapter( - child: SizedBox(height: _isSelectionMode ? 120 : 32), + child: SizedBox(height: isSelectionMode ? 120 : 32), ), SliverToBoxAdapter(child: SizedBox(height: bottomInset)), ], ), - if (_isSelectionMode) + if (isSelectionMode) _buildSelectionBar(context, colorScheme, albums), ], ), @@ -556,45 +557,21 @@ class _ArtistScreenState extends ConsumerState { ); } - void _exitSelectionMode() { + @override + void exitSelectionMode() { HapticFeedback.lightImpact(); - setState(() { - _isSelectionMode = false; - _selectedAlbumIds.clear(); - }); + super.exitSelectionMode(); } - void _enterSelectionMode(String albumId) { - HapticFeedback.mediumImpact(); - setState(() { - _isSelectionMode = true; - _selectedAlbumIds.add(albumId); - }); - } - - void _toggleAlbumSelection(String albumId) { + @override + void toggleSelection(String itemId) { HapticFeedback.selectionClick(); - setState(() { - if (_selectedAlbumIds.contains(albumId)) { - _selectedAlbumIds.remove(albumId); - if (_selectedAlbumIds.isEmpty) { - _isSelectionMode = false; - } - } else { - _selectedAlbumIds.add(albumId); - } - }); - } - - void _selectAll(List albums) { - setState(() { - _selectedAlbumIds.addAll(albums.map((a) => a.id)); - }); + super.toggleSelection(itemId); } void _deselectAll() { setState(() { - _selectedAlbumIds.clear(); + selectedIds.clear(); }); } @@ -603,10 +580,10 @@ class _ArtistScreenState extends ConsumerState { ColorScheme colorScheme, List allAlbums, ) { - final allSelected = _selectedAlbumIds.length == allAlbums.length; - final selectedCount = _selectedAlbumIds.length; + final allSelected = selectedIds.length == allAlbums.length; + final selectedCount = selectedIds.length; final selectedAlbums = allAlbums - .where((a) => _selectedAlbumIds.contains(a.id)) + .where((a) => selectedIds.contains(a.id)) .toList(); final totalTracks = selectedAlbums.fold( 0, @@ -643,7 +620,7 @@ class _ArtistScreenState extends ConsumerState { Row( children: [ IconButton( - onPressed: _exitSelectionMode, + onPressed: exitSelectionMode, icon: const Icon(Icons.close), tooltip: context.l10n.dialogCancel, ), @@ -684,7 +661,7 @@ class _ArtistScreenState extends ConsumerState { child: OutlinedButton( onPressed: allSelected ? _deselectAll - : () => _selectAll(allAlbums), + : () => selectAll(allAlbums.map((a) => a.id)), child: FittedBox( fit: BoxFit.scaleDown, child: Text( @@ -719,7 +696,7 @@ class _ArtistScreenState extends ConsumerState { : Row( children: [ IconButton( - onPressed: _exitSelectionMode, + onPressed: exitSelectionMode, icon: const Icon(Icons.close), tooltip: context.l10n.dialogCancel, ), @@ -754,7 +731,7 @@ class _ArtistScreenState extends ConsumerState { TextButton( onPressed: allSelected ? _deselectAll - : () => _selectAll(allAlbums), + : () => selectAll(allAlbums.map((a) => a.id)), child: Text( allSelected ? context.l10n.actionDeselect @@ -880,7 +857,7 @@ class _ArtistScreenState extends ConsumerState { subtitle: context.l10n.discographySelectAlbumsSubtitle, onTap: () { Navigator.pop(context); - _enterSelectionMode(albums.first.id); + enterSelectionMode(albums.first.id); }, ), const SizedBox(height: 8), @@ -913,7 +890,7 @@ class _ArtistScreenState extends ConsumerState { BuildContext context, List albums, ) async { - _exitSelectionMode(); + exitSelectionMode(); await _downloadAlbums(context, albums); } @@ -1308,7 +1285,7 @@ class _ArtistScreenState extends ConsumerState { ], ), ), - if (!_isSelectionMode) ...[ + if (!isSelectionMode) ...[ const SizedBox(width: 12), Container( width: 52, @@ -1334,7 +1311,7 @@ class _ArtistScreenState extends ConsumerState { ), ), ], - if (hasDiscography && !_isSelectionMode) ...[ + if (hasDiscography && !isSelectionMode) ...[ const SizedBox(width: 12), Container( width: 52, @@ -1745,25 +1722,25 @@ class _ArtistScreenState extends ConsumerState { required double sectionHeight, bool showTypeBadge = false, }) { - final isSelected = _selectedAlbumIds.contains(album.id); + final isSelected = selectedIds.contains(album.id); return Semantics( button: true, - selected: _isSelectionMode && isSelected, - label: _isSelectionMode + selected: isSelectionMode && isSelected, + label: isSelectionMode ? context.l10n.a11ySelectAlbum(album.name) : context.l10n.a11yOpenAlbum(album.name), child: GestureDetector( onTap: () { - if (_isSelectionMode) { - _toggleAlbumSelection(album.id); + if (isSelectionMode) { + toggleSelection(album.id); } else { _navigateToAlbum(album); } }, onLongPress: () { - if (!_isSelectionMode) { - _enterSelectionMode(album.id); + if (!isSelectionMode) { + enterSelectionMode(album.id); } }, child: Container( @@ -1809,7 +1786,7 @@ class _ArtistScreenState extends ConsumerState { ), ), ), - if (_isSelectionMode) + if (isSelectionMode) Positioned.fill( child: AnimatedContainer( duration: const Duration(milliseconds: 200), @@ -1827,7 +1804,7 @@ class _ArtistScreenState extends ConsumerState { ), ), ), - if (_isSelectionMode) + if (isSelectionMode) Positioned( top: 8, right: 8, @@ -1938,7 +1915,6 @@ class _ArtistScreenState extends ConsumerState { ); } } - } class _DiscographyOptionTile extends StatelessWidget { diff --git a/lib/screens/library_tracks_folder_screen.dart b/lib/screens/library_tracks_folder_screen.dart index ee97c0cd..0dbe4150 100644 --- a/lib/screens/library_tracks_folder_screen.dart +++ b/lib/screens/library_tracks_folder_screen.dart @@ -4,7 +4,6 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:spotiflac_android/widgets/album_detail_header.dart'; import 'package:spotiflac_android/widgets/cached_cover_image.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/models/track.dart'; @@ -16,6 +15,8 @@ import 'package:spotiflac_android/providers/local_library_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/utils/nav_bar_inset.dart'; import 'package:spotiflac_android/utils/cover_art_utils.dart'; +import 'package:spotiflac_android/screens/collapsing_header_scroll_mixin.dart'; +import 'package:spotiflac_android/screens/selection_mode_mixin.dart'; import 'package:spotiflac_android/screens/track_metadata_screen.dart'; import 'package:spotiflac_android/widgets/selection_action_button.dart'; import 'package:spotiflac_android/widgets/selection_bottom_bar.dart'; @@ -50,41 +51,12 @@ class LibraryTracksFolderScreen extends ConsumerStatefulWidget { } class _LibraryTracksFolderScreenState - extends ConsumerState { - bool _showTitleInAppBar = false; - final ScrollController _scrollController = ScrollController(); - - bool _isSelectionMode = false; - final Set _selectedKeys = {}; + extends ConsumerState + with + SelectionModeMixin, + CollapsingHeaderScrollMixin { UserPlaylistCollection? playlist; - @override - void initState() { - super.initState(); - _scrollController.addListener(_onScroll); - } - - @override - void dispose() { - _scrollController.removeListener(_onScroll); - _scrollController.dispose(); - super.dispose(); - } - - void _onScroll() { - final expandedHeight = _calculateExpandedHeight(context); - final shouldShow = - _scrollController.offset > (expandedHeight - kToolbarHeight - 20); - if (shouldShow != _showTitleInAppBar) { - setState(() => _showTitleInAppBar = shouldShow); - } - } - - double _calculateExpandedHeight(BuildContext context) { - final mediaSize = MediaQuery.sizeOf(context); - return (mediaSize.height * 0.6).clamp(400.0, 580.0); - } - IconData _modeIcon() { return switch (widget.mode) { LibraryTracksFolderMode.wishlist => Icons.bookmark, @@ -104,42 +76,8 @@ class _LibraryTracksFolderScreenState return null; } - void _enterSelectionMode(String key) { - HapticFeedback.mediumImpact(); - setState(() { - _isSelectionMode = true; - _selectedKeys.add(key); - }); - } - - void _exitSelectionMode() { - setState(() { - _isSelectionMode = false; - _selectedKeys.clear(); - }); - } - - void _toggleSelection(String key) { - setState(() { - if (_selectedKeys.contains(key)) { - _selectedKeys.remove(key); - if (_selectedKeys.isEmpty) { - _isSelectionMode = false; - } - } else { - _selectedKeys.add(key); - } - }); - } - - void _selectAll(List entries) { - setState(() { - _selectedKeys.addAll(entries.map((e) => e.key)); - }); - } - Future _removeSelected(List entries) async { - final keysToRemove = _selectedKeys.toSet(); + final keysToRemove = selectedIds.toSet(); if (keysToRemove.isEmpty) return; final count = keysToRemove.length; @@ -161,7 +99,7 @@ class _LibraryTracksFolderScreenState } } - _exitSelectionMode(); + exitSelectionMode(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( @@ -202,12 +140,12 @@ class _LibraryTracksFolderScreenState var count = 0; for (final entry in entries) { - if (!_selectedKeys.contains(entry.key)) continue; + if (!selectedIds.contains(entry.key)) continue; queueNotifier.addToQueue(entry.track, service); count++; } - _exitSelectionMode(); + exitSelectionMode(); if (!mounted || count == 0) return; ScaffoldMessenger.of(context).showSnackBar( @@ -217,7 +155,7 @@ class _LibraryTracksFolderScreenState void _addSelectedToPlaylist(List entries) { final selectedTracks = entries - .where((e) => _selectedKeys.contains(e.key)) + .where((e) => selectedIds.contains(e.key)) .map((e) => e.track) .toList(growable: false); if (selectedTracks.isEmpty) return; @@ -257,15 +195,7 @@ class _LibraryTracksFolderScreenState break; } - if (_isSelectionMode) { - final validKeys = entries.map((e) => e.key).toSet(); - _selectedKeys.removeWhere((key) => !validKeys.contains(key)); - if (_selectedKeys.isEmpty && _isSelectionMode) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) setState(() => _isSelectionMode = false); - }); - } - } + pruneSelection(entries.map((e) => e.key).toSet()); final title = switch (widget.mode) { LibraryTracksFolderMode.wishlist => context.l10n.collectionWishlist, @@ -308,17 +238,17 @@ class _LibraryTracksFolderScreenState final bottomInset = context.navBarBottomInset; return PopScope( - canPop: !_isSelectionMode, + canPop: !isSelectionMode, onPopInvokedWithResult: (didPop, result) { - if (!didPop && _isSelectionMode) { - _exitSelectionMode(); + if (!didPop && isSelectionMode) { + exitSelectionMode(); } }, child: Scaffold( body: Stack( children: [ CustomScrollView( - controller: _scrollController, + controller: scrollController, slivers: [ _buildAppBar(context, colorScheme, title, entries, playlist), if (entries.isEmpty) @@ -333,7 +263,7 @@ class _LibraryTracksFolderScreenState SliverList( delegate: SliverChildBuilderDelegate((context, index) { final entry = entries[index]; - final isSelected = _selectedKeys.contains(entry.key); + final isSelected = selectedIds.contains(entry.key); final isInHistory = existingHistoryKeys.contains( historyLookups[index].lookupKey, ); @@ -347,21 +277,21 @@ class _LibraryTracksFolderScreenState playlistId: widget.playlistId, folderTracks: folderTracks, isInHistory: isInHistory, - isSelectionMode: _isSelectionMode, + isSelectionMode: isSelectionMode, isSelected: isSelected, - onTap: _isSelectionMode - ? () => _toggleSelection(entry.key) + onTap: isSelectionMode + ? () => toggleSelection(entry.key) : null, - onLongPress: _isSelectionMode + onLongPress: isSelectionMode ? null - : () => _enterSelectionMode(entry.key), + : () => enterSelectionMode(entry.key), ), ), ); }, childCount: entries.length), ), SliverToBoxAdapter( - child: SizedBox(height: _isSelectionMode ? 200 : 32), + child: SizedBox(height: isSelectionMode ? 200 : 32), ), SliverToBoxAdapter(child: SizedBox(height: bottomInset)), ], @@ -372,7 +302,7 @@ class _LibraryTracksFolderScreenState curve: Curves.easeOutCubic, left: 0, right: 0, - bottom: _isSelectionMode ? 0 : -(280 + bottomPadding), + bottom: isSelectionMode ? 0 : -(280 + bottomPadding), child: _buildSelectionBottomBar( context, colorScheme, @@ -392,19 +322,19 @@ class _LibraryTracksFolderScreenState List entries, double bottomPadding, ) { - final selectedCount = _selectedKeys.length; + final selectedCount = selectedIds.length; final allSelected = selectedCount == entries.length && entries.isNotEmpty; final isWishlist = widget.mode == LibraryTracksFolderMode.wishlist; return SelectionBottomBar( selectedCount: selectedCount, allSelected: allSelected, - onClose: _exitSelectionMode, + onClose: exitSelectionMode, onToggleSelectAll: () { if (allSelected) { - _exitSelectionMode(); + exitSelectionMode(); } else { - _selectAll(entries); + selectAll(entries.map((e) => e.key)); } }, bottomPadding: bottomPadding, @@ -502,7 +432,7 @@ class _LibraryTracksFolderScreenState List entries, UserPlaylistCollection? playlist, ) { - final expandedHeight = _calculateExpandedHeight(context); + final expandedHeight = calculateExpandedHeight(context); final customCoverPath = playlist?.coverImagePath; final isLovedMode = widget.mode == LibraryTracksFolderMode.loved; final isPlaylistMode = widget.mode == LibraryTracksFolderMode.playlist; @@ -563,11 +493,11 @@ class _LibraryTracksFolderScreenState return AlbumDetailHeader( title: title, - appBarTitle: _isSelectionMode - ? context.l10n.selectionSelected(_selectedKeys.length) + appBarTitle: isSelectionMode + ? context.l10n.selectionSelected(selectedIds.length) : title, expandedHeight: expandedHeight, - showTitleInAppBar: _showTitleInAppBar, + showTitleInAppBar: showTitleInAppBar, background: background, blurAndScrimBackground: hasCustomCover || hasCoverUrl, coverBuilder: (context, coverSize) { @@ -633,7 +563,7 @@ class _LibraryTracksFolderScreenState ) : null, appBarActions: [ - if (isPlaylistMode && !_isSelectionMode) ...[ + if (isPlaylistMode && !isSelectionMode) ...[ IconButton( tooltip: context.l10n.collectionRenamePlaylist, icon: Container( @@ -672,7 +602,7 @@ class _LibraryTracksFolderScreenState ], ], leading: IconButton( - tooltip: _isSelectionMode + tooltip: isSelectionMode ? MaterialLocalizations.of(context).closeButtonTooltip : MaterialLocalizations.of(context).backButtonTooltip, icon: Container( @@ -682,12 +612,12 @@ class _LibraryTracksFolderScreenState shape: BoxShape.circle, ), child: Icon( - _isSelectionMode ? Icons.close : Icons.arrow_back, + isSelectionMode ? Icons.close : Icons.arrow_back, color: Colors.white, ), ), - onPressed: _isSelectionMode - ? _exitSelectionMode + onPressed: isSelectionMode + ? exitSelectionMode : () => Navigator.pop(context), ), ); @@ -712,7 +642,11 @@ class _LibraryTracksFolderScreenState } void _confirmDownloadAll(List tracks) { - confirmDownloadAllDialog(context, tracks.length, () => _downloadAll(tracks)); + confirmDownloadAllDialog( + context, + tracks.length, + () => _downloadAll(tracks), + ); } Future _downloadAll(List tracks) async { diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index e83a1905..04c5dfa3 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -17,6 +17,7 @@ import 'package:spotiflac_android/widgets/cached_cover_image.dart'; import 'package:spotiflac_android/widgets/motion_header_banner.dart'; import 'package:spotiflac_android/widgets/track_list_tile.dart'; import 'package:spotiflac_android/widgets/track_detail_actions.dart'; +import 'package:spotiflac_android/screens/collapsing_header_scroll_mixin.dart'; import 'package:spotiflac_android/widgets/error_card.dart'; class PlaylistScreen extends ConsumerStatefulWidget { @@ -41,9 +42,8 @@ class PlaylistScreen extends ConsumerStatefulWidget { ConsumerState createState() => _PlaylistScreenState(); } -class _PlaylistScreenState extends ConsumerState { - bool _showTitleInAppBar = false; - final ScrollController _scrollController = ScrollController(); +class _PlaylistScreenState extends ConsumerState + with CollapsingHeaderScrollMixin { List? _fetchedTracks; bool _isLoading = false; String? _error; @@ -57,8 +57,6 @@ class _PlaylistScreenState extends ConsumerState { String? get _headerVideoUrl => _resolvedHeaderVideoUrl ?? widget.headerVideoUrl; - - String? _metadataProviderId(String playlistId) { final providerId = legacyProviderIdFromResourceId(playlistId); if (providerId == null) return null; @@ -109,17 +107,9 @@ class _PlaylistScreenState extends ConsumerState { @override void initState() { super.initState(); - _scrollController.addListener(_onScroll); _fetchTracksIfNeeded(); } - @override - void dispose() { - _scrollController.removeListener(_onScroll); - _scrollController.dispose(); - super.dispose(); - } - Future _fetchTracksIfNeeded() async { if (widget.tracks.isNotEmpty || widget.playlistId == null) return; @@ -178,27 +168,13 @@ class _PlaylistScreenState extends ConsumerState { } } - void _onScroll() { - final expandedHeight = _calculateExpandedHeight(context); - final shouldShow = - _scrollController.offset > (expandedHeight - kToolbarHeight - 20); - if (shouldShow != _showTitleInAppBar) { - setState(() => _showTitleInAppBar = shouldShow); - } - } - - double _calculateExpandedHeight(BuildContext context) { - final mediaSize = MediaQuery.of(context).size; - return (mediaSize.height * 0.6).clamp(400.0, 580.0); - } - @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Scaffold( body: CustomScrollView( - controller: _scrollController, + controller: scrollController, slivers: [ _buildAppBar(context, colorScheme), _buildTrackList(context, colorScheme), @@ -212,7 +188,7 @@ class _PlaylistScreenState extends ConsumerState { } Widget _buildAppBar(BuildContext context, ColorScheme colorScheme) { - final expandedHeight = _calculateExpandedHeight(context); + final expandedHeight = calculateExpandedHeight(context); final cacheWidth = coverCacheWidthForViewport(context); final motionUrl = _headerVideoUrl; final hasMotion = @@ -243,7 +219,7 @@ class _PlaylistScreenState extends ConsumerState { return AlbumDetailHeader( title: _playlistName, expandedHeight: expandedHeight, - showTitleInAppBar: _showTitleInAppBar, + showTitleInAppBar: showTitleInAppBar, background: hasMotion ? MotionHeaderBanner(videoUrl: motionUrl, fallback: coverImage) : coverImage, @@ -403,59 +379,18 @@ class _PlaylistScreenState extends ConsumerState { ); } - Widget _buildCircleButton({ - required IconData icon, - required String tooltip, - required VoidCallback? onPressed, - }) { - return Container( - width: 48, - height: 48, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.white.withValues(alpha: 0.15), - border: Border.all( - color: Colors.white.withValues(alpha: 0.3), - width: 1, - ), - ), - child: IconButton( - onPressed: onPressed, - icon: Icon(icon, size: 22, color: Colors.white), - tooltip: tooltip, - padding: EdgeInsets.zero, - ), - ); - } - Widget _buildLoveAllButton() { final collectionsState = ref.watch(libraryCollectionsProvider); final allLoved = _tracks.isNotEmpty && _tracks.every((t) => collectionsState.isLoved(t)); - return Container( - width: 48, - height: 48, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.white.withValues(alpha: 0.15), - border: Border.all( - color: Colors.white.withValues(alpha: 0.3), - width: 1, - ), - ), - child: IconButton( - onPressed: _tracks.isEmpty ? null : () => _loveAll(_tracks), - icon: Icon( - allLoved ? Icons.favorite : Icons.favorite_border, - size: 22, - color: allLoved ? Colors.redAccent : Colors.white, - ), - tooltip: allLoved - ? context.l10n.trackOptionRemoveFromLoved - : context.l10n.tooltipLoveAll, - padding: EdgeInsets.zero, - ), + return HeaderCircleButton( + icon: allLoved ? Icons.favorite : Icons.favorite_border, + iconColor: allLoved ? Colors.redAccent : Colors.white, + tooltip: allLoved + ? context.l10n.trackOptionRemoveFromLoved + : context.l10n.tooltipLoveAll, + onPressed: _tracks.isEmpty ? null : () => _loveAll(_tracks), ); } @@ -478,7 +413,7 @@ class _PlaylistScreenState extends ConsumerState { } Widget _buildAddToPlaylistButton(BuildContext context) { - return _buildCircleButton( + return HeaderCircleButton( icon: Icons.playlist_add, tooltip: context.l10n.tooltipAddToPlaylist, onPressed: _tracks.isEmpty @@ -500,7 +435,8 @@ class _PlaylistScreenState extends ConsumerState { ); } - Future _loveAll(List tracks) => loveAllTracks(context, ref, tracks); + Future _loveAll(List tracks) => + loveAllTracks(context, ref, tracks); void _downloadAll(BuildContext context) { _downloadTracks(context, _tracks); diff --git a/lib/screens/queue_tab_selection.dart b/lib/screens/queue_tab_selection.dart index a741744e..815c1d99 100644 --- a/lib/screens/queue_tab_selection.dart +++ b/lib/screens/queue_tab_selection.dart @@ -336,158 +336,77 @@ extension _QueueTabSelectionActions on _QueueTabState { final allSelected = selectedCount == playlists.length && playlists.isNotEmpty; - return Container( - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHigh, - borderRadius: const BorderRadius.vertical(top: Radius.circular(28)), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.15), - blurRadius: 12, - offset: const Offset(0, -4), - ), - ], - ), - child: SafeArea( - top: false, - child: Padding( - padding: EdgeInsets.fromLTRB(16, 16, 16, bottomPadding > 0 ? 8 : 16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 32, - height: 4, - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: colorScheme.outlineVariant, - borderRadius: BorderRadius.circular(2), - ), + return SelectionBottomBar( + selectedCount: selectedCount, + allSelected: allSelected, + onClose: _exitPlaylistSelectionMode, + onToggleSelectAll: () { + if (allSelected) { + _exitPlaylistSelectionMode(); + } else { + _selectAllPlaylists(playlists); + } + }, + bottomPadding: bottomPadding, + allSelectedLabel: context.l10n.selectionAllPlaylistsSelected, + tapToSelectLabel: context.l10n.selectionTapPlaylistsToSelect, + children: [ + SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: selectedCount > 0 + ? () => _downloadAllSelectedPlaylists(context) + : null, + icon: const Icon(Icons.download_rounded), + label: Text( + selectedCount > 0 + ? context.l10n.bulkDownloadPlaylistsButton(selectedCount) + : context.l10n.bulkDownloadSelectPlaylists, + ), + style: FilledButton.styleFrom( + backgroundColor: selectedCount > 0 + ? colorScheme.primary + : colorScheme.surfaceContainerHighest, + foregroundColor: selectedCount > 0 + ? colorScheme.onPrimary + : colorScheme.onSurfaceVariant, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), ), - - Row( - children: [ - IconButton.filledTonal( - onPressed: _exitPlaylistSelectionMode, - tooltip: MaterialLocalizations.of( - context, - ).closeButtonTooltip, - icon: const Icon(Icons.close), - style: IconButton.styleFrom( - backgroundColor: colorScheme.surfaceContainerHighest, - ), - ), - const SizedBox(width: 12), - - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.l10n.selectionSelected(selectedCount), - style: Theme.of(context).textTheme.titleMedium - ?.copyWith(fontWeight: FontWeight.bold), - ), - Text( - allSelected - ? context.l10n.selectionAllPlaylistsSelected - : context.l10n.selectionTapPlaylistsToSelect, - style: Theme.of(context).textTheme.bodySmall - ?.copyWith(color: colorScheme.onSurfaceVariant), - ), - ], - ), - ), - - TextButton.icon( - onPressed: () { - if (allSelected) { - _exitPlaylistSelectionMode(); - } else { - _selectAllPlaylists(playlists); - } - }, - icon: Icon( - allSelected ? Icons.deselect : Icons.select_all, - size: 20, - ), - label: Text( - allSelected - ? context.l10n.actionDeselect - : context.l10n.actionSelectAll, - ), - style: TextButton.styleFrom( - foregroundColor: colorScheme.primary, - ), - ), - ], - ), - - const SizedBox(height: 12), - - SizedBox( - width: double.infinity, - child: FilledButton.icon( - onPressed: selectedCount > 0 - ? () => _downloadAllSelectedPlaylists(context) - : null, - icon: const Icon(Icons.download_rounded), - label: Text( - selectedCount > 0 - ? context.l10n.bulkDownloadPlaylistsButton( - selectedCount, - ) - : context.l10n.bulkDownloadSelectPlaylists, - ), - style: FilledButton.styleFrom( - backgroundColor: selectedCount > 0 - ? colorScheme.primary - : colorScheme.surfaceContainerHighest, - foregroundColor: selectedCount > 0 - ? colorScheme.onPrimary - : colorScheme.onSurfaceVariant, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - ), - ), - ), - - const SizedBox(height: 8), - - SizedBox( - width: double.infinity, - child: FilledButton.icon( - onPressed: selectedCount > 0 - ? () => _deleteSelectedPlaylists(context) - : null, - icon: const Icon(Icons.delete_outline), - label: Text( - selectedCount > 0 - ? context.l10n.selectionDeletePlaylistsCount( - selectedCount, - ) - : context.l10n.selectionSelectPlaylistsToDelete, - ), - style: FilledButton.styleFrom( - backgroundColor: selectedCount > 0 - ? colorScheme.error - : colorScheme.surfaceContainerHighest, - foregroundColor: selectedCount > 0 - ? colorScheme.onError - : colorScheme.onSurfaceVariant, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - ), - ), - ), - ], + ), ), ), - ), + + const SizedBox(height: 8), + + SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: selectedCount > 0 + ? () => _deleteSelectedPlaylists(context) + : null, + icon: const Icon(Icons.delete_outline), + label: Text( + selectedCount > 0 + ? context.l10n.selectionDeletePlaylistsCount(selectedCount) + : context.l10n.selectionSelectPlaylistsToDelete, + ), + style: FilledButton.styleFrom( + backgroundColor: selectedCount > 0 + ? colorScheme.error + : colorScheme.surfaceContainerHighest, + foregroundColor: selectedCount > 0 + ? colorScheme.onError + : colorScheme.onSurfaceVariant, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ), + ), + ], ); } @@ -585,5 +504,4 @@ extension _QueueTabSelectionActions on _QueueTabState { Future _readFileModTimeMillis(String? filePath) async { return DownloadedEmbeddedCoverResolver.readFileModTimeMillis(filePath); } - } diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 28a2b0af..e3460261 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -33,6 +33,7 @@ import 'package:spotiflac_android/widgets/batch_convert_sheet.dart'; import 'package:spotiflac_android/widgets/cached_cover_image.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; import 'package:spotiflac_android/constants/music_services.dart'; +import 'package:spotiflac_android/screens/collapsing_header_scroll_mixin.dart'; part 'track_metadata_edit_sheet.dart'; part 'track_metadata_cards.dart'; @@ -92,7 +93,8 @@ class TrackMetadataScreen extends ConsumerStatefulWidget { _TrackMetadataScreenState(); } -class _TrackMetadataScreenState extends ConsumerState { +class _TrackMetadataScreenState extends ConsumerState + with CollapsingHeaderScrollMixin { static const int _maxCoverPreviewCacheEntries = 96; static final Map _embeddedCoverPreviewCache = {}; @@ -105,7 +107,6 @@ class _TrackMetadataScreenState extends ConsumerState { bool _lyricsLoading = false; String? _lyricsError; String? _lyricsSource; - bool _showTitleInAppBar = false; bool _lyricsEmbedded = false; bool _isEmbedding = false; bool _isInstrumental = false; @@ -121,7 +122,6 @@ class _TrackMetadataScreenState extends ConsumerState { late int? _currentNavigationIndex; Map? _editedMetadata; String? _embeddedCoverPreviewPath; - final ScrollController _scrollController = ScrollController(); static final RegExp _lrcTimestampPattern = RegExp( r'^\[\d{2}:\d{2}\.\d{2,3}\]', ); @@ -247,15 +247,12 @@ class _TrackMetadataScreenState extends ConsumerState { _currentDownloadItem = widget.item; _currentLocalLibraryItem = widget.localItem; _currentNavigationIndex = widget.navigationIndex; - _scrollController.addListener(_onScroll); _checkFile(); } @override void dispose() { unawaited(_cleanupTempFileAndParentIfNotCached(_embeddedCoverPreviewPath)); - _scrollController.removeListener(_onScroll); - _scrollController.dispose(); super.dispose(); } @@ -263,16 +260,8 @@ class _TrackMetadataScreenState extends ConsumerState { /// through this forwarder. void _setState(VoidCallback fn) => setState(fn); - void _onScroll() { - final expandedHeight = _calculateExpandedHeight(context); - final shouldShow = - _scrollController.offset > (expandedHeight - kToolbarHeight - 20); - if (shouldShow != _showTitleInAppBar) { - setState(() => _showTitleInAppBar = shouldShow); - } - } - - double _calculateExpandedHeight(BuildContext context) { + @override + double calculateExpandedHeight(BuildContext context) { final mediaSize = MediaQuery.sizeOf(context); return (mediaSize.height * 0.55).clamp(360.0, 520.0); } @@ -775,7 +764,8 @@ class _TrackMetadataScreenState extends ConsumerState { String? get _localCoverPath => _isLocalItem ? _localLibraryItem!.coverPath : null; String? get _spotifyId => _isLocalItem ? null : _downloadItem!.spotifyId; - String get _service => _isLocalItem ? MusicServices.local : _downloadItem!.service; + String get _service => + _isLocalItem ? MusicServices.local : _downloadItem!.service; DateTime get _addedAt { if (_isLocalItem) { final modTime = _localLibraryItem!.fileModTime; @@ -1083,7 +1073,7 @@ class _TrackMetadataScreenState extends ConsumerState { _lyricsLoading = false; _lyricsError = null; _lyricsSource = null; - _showTitleInAppBar = false; + showTitleInAppBar = false; _lyricsEmbedded = false; _isInstrumental = false; _embeddedLyricsChecked = false; @@ -1092,8 +1082,8 @@ class _TrackMetadataScreenState extends ConsumerState { _embeddedCoverPreviewPath = null; }); - if (_scrollController.hasClients) { - _scrollController.jumpTo(0); + if (scrollController.hasClients) { + scrollController.jumpTo(0); } if (oldPreviewPath != null) { @@ -1110,7 +1100,7 @@ class _TrackMetadataScreenState extends ConsumerState { @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - final expandedHeight = _calculateExpandedHeight(context); + final expandedHeight = calculateExpandedHeight(context); final bottomInset = context.navBarBottomInset; return GestureDetector( @@ -1118,7 +1108,7 @@ class _TrackMetadataScreenState extends ConsumerState { onHorizontalDragEnd: _handleHorizontalDragEnd, child: Scaffold( body: CustomScrollView( - controller: _scrollController, + controller: scrollController, slivers: [ SliverAppBar( expandedHeight: expandedHeight, @@ -1128,7 +1118,7 @@ class _TrackMetadataScreenState extends ConsumerState { surfaceTintColor: Colors.transparent, title: AnimatedOpacity( duration: const Duration(milliseconds: 200), - opacity: _showTitleInAppBar ? 1.0 : 0.0, + opacity: showTitleInAppBar ? 1.0 : 0.0, child: Text( trackName, style: TextStyle( @@ -1500,7 +1490,6 @@ class _TrackMetadataScreenState extends ConsumerState { return placeholder(); } - } class _MetadataOption { diff --git a/lib/widgets/album_detail_header.dart b/lib/widgets/album_detail_header.dart index 32ba16d6..31c64e69 100644 --- a/lib/widgets/album_detail_header.dart +++ b/lib/widgets/album_detail_header.dart @@ -283,6 +283,45 @@ class AlbumPlayActions extends StatelessWidget { } } +/// 48x48 translucent-white circle icon button used in album/playlist headers +/// (add-to-playlist, love-all, ...). +class HeaderCircleButton extends StatelessWidget { + const HeaderCircleButton({ + super.key, + required this.icon, + required this.tooltip, + required this.onPressed, + this.iconColor = Colors.white, + }); + + final IconData icon; + final String tooltip; + final VoidCallback? onPressed; + final Color iconColor; + + @override + Widget build(BuildContext context) { + return Container( + width: 48, + height: 48, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withValues(alpha: 0.15), + border: Border.all( + color: Colors.white.withValues(alpha: 0.3), + width: 1, + ), + ), + child: IconButton( + onPressed: onPressed, + icon: Icon(icon, size: 22, color: iconColor), + tooltip: tooltip, + padding: EdgeInsets.zero, + ), + ); + } +} + /// "•"-joined row of white [HeaderMetaItem]s shown under the header subtitle /// (track count, duration, quality, ...). class HeaderMetaRow extends StatelessWidget { diff --git a/lib/widgets/selection_bottom_bar.dart b/lib/widgets/selection_bottom_bar.dart index 0067427d..382df44f 100644 --- a/lib/widgets/selection_bottom_bar.dart +++ b/lib/widgets/selection_bottom_bar.dart @@ -13,6 +13,8 @@ class SelectionBottomBar extends StatelessWidget { required this.onToggleSelectAll, required this.bottomPadding, required this.children, + this.allSelectedLabel, + this.tapToSelectLabel, }); final int selectedCount; @@ -22,6 +24,12 @@ class SelectionBottomBar extends StatelessWidget { final double bottomPadding; final List children; + /// Overrides the default "All tracks selected" subtitle. + final String? allSelectedLabel; + + /// Overrides the default "Tap tracks to select" subtitle. + final String? tapToSelectLabel; + @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; @@ -80,8 +88,10 @@ class SelectionBottomBar extends StatelessWidget { ), Text( allSelected - ? context.l10n.selectionAllSelected - : context.l10n.downloadedAlbumTapToSelect, + ? allSelectedLabel ?? + context.l10n.selectionAllSelected + : tapToSelectLabel ?? + context.l10n.downloadedAlbumTapToSelect, style: Theme.of(context).textTheme.bodySmall ?.copyWith(color: colorScheme.onSurfaceVariant), ),