mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 00:59:28 +01:00
At some places <something>.of(context) was used multiple times in the same widget. This, although small, can have an impact on performance that's just plain unnecessary. It's better to just get things you need out of the context first before you do anything else.
438 lines
11 KiB
Dart
438 lines
11 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) {
|
|
final l = AppLocalizations.of(context);
|
|
|
|
return ListTile(
|
|
leading: CircleClip(
|
|
child: ArtistArtImage(artistId: artist.id),
|
|
),
|
|
title: Text(artist.name),
|
|
subtitle: Text(l.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) {
|
|
final l = AppLocalizations.of(context);
|
|
|
|
// 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(l.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);
|
|
}
|
|
}
|