Files
SpotiFLAC-Mobile/lib/screens/artist_screen_widgets.dart
T
zarzet 9a62244263 feat(library): refine album and artist artwork headers
Raise album details and artist actions over the artwork, soften the lower artist banner, and derive action colors from the artist logo. Match loading skeletons to the updated header layouts and preserve scrolling and large text behavior.
2026-09-25 01:20:54 +07:00

1222 lines
43 KiB
Dart

// ignore_for_file: invalid_use_of_protected_member
part of 'artist_screen.dart';
extension _ArtistScreenSections on _ArtistScreenState {
List<Widget> _buildHeader(
BuildContext context,
ColorScheme colorScheme, {
required List<ArtistAlbum> albums,
required bool hasDiscography,
}) {
String? imageUrl = _headerImageUrl;
if (imageUrl == null || imageUrl.isEmpty) {
imageUrl = widget.headerImageUrl;
}
if (imageUrl == null || imageUrl.isEmpty) {
imageUrl = widget.coverUrl;
}
final hasValidImage =
imageUrl != null &&
imageUrl.isNotEmpty &&
Uri.tryParse(imageUrl)?.hasAuthority == true;
String? headerVideoUrl = _headerVideoUrl;
if (headerVideoUrl == null || headerVideoUrl.isEmpty) {
headerVideoUrl = widget.headerVideoUrl;
}
final hasMotionBanner =
headerVideoUrl != null &&
headerVideoUrl.isNotEmpty &&
Uri.tryParse(headerVideoUrl)?.hasAuthority == true;
String? listenersText;
final listeners = _monthlyListeners ?? widget.monthlyListeners;
if (listeners != null && listeners > 0) {
final formatter = NumberFormat.compact();
listenersText = context.l10n.artistMonthlyListeners(
formatter.format(listeners),
);
}
final favoriteProviderId = _directMetadataProviderId();
final favoriteArtistId = _metadataResourceId(favoriteProviderId ?? '');
final isFavoriteArtist = ref.watch(
libraryCollectionsProvider.select(
(state) => state.isFavoriteArtist(
artistId: favoriteArtistId,
providerId: favoriteProviderId,
),
),
);
if (context.isMornye) {
final artwork = hasValidImage
? DownloadableCover(
coverUrl: imageUrl,
baseName: widget.artistName,
child: CachedCoverImage(
imageUrl: imageUrl,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
memCacheWidth: 800,
errorWidget: (_, _, _) =>
const Center(child: Icon(CupertinoIcons.person, size: 80)),
),
)
: const Center(child: Icon(CupertinoIcons.person, size: 80));
return MornyeArtistHeader(
name: widget.artistName,
logoUrl: _headerLogoUrl ?? widget.headerLogoUrl,
listeners: listenersText,
showTitle: _showTitleInAppBar,
artwork: hasMotionBanner
? MotionHeaderBanner(videoUrl: headerVideoUrl, fallback: artwork)
: artwork,
actions: isSelectionMode
? const []
: [
HeaderCircleButton(
icon: CupertinoIcons.square_arrow_up,
buttonSize: 50,
iconSize: 24,
iconColor: colorScheme.primary,
glassTintColor: Colors.white,
glassTintOpacity: 0.10,
tooltip: context.l10n.openInOtherServices,
onPressed: () => _showShareSheet(context),
),
if (_isLoadingDiscography)
const ShimmerLoading(
child: SkeletonBox(width: 74, height: 74, borderRadius: 37),
)
else if (hasDiscography)
CupertinoButton(
padding: EdgeInsets.zero,
borderRadius: BorderRadius.circular(37),
color: colorScheme.primary,
onPressed: () =>
_showDiscographyOptions(context, colorScheme, albums),
child: SizedBox.square(
dimension: 74,
child: Semantics(
label: context.l10n.discographyDownload,
child: Icon(
CupertinoIcons.arrow_down,
size: 34,
color: colorScheme.onPrimary,
),
),
),
),
HeaderCircleButton(
icon: isFavoriteArtist
? CupertinoIcons.star_fill
: CupertinoIcons.star,
buttonSize: 50,
iconSize: 24,
iconColor: colorScheme.primary,
glassTintColor: Colors.white,
glassTintOpacity: 0.10,
tooltip: isFavoriteArtist
? context.l10n.artistOptionRemoveFromFavorites
: context.l10n.artistOptionAddToFavorites,
onPressed: () => _toggleFavoriteArtist(context),
),
],
).buildSlivers(context);
}
return [
CoverPaletteBuilder(
imageSource: hasValidImage ? imageUrl : null,
builder: (context, headerScheme) => _buildArtistAppBar(
context,
colorScheme,
headerScheme,
albums: albums,
hasDiscography: hasDiscography,
imageUrl: imageUrl,
hasValidImage: hasValidImage,
headerVideoUrl: headerVideoUrl,
hasMotionBanner: hasMotionBanner,
listenersText: listenersText,
isFavoriteArtist: isFavoriteArtist,
),
),
];
}
Widget _buildArtistAppBar(
BuildContext context,
ColorScheme colorScheme,
ColorScheme headerScheme, {
required List<ArtistAlbum> albums,
required bool hasDiscography,
required String? imageUrl,
required bool hasValidImage,
required String? headerVideoUrl,
required bool hasMotionBanner,
required String? listenersText,
required bool isFavoriteArtist,
}) {
final edgeInset = detailHeaderEdgeInset(context);
return SliverAppBar(
expandedHeight: hasDiscography ? 420 : 380,
pinned: true,
stretch: true,
backgroundColor: colorScheme.surface,
surfaceTintColor: Colors.transparent,
title: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: _showTitleInAppBar ? 1.0 : 0.0,
child: Text(
widget.artistName,
style: TextStyle(
color: colorScheme.onSurface,
fontWeight: FontWeight.w600,
fontSize: 16,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.none,
background: Stack(
fit: StackFit.expand,
children: [
if (hasMotionBanner)
MotionHeaderBanner(
videoUrl: headerVideoUrl!,
fallback: hasValidImage
? DownloadableCover(
coverUrl: imageUrl,
baseName: widget.artistName,
child: CachedCoverImage(
imageUrl: imageUrl!,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
memCacheWidth: 800,
placeholder: (context, url) => Container(
color: colorScheme.surfaceContainerHighest,
),
errorWidget: (context, url, error) => Container(
color: colorScheme.surfaceContainerHighest,
child: Icon(
Icons.person,
size: 80,
color: colorScheme.onSurfaceVariant,
),
),
),
)
: Container(
color: colorScheme.surfaceContainerHighest,
child: Icon(
Icons.person,
size: 80,
color: colorScheme.onSurfaceVariant,
),
),
)
else if (hasValidImage)
DownloadableCover(
coverUrl: imageUrl,
baseName: widget.artistName,
child: CachedCoverImage(
imageUrl: imageUrl!,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
memCacheWidth: 800,
placeholder: (context, url) =>
Container(color: colorScheme.surfaceContainerHighest),
errorWidget: (context, url, error) => Container(
color: colorScheme.surfaceContainerHighest,
child: Icon(
Icons.person,
size: 80,
color: colorScheme.onSurfaceVariant,
),
),
),
)
else
Container(
color: colorScheme.surfaceContainerHighest,
child: Icon(
Icons.person,
size: 80,
color: colorScheme.onSurfaceVariant,
),
),
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
headerScheme.surface.withValues(alpha: 0),
headerScheme.surface.withValues(alpha: 0.35),
headerScheme.surface.withValues(alpha: 0.75),
headerScheme.surface,
],
stops: const [0.0, 0.5, 0.75, 1.0],
),
),
),
Positioned(
left: 16 + edgeInset,
right: 16 + edgeInset,
bottom: 16,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.artistName,
style: Theme.of(context).textTheme.headlineLarge
?.copyWith(
fontWeight: FontWeight.bold,
color: headerScheme.onSurface,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (listenersText != null) ...[
const SizedBox(height: 4),
Text(
listenersText,
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(
color: headerScheme.onSurfaceVariant,
),
),
],
],
),
),
if (!isSelectionMode) ...[
const SizedBox(width: 12),
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
color: headerScheme.primaryContainer,
shape: BoxShape.circle,
),
child: IconButton(
onPressed: () => _toggleFavoriteArtist(context),
icon: Icon(
isFavoriteArtist
? Icons.favorite
: Icons.favorite_border,
size: 26,
),
color: isFavoriteArtist
? headerScheme.error
: headerScheme.onPrimaryContainer,
tooltip: isFavoriteArtist
? context.l10n.artistOptionRemoveFromFavorites
: context.l10n.artistOptionAddToFavorites,
),
),
],
if (hasDiscography && !isSelectionMode) ...[
const SizedBox(width: 12),
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
color: headerScheme.primaryContainer,
shape: BoxShape.circle,
),
child: IconButton(
onPressed: () => _showDiscographyOptions(
context,
colorScheme,
albums,
),
icon: const Icon(Icons.download_rounded, size: 26),
color: headerScheme.onPrimaryContainer,
tooltip: context.l10n.discographyDownload,
),
),
],
],
),
),
],
),
stretchModes: const [StretchMode.zoomBackground],
),
leadingWidth: kToolbarHeight + edgeInset,
leading: Padding(
padding: EdgeInsets.only(left: edgeInset),
child: HeaderCircleButton(
icon: Icons.arrow_back,
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
onPressed: () => Navigator.pop(context),
),
),
actionsPadding: EdgeInsets.only(right: edgeInset),
actions: [
HeaderCircleButton(
icon: Icons.open_in_new_rounded,
tooltip: context.l10n.openInOtherServices,
onPressed: () => _showShareSheet(context),
),
],
);
}
void _showShareSheet(BuildContext context) {
CrossExtensionShareSheet.show(
context,
name: widget.artistName,
artists: '',
type: 'artist',
sourceExtensionId: _directMetadataProviderId() ?? '',
);
}
Widget _buildMornyeFeaturedAlbum(BuildContext context, ArtistAlbum album) {
return Padding(
padding: const EdgeInsets.fromLTRB(20, 28, 20, 0),
child: Material(
color: Colors.white.withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(14),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => _navigateToAlbum(album),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(5),
child: SizedBox.square(
dimension: 76,
child: album.coverUrl != null
? CachedCoverImage(
imageUrl: album.coverUrl!,
fit: BoxFit.cover,
)
: const Icon(CupertinoIcons.square_stack, size: 32),
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (album.releaseDate.isNotEmpty)
Text(
album.releaseDate.split('-').first,
style: TextStyle(
fontSize: 12,
color: Colors.white.withValues(alpha: 0.7),
),
),
const SizedBox(height: 4),
Text(
album.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
if (album.totalTracks > 0) ...[
const SizedBox(height: 4),
Text(
context.l10n.tracksCount(album.totalTracks),
style: TextStyle(
fontSize: 12,
color: Colors.white.withValues(alpha: 0.7),
),
),
],
],
),
),
const SizedBox(width: 8),
Icon(
CupertinoIcons.chevron_forward,
size: 14,
color: Colors.white.withValues(alpha: 0.7),
),
],
),
),
),
),
);
}
Widget _buildPopularSection(BuildContext context, ColorScheme colorScheme) {
if (_topTracks == null || _topTracks!.isEmpty) {
return const SizedBox.shrink();
}
final tracks = _topTracks!;
_historySnapshot.update(tracks);
final historyLookups = _historySnapshot.lookups;
final existingHistoryKeys = ref.watch(
downloadHistoryVisibleBatchExistsProvider(_historySnapshot.request),
);
const tracksPerPage = 5;
final pageCount = (tracks.length / tracksPerPage).ceil();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 12),
child: Text(
context.l10n.artistPopular,
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
),
SizedBox(
height: tracksPerPage * 64.0,
child: PageView.builder(
controller: _popularPageController,
itemCount: pageCount,
onPageChanged: (page) {
setState(() {
_popularCurrentPage = page;
});
},
itemBuilder: (context, pageIndex) {
final startIndex = pageIndex * tracksPerPage;
final endIndex = (startIndex + tracksPerPage).clamp(
0,
tracks.length,
);
final pageTracks = tracks.sublist(startIndex, endIndex);
return Column(
children: pageTracks.asMap().entries.map((entry) {
final globalIndex = startIndex + entry.key;
return _buildPopularTrackItem(
globalIndex + 1,
entry.value,
colorScheme,
existingHistoryKeys.contains(
historyLookups[globalIndex].lookupKey,
),
);
}).toList(),
);
},
),
),
if (pageCount > 1)
Center(
child: Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(pageCount, (index) {
final isActive = _popularCurrentPage == index;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 3),
width: isActive ? 8 : 6,
height: isActive ? 8 : 6,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isActive
? colorScheme.primary
: colorScheme.onSurfaceVariant.withValues(alpha: 0.3),
),
);
}),
),
),
),
],
);
}
Widget _buildPopularTrackItem(
int rank,
Track track,
ColorScheme colorScheme,
bool isInHistory,
) {
return Consumer(
builder: (context, ref, child) {
final queueItem = ref.watch(
downloadQueueLookupProvider.select(
(lookup) => lookup.byTrackId[track.id],
),
);
final showLocalLibraryIndicator = ref.watch(
settingsProvider.select(
(s) => s.localLibraryEnabled && s.localLibraryShowDuplicates,
),
);
final isInLocalLibrary = showLocalLibraryIndicator
? ref.watch(
localLibraryProvider.select(
(state) => state.existsInLibrary(
isrc: track.isrc,
trackName: track.name,
artistName: track.artistName,
),
),
)
: false;
final isQueued = queueItem != null;
final row = InkWell(
onTap: () => _handlePopularTrackTap(
track,
isQueued: isQueued,
isInHistory: isInHistory,
isInLocalLibrary: isInLocalLibrary,
),
onLongPress: () => TrackCollectionQuickActions.showTrackOptionsSheet(
context,
ref,
track,
hasLocalPlaybackCandidate: isInHistory || isInLocalLibrary,
),
child: Padding(
padding: EdgeInsets.fromLTRB(context.isMornye ? 20 : 16, 8, 10, 8),
child: Row(
children: [
if (!context.isMornye) ...[
SizedBox(
width: 24,
child: Text(
'$rank',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
),
const SizedBox(width: 12),
],
track.coverUrl != null
? DownloadableCover(
coverUrl: track.coverUrl,
baseName: '${track.artistName} - ${track.name}',
child: CachedCoverImage(
imageUrl: track.coverUrl!,
width: 48,
height: 48,
borderRadius: BorderRadius.circular(4),
placeholder: (context, url) => ShimmerLoading(
child: Container(
width: 48,
height: 48,
color: colorScheme.surfaceContainerHighest,
),
),
errorWidget: (context, url, error) => Container(
width: 48,
height: 48,
color: colorScheme.surfaceContainerHighest,
child: Icon(
Icons.music_note,
color: colorScheme.onSurfaceVariant,
size: 24,
),
),
),
)
: Container(
width: 48,
height: 48,
color: colorScheme.surfaceContainerHighest,
child: Icon(
Icons.music_note,
color: colorScheme.onSurfaceVariant,
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
track.name,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: context.isMornye ? 17 : null,
fontWeight: context.isMornye
? FontWeight.w400
: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (track.albumName.isNotEmpty ||
isInLocalLibrary ||
isInHistory)
Row(
children: [
if (track.albumName.isNotEmpty)
Expanded(
child: ClickableAlbumName(
albumName: track.albumName,
albumId: track.albumId,
artistName: track.artistName,
coverUrl: track.coverUrl,
extensionId: widget.extensionId,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(
color: colorScheme.onSurfaceVariant,
fontSize: context.isMornye ? 15 : null,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (isInLocalLibrary || isInHistory) ...[
if (track.albumName.isNotEmpty)
const SizedBox(width: 6),
const InLibraryBadge(),
],
],
),
],
),
),
TrackCollectionQuickActions(
track: track,
hasLocalPlaybackCandidate: isInHistory || isInLocalLibrary,
),
],
),
),
);
if (!context.isMornye) return row;
return Stack(
children: [
row,
Positioned(
left: 80,
right: 20,
bottom: 0,
child: Divider(
height: 1,
thickness: 0.5,
color: Colors.white.withValues(alpha: 0.12),
),
),
],
);
},
);
}
void _handlePopularTrackTap(
Track track, {
required bool isQueued,
required bool isInHistory,
required bool isInLocalLibrary,
}) async {
if (isQueued) return;
final settings = ref.read(settingsProvider);
if (settings.allowQualityVariants && (isInHistory || isInLocalLibrary)) {
_downloadTrack(track);
return;
}
final playedLocal = await playLocalIfAvailable(context, ref, track);
if (playedLocal) {
return;
}
_downloadTrack(track);
}
void _downloadTrack(Track track) {
final settings = ref.read(settingsProvider);
ref.read(settingsProvider.notifier).setHasSearchedBefore();
void enqueue(String service, {String? quality}) {
ref
.read(downloadQueueProvider.notifier)
.addToQueue(track, service, qualityOverride: quality);
showAddedToQueueSnackBar(context, track.name);
}
if (settings.askQualityBeforeDownload || settings.allowQualityVariants) {
DownloadServicePicker.show(
context,
recommendedService: _recommendedDownloadService(),
onSelect: (quality, service) {
if (!mounted) return;
enqueue(service, quality: quality);
},
);
return;
}
final extensionState = ref.read(extensionProvider);
final service = resolveEffectiveDownloadService(
settings.defaultService,
extensionState,
);
if (service.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.extensionsNoDownloadProvider)),
);
return;
}
enqueue(service);
}
Widget _buildAlbumSection(
BuildContext context,
String title,
List<ArtistAlbum> albums,
ColorScheme colorScheme, {
bool showTypeBadge = false,
}) {
final sectionHeight = _artistAlbumSectionHeight();
final tileSize = _artistAlbumTileSize();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 12),
child: Text(
'$title (${albums.length})',
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
),
NotificationListener<ScrollNotification>(
onNotification: _onAlbumsScroll,
child: SizedBox(
height: sectionHeight,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: albums.length,
itemBuilder: (context, index) {
final album = albums[index];
return KeyedSubtree(
key: ValueKey(album.id),
child: _buildAlbumCard(
context,
album,
colorScheme,
tileSize: tileSize,
sectionHeight: sectionHeight,
showTypeBadge: showTypeBadge,
),
);
},
),
),
),
],
);
}
Widget _buildAlbumCard(
BuildContext context,
ArtistAlbum album,
ColorScheme colorScheme, {
required double tileSize,
required double sectionHeight,
bool showTypeBadge = false,
}) {
final isSelected = selectedIds.contains(album.id);
return Semantics(
button: true,
selected: isSelectionMode && isSelected,
label: isSelectionMode
? context.l10n.a11ySelectAlbum(album.name)
: context.l10n.a11yOpenAlbum(album.name),
child: GestureDetector(
onTap: () {
if (isSelectionMode) {
toggleSelection(album.id);
} else {
_navigateToAlbum(album);
}
},
onLongPress: () {
if (!isSelectionMode) {
enterSelectionMode(album.id);
}
},
child: Container(
width: tileSize,
height: sectionHeight,
margin: const EdgeInsets.symmetric(horizontal: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox.square(
dimension: tileSize,
child: Stack(
fit: StackFit.expand,
children: [
DownloadableCover(
coverUrl: album.coverUrl,
baseName: '${album.artists} - ${album.name}',
enabled: !isSelectionMode,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: album.coverUrl != null
? CachedCoverImage(
imageUrl: album.coverUrl!,
width: tileSize,
height: tileSize,
fit: BoxFit.cover,
memCacheWidth: (tileSize * 2).round(),
memCacheHeight: (tileSize * 2).round(),
placeholder: (context, url) => ShimmerLoading(
child: Container(
color: colorScheme.surfaceContainerHighest,
),
),
errorWidget: (context, url, error) => Container(
color: colorScheme.surfaceContainerHighest,
child: Icon(
Icons.album,
color: colorScheme.onSurfaceVariant,
size: 40,
),
),
)
: Container(
color: colorScheme.surfaceContainerHighest,
child: Icon(
Icons.album,
color: colorScheme.onSurfaceVariant,
size: 40,
),
),
),
),
if (isSelectionMode)
Positioned.fill(
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: isSelected
? colorScheme.primary.withValues(alpha: 0.3)
: Colors.black.withValues(alpha: 0.1),
border: isSelected
? Border.all(
color: colorScheme.primary,
width: 3,
)
: null,
),
),
),
if (isSelectionMode)
Positioned(
top: 8,
right: 8,
child: AnimatedSelectionCheckbox(
visible: true,
selected: isSelected,
colorScheme: colorScheme,
size: 28,
unselectedColor: colorScheme.surface.withValues(
alpha: 0.9,
),
),
),
if (showTypeBadge)
Positioned(
left: 6,
bottom: 6,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.7),
borderRadius: BorderRadius.circular(4),
),
child: Text(
album.albumType == 'ep'
? context.l10n.releaseTypeEp
: context.l10n.releaseTypeSingle,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
const SizedBox(height: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
album.name,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(height: 2),
Text(
album.totalTracks > 0
? '${album.releaseDate.length >= 4 ? album.releaseDate.substring(0, 4) : album.releaseDate} ${context.l10n.tracksCount(album.totalTracks)}'
: album.releaseDate.length >= 4
? album.releaseDate.substring(0, 4)
: album.releaseDate,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
),
);
}
void _navigateToAlbum(ArtistAlbum album) {
ref.read(settingsProvider.notifier).setHasSearchedBefore();
if (album.providerId != null && album.providerId!.isNotEmpty) {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => ExtensionAlbumScreen(
extensionId: album.providerId!,
albumId: album.id,
albumName: album.name,
coverUrl: album.coverUrl,
initialAlbumType: album.albumType,
initialTotalTracks: album.totalTracks,
),
),
);
} else {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => AlbumScreen(
albumId: album.id,
albumName: album.name,
coverUrl: album.coverUrl,
),
),
);
}
}
}
class _DiscographyOptionTile extends StatelessWidget {
final IconData icon;
final String title;
final String subtitle;
final VoidCallback onTap;
final bool showDivider;
const _DiscographyOptionTile({
required this.icon,
required this.title,
required this.subtitle,
required this.onTap,
this.showDivider = true,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
if (context.isMornye) {
final textTheme = Theme.of(context).textTheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CupertinoButton(
padding: const EdgeInsets.symmetric(vertical: 16),
onPressed: onTap,
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: textTheme.bodyLarge),
const SizedBox(height: 4),
Text(
subtitle,
style: textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurface.withValues(
alpha: 0.72,
),
),
),
],
),
),
const SizedBox(width: 16),
Icon(
mornyeIconFor(icon),
size: 22,
color: colorScheme.onSurface,
),
],
),
),
if (showDivider) const Divider(height: 0.5, thickness: 0.5),
],
),
);
}
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
leading: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: colorScheme.onPrimaryContainer, size: 24),
),
title: Text(title, style: const TextStyle(fontWeight: FontWeight.w500)),
subtitle: Text(
subtitle,
style: TextStyle(color: colorScheme.onSurfaceVariant, fontSize: 12),
),
trailing: Icon(Icons.chevron_right, color: colorScheme.onSurfaceVariant),
onTap: onTap,
);
}
}
class _FetchingProgressDialog extends StatefulWidget {
final int totalAlbums;
final VoidCallback onCancel;
const _FetchingProgressDialog({
super.key,
required this.totalAlbums,
required this.onCancel,
});
@override
State<_FetchingProgressDialog> createState() =>
_FetchingProgressDialogState();
}
class _FetchingProgressDialogState extends State<_FetchingProgressDialog> {
int _current = 0;
int _total = 0;
@override
void initState() {
super.initState();
_total = widget.totalAlbums;
}
void updateProgress(int current, int total) {
if (mounted) {
setState(() {
_current = current;
_total = total;
});
}
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final progress = _total > 0 ? _current / _total : 0.0;
return AppAlertDialog(
backgroundColor: colorScheme.surfaceContainerHigh,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
SizedBox(
width: 64,
height: 64,
child: Stack(
alignment: Alignment.center,
children: [
CircularProgressIndicator(
value: progress > 0 ? progress : null,
strokeWidth: 4,
backgroundColor: colorScheme.surfaceContainerHighest,
),
Icon(Icons.library_music, color: colorScheme.primary, size: 24),
],
),
),
const SizedBox(height: 20),
Text(
context.l10n.discographyFetchingTracks,
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
Text(
context.l10n.discographyFetchingAlbum(_current, _total),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: progress > 0 ? progress : null,
backgroundColor: colorScheme.surfaceContainerHighest,
minHeight: 6,
),
),
],
),
actions: [
AppDialogAction(
onPressed: widget.onCancel,
child: Text(context.l10n.dialogCancel),
),
],
);
}
}