mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat: right click to open track option
This commit is contained in:
parent
d4f99ec899
commit
1540999f50
@ -78,6 +78,31 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
|
||||
'Either icon or child must be provided',
|
||||
);
|
||||
|
||||
Future<T?> showPopupMenu(BuildContext context, RelativeRect position) {
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
|
||||
return showMenu<T>(
|
||||
context: context,
|
||||
useRootNavigator: useRootNavigator,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: mediaQuery.size.height * 0.6,
|
||||
),
|
||||
position: position,
|
||||
items: children
|
||||
.map(
|
||||
(item) => PopupMenuItem<T>(
|
||||
padding: EdgeInsets.zero,
|
||||
enabled: false,
|
||||
child: _AdaptivePopSheetListItem<T>(
|
||||
item: item,
|
||||
onSelected: onSelected,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
|
@ -14,7 +14,6 @@ import 'package:spotube/components/shared/heart_button.dart';
|
||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/models/local_track.dart';
|
||||
import 'package:spotube/models/spotube_track.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/provider/blacklist_provider.dart';
|
||||
import 'package:spotube/provider/download_manager_provider.dart';
|
||||
@ -40,9 +39,11 @@ class TrackOptions extends HookConsumerWidget {
|
||||
final Track track;
|
||||
final bool userPlaylist;
|
||||
final String? playlistId;
|
||||
final ObjectRef<ValueChanged<RelativeRect>?>? showMenuCbRef;
|
||||
const TrackOptions({
|
||||
Key? key,
|
||||
required this.track,
|
||||
this.showMenuCbRef,
|
||||
this.userPlaylist = false,
|
||||
this.playlistId,
|
||||
}) : super(key: key);
|
||||
@ -114,210 +115,216 @@ class TrackOptions extends HookConsumerWidget {
|
||||
return downloadManager.getProgressNotifier(spotubeTrack);
|
||||
});
|
||||
|
||||
final adaptivePopSheetList = AdaptivePopSheetList<TrackOptionValue>(
|
||||
onSelected: (value) async {
|
||||
switch (value) {
|
||||
case TrackOptionValue.delete:
|
||||
await File((track as LocalTrack).path).delete();
|
||||
ref.refresh(localTracksProvider);
|
||||
break;
|
||||
case TrackOptionValue.addToQueue:
|
||||
await playback.addTrack(track);
|
||||
if (context.mounted) {
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.added_track_to_queue(track.name!),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case TrackOptionValue.playNext:
|
||||
playback.addTracksAtFirst([track]);
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.track_will_play_next(track.name!),
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case TrackOptionValue.removeFromQueue:
|
||||
playback.removeTrack(track.id!);
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.removed_track_from_queue(
|
||||
track.name!,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case TrackOptionValue.favorite:
|
||||
favorites.toggleTrackLike.mutate(favorites.isLiked);
|
||||
break;
|
||||
case TrackOptionValue.addToPlaylist:
|
||||
actionAddToPlaylist(context, track);
|
||||
break;
|
||||
case TrackOptionValue.removeFromPlaylist:
|
||||
removingTrack.value = track.uri;
|
||||
removeTrack.mutate(track.uri!);
|
||||
break;
|
||||
case TrackOptionValue.blacklist:
|
||||
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!),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case TrackOptionValue.share:
|
||||
actionShare(context, track);
|
||||
break;
|
||||
case TrackOptionValue.details:
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => TrackDetailsDialog(track: track),
|
||||
);
|
||||
break;
|
||||
case TrackOptionValue.download:
|
||||
await downloadManager.addToQueue(track);
|
||||
break;
|
||||
}
|
||||
},
|
||||
icon: const Icon(SpotubeIcons.moreHorizontal),
|
||||
headings: [
|
||||
ListTile(
|
||||
dense: true,
|
||||
leading: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: UniversalImage(
|
||||
path: TypeConversionUtils.image_X_UrlString(track.album!.images,
|
||||
placeholder: ImagePlaceholder.albumArt),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
track.name!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
subtitle: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TypeConversionUtils.artists_X_ClickableArtists(
|
||||
track.artists!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
children: switch (track.runtimeType) {
|
||||
LocalTrack => [
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.delete,
|
||||
leading: const Icon(SpotubeIcons.trash),
|
||||
title: Text(context.l10n.delete),
|
||||
)
|
||||
],
|
||||
_ => [
|
||||
if (!playlist.containsTrack(track)) ...[
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.addToQueue,
|
||||
leading: const Icon(SpotubeIcons.queueAdd),
|
||||
title: Text(context.l10n.add_to_queue),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.playNext,
|
||||
leading: const Icon(SpotubeIcons.lightning),
|
||||
title: Text(context.l10n.play_next),
|
||||
),
|
||||
] else
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.removeFromQueue,
|
||||
enabled: playlist.activeTrack?.id != track.id,
|
||||
leading: const Icon(SpotubeIcons.queueRemove),
|
||||
title: Text(context.l10n.remove_from_queue),
|
||||
),
|
||||
if (favorites.me.hasData)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.favorite,
|
||||
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)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.addToPlaylist,
|
||||
leading: const Icon(SpotubeIcons.playlistAdd),
|
||||
title: Text(context.l10n.add_to_playlist),
|
||||
),
|
||||
if (userPlaylist && auth != null)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.removeFromPlaylist,
|
||||
leading: (removeTrack.isMutating || !removeTrack.hasData) &&
|
||||
removingTrack.value == track.uri
|
||||
? const CircularProgressIndicator()
|
||||
: const Icon(SpotubeIcons.removeFilled),
|
||||
title: Text(context.l10n.remove_from_playlist),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.download,
|
||||
enabled: !isInQueue,
|
||||
leading: isInQueue
|
||||
? HookBuilder(builder: (context) {
|
||||
final progress = useListenable(progressNotifier!);
|
||||
return CircularProgressIndicator(
|
||||
value: progress.value,
|
||||
);
|
||||
})
|
||||
: const Icon(SpotubeIcons.download),
|
||||
title: Text(context.l10n.download_track),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.blacklist,
|
||||
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,
|
||||
),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.share,
|
||||
leading: const Icon(SpotubeIcons.share),
|
||||
title: Text(context.l10n.share),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.details,
|
||||
leading: const Icon(SpotubeIcons.info),
|
||||
title: Text(context.l10n.details),
|
||||
),
|
||||
]
|
||||
},
|
||||
);
|
||||
|
||||
//! This is the most ANTI pattern I've ever done, but it works
|
||||
showMenuCbRef?.value = (relativeRect) {
|
||||
adaptivePopSheetList.showPopupMenu(context, relativeRect);
|
||||
};
|
||||
|
||||
return ListTileTheme(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: AdaptivePopSheetList<TrackOptionValue>(
|
||||
onSelected: (value) async {
|
||||
switch (value) {
|
||||
case TrackOptionValue.delete:
|
||||
await File((track as LocalTrack).path).delete();
|
||||
ref.refresh(localTracksProvider);
|
||||
break;
|
||||
case TrackOptionValue.addToQueue:
|
||||
await playback.addTrack(track);
|
||||
if (context.mounted) {
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.added_track_to_queue(track.name!),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case TrackOptionValue.playNext:
|
||||
playback.addTracksAtFirst([track]);
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.track_will_play_next(track.name!),
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case TrackOptionValue.removeFromQueue:
|
||||
playback.removeTrack(track.id!);
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.removed_track_from_queue(
|
||||
track.name!,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case TrackOptionValue.favorite:
|
||||
favorites.toggleTrackLike.mutate(favorites.isLiked);
|
||||
break;
|
||||
case TrackOptionValue.addToPlaylist:
|
||||
actionAddToPlaylist(context, track);
|
||||
break;
|
||||
case TrackOptionValue.removeFromPlaylist:
|
||||
removingTrack.value = track.uri;
|
||||
removeTrack.mutate(track.uri!);
|
||||
break;
|
||||
case TrackOptionValue.blacklist:
|
||||
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!),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case TrackOptionValue.share:
|
||||
actionShare(context, track);
|
||||
break;
|
||||
case TrackOptionValue.details:
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => TrackDetailsDialog(track: track),
|
||||
);
|
||||
break;
|
||||
case TrackOptionValue.download:
|
||||
await downloadManager.addToQueue(track);
|
||||
break;
|
||||
}
|
||||
},
|
||||
icon: const Icon(SpotubeIcons.moreHorizontal),
|
||||
headings: [
|
||||
ListTile(
|
||||
dense: true,
|
||||
leading: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: UniversalImage(
|
||||
path: TypeConversionUtils.image_X_UrlString(
|
||||
track.album!.images,
|
||||
placeholder: ImagePlaceholder.albumArt),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
track.name!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
subtitle: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TypeConversionUtils.artists_X_ClickableArtists(
|
||||
track.artists!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
children: switch (track.runtimeType) {
|
||||
LocalTrack => [
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.delete,
|
||||
leading: const Icon(SpotubeIcons.trash),
|
||||
title: Text(context.l10n.delete),
|
||||
)
|
||||
],
|
||||
_ => [
|
||||
if (!playlist.containsTrack(track)) ...[
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.addToQueue,
|
||||
leading: const Icon(SpotubeIcons.queueAdd),
|
||||
title: Text(context.l10n.add_to_queue),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.playNext,
|
||||
leading: const Icon(SpotubeIcons.lightning),
|
||||
title: Text(context.l10n.play_next),
|
||||
),
|
||||
] else
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.removeFromQueue,
|
||||
enabled: playlist.activeTrack?.id != track.id,
|
||||
leading: const Icon(SpotubeIcons.queueRemove),
|
||||
title: Text(context.l10n.remove_from_queue),
|
||||
),
|
||||
if (favorites.me.hasData)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.favorite,
|
||||
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)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.addToPlaylist,
|
||||
leading: const Icon(SpotubeIcons.playlistAdd),
|
||||
title: Text(context.l10n.add_to_playlist),
|
||||
),
|
||||
if (userPlaylist && auth != null)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.removeFromPlaylist,
|
||||
leading: (removeTrack.isMutating || !removeTrack.hasData) &&
|
||||
removingTrack.value == track.uri
|
||||
? const CircularProgressIndicator()
|
||||
: const Icon(SpotubeIcons.removeFilled),
|
||||
title: Text(context.l10n.remove_from_playlist),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.download,
|
||||
enabled: !isInQueue,
|
||||
leading: isInQueue
|
||||
? HookBuilder(builder: (context) {
|
||||
final progress = useListenable(progressNotifier!);
|
||||
return CircularProgressIndicator(
|
||||
value: progress.value,
|
||||
);
|
||||
})
|
||||
: const Icon(SpotubeIcons.download),
|
||||
title: Text(context.l10n.download_track),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.blacklist,
|
||||
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,
|
||||
),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.share,
|
||||
leading: const Icon(SpotubeIcons.share),
|
||||
title: Text(context.l10n.share),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.details,
|
||||
leading: const Icon(SpotubeIcons.info),
|
||||
title: Text(context.l10n.details),
|
||||
),
|
||||
]
|
||||
},
|
||||
),
|
||||
child: adaptivePopSheetList,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@ -57,174 +58,191 @@ class TrackTile extends HookConsumerWidget {
|
||||
[blacklist, track],
|
||||
);
|
||||
|
||||
final showOptionCbRef = useRef<ValueChanged<RelativeRect>?>(null);
|
||||
|
||||
final isPlaying = track.id == playlist.activeTrack?.id;
|
||||
|
||||
return LayoutBuilder(builder: (context, constrains) {
|
||||
return HoverBuilder(
|
||||
permanentState: isPlaying || constrains.smAndDown ? 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.smAndDown)
|
||||
const SizedBox(width: 16),
|
||||
if (onChanged != null)
|
||||
Checkbox(
|
||||
value: selected,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: UniversalImage(
|
||||
path: TypeConversionUtils.image_X_UrlString(
|
||||
track.album?.images,
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
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, color: Colors.white),
|
||||
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,
|
||||
push: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
subtitle: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: track is LocalTrack
|
||||
? Text(
|
||||
TypeConversionUtils.artists_X_String<Artist>(
|
||||
track.artists ?? [],
|
||||
),
|
||||
)
|
||||
: ClipRect(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 40),
|
||||
child: 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,
|
||||
),
|
||||
],
|
||||
return Listener(
|
||||
onPointerDown: (event) {
|
||||
if (event.buttons != kSecondaryMouseButton) return;
|
||||
showOptionCbRef.value?.call(
|
||||
RelativeRect.fromLTRB(
|
||||
event.position.dx,
|
||||
event.position.dy,
|
||||
constrains.maxWidth - event.position.dx,
|
||||
constrains.maxHeight - event.position.dy,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: HoverBuilder(
|
||||
permanentState: isPlaying || constrains.smAndDown ? 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.smAndDown)
|
||||
const SizedBox(width: 16),
|
||||
if (onChanged != null)
|
||||
Checkbox(
|
||||
value: selected,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: UniversalImage(
|
||||
path: TypeConversionUtils.image_X_UrlString(
|
||||
track.album?.images,
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
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, color: Colors.white),
|
||||
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,
|
||||
push: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
subtitle: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: track is LocalTrack
|
||||
? Text(
|
||||
TypeConversionUtils.artists_X_String<Artist>(
|
||||
track.artists ?? [],
|
||||
),
|
||||
)
|
||||
: ClipRect(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 40),
|
||||
child: 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,
|
||||
showMenuCbRef: showOptionCbRef,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user