mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00

Downloaded tracks are saved with metadata. Only MP3 file metadata support is available in local track player for now
328 lines
12 KiB
Dart
328 lines
12 KiB
Dart
import 'package:flutter/material.dart' hide Action;
|
|
import 'package:flutter/services.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/components/Shared/AdaptivePopupMenuButton.dart';
|
|
import 'package:spotube/components/Shared/LinkText.dart';
|
|
import 'package:spotube/components/Shared/UniversalImage.dart';
|
|
import 'package:spotube/hooks/useBreakpoints.dart';
|
|
import 'package:spotube/hooks/useForceUpdate.dart';
|
|
import 'package:spotube/models/Logger.dart';
|
|
import 'package:spotube/provider/Auth.dart';
|
|
import 'package:spotube/provider/Playback.dart';
|
|
import 'package:spotube/provider/SpotifyDI.dart';
|
|
import 'package:spotube/provider/SpotifyRequests.dart';
|
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
|
|
class TrackTile extends HookConsumerWidget {
|
|
final Playback playback;
|
|
final MapEntry<int, Track> track;
|
|
final String duration;
|
|
final String? thumbnailUrl;
|
|
final void Function(Track currentTrack)? onTrackPlayButtonPressed;
|
|
final logger = getLogger(TrackTile);
|
|
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;
|
|
|
|
TrackTile(
|
|
this.playback, {
|
|
required this.track,
|
|
required this.duration,
|
|
required this.isActive,
|
|
this.playlistId,
|
|
this.userPlaylist = false,
|
|
this.thumbnailUrl,
|
|
this.onTrackPlayButtonPressed,
|
|
this.showAlbum = true,
|
|
this.isChecked = false,
|
|
this.showCheck = false,
|
|
this.isLocal = false,
|
|
this.onCheckChange,
|
|
Key? key,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final isReallyLocal = isLocal ||
|
|
ref.watch(
|
|
playbackProvider.select((s) => s.playlist?.isLocal == true),
|
|
);
|
|
final breakpoint = useBreakpoints();
|
|
final auth = ref.watch(authProvider);
|
|
final spotify = ref.watch(spotifyProvider);
|
|
final update = useForceUpdate();
|
|
|
|
final savedTracksSnapshot = ref.watch(currentUserSavedTracksQuery);
|
|
|
|
final isSaved = savedTracksSnapshot.asData?.value.any(
|
|
(e) => track.value.id == e.id,
|
|
) ??
|
|
false;
|
|
|
|
final actionFavorite = useCallback((bool isLiked) async {
|
|
try {
|
|
isLiked
|
|
? await spotify.tracks.me.removeOne(track.value.id!)
|
|
: await spotify.tracks.me.saveOne(track.value.id!);
|
|
} catch (e, stack) {
|
|
logger.e("FavoriteButton.onPressed", e, stack);
|
|
} finally {
|
|
update();
|
|
ref.refresh(currentUserSavedTracksQuery);
|
|
ref.refresh(playlistTracksQuery("user-liked-tracks"));
|
|
}
|
|
}, [track.value.id, spotify]);
|
|
|
|
final actionRemoveFromPlaylist = useCallback(() async {
|
|
if (playlistId == null) return;
|
|
return await spotify.playlists.removeTrack(track.value.uri!, playlistId!);
|
|
}, [playlistId, spotify, track.value.uri]);
|
|
|
|
void actionShare(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(
|
|
"Copied $data to clipboard",
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
Future<void> actionAddToPlaylist() async {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
return FutureBuilder<Iterable<PlaylistSimple>>(
|
|
future: spotify.playlists.me.all().then((playlists) async {
|
|
final me = await spotify.me.get();
|
|
return playlists.where((playlist) =>
|
|
playlist.owner?.id != null &&
|
|
playlist.owner!.id == me.id);
|
|
}),
|
|
builder: (context, snapshot) {
|
|
return HookBuilder(builder: (context) {
|
|
final playlistsCheck = useState(<String, bool>{});
|
|
return AlertDialog(
|
|
title: Text(
|
|
"Add `${track.value.name}` to following Playlists"),
|
|
titleTextStyle:
|
|
Theme.of(context).textTheme.bodyText1?.copyWith(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
child: const Text("Cancel"),
|
|
onPressed: () => Navigator.pop(context),
|
|
),
|
|
ElevatedButton(
|
|
child: const Text("Add"),
|
|
onPressed: () async {
|
|
final selectedPlaylists = playlistsCheck
|
|
.value.entries
|
|
.where((entry) => entry.value)
|
|
.map((entry) => entry.key);
|
|
|
|
await Future.wait(
|
|
selectedPlaylists.map(
|
|
(playlistId) => spotify.playlists
|
|
.addTrack(track.value.uri!, playlistId),
|
|
),
|
|
).then((_) => Navigator.pop(context));
|
|
},
|
|
)
|
|
],
|
|
content: SizedBox(
|
|
height: 300,
|
|
width: 300,
|
|
child: !snapshot.hasData
|
|
? const Center(
|
|
child: CircularProgressIndicator.adaptive())
|
|
: ListView.builder(
|
|
shrinkWrap: true,
|
|
itemCount: snapshot.data!.length,
|
|
itemBuilder: (context, index) {
|
|
final playlist =
|
|
snapshot.data!.elementAt(index);
|
|
return CheckboxListTile(
|
|
title: Text(playlist.name!),
|
|
controlAffinity:
|
|
ListTileControlAffinity.leading,
|
|
value: playlistsCheck.value[playlist.id] ??
|
|
false,
|
|
onChanged: (val) {
|
|
playlistsCheck.value = {
|
|
...playlistsCheck.value,
|
|
playlist.id!: val == true
|
|
};
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 500),
|
|
decoration: BoxDecoration(
|
|
color: isActive
|
|
? Theme.of(context).popupMenuTheme.color
|
|
: Colors.transparent,
|
|
borderRadius: BorderRadius.circular(isActive ? 10 : 0),
|
|
),
|
|
child: Material(
|
|
type: MaterialType.transparency,
|
|
child: Row(
|
|
children: [
|
|
if (showCheck)
|
|
Checkbox(
|
|
value: isChecked,
|
|
onChanged: (s) => onCheckChange?.call(s),
|
|
)
|
|
else
|
|
SizedBox(
|
|
height: 20,
|
|
width: 25,
|
|
child: Center(
|
|
child: Text((track.key + 1).toString()),
|
|
),
|
|
),
|
|
if (thumbnailUrl != null)
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: breakpoint.isMoreThan(Breakpoints.md) ? 8.0 : 0,
|
|
vertical: 8.0,
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
|
child: UniversalImage(
|
|
path: thumbnailUrl!,
|
|
height: 40,
|
|
width: 40,
|
|
placeholder: (context, url) {
|
|
return Image.asset(
|
|
"assets/album-placeholder.png",
|
|
height: 40,
|
|
width: 40,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: Icon(
|
|
playback.track?.id != null &&
|
|
playback.track?.id == track.value.id
|
|
? Icons.pause_circle_rounded
|
|
: Icons.play_circle_rounded,
|
|
color: Theme.of(context).primaryColor,
|
|
),
|
|
onPressed: () => onTrackPlayButtonPressed?.call(
|
|
track.value,
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
track.value.name ?? "",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: breakpoint.isSm ? 14 : 17,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
isReallyLocal
|
|
? Text(
|
|
TypeConversionUtils.artists_X_String<Artist>(
|
|
track.value.artists ?? []),
|
|
)
|
|
: TypeConversionUtils.artists_X_ClickableArtists(
|
|
track.value.artists ?? [],
|
|
textStyle: TextStyle(
|
|
fontSize: breakpoint.isLessThan(Breakpoints.lg)
|
|
? 12
|
|
: 14)),
|
|
],
|
|
),
|
|
),
|
|
if (breakpoint.isMoreThan(Breakpoints.md) && showAlbum)
|
|
Expanded(
|
|
child: isReallyLocal
|
|
? Text(track.value.album?.name ?? "")
|
|
: LinkText(
|
|
track.value.album!.name!,
|
|
"/album/${track.value.album?.id}",
|
|
extra: track.value.album,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
if (!breakpoint.isSm) ...[
|
|
const SizedBox(width: 10),
|
|
Text(duration),
|
|
],
|
|
const SizedBox(width: 10),
|
|
if (!isReallyLocal)
|
|
AdaptiveActions(
|
|
actions: [
|
|
if (auth.isLoggedIn)
|
|
Action(
|
|
icon: Icon(isSaved
|
|
? Icons.favorite_rounded
|
|
: Icons.favorite_border_rounded),
|
|
text: const Text("Save as favorite"),
|
|
onPressed: () {
|
|
actionFavorite(isSaved);
|
|
},
|
|
),
|
|
if (auth.isLoggedIn)
|
|
Action(
|
|
icon: const Icon(Icons.add_box_rounded),
|
|
text: const Text("Add To playlist"),
|
|
onPressed: actionAddToPlaylist,
|
|
),
|
|
if (userPlaylist && auth.isLoggedIn)
|
|
Action(
|
|
icon: const Icon(Icons.remove_circle_outline_rounded),
|
|
text: const Text("Remove from playlist"),
|
|
onPressed: actionRemoveFromPlaylist,
|
|
),
|
|
Action(
|
|
icon: const Icon(Icons.share_rounded),
|
|
text: const Text("Share"),
|
|
onPressed: () {
|
|
actionShare(track.value);
|
|
},
|
|
)
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|