mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 00:59:28 +01:00
434 lines
10 KiB
Dart
434 lines
10 KiB
Dart
import 'package:collection/collection.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
|
|
import '../models/music.dart';
|
|
import '../services/cache_service.dart';
|
|
import '../services/download_service.dart';
|
|
import '../state/audio.dart';
|
|
import '../state/music.dart';
|
|
import '../state/theme.dart';
|
|
import 'context_menus.dart';
|
|
import 'images.dart';
|
|
import 'pages/songs_page.dart';
|
|
|
|
enum CardStyle {
|
|
imageOnly,
|
|
withText,
|
|
}
|
|
|
|
enum AlbumSubtitle {
|
|
artist,
|
|
year,
|
|
}
|
|
|
|
class AlbumCard extends HookConsumerWidget {
|
|
final Album album;
|
|
final void Function()? onTap;
|
|
final CardStyle style;
|
|
final AlbumSubtitle subtitle;
|
|
|
|
const AlbumCard({
|
|
super.key,
|
|
required this.album,
|
|
this.onTap,
|
|
this.style = CardStyle.withText,
|
|
this.subtitle = AlbumSubtitle.artist,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
// generate the palette used in other views ahead of time
|
|
ref.watch(albumArtPaletteProvider(album.id));
|
|
final cache = ref.watch(cacheServiceProvider);
|
|
final info = cache.albumArt(album);
|
|
|
|
final image = CardClip(child: UriCacheInfoImage(cache: info));
|
|
Widget content;
|
|
if (style == CardStyle.imageOnly) {
|
|
content = image;
|
|
} else {
|
|
content = Column(
|
|
children: [
|
|
image,
|
|
_AlbumCardText(album: album, subtitle: subtitle),
|
|
],
|
|
);
|
|
}
|
|
|
|
return ImageCard(
|
|
onTap: onTap,
|
|
onLongPress: () {
|
|
showContextMenu(
|
|
context: context,
|
|
ref: ref,
|
|
builder: (context) => BottomSheetMenu(
|
|
child: AlbumContextMenu(album: album),
|
|
),
|
|
);
|
|
},
|
|
child: content,
|
|
);
|
|
}
|
|
}
|
|
|
|
class ImageCard extends StatelessWidget {
|
|
final Widget child;
|
|
final void Function()? onTap;
|
|
final void Function()? onLongPress;
|
|
|
|
const ImageCard({
|
|
super.key,
|
|
required this.child,
|
|
this.onTap,
|
|
this.onLongPress,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Card(
|
|
surfaceTintColor: Colors.transparent,
|
|
margin: const EdgeInsets.all(0),
|
|
child: Stack(
|
|
fit: StackFit.passthrough,
|
|
alignment: Alignment.bottomCenter,
|
|
children: [
|
|
child,
|
|
Positioned.fill(
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
onLongPress: onLongPress,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AlbumCardText extends StatelessWidget {
|
|
final Album album;
|
|
final AlbumSubtitle subtitle;
|
|
|
|
const _AlbumCardText({
|
|
required this.album,
|
|
required this.subtitle,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final textTheme = Theme.of(context).textTheme;
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.only(top: 4, bottom: 8),
|
|
width: double.infinity,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: Text(
|
|
album.name,
|
|
maxLines: 1,
|
|
softWrap: false,
|
|
overflow: TextOverflow.fade,
|
|
textAlign: TextAlign.start,
|
|
style: textTheme.bodyMedium!.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
(subtitle == AlbumSubtitle.artist
|
|
? album.albumArtist
|
|
: album.year?.toString()) ??
|
|
'',
|
|
maxLines: 1,
|
|
softWrap: false,
|
|
overflow: TextOverflow.fade,
|
|
textAlign: TextAlign.start,
|
|
style: textTheme.bodySmall,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class AlbumListTile extends HookConsumerWidget {
|
|
final Album album;
|
|
final void Function()? onTap;
|
|
|
|
const AlbumListTile({
|
|
super.key,
|
|
required this.album,
|
|
this.onTap,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final artist = ref.watch(albumProvider(album.artistId!)).valueOrNull;
|
|
|
|
return ListTile(
|
|
leading: AlbumArt(album: album),
|
|
title: Text(album.name),
|
|
subtitle: Text(album.albumArtist ?? artist!.name),
|
|
onTap: onTap,
|
|
onLongPress: () {
|
|
showContextMenu(
|
|
context: context,
|
|
ref: ref,
|
|
builder: (context) => BottomSheetMenu(
|
|
size: MenuSize.small,
|
|
child: AlbumContextMenu(album: album),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class ArtistListTile extends HookConsumerWidget {
|
|
final Artist artist;
|
|
final void Function()? onTap;
|
|
|
|
const ArtistListTile({
|
|
super.key,
|
|
required this.artist,
|
|
this.onTap,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return ListTile(
|
|
leading: CircleClip(
|
|
child: ArtistArtImage(artistId: artist.id),
|
|
),
|
|
title: Text(artist.name),
|
|
subtitle: Text(AppLocalizations.of(context).resourcesAlbumCount(
|
|
artist.albumCount,
|
|
)),
|
|
onTap: onTap,
|
|
onLongPress: () {
|
|
showContextMenu(
|
|
context: context,
|
|
ref: ref,
|
|
builder: (context) => BottomSheetMenu(
|
|
size: MenuSize.small,
|
|
child: ArtistContextMenu(artist: artist),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class PlaylistListTile extends HookConsumerWidget {
|
|
final Playlist playlist;
|
|
final void Function()? onTap;
|
|
|
|
const PlaylistListTile({
|
|
super.key,
|
|
required this.playlist,
|
|
this.onTap,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
// generate the palette used in other views ahead of time
|
|
ref.watch(playlistArtPaletteProvider(playlist.id));
|
|
final cache = ref.watch(cacheServiceProvider).playlistArt(playlist);
|
|
|
|
return ListTile(
|
|
leading: CardClip(
|
|
child: UriCacheInfoImage(cache: cache),
|
|
),
|
|
title: Text(playlist.name),
|
|
subtitle: Text(AppLocalizations.of(context).resourcesSongCount(
|
|
playlist.songCount,
|
|
)),
|
|
onTap: onTap,
|
|
onLongPress: () {
|
|
showContextMenu(
|
|
context: context,
|
|
ref: ref,
|
|
builder: (context) => BottomSheetMenu(
|
|
size: MenuSize.small,
|
|
child: PlaylistContextMenu(playlist: playlist),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class SongListTile extends HookConsumerWidget {
|
|
final Song song;
|
|
final void Function()? onTap;
|
|
final bool image;
|
|
|
|
const SongListTile({
|
|
super.key,
|
|
required this.song,
|
|
this.onTap,
|
|
this.image = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return Material(
|
|
type: MaterialType.transparency,
|
|
child: ListTile(
|
|
title: _SongTitle(song: song),
|
|
subtitle: _SongSubtitle(song: song),
|
|
leading: image ? SongAlbumArt(song: song) : null,
|
|
trailing: IconButton(
|
|
icon: const Icon(
|
|
Icons.star_outline_rounded,
|
|
size: 36,
|
|
),
|
|
onPressed: () {},
|
|
),
|
|
onTap: onTap,
|
|
onLongPress: () {
|
|
showContextMenu(
|
|
context: context,
|
|
ref: ref,
|
|
builder: (context) => BottomSheetMenu(
|
|
child: SongContextMenu(song: song),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SongSubtitle extends HookConsumerWidget {
|
|
final Song song;
|
|
|
|
const _SongSubtitle({
|
|
required this.song,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final downloadTaskId = ref.watch(songProvider(song.id).select(
|
|
(value) => value.valueOrNull?.downloadTaskId,
|
|
));
|
|
final downloadFilePath = ref.watch(songProvider(song.id).select(
|
|
(value) => value.valueOrNull?.downloadFilePath,
|
|
));
|
|
final download = ref.watch(downloadServiceProvider.select(
|
|
(value) => value.downloads.firstWhereOrNull(
|
|
(e) => e.taskId == downloadTaskId,
|
|
),
|
|
));
|
|
|
|
final inheritedStyle = DefaultTextStyle.of(context).style;
|
|
|
|
Widget? downloadIndicator;
|
|
if (downloadFilePath != null) {
|
|
downloadIndicator = const Padding(
|
|
padding: EdgeInsetsDirectional.only(end: 3),
|
|
child: Icon(
|
|
Icons.download_done_rounded,
|
|
size: 20,
|
|
),
|
|
);
|
|
} else if (downloadTaskId != null || download != null) {
|
|
downloadIndicator = Padding(
|
|
padding: const EdgeInsetsDirectional.only(start: 4, end: 9),
|
|
child: SizedBox(
|
|
height: 10,
|
|
width: 10,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
value: download != null && download.progress > 0
|
|
? download.progress / 100
|
|
: null,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return Row(
|
|
children: [
|
|
if (downloadIndicator != null) downloadIndicator,
|
|
Expanded(
|
|
child: Text(
|
|
song.artist ?? song.album ?? '',
|
|
maxLines: 1,
|
|
softWrap: false,
|
|
overflow: TextOverflow.fade,
|
|
style: TextStyle(
|
|
color: inheritedStyle.color,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SongTitle extends HookConsumerWidget {
|
|
final Song song;
|
|
|
|
const _SongTitle({
|
|
required this.song,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final mediaItem = ref.watch(mediaItemProvider).valueOrNull;
|
|
final mediaItemData = ref.watch(mediaItemDataProvider);
|
|
final inheritedStyle = DefaultTextStyle.of(context).style;
|
|
final theme = Theme.of(context);
|
|
final queueContext = QueueContext.maybeOf(context);
|
|
|
|
final playing = mediaItem != null &&
|
|
mediaItemData != null &&
|
|
mediaItem.id == song.id &&
|
|
mediaItemData.contextId == queueContext?.id &&
|
|
mediaItemData.contextType == queueContext?.type;
|
|
|
|
return Row(
|
|
children: [
|
|
if (playing)
|
|
Padding(
|
|
padding: const EdgeInsetsDirectional.only(end: 2),
|
|
child: Icon(
|
|
Icons.play_arrow_rounded,
|
|
size: 18,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Text(
|
|
song.title,
|
|
maxLines: 1,
|
|
softWrap: false,
|
|
overflow: TextOverflow.fade,
|
|
style: TextStyle(
|
|
color: playing ? theme.colorScheme.primary : inheritedStyle.color,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class FabPadding extends StatelessWidget {
|
|
const FabPadding({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return const SizedBox(height: 86);
|
|
}
|
|
}
|