mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-14 16:25:16 +00:00
feat: improved track item API and UI
This commit is contained in:
parent
886bc8033d
commit
617aa89409
@ -25,7 +25,6 @@ import 'package:spotube/models/local_track.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException;
|
||||
@ -156,7 +155,6 @@ class UserLocalTracks extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final sortBy = useState<SortBy>(SortBy.none);
|
||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||
final trackSnapshot = ref.watch(localTracksProvider);
|
||||
final isPlaylistPlaying =
|
||||
playlist.containsTracks(trackSnapshot.value ?? []);
|
||||
@ -272,42 +270,16 @@ class UserLocalTracks extends HookConsumerWidget {
|
||||
itemBuilder: (context, index) {
|
||||
final track = filteredTracks[index];
|
||||
return TrackTile(
|
||||
playlist,
|
||||
duration:
|
||||
"${track.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.duration?.inSeconds.remainder(60) ?? 0)}",
|
||||
track: MapEntry(index, track),
|
||||
isActive: playlist.activeTrack?.id == track.id,
|
||||
isChecked: false,
|
||||
showCheck: false,
|
||||
isLocal: true,
|
||||
onTrackPlayButtonPressed: (currentTrack) {
|
||||
return playLocalTracks(
|
||||
index: index,
|
||||
track: track,
|
||||
userPlaylist: false,
|
||||
onTap: () {
|
||||
playLocalTracks(
|
||||
ref,
|
||||
sortedTracks,
|
||||
currentTrack: track,
|
||||
);
|
||||
},
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
icon: const Icon(SpotubeIcons.moreHorizontal),
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: "delete",
|
||||
onTap: () async {
|
||||
await File(track.path).delete();
|
||||
ref.refresh(localTracksProvider);
|
||||
},
|
||||
padding: EdgeInsets.zero,
|
||||
child: ListTile(
|
||||
leading: const Icon(SpotubeIcons.trash),
|
||||
title: Text(context.l10n.delete),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -11,7 +11,6 @@ import 'package:spotube/components/shared/track_table/track_tile.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/hooks/use_auto_scroll_controller.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
|
||||
class PlayerQueue extends HookConsumerWidget {
|
||||
final bool floating;
|
||||
@ -120,9 +119,7 @@ class PlayerQueue extends HookConsumerWidget {
|
||||
shrinkWrap: true,
|
||||
buildDefaultDragHandles: false,
|
||||
itemBuilder: (context, i) {
|
||||
final track = tracks.toList().asMap().entries.elementAt(i);
|
||||
String duration =
|
||||
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
|
||||
final track = tracks.elementAt(i);
|
||||
return AutoScrollTag(
|
||||
key: ValueKey(i),
|
||||
controller: controller,
|
||||
@ -130,15 +127,13 @@ class PlayerQueue extends HookConsumerWidget {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: TrackTile(
|
||||
playlist,
|
||||
index: i,
|
||||
track: track,
|
||||
duration: duration,
|
||||
isActive: playlist.activeTrack?.id == track.value.id,
|
||||
onTrackPlayButtonPressed: (currentTrack) async {
|
||||
if (playlist.activeTrack?.id == track.value.id) {
|
||||
onTap: () async {
|
||||
if (playlist.activeTrack?.id == track.id) {
|
||||
return;
|
||||
}
|
||||
await playlistNotifier.jumpToTrack(currentTrack);
|
||||
await playlistNotifier.jumpToTrack(track);
|
||||
},
|
||||
leadingActions: [
|
||||
ReorderableDragStartListener(
|
||||
|
@ -2,9 +2,11 @@ import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
class HoverBuilder extends HookWidget {
|
||||
final bool? permanentState;
|
||||
final Widget Function(BuildContext context, bool isHovering) builder;
|
||||
const HoverBuilder({
|
||||
required this.builder,
|
||||
this.permanentState,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -12,6 +14,10 @@ class HoverBuilder extends HookWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final hovering = useState(false);
|
||||
|
||||
if (permanentState != null) {
|
||||
return builder(context, permanentState!);
|
||||
}
|
||||
|
||||
return MouseRegion(
|
||||
onEnter: (_) {
|
||||
if (!hovering.value) hovering.value = true;
|
||||
|
302
lib/components/shared/track_table/track_options.dart
Normal file
302
lib/components/shared/track_table/track_options.dart
Normal file
@ -0,0 +1,302 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/library/user_local_tracks.dart';
|
||||
import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart';
|
||||
import 'package:spotube/components/shared/heart_button.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/models/local_track.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/provider/blacklist_provider.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/services/mutations/mutations.dart';
|
||||
|
||||
class TrackOptions extends HookConsumerWidget {
|
||||
final Track track;
|
||||
final bool userPlaylist;
|
||||
final String? playlistId;
|
||||
const TrackOptions({
|
||||
Key? key,
|
||||
required this.track,
|
||||
this.userPlaylist = false,
|
||||
this.playlistId,
|
||||
}) : super(key: key);
|
||||
|
||||
void actionShare(BuildContext context, Track track) {
|
||||
final data = "https://open.spotify.com/track/${track.id}";
|
||||
Clipboard.setData(ClipboardData(text: data)).then((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
width: 300,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
content: Text(
|
||||
context.l10n.copied_to_clipboard(data),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void actionAddToPlaylist(BuildContext context, Track track) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => PlaylistAddTrackDialog(
|
||||
tracks: [track],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||
final playback = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||
|
||||
final blacklist = ref.watch(BlackListNotifier.provider);
|
||||
|
||||
final favorites = useTrackToggleLike(track, ref);
|
||||
|
||||
final isBlackListed = useMemoized(
|
||||
() => blacklist.contains(
|
||||
BlacklistedElement.track(
|
||||
track.id!,
|
||||
track.name!,
|
||||
),
|
||||
),
|
||||
[blacklist, track],
|
||||
);
|
||||
|
||||
final removingTrack = useState<String?>(null);
|
||||
final removeTrack = useMutations.playlist.removeTrackOf(
|
||||
ref,
|
||||
playlistId ?? "",
|
||||
);
|
||||
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
|
||||
final createItems = useCallback(
|
||||
(BuildContext context) {
|
||||
if (track is LocalTrack) {
|
||||
return [
|
||||
if (mediaQuery.isSm) ...[
|
||||
Text(
|
||||
track.name!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
Divider(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
thickness: 0.2,
|
||||
indent: 16,
|
||||
endIndent: 16,
|
||||
),
|
||||
],
|
||||
ListTile(
|
||||
onTap: () async {
|
||||
await File((track as LocalTrack).path).delete();
|
||||
ref.refresh(localTracksProvider);
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.trash),
|
||||
title: Text(context.l10n.delete),
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
if (mediaQuery.isSm) ...[
|
||||
Text(
|
||||
track.name!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
Divider(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
thickness: 0.2,
|
||||
indent: 16,
|
||||
endIndent: 16,
|
||||
),
|
||||
],
|
||||
if (!playlist.containsTrack(track)) ...[
|
||||
ListTile(
|
||||
onTap: () async {
|
||||
await playback.addTrack(track);
|
||||
if (context.mounted) {
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.added_track_to_queue(track.name!),
|
||||
),
|
||||
),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.queueAdd),
|
||||
title: Text(context.l10n.add_to_queue),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () {
|
||||
playback.addTracksAtFirst([track]);
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.track_will_play_next(track.name!),
|
||||
),
|
||||
),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.lightning),
|
||||
title: Text(context.l10n.play_next),
|
||||
),
|
||||
] else
|
||||
ListTile(
|
||||
onTap: playlist.activeTrack?.id == track.id
|
||||
? null
|
||||
: () {
|
||||
playback.removeTrack(track.id!);
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.removed_track_from_queue(
|
||||
track.name!,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
enabled: playlist.activeTrack?.id != track.id,
|
||||
leading: const Icon(SpotubeIcons.queueRemove),
|
||||
title: Text(context.l10n.remove_from_queue),
|
||||
),
|
||||
if (favorites.me.hasData)
|
||||
ListTile(
|
||||
onTap: () {
|
||||
favorites.toggleTrackLike.mutate(favorites.isLiked);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
leading: favorites.isLiked
|
||||
? const Icon(
|
||||
SpotubeIcons.heartFilled,
|
||||
color: Colors.pink,
|
||||
)
|
||||
: const Icon(SpotubeIcons.heart),
|
||||
title: Text(
|
||||
favorites.isLiked
|
||||
? context.l10n.remove_from_favorites
|
||||
: context.l10n.save_as_favorite,
|
||||
),
|
||||
),
|
||||
if (auth != null)
|
||||
ListTile(
|
||||
onTap: () {
|
||||
actionAddToPlaylist(context, track);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.playlistAdd),
|
||||
title: Text(context.l10n.add_to_playlist),
|
||||
),
|
||||
if (userPlaylist && auth != null)
|
||||
ListTile(
|
||||
onTap: () {
|
||||
removingTrack.value = track.uri;
|
||||
removeTrack.mutate(track.uri!);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
leading: (removeTrack.isMutating || !removeTrack.hasData) &&
|
||||
removingTrack.value == track.uri
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: const Icon(SpotubeIcons.removeFilled),
|
||||
title: Text(context.l10n.remove_from_playlist),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () {
|
||||
if (isBlackListed) {
|
||||
ref.read(BlackListNotifier.provider.notifier).remove(
|
||||
BlacklistedElement.track(track.id!, track.name!),
|
||||
);
|
||||
} else {
|
||||
ref.read(BlackListNotifier.provider.notifier).add(
|
||||
BlacklistedElement.track(track.id!, track.name!),
|
||||
);
|
||||
}
|
||||
Navigator.pop(context);
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.playlistRemove),
|
||||
iconColor: !isBlackListed ? Colors.red[400] : null,
|
||||
textColor: !isBlackListed ? Colors.red[400] : null,
|
||||
title: Text(
|
||||
isBlackListed
|
||||
? context.l10n.remove_from_blacklist
|
||||
: context.l10n.add_to_blacklist,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () {
|
||||
actionShare(context, track);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.share),
|
||||
title: Text(context.l10n.share),
|
||||
)
|
||||
];
|
||||
},
|
||||
[track, playlist, favorites, auth, isBlackListed, mediaQuery],
|
||||
);
|
||||
|
||||
if (mediaQuery.isSm) {
|
||||
return IconButton(
|
||||
icon: const Icon(SpotubeIcons.moreHorizontal),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: ListTileTheme(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
horizontalTitleGap: 5,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: createItems(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
useRootNavigator: true,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return PopupMenuButton(
|
||||
icon: const Icon(SpotubeIcons.moreHorizontal),
|
||||
position: PopupMenuPosition.under,
|
||||
tooltip: context.l10n.more_actions,
|
||||
itemBuilder: (context) {
|
||||
return createItems(context)
|
||||
.map(
|
||||
(e) => PopupMenuItem(
|
||||
padding: EdgeInsets.zero,
|
||||
child: e,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,404 +1,220 @@
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart' hide Action;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import 'package:spotify/spotify.dart' hide Image;
|
||||
import 'package:spotube/collections/assets.gen.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart';
|
||||
import 'package:spotube/components/shared/heart_button.dart';
|
||||
import 'package:spotube/components/shared/links/link_text.dart';
|
||||
import 'package:spotube/components/shared/hover_builder.dart';
|
||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||
import 'package:spotube/components/shared/links/link_text.dart';
|
||||
import 'package:spotube/components/shared/track_table/track_options.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/models/logger.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/extensions/duration.dart';
|
||||
import 'package:spotube/models/local_track.dart';
|
||||
import 'package:spotube/provider/blacklist_provider.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/services/mutations/mutations.dart';
|
||||
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
class TrackTile extends HookConsumerWidget {
|
||||
final ProxyPlaylist playlist;
|
||||
final MapEntry<int, Track> track;
|
||||
final String duration;
|
||||
final void Function(Track currentTrack)? onTrackPlayButtonPressed;
|
||||
final logger = getLogger(TrackTile);
|
||||
/// [index] will not be shown if null
|
||||
final int? index;
|
||||
final Track track;
|
||||
final bool selected;
|
||||
final ValueChanged<bool?>? onChanged;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
final bool userPlaylist;
|
||||
// null playlistId indicates its not inside a playlist
|
||||
final String? playlistId;
|
||||
|
||||
final bool showAlbum;
|
||||
|
||||
final bool isActive;
|
||||
|
||||
final bool isChecked;
|
||||
final bool showCheck;
|
||||
|
||||
final bool isLocal;
|
||||
final void Function(bool?)? onCheckChange;
|
||||
|
||||
final List<Widget>? actions;
|
||||
final List<Widget>? leadingActions;
|
||||
|
||||
TrackTile(
|
||||
this.playlist, {
|
||||
required this.track,
|
||||
required this.duration,
|
||||
required this.isActive,
|
||||
this.playlistId,
|
||||
this.userPlaylist = false,
|
||||
this.onTrackPlayButtonPressed,
|
||||
this.showAlbum = true,
|
||||
this.isChecked = false,
|
||||
this.showCheck = false,
|
||||
this.isLocal = false,
|
||||
this.onCheckChange,
|
||||
this.actions,
|
||||
this.leadingActions,
|
||||
const TrackTile({
|
||||
Key? key,
|
||||
this.index,
|
||||
required this.track,
|
||||
this.selected = false,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.onChanged,
|
||||
this.userPlaylist = false,
|
||||
this.playlistId,
|
||||
this.leadingActions,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||
final theme = Theme.of(context);
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||
final isBlackListed = ref.watch(
|
||||
BlackListNotifier.provider.select(
|
||||
(blacklist) => blacklist.contains(
|
||||
BlacklistedElement.track(track.value.id!, track.value.name!),
|
||||
|
||||
final blacklist = ref.watch(BlackListNotifier.provider);
|
||||
|
||||
final isBlackListed = useMemoized(
|
||||
() => blacklist.contains(
|
||||
BlacklistedElement.track(
|
||||
track.id!,
|
||||
track.name!,
|
||||
),
|
||||
),
|
||||
);
|
||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||
final playback = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||
|
||||
final removingTrack = useState<String?>(null);
|
||||
final removeTrack = useMutations.playlist.removeTrackOf(
|
||||
ref,
|
||||
playlistId ?? "",
|
||||
[blacklist, track],
|
||||
);
|
||||
|
||||
void actionShare(Track track) {
|
||||
final data = "https://open.spotify.com/track/${track.id}";
|
||||
Clipboard.setData(ClipboardData(text: data)).then((_) {
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
width: 300,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
content: Text(
|
||||
context.l10n.copied_to_clipboard(data),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
final isPlaying = track.id == playlist.activeTrack?.id;
|
||||
|
||||
Future<void> actionAddToPlaylist() async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => PlaylistAddTrackDialog(
|
||||
tracks: [track.value],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final String thumbnailUrl = TypeConversionUtils.image_X_UrlString(
|
||||
track.value.album?.images,
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
index: track.value.album?.images?.length == 1 ? 0 : 2,
|
||||
);
|
||||
|
||||
final toggler = useTrackToggleLike(track.value, ref);
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive
|
||||
? theme.colorScheme.surfaceVariant.withOpacity(0.5)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Row(
|
||||
children: [
|
||||
...?leadingActions,
|
||||
if (showCheck && !isBlackListed)
|
||||
Checkbox(
|
||||
value: isChecked,
|
||||
onChanged: (s) => onCheckChange?.call(s),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: 35,
|
||||
child: Center(
|
||||
child: AutoSizeText(
|
||||
(track.key + 1).toString(),
|
||||
return LayoutBuilder(builder: (context, constrains) {
|
||||
return HoverBuilder(
|
||||
permanentState: isPlaying || constrains.isSm ? true : null,
|
||||
builder: (context, isHovering) {
|
||||
return ListTile(
|
||||
selected: isPlaying,
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
enabled: !isBlackListed,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
tileColor: isBlackListed ? theme.colorScheme.errorContainer : null,
|
||||
horizontalTitleGap: 12,
|
||||
leadingAndTrailingTextStyle: theme.textTheme.bodyMedium,
|
||||
leading: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...?leadingActions,
|
||||
if (index != null && onChanged == null && constrains.mdAndUp)
|
||||
SizedBox(
|
||||
width: 34,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: Text(
|
||||
'$index',
|
||||
maxLines: 1,
|
||||
style: theme.textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (constrains.isSm)
|
||||
const SizedBox(width: 16),
|
||||
if (onChanged != null)
|
||||
Checkbox.adaptive(
|
||||
value: selected,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: mediaQuery.lgAndUp ? 8.0 : 0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
child: UniversalImage(
|
||||
path: thumbnailUrl,
|
||||
height: 40,
|
||||
width: 40,
|
||||
placeholder: Assets.albumPlaceholder.path,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.inversePrimary,
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
onPressed: !isBlackListed
|
||||
? () => onTrackPlayButtonPressed?.call(
|
||||
track.value,
|
||||
)
|
||||
: null,
|
||||
child: Icon(
|
||||
playlist.activeTrack?.id == track.value.id
|
||||
? SpotubeIcons.pause
|
||||
: SpotubeIcons.play,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
track.value.name ?? "",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: mediaQuery.isSm ? 14 : 17,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (isBlackListed) ...[
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
context.l10n.blacklisted,
|
||||
style: TextStyle(
|
||||
color: Colors.red[400],
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
isLocal
|
||||
? Text(
|
||||
TypeConversionUtils.artists_X_String<Artist>(
|
||||
track.value.artists ?? []),
|
||||
)
|
||||
: TypeConversionUtils.artists_X_ClickableArtists(
|
||||
track.value.artists ?? [],
|
||||
textStyle: TextStyle(
|
||||
fontSize: mediaQuery.isSm || mediaQuery.isMd
|
||||
? 12
|
||||
: 14)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (mediaQuery.lgAndUp && showAlbum)
|
||||
Expanded(
|
||||
child: isLocal
|
||||
? Text(track.value.album?.name ?? "")
|
||||
: LinkText(
|
||||
track.value.album!.name!,
|
||||
"/album/${track.value.album?.id}",
|
||||
extra: track.value.album,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (!mediaQuery.isSm) ...[
|
||||
const SizedBox(width: 10),
|
||||
Text(duration),
|
||||
],
|
||||
const SizedBox(width: 10),
|
||||
if (!isLocal)
|
||||
PopupMenuButton(
|
||||
icon: const Icon(SpotubeIcons.moreHorizontal),
|
||||
position: PopupMenuPosition.under,
|
||||
tooltip: context.l10n.more_actions,
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
if (!playlist.containsTrack(track.value)) ...[
|
||||
PopupMenuItem(
|
||||
padding: EdgeInsets.zero,
|
||||
onTap: () async {
|
||||
await playback.addTrack(track.value);
|
||||
if (context.mounted) {
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n
|
||||
.added_track_to_queue(track.value.name!),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: ListTile(
|
||||
leading: const Icon(SpotubeIcons.queueAdd),
|
||||
title: Text(context.l10n.add_to_queue),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
padding: EdgeInsets.zero,
|
||||
onTap: () {
|
||||
playback.addTracksAtFirst([track.value]);
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n
|
||||
.track_will_play_next(track.value.name!),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: ListTile(
|
||||
leading: const Icon(SpotubeIcons.lightning),
|
||||
title: Text(context.l10n.play_next),
|
||||
),
|
||||
),
|
||||
] else
|
||||
PopupMenuItem(
|
||||
padding: EdgeInsets.zero,
|
||||
onTap: playlist.activeTrack?.id == track.value.id
|
||||
? null
|
||||
: () {
|
||||
playback.removeTrack(track.value.id!);
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.removed_track_from_queue(
|
||||
track.value.name!,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
enabled: playlist.activeTrack?.id != track.value.id,
|
||||
child: ListTile(
|
||||
leading: const Icon(SpotubeIcons.queueRemove),
|
||||
title: Text(context.l10n.remove_from_queue),
|
||||
),
|
||||
),
|
||||
if (toggler.me.hasData)
|
||||
PopupMenuItem(
|
||||
padding: EdgeInsets.zero,
|
||||
onTap: () {
|
||||
toggler.toggleTrackLike.mutate(toggler.isLiked);
|
||||
},
|
||||
child: ListTile(
|
||||
leading: toggler.isLiked
|
||||
? const Icon(
|
||||
SpotubeIcons.heartFilled,
|
||||
color: Colors.pink,
|
||||
)
|
||||
: const Icon(SpotubeIcons.heart),
|
||||
title: Text(
|
||||
toggler.isLiked
|
||||
? context.l10n.remove_from_favorites
|
||||
: context.l10n.save_as_favorite,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (auth != null)
|
||||
PopupMenuItem(
|
||||
padding: EdgeInsets.zero,
|
||||
onTap: actionAddToPlaylist,
|
||||
child: ListTile(
|
||||
leading: const Icon(SpotubeIcons.playlistAdd),
|
||||
title: Text(context.l10n.add_to_playlist),
|
||||
),
|
||||
),
|
||||
if (userPlaylist && auth != null)
|
||||
PopupMenuItem(
|
||||
padding: EdgeInsets.zero,
|
||||
onTap: () {
|
||||
removingTrack.value = track.value.uri;
|
||||
removeTrack.mutate(track.value.uri!);
|
||||
},
|
||||
child: ListTile(
|
||||
leading: (removeTrack.isMutating ||
|
||||
!removeTrack.hasData) &&
|
||||
removingTrack.value == track.value.uri
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: const Icon(SpotubeIcons.removeFilled),
|
||||
title: Text(context.l10n.remove_from_playlist),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
padding: EdgeInsets.zero,
|
||||
onTap: () {
|
||||
if (isBlackListed) {
|
||||
ref.read(BlackListNotifier.provider.notifier).remove(
|
||||
BlacklistedElement.track(
|
||||
track.value.id!, track.value.name!),
|
||||
);
|
||||
} else {
|
||||
ref.read(BlackListNotifier.provider.notifier).add(
|
||||
BlacklistedElement.track(
|
||||
track.value.id!, track.value.name!),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: ListTile(
|
||||
leading: const Icon(SpotubeIcons.playlistRemove),
|
||||
iconColor: !isBlackListed ? Colors.red[400] : null,
|
||||
textColor: !isBlackListed ? Colors.red[400] : null,
|
||||
title: Text(
|
||||
isBlackListed
|
||||
? context.l10n.remove_from_blacklist
|
||||
: context.l10n.add_to_blacklist,
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: UniversalImage(
|
||||
path: TypeConversionUtils.image_X_UrlString(
|
||||
track.album?.images,
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
padding: EdgeInsets.zero,
|
||||
onTap: () {
|
||||
actionShare(track.value);
|
||||
},
|
||||
child: ListTile(
|
||||
leading: const Icon(SpotubeIcons.share),
|
||||
title: Text(context.l10n.share),
|
||||
Positioned.fill(
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: isHovering
|
||||
? Colors.black.withOpacity(0.4)
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: IconTheme(
|
||||
data: theme.iconTheme.copyWith(size: 26),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: !isHovering
|
||||
? const SizedBox.shrink()
|
||||
: isPlaying && playlist.isFetching
|
||||
? const SizedBox(
|
||||
width: 26,
|
||||
height: 26,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1.5,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: isPlaying
|
||||
? Icon(
|
||||
SpotubeIcons.pause,
|
||||
color: theme.colorScheme.primary,
|
||||
)
|
||||
: const Icon(SpotubeIcons.play),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 6,
|
||||
child: Text(
|
||||
track.name!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (constrains.mdAndUp) ...[
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: switch (track.runtimeType) {
|
||||
LocalTrack => Text(
|
||||
track.album!.name!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
_ => Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: LinkText(
|
||||
track.album!.name!,
|
||||
"/album/${track.album?.id}",
|
||||
extra: track.album,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
subtitle: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: track is LocalTrack
|
||||
? Text(
|
||||
TypeConversionUtils.artists_X_String<Artist>(
|
||||
track.artists ?? [],
|
||||
),
|
||||
)
|
||||
];
|
||||
},
|
||||
),
|
||||
...?actions,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
: TypeConversionUtils.artists_X_ClickableArtists(
|
||||
track.artists ?? [],
|
||||
),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
Duration(milliseconds: track.durationMs ?? 0)
|
||||
.toHumanReadableString(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
TrackOptions(
|
||||
track: track,
|
||||
playlistId: playlistId,
|
||||
userPlaylist: userPlaylist,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@ -16,7 +17,6 @@ import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/download_manager_provider.dart';
|
||||
import 'package:spotube/provider/blacklist_provider.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
|
||||
final trackCollectionSortState =
|
||||
@ -251,84 +251,53 @@ class TracksTableView extends HookConsumerWidget {
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
),
|
||||
...sortedTracks.asMap().entries.map((track) {
|
||||
String duration =
|
||||
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
|
||||
return Consumer(builder: (context, ref, _) {
|
||||
final isBlackListed = ref.watch(
|
||||
BlackListNotifier.provider.select(
|
||||
(blacklist) => blacklist.contains(
|
||||
BlacklistedElement.track(
|
||||
track.value.id!, track.value.name!),
|
||||
),
|
||||
),
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onLongPress: isBlackListed
|
||||
? null
|
||||
: () {
|
||||
showCheck.value = true;
|
||||
selected.value = [
|
||||
...selected.value,
|
||||
track.value.id!
|
||||
];
|
||||
},
|
||||
onTap: isBlackListed
|
||||
? null
|
||||
: () {
|
||||
if (showCheck.value) {
|
||||
final alreadyChecked =
|
||||
selected.value.contains(track.value.id);
|
||||
if (alreadyChecked) {
|
||||
selected.value = selected.value
|
||||
.where((id) => id != track.value.id)
|
||||
.toList();
|
||||
} else {
|
||||
selected.value = [
|
||||
...selected.value,
|
||||
track.value.id!
|
||||
];
|
||||
}
|
||||
} else {
|
||||
final isBlackListed = ref.read(
|
||||
BlackListNotifier.provider.select(
|
||||
(blacklist) => blacklist.contains(
|
||||
BlacklistedElement.track(
|
||||
track.value.id!, track.value.name!),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!isBlackListed) {
|
||||
onTrackPlayButtonPressed?.call(track.value);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: TrackTile(
|
||||
playlist,
|
||||
playlistId: playlistId,
|
||||
track: track,
|
||||
duration: duration,
|
||||
userPlaylist: userPlaylist,
|
||||
isActive: playlist.activeTrack?.id == track.value.id,
|
||||
onTrackPlayButtonPressed: onTrackPlayButtonPressed,
|
||||
isChecked: selected.value.contains(track.value.id),
|
||||
showCheck: showCheck.value,
|
||||
onCheckChange: (checked) {
|
||||
if (checked == true) {
|
||||
selected.value = [...selected.value, track.value.id!];
|
||||
...sortedTracks.mapIndexed((i, track) {
|
||||
return TrackTile(
|
||||
index: i,
|
||||
track: track,
|
||||
selected: selected.value.contains(track.id),
|
||||
userPlaylist: userPlaylist,
|
||||
playlistId: playlistId,
|
||||
onTap: () {
|
||||
if (showCheck.value) {
|
||||
final alreadyChecked = selected.value.contains(track.id);
|
||||
if (alreadyChecked) {
|
||||
selected.value =
|
||||
selected.value.where((id) => id != track.id).toList();
|
||||
} else {
|
||||
selected.value = [...selected.value, track.id!];
|
||||
}
|
||||
} else {
|
||||
final isBlackListed = ref.read(
|
||||
BlackListNotifier.provider.select(
|
||||
(blacklist) => blacklist.contains(
|
||||
BlacklistedElement.track(track.id!, track.name!),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!isBlackListed) {
|
||||
onTrackPlayButtonPressed?.call(track);
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
if (showCheck.value) return;
|
||||
showCheck.value = true;
|
||||
selected.value = [...selected.value, track.id!];
|
||||
},
|
||||
onChanged: !showCheck.value
|
||||
? null
|
||||
: (value) {
|
||||
if (value == null) return;
|
||||
if (value) {
|
||||
selected.value = [...selected.value, track.id!];
|
||||
} else {
|
||||
selected.value = selected.value
|
||||
.where((id) => id != track.value.id)
|
||||
.where((id) => id != track.id)
|
||||
.toList();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
);
|
||||
}).toList(),
|
||||
];
|
||||
|
||||
|
6
lib/extensions/duration.dart
Normal file
6
lib/extensions/duration.dart
Normal file
@ -0,0 +1,6 @@
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
|
||||
extension DurationToHumanReadableString on Duration {
|
||||
toHumanReadableString() =>
|
||||
"${inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(inSeconds.remainder(60))}";
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -89,7 +90,6 @@ class ArtistPage extends HookConsumerWidget {
|
||||
|
||||
return SingleChildScrollView(
|
||||
controller: parentScrollController,
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -99,12 +99,15 @@ class ArtistPage extends HookConsumerWidget {
|
||||
runAlignment: WrapAlignment.center,
|
||||
children: [
|
||||
const SizedBox(width: 50),
|
||||
CircleAvatar(
|
||||
radius: avatarWidth,
|
||||
backgroundImage: UniversalImage.imageProvider(
|
||||
TypeConversionUtils.image_X_UrlString(
|
||||
data.images,
|
||||
placeholder: ImagePlaceholder.artist,
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: CircleAvatar(
|
||||
radius: avatarWidth,
|
||||
backgroundImage: UniversalImage.imageProvider(
|
||||
TypeConversionUtils.image_X_UrlString(
|
||||
data.images,
|
||||
placeholder: ImagePlaceholder.artist,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -331,81 +334,87 @@ class ArtistPage extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
return Column(children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.top_tracks,
|
||||
style: theme.textTheme.headlineSmall,
|
||||
),
|
||||
if (!isPlaylistPlaying)
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
SpotubeIcons.queueAdd,
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
context.l10n.top_tracks,
|
||||
style: theme.textTheme.headlineSmall,
|
||||
),
|
||||
onPressed: () {
|
||||
playlistNotifier
|
||||
.addTracks(topTracks.toList());
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
width: 300,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
content: Text(
|
||||
context.l10n.added_to_queue(
|
||||
topTracks.length,
|
||||
),
|
||||
if (!isPlaylistPlaying)
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
SpotubeIcons.queueAdd,
|
||||
),
|
||||
onPressed: () {
|
||||
playlistNotifier
|
||||
.addTracks(topTracks.toList());
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
width: 300,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
content: Text(
|
||||
context.l10n.added_to_queue(
|
||||
topTracks.length,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isPlaylistPlaying
|
||||
? SpotubeIcons.stop
|
||||
: SpotubeIcons.play,
|
||||
color: Colors.white,
|
||||
),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
),
|
||||
onPressed: () =>
|
||||
playPlaylist(topTracks.toList()),
|
||||
)
|
||||
],
|
||||
),
|
||||
...topTracks.toList().asMap().entries.map((track) {
|
||||
String duration =
|
||||
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
|
||||
return TrackTile(
|
||||
playlist,
|
||||
duration: duration,
|
||||
track: track,
|
||||
isActive:
|
||||
playlist.activeTrack?.id == track.value.id,
|
||||
onTrackPlayButtonPressed: (currentTrack) =>
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isPlaylistPlaying
|
||||
? SpotubeIcons.stop
|
||||
: SpotubeIcons.play,
|
||||
color: Colors.white,
|
||||
),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
),
|
||||
onPressed: () =>
|
||||
playPlaylist(topTracks.toList()),
|
||||
)
|
||||
],
|
||||
),
|
||||
...topTracks.mapIndexed((i, track) {
|
||||
return TrackTile(
|
||||
index: i,
|
||||
track: track,
|
||||
onTap: () {
|
||||
playPlaylist(
|
||||
topTracks.toList(),
|
||||
currentTrack: track.value,
|
||||
),
|
||||
);
|
||||
}),
|
||||
]);
|
||||
topTracks.toList(),
|
||||
currentTrack: track,
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 50),
|
||||
Text(
|
||||
context.l10n.albums,
|
||||
style: theme.textTheme.headlineSmall,
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
context.l10n.albums,
|
||||
style: theme.textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ArtistAlbumList(artistId),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
context.l10n.fans_also_like,
|
||||
style: theme.textTheme.headlineSmall,
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
context.l10n.fans_also_like,
|
||||
style: theme.textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
HookBuilder(
|
||||
|
@ -23,7 +23,6 @@ import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
@ -147,20 +146,14 @@ class SearchPage extends HookConsumerWidget {
|
||||
"",
|
||||
)
|
||||
else
|
||||
...tracks.asMap().entries.map((track) {
|
||||
String duration =
|
||||
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
|
||||
...tracks.mapIndexed((i, track) {
|
||||
return TrackTile(
|
||||
playlist,
|
||||
index: i,
|
||||
track: track,
|
||||
duration: duration,
|
||||
isActive: playlist.activeTrack?.id ==
|
||||
track.value.id,
|
||||
onTrackPlayButtonPressed:
|
||||
(currentTrack) async {
|
||||
onTap: () async {
|
||||
final isTrackPlaying =
|
||||
playlist.activeTrack?.id ==
|
||||
currentTrack.id;
|
||||
track.id;
|
||||
if (!isTrackPlaying &&
|
||||
context.mounted) {
|
||||
final shouldPlay =
|
||||
@ -169,7 +162,7 @@ class SearchPage extends HookConsumerWidget {
|
||||
context: context,
|
||||
title: context.l10n
|
||||
.playing_track(
|
||||
currentTrack.name!,
|
||||
track.name!,
|
||||
),
|
||||
message: context.l10n
|
||||
.queue_clear_alert(
|
||||
@ -181,7 +174,7 @@ class SearchPage extends HookConsumerWidget {
|
||||
|
||||
if (shouldPlay) {
|
||||
await playlistNotifier.load(
|
||||
[currentTrack],
|
||||
[track],
|
||||
autoPlay: true,
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user