mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 23:21:59 +01:00
v2
This commit is contained in:
@@ -0,0 +1,433 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user