mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05: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',
|
'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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final mediaQuery = MediaQuery.of(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/components/shared/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/local_track.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/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
@ -40,9 +39,11 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
final Track track;
|
final Track track;
|
||||||
final bool userPlaylist;
|
final bool userPlaylist;
|
||||||
final String? playlistId;
|
final String? playlistId;
|
||||||
|
final ObjectRef<ValueChanged<RelativeRect>?>? showMenuCbRef;
|
||||||
const TrackOptions({
|
const TrackOptions({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.track,
|
required this.track,
|
||||||
|
this.showMenuCbRef,
|
||||||
this.userPlaylist = false,
|
this.userPlaylist = false,
|
||||||
this.playlistId,
|
this.playlistId,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
@ -114,210 +115,216 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
return downloadManager.getProgressNotifier(spotubeTrack);
|
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(
|
return ListTileTheme(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: AdaptivePopSheetList<TrackOptionValue>(
|
child: adaptivePopSheetList,
|
||||||
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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@ -57,174 +58,191 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
[blacklist, track],
|
[blacklist, track],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final showOptionCbRef = useRef<ValueChanged<RelativeRect>?>(null);
|
||||||
|
|
||||||
final isPlaying = track.id == playlist.activeTrack?.id;
|
final isPlaying = track.id == playlist.activeTrack?.id;
|
||||||
|
|
||||||
return LayoutBuilder(builder: (context, constrains) {
|
return LayoutBuilder(builder: (context, constrains) {
|
||||||
return HoverBuilder(
|
return Listener(
|
||||||
permanentState: isPlaying || constrains.smAndDown ? true : null,
|
onPointerDown: (event) {
|
||||||
builder: (context, isHovering) {
|
if (event.buttons != kSecondaryMouseButton) return;
|
||||||
return ListTile(
|
showOptionCbRef.value?.call(
|
||||||
selected: isPlaying,
|
RelativeRect.fromLTRB(
|
||||||
onTap: onTap,
|
event.position.dx,
|
||||||
onLongPress: onLongPress,
|
event.position.dy,
|
||||||
enabled: !isBlackListed,
|
constrains.maxWidth - event.position.dx,
|
||||||
contentPadding: EdgeInsets.zero,
|
constrains.maxHeight - event.position.dy,
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
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