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
This commit is contained in:
zarzet
2026-07-12 19:38:50 +07:00
parent 44de61a06a
commit 62015a4e2d
8 changed files with 259 additions and 559 deletions
+30 -132
View File
@@ -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<AlbumScreen> createState() => _AlbumScreenState();
}
class _AlbumScreenState extends ConsumerState<AlbumScreen> {
class _AlbumScreenState extends ConsumerState<AlbumScreen>
with CollapsingHeaderScrollMixin<AlbumScreen> {
List<Track>? _tracks;
bool _isLoading = false;
String? _error;
bool _showTitleInAppBar = false;
String? _artistId;
String? _albumType;
int? _albumTotalTracks;
String? _headerVideoUrl;
String? _headerImageUrl;
List<String> _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<AlbumScreen> {
);
}
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
WidgetsBinding.instance.addPostFrameCallback((_) {
final providerId = _effectiveMetadataProviderIdFromAlbumId();
ref
@@ -129,30 +124,6 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
}
}
@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<void> _fetchTracks() async {
setState(() => _isLoading = true);
try {
@@ -258,25 +229,6 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
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<Widget> _audioTraitInline() {
final traits = _audioTraits
.map((t) => t.toLowerCase().trim())
@@ -288,15 +240,15 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
final items = <Widget>[];
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<AlbumScreen> {
Widget _buildHeaderMeta(BuildContext context, String? releaseDate) {
final items = <Widget>[];
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<AlbumScreen> {
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<AlbumScreen> {
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<AlbumScreen> {
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<AlbumScreen> {
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<AlbumScreen> {
);
}
Future<void> _loveAll(List<Track> tracks) => loveAllTracks(context, ref, tracks);
Future<void> _loveAll(List<Track> tracks) =>
loveAllTracks(context, ref, tracks);
}
+39 -63
View File
@@ -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<ArtistScreen> createState() => _ArtistScreenState();
}
class _ArtistScreenState extends ConsumerState<ArtistScreen> {
class _ArtistScreenState extends ConsumerState<ArtistScreen>
with SelectionModeMixin<ArtistScreen> {
bool _isLoadingDiscography = false;
List<ArtistAlbum>? _albums;
List<ArtistAlbum>? _releases;
@@ -119,8 +121,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
final PageController _popularPageController = PageController();
int _popularCurrentPage = 0;
bool _isSelectionMode = false;
final Set<String> _selectedAlbumIds = {};
bool _isFetchingDiscography = false;
List<ArtistAlbum>? _albumBucketSource;
List<ArtistAlbum> _albumsOnlyBucket = const [];
@@ -162,7 +162,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
return _directMetadataProviderId();
}
String _effectiveMetadataProviderIdFromArtistId() {
if (widget.extensionId != null && widget.extensionId!.isNotEmpty) {
return widget.extensionId!;
@@ -173,7 +172,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
);
}
String? _directMetadataProviderId() {
final providerId = _effectiveMetadataProviderIdFromArtistId();
return providerId.isEmpty ? null : providerId;
@@ -464,10 +462,10 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
!_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<ArtistScreen> {
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<ArtistScreen> {
),
],
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<ArtistScreen> {
);
}
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<ArtistAlbum> 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<ArtistScreen> {
ColorScheme colorScheme,
List<ArtistAlbum> 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<int>(
0,
@@ -643,7 +620,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
Row(
children: [
IconButton(
onPressed: _exitSelectionMode,
onPressed: exitSelectionMode,
icon: const Icon(Icons.close),
tooltip: context.l10n.dialogCancel,
),
@@ -684,7 +661,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
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<ArtistScreen> {
: Row(
children: [
IconButton(
onPressed: _exitSelectionMode,
onPressed: exitSelectionMode,
icon: const Icon(Icons.close),
tooltip: context.l10n.dialogCancel,
),
@@ -754,7 +731,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
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<ArtistScreen> {
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<ArtistScreen> {
BuildContext context,
List<ArtistAlbum> albums,
) async {
_exitSelectionMode();
exitSelectionMode();
await _downloadAlbums(context, albums);
}
@@ -1308,7 +1285,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
],
),
),
if (!_isSelectionMode) ...[
if (!isSelectionMode) ...[
const SizedBox(width: 12),
Container(
width: 52,
@@ -1334,7 +1311,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
),
),
],
if (hasDiscography && !_isSelectionMode) ...[
if (hasDiscography && !isSelectionMode) ...[
const SizedBox(width: 12),
Container(
width: 52,
@@ -1745,25 +1722,25 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
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<ArtistScreen> {
),
),
),
if (_isSelectionMode)
if (isSelectionMode)
Positioned.fill(
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
@@ -1827,7 +1804,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
),
),
),
if (_isSelectionMode)
if (isSelectionMode)
Positioned(
top: 8,
right: 8,
@@ -1938,7 +1915,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
);
}
}
}
class _DiscographyOptionTile extends StatelessWidget {
+42 -108
View File
@@ -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<LibraryTracksFolderScreen> {
bool _showTitleInAppBar = false;
final ScrollController _scrollController = ScrollController();
bool _isSelectionMode = false;
final Set<String> _selectedKeys = {};
extends ConsumerState<LibraryTracksFolderScreen>
with
SelectionModeMixin<LibraryTracksFolderScreen>,
CollapsingHeaderScrollMixin<LibraryTracksFolderScreen> {
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<CollectionTrackEntry> entries) {
setState(() {
_selectedKeys.addAll(entries.map((e) => e.key));
});
}
Future<void> _removeSelected(List<CollectionTrackEntry> 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<CollectionTrackEntry> 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<CollectionTrackEntry> 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<CollectionTrackEntry> 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<Track> tracks) {
confirmDownloadAllDialog(context, tracks.length, () => _downloadAll(tracks));
confirmDownloadAllDialog(
context,
tracks.length,
() => _downloadAll(tracks),
);
}
Future<void> _downloadAll(List<Track> tracks) async {
+16 -80
View File
@@ -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<PlaylistScreen> createState() => _PlaylistScreenState();
}
class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
bool _showTitleInAppBar = false;
final ScrollController _scrollController = ScrollController();
class _PlaylistScreenState extends ConsumerState<PlaylistScreen>
with CollapsingHeaderScrollMixin<PlaylistScreen> {
List<Track>? _fetchedTracks;
bool _isLoading = false;
String? _error;
@@ -57,8 +57,6 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
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<PlaylistScreen> {
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
_fetchTracksIfNeeded();
}
@override
void dispose() {
_scrollController.removeListener(_onScroll);
_scrollController.dispose();
super.dispose();
}
Future<void> _fetchTracksIfNeeded() async {
if (widget.tracks.isNotEmpty || widget.playlistId == null) return;
@@ -178,27 +168,13 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
}
}
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<PlaylistScreen> {
}
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<PlaylistScreen> {
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<PlaylistScreen> {
);
}
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<PlaylistScreen> {
}
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<PlaylistScreen> {
);
}
Future<void> _loveAll(List<Track> tracks) => loveAllTracks(context, ref, tracks);
Future<void> _loveAll(List<Track> tracks) =>
loveAllTracks(context, ref, tracks);
void _downloadAll(BuildContext context) {
_downloadTracks(context, _tracks);
+68 -150
View File
@@ -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<int?> _readFileModTimeMillis(String? filePath) async {
return DownloadedEmbeddedCoverResolver.readFileModTimeMillis(filePath);
}
}
+13 -24
View File
@@ -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<TrackMetadataScreen> {
class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen>
with CollapsingHeaderScrollMixin<TrackMetadataScreen> {
static const int _maxCoverPreviewCacheEntries = 96;
static final Map<String, _EmbeddedCoverPreviewCacheEntry>
_embeddedCoverPreviewCache = {};
@@ -105,7 +107,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
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<TrackMetadataScreen> {
late int? _currentNavigationIndex;
Map<String, dynamic>? _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<TrackMetadataScreen> {
_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<TrackMetadataScreen> {
/// 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<TrackMetadataScreen> {
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<TrackMetadataScreen> {
_lyricsLoading = false;
_lyricsError = null;
_lyricsSource = null;
_showTitleInAppBar = false;
showTitleInAppBar = false;
_lyricsEmbedded = false;
_isInstrumental = false;
_embeddedLyricsChecked = false;
@@ -1092,8 +1082,8 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
_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<TrackMetadataScreen> {
@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<TrackMetadataScreen> {
onHorizontalDragEnd: _handleHorizontalDragEnd,
child: Scaffold(
body: CustomScrollView(
controller: _scrollController,
controller: scrollController,
slivers: [
SliverAppBar(
expandedHeight: expandedHeight,
@@ -1128,7 +1118,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
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<TrackMetadataScreen> {
return placeholder();
}
}
class _MetadataOption {
+39
View File
@@ -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 {
+12 -2
View File
@@ -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<Widget> 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),
),