refactor: remove old spotify.dart types and custom spotube metadata types

This commit is contained in:
Kingkor Roy Tirtho 2025-06-19 14:42:29 +06:00
parent 4e6db8b9e1
commit 5f47dc3d6d
170 changed files with 1649 additions and 10215 deletions

View File

@ -1,19 +1,13 @@
import 'package:spotify/spotify.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/models/spotify/home_feed.dart';
import 'package:spotube/models/spotify_friends.dart';
import 'package:spotube/provider/history/summary.dart'; import 'package:spotube/provider/history/summary.dart';
abstract class FakeData { abstract class FakeData {
static final Image image = Image() static final SpotubeImageObject image = SpotubeImageObject(
..height = 1 height: 100,
..width = 1 width: 100,
..url = "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg"; url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
);
static final Followers followers = Followers()
..href = "text"
..total = 1;
static final SpotubeFullArtistObject artist = SpotubeFullArtistObject( static final SpotubeFullArtistObject artist = SpotubeFullArtistObject(
id: "1", id: "1",
@ -30,43 +24,26 @@ abstract class FakeData {
], ],
); );
static final externalIds = ExternalIds() static final SpotubeFullAlbumObject album = SpotubeFullAlbumObject(
..isrc = "text" id: "1",
..ean = "text" name: "A good album",
..upc = "text"; externalUri: "https://example.com",
artists: [artistSimple],
releaseDate: "2021-01-01",
albumType: SpotubeAlbumType.album,
images: [image],
totalTracks: 10,
genres: ["genre"],
recordLabel: "Record Label",
);
static final externalUrls = ExternalUrls()..spotify = "text"; static final SpotubeSimpleArtistObject artistSimple =
SpotubeSimpleArtistObject(
static final Album album = Album() id: "1",
..id = "1" name: "What an artist",
..genres = ["genre"] externalUri: "https://example.com",
..label = "label" images: null,
..popularity = 1 );
..albumType = AlbumType.album
// ..artists = [artist]
..availableMarkets = [Market.BD]
..externalUrls = externalUrls
..href = "text"
..images = [image]
..name = "Another good album"
..releaseDate = "2021-01-01"
..releaseDatePrecision = DatePrecision.day
..tracks = [track]
..type = "type"
..uri = "uri"
..externalIds = externalIds
..copyrights = [
Copyright()
..type = CopyrightType.C
..text = "text",
];
static final ArtistSimple artistSimple = ArtistSimple()
..id = "1"
..name = "What an artist"
..type = "type"
..uri = "uri"
..externalUrls = externalUrls;
static final SpotubeSimpleAlbumObject albumSimple = SpotubeSimpleAlbumObject( static final SpotubeSimpleAlbumObject albumSimple = SpotubeSimpleAlbumObject(
albumType: SpotubeAlbumType.album, albumType: SpotubeAlbumType.album,
@ -84,163 +61,51 @@ abstract class FakeData {
], ],
); );
static final Track track = Track() static final SpotubeFullTrackObject track = SpotubeTrackObject.full(
..id = "1"
// ..artists = [artist, artist, artist]
// ..album = albumSimple
..availableMarkets = [Market.BD]
..discNumber = 1
..durationMs = 50000
..explicit = false
..externalUrls = externalUrls
..href = "text"
..name = "A Track Name"
..popularity = 1
..previewUrl = "url"
..trackNumber = 1
..type = "type"
..uri = "uri"
..externalIds = externalIds
..isPlayable = true
..explicit = false
..linkedFrom = trackLink;
static final simpleTrack = SpotubeSimpleTrackObject(
id: "1", id: "1",
name: "A Track Name", name: "A good track",
artists: [],
album: albumSimple,
externalUri: "https://example.com", externalUri: "https://example.com",
durationMs: 50000, album: albumSimple,
durationMs: 3 * 60 * 1000, // 3 minutes
isrc: "USUM72112345",
explicit: false, explicit: false,
) as SpotubeFullTrackObject;
static final SpotubeUserObject user = SpotubeUserObject(
id: "1",
name: "User Name",
externalUri: "https://example.com",
images: [image],
); );
static final TrackLink trackLink = TrackLink() static final SpotubeFullPlaylistObject playlist = SpotubeFullPlaylistObject(
..id = "1" id: "1",
..type = "type" name: "A good playlist",
..uri = "uri" description: "A very good playlist description",
..externalUrls = {"spotify": "text"} externalUri: "https://example.com",
..href = "text"; collaborative: false,
public: true,
owner: user,
images: [image],
collaborators: [user]);
static final Paging<Track> paging = Paging() static final SpotubeSimplePlaylistObject playlistSimple =
..href = "text" SpotubeSimplePlaylistObject(
..itemsNative = [track.toJson()] id: "1",
..limit = 1 name: "A good playlist",
..next = "text" description: "A very good playlist description",
..offset = 1 externalUri: "https://example.com",
..previous = "text" owner: user,
..total = 1; images: [image],
static final User user = User()
..id = "1"
..displayName = "Your Name"
..birthdate = "2021-01-01"
..country = Market.BD
..email = "test@email.com"
..followers = followers
..href = "text"
..images = [image]
..type = "type"
..uri = "uri";
static final TracksLink tracksLink = TracksLink()
..href = "text"
..total = 1;
static final Playlist playlist = Playlist()
..id = "1"
..collaborative = false
..description = "A very good playlist description"
..externalUrls = externalUrls
..followers = followers
..href = "text"
..images = [image]
..name = "A good playlist"
..owner = user
..public = true
..snapshotId = "text"
..tracks = paging
..tracksLink = tracksLink
..type = "type"
..uri = "uri";
static final PlaylistSimple playlistSimple = PlaylistSimple()
..id = "1"
..collaborative = false
..externalUrls = externalUrls
..href = "text"
..images = [image]
..name = "A good playlist"
..owner = user
..public = true
..snapshotId = "text"
..tracksLink = tracksLink
..type = "type"
..description = "A very good playlist description"
..uri = "uri";
static final Category category = Category()
..href = "text"
..icons = [image]
..id = "1"
..name = "category";
static final friends = SpotifyFriends(
friends: [
for (var i = 0; i < 3; i++)
SpotifyFriendActivity(
user: const SpotifyFriend(
name: "name",
imageUrl: "imageUrl",
uri: "uri",
),
track: SpotifyActivityTrack(
name: "name",
artist: const SpotifyActivityArtist(
name: "name",
uri: "uri",
),
album: const SpotifyActivityAlbum(
name: "name",
uri: "uri",
),
context: SpotifyActivityContext(
name: "name",
index: i,
uri: "uri",
),
imageUrl: "imageUrl",
uri: "uri",
),
),
],
); );
static final feedSection = SpotifyHomeFeedSection( static final SpotubeBrowseSectionObject browseSection =
typename: "HomeGenericSectionData", SpotubeBrowseSectionObject(
uri: "spotify:section:lol", id: "section-id",
title: "Dummy", title: "Browse Section",
items: [ browseMore: true,
for (int i = 0; i < 10; i++) externalUri: "https://example.com/browse/section",
SpotifyHomeFeedSectionItem( items: [playlistSimple, playlistSimple, playlistSimple]);
typename: "PlaylistResponseWrapper",
playlist: SpotifySectionPlaylist(
name: "Playlist $i",
description: "Really super important description $i",
format: "daily-mix",
images: [
const SpotifySectionItemImage(
height: 1,
width: 1,
url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
),
],
owner: "Spotify",
uri: "spotify:playlist:id",
),
)
],
);
static const historySummary = PlaybackHistorySummary( static const historySummary = PlaybackHistorySummary(
albums: 1, albums: 1,

View File

@ -1,6 +1,6 @@
// Country Codes contributed by momobobe <https://github.com/momobobe> // Country Codes contributed by momobobe <https://github.com/momobobe>
import 'package:spotify/spotify.dart'; import 'package:spotube/models/metadata/market.dart';
final spotifyMarkets = [ final spotifyMarkets = [
(Market.AL, "Albania (AL)"), (Market.AL, "Albania (AL)"),

View File

@ -1,18 +1,18 @@
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';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/modules/playlist/playlist_create_dialog.dart'; import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/metadata_plugin/user.dart';
class PlaylistAddTrackDialog extends HookConsumerWidget { class PlaylistAddTrackDialog extends HookConsumerWidget {
/// The id of the playlist this dialog was opened from /// The id of the playlist this dialog was opened from
final String? openFromPlaylist; final String? openFromPlaylist;
final List<Track> tracks; final List<SpotubeTrackObject> tracks;
const PlaylistAddTrackDialog({ const PlaylistAddTrackDialog({
required this.tracks, required this.tracks,
required this.openFromPlaylist, required this.openFromPlaylist,
@ -22,24 +22,23 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final typography = Theme.of(context).typography; final typography = Theme.of(context).typography;
final userPlaylists = ref.watch(favoritePlaylistsProvider); final userPlaylists = ref.watch(metadataPluginSavedPlaylistsProvider);
final favoritePlaylistsNotifier = final favoritePlaylistsNotifier =
ref.watch(favoritePlaylistsProvider.notifier); ref.watch(metadataPluginSavedPlaylistsProvider.notifier);
final me = ref.watch(meProvider); final me = ref.watch(metadataPluginUserProvider);
final filteredPlaylists = useMemoized( final filteredPlaylists = useMemoized(
() => () =>
userPlaylists.asData?.value.items userPlaylists.asData?.value.items
.where( .where(
(playlist) => (playlist) =>
playlist.owner?.id != null && playlist.owner.id == me.asData?.value?.id &&
playlist.owner!.id == me.asData?.value.id &&
playlist.id != openFromPlaylist, playlist.id != openFromPlaylist,
) )
.toList() ?? .toList() ??
[], [],
[userPlaylists.asData?.value, me.asData?.value.id, openFromPlaylist], [userPlaylists.asData?.value, me.asData?.value?.id, openFromPlaylist],
); );
final playlistsCheck = useState(<String, bool>{}); final playlistsCheck = useState(<String, bool>{});
@ -60,7 +59,7 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
selectedPlaylists.map( selectedPlaylists.map(
(playlistId) => favoritePlaylistsNotifier.addTracks( (playlistId) => favoritePlaylistsNotifier.addTracks(
playlistId, playlistId,
tracks.map((e) => e.id!).toList(), tracks.map((e) => e.id).toList(),
), ),
), ),
).then((_) => context.mounted ? Navigator.pop(context, true) : null); ).then((_) => context.mounted ? Navigator.pop(context, true) : null);
@ -109,8 +108,7 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
}, },
), ),
leading: Avatar( leading: Avatar(
initials: initials: Avatar.getInitials(playlist.name),
Avatar.getInitials(playlist.name ?? "Playlist"),
provider: UniversalImage.imageProvider( provider: UniversalImage.imageProvider(
playlist.images.asUrlString( playlist.images.asUrlString(
placeholder: ImagePlaceholder.collection, placeholder: ImagePlaceholder.collection,
@ -124,20 +122,20 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
onChanged: (val) { onChanged: (val) {
playlistsCheck.value = { playlistsCheck.value = {
...playlistsCheck.value, ...playlistsCheck.value,
playlist.id!: val == CheckboxState.checked, playlist.id: val == CheckboxState.checked,
}; };
}, },
), ),
onPressed: () { onPressed: () {
playlistsCheck.value = { playlistsCheck.value = {
...playlistsCheck.value, ...playlistsCheck.value,
playlist.id!: playlist.id:
!(playlistsCheck.value[playlist.id] ?? false), !(playlistsCheck.value[playlist.id] ?? false),
}; };
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 8.0), padding: const EdgeInsets.only(left: 8.0),
child: Text(playlist.name!), child: Text(playlist.name),
), ),
); );
}, },

View File

@ -1,13 +1,13 @@
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/metadata/metadata.dart';
final replaceDownloadedFileState = StateProvider<bool?>((ref) => null); final replaceDownloadedFileState = StateProvider<bool?>((ref) => null);
class ReplaceDownloadedDialog extends ConsumerWidget { class ReplaceDownloadedDialog extends ConsumerWidget {
final Track track; final SpotubeTrackObject track;
const ReplaceDownloadedDialog({required this.track, super.key}); const ReplaceDownloadedDialog({required this.track, super.key});
@override @override
@ -16,7 +16,7 @@ class ReplaceDownloadedDialog extends ConsumerWidget {
final replaceAll = ref.watch(replaceDownloadedFileState); final replaceAll = ref.watch(replaceDownloadedFileState);
return AlertDialog( return AlertDialog(
title: Text(context.l10n.track_exists(track.name ?? "")), title: Text(context.l10n.track_exists(track.name)),
content: RadioGroup( content: RadioGroup(
value: groupValue, value: groupValue,
onChanged: (value) { onChanged: (value) {

View File

@ -1,32 +1,34 @@
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/components/links/hyper_link.dart'; import 'package:spotube/components/links/hyper_link.dart';
import 'package:spotube/components/links/link_text.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/duration.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/provider/server/track_sources.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
class TrackDetailsDialog extends HookWidget { class TrackDetailsDialog extends HookConsumerWidget {
final Track track; final SpotubeFullTrackObject track;
const TrackDetailsDialog({ const TrackDetailsDialog({
super.key, super.key,
required this.track, required this.track,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final sourcedTrack =
ref.read(trackSourcesProvider(TrackSourceQuery.fromTrack(track)));
final detailsMap = { final detailsMap = {
context.l10n.title: track.name!, context.l10n.title: track.name,
context.l10n.artist: ArtistLink( context.l10n.artist: ArtistLink(
artists: track.artists ?? <Artist>[], artists: track.artists,
mainAxisAlignment: WrapAlignment.start, mainAxisAlignment: WrapAlignment.start,
textStyle: const TextStyle(color: Colors.blue), textStyle: const TextStyle(color: Colors.blue),
hideOverflowArtist: false, hideOverflowArtist: false,
@ -37,17 +39,15 @@ class TrackDetailsDialog extends HookWidget {
// overflow: TextOverflow.ellipsis, // overflow: TextOverflow.ellipsis,
// style: const TextStyle(color: Colors.blue), // style: const TextStyle(color: Colors.blue),
// ), // ),
context.l10n.duration: (track is SourcedTrack context.l10n.duration: sourcedTrack.asData != null
? (track as SourcedTrack).sourceInfo.duration ? Duration(milliseconds: sourcedTrack.asData!.value.info.durationMs)
: track.duration!) .toHumanReadableString()
.toHumanReadableString(), : Duration(milliseconds: track.durationMs).toHumanReadableString(),
if (track.album!.releaseDate != null) if (track.album.releaseDate != null)
context.l10n.released: track.album!.releaseDate, context.l10n.released: track.album.releaseDate,
context.l10n.popularity: track.popularity?.toString() ?? "0",
}; };
final sourceInfo = final sourceInfo = sourcedTrack.asData?.value.info;
track is SourcedTrack ? (track as SourcedTrack).sourceInfo : null;
final ytTracksDetailsMap = sourceInfo == null final ytTracksDetailsMap = sourceInfo == null
? {} ? {}
@ -58,12 +58,7 @@ class TrackDetailsDialog extends HookWidget {
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
context.l10n.channel: Hyperlink( context.l10n.channel: Text(sourceInfo.artists),
sourceInfo.artist,
sourceInfo.artistUrl,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
context.l10n.streamUrl: Hyperlink( context.l10n.streamUrl: Hyperlink(
(track as SourcedTrack).url, (track as SourcedTrack).url,
(track as SourcedTrack).url, (track as SourcedTrack).url,

View File

@ -1,11 +1,12 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/heart_button/use_track_toggle_like.dart'; import 'package:spotube/components/heart_button/use_track_toggle_like.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
import 'package:spotube/provider/metadata_plugin/user.dart';
class HeartButton extends HookConsumerWidget { class HeartButton extends HookConsumerWidget {
final bool isLiked; final bool isLiked;
@ -63,7 +64,7 @@ class HeartButton extends HookConsumerWidget {
} }
class TrackHeartButton extends HookConsumerWidget { class TrackHeartButton extends HookConsumerWidget {
final Track track; final SpotubeTrackObject track;
const TrackHeartButton({ const TrackHeartButton({
super.key, super.key,
required this.track, required this.track,
@ -71,8 +72,8 @@ class TrackHeartButton extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final savedTracks = ref.watch(likedTracksProvider); final savedTracks = ref.watch(metadataPluginSavedTracksProvider);
final me = ref.watch(meProvider); final me = ref.watch(metadataPluginUserProvider);
final (:isLiked, :toggleTrackLike) = useTrackToggleLike(track, ref); final (:isLiked, :toggleTrackLike) = useTrackToggleLike(track, ref);
if (me.isLoading) { if (me.isLoading) {

View File

@ -1,12 +1,12 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/links/anchor_button.dart'; import 'package:spotube/components/links/anchor_button.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/metadata/metadata.dart';
class ArtistLink extends StatelessWidget { class ArtistLink extends StatelessWidget {
final List<ArtistSimple> artists; final List<SpotubeSimpleArtistObject> artists;
final WrapCrossAlignment crossAxisAlignment; final WrapCrossAlignment crossAxisAlignment;
final WrapAlignment mainAxisAlignment; final WrapAlignment mainAxisAlignment;
final TextStyle textStyle; final TextStyle textStyle;
@ -38,19 +38,16 @@ class ArtistLink extends StatelessWidget {
.entries .entries
.map( .map(
(artist) => Builder(builder: (context) { (artist) => Builder(builder: (context) {
if (artist.value.name == null) {
return Text("Spotify", style: textStyle);
}
return AnchorButton( return AnchorButton(
(artist.key != artists.length - 1) (artist.key != artists.length - 1)
? "${artist.value.name}, " ? "${artist.value.name}, "
: artist.value.name!, : artist.value.name,
onTap: () { onTap: () {
if (onRouteChange != null) { if (onRouteChange != null) {
onRouteChange?.call("/artist/${artist.value.id}"); onRouteChange?.call("/artist/${artist.value.id}");
} else { } else {
context context
.navigateTo(ArtistRoute(artistId: artist.value.id!)); .navigateTo(ArtistRoute(artistId: artist.value.id));
} }
}, },
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,

View File

@ -1,7 +1,6 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart';
import 'package:spotube/components/dialogs/confirm_download_dialog.dart'; import 'package:spotube/components/dialogs/confirm_download_dialog.dart';
@ -10,6 +9,7 @@ import 'package:spotube/components/track_presentation/presentation_props.dart';
import 'package:spotube/components/track_presentation/presentation_state.dart'; import 'package:spotube/components/track_presentation/presentation_state.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/history/history.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
@ -76,9 +76,11 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
Future<void> actionDownloadTracks({ Future<void> actionDownloadTracks({
required BuildContext context, required BuildContext context,
required List<Track> tracks, required List<SpotubeTrackObject> tracks,
required String action, required String action,
}) async { }) async {
final fullTrackObjects =
tracks.whereType<SpotubeFullTrackObject>().toList();
final confirmed = audioSource == AudioSource.piped || final confirmed = audioSource == AudioSource.piped ||
(await showDialog<bool>( (await showDialog<bool>(
context: context, context: context,
@ -88,10 +90,10 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
) ?? ) ??
false); false);
if (confirmed != true) return; if (confirmed != true) return;
downloader.batchAddToQueue(tracks); downloader.batchAddToQueue(fullTrackObjects);
notifier.deselectAllTracks(); notifier.deselectAllTracks();
if (!context.mounted) return; if (!context.mounted) return;
showToastForAction(context, action, tracks.length); showToastForAction(context, action, fullTrackObjects.length);
} }
return AdaptivePopSheetList( return AdaptivePopSheetList(
@ -143,11 +145,12 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
{ {
playlistNotifier.addTracksAtFirst(tracks); playlistNotifier.addTracksAtFirst(tracks);
playlistNotifier.addCollection(options.collectionId); playlistNotifier.addCollection(options.collectionId);
if (options.collection is AlbumSimple) { if (options.collection is SpotubeSimpleAlbumObject) {
historyNotifier.addAlbums([options.collection as AlbumSimple]); historyNotifier.addAlbums(
[options.collection as SpotubeSimpleAlbumObject]);
} else { } else {
historyNotifier historyNotifier.addPlaylists(
.addPlaylists([options.collection as PlaylistSimple]); [options.collection as SpotubeSimplePlaylistObject]);
} }
notifier.deselectAllTracks(); notifier.deselectAllTracks();
if (!context.mounted) return; if (!context.mounted) return;
@ -158,11 +161,12 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
{ {
playlistNotifier.addTracks(tracks); playlistNotifier.addTracks(tracks);
playlistNotifier.addCollection(options.collectionId); playlistNotifier.addCollection(options.collectionId);
if (options.collection is AlbumSimple) { if (options.collection is SpotubeSimpleAlbumObject) {
historyNotifier.addAlbums([options.collection as AlbumSimple]); historyNotifier.addAlbums(
[options.collection as SpotubeSimpleAlbumObject]);
} else { } else {
historyNotifier historyNotifier.addPlaylists(
.addPlaylists([options.collection as PlaylistSimple]); [options.collection as SpotubeSimplePlaylistObject]);
} }
notifier.deselectAllTracks(); notifier.deselectAllTracks();
if (!context.mounted) return; if (!context.mounted) return;

View File

@ -1,14 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart'; import 'package:spotube/models/metadata/metadata.dart';
class PaginationProps { class PaginationProps {
final bool hasNextPage; final bool hasNextPage;
final bool isLoading; final bool isLoading;
final VoidCallback onFetchMore; final VoidCallback onFetchMore;
final Future<void> Function() onRefresh; final Future<void> Function() onRefresh;
final Future<List<Track>> Function() onFetchAll; final Future<List<SpotubeFullTrackObject>> Function() onFetchAll;
const PaginationProps({ const PaginationProps({
required this.hasNextPage, required this.hasNextPage,
@ -46,7 +46,7 @@ class TrackPresentationOptions {
final String? ownerImage; final String? ownerImage;
final String image; final String image;
final String routePath; final String routePath;
final List<Track> tracks; final List<SpotubeFullTrackObject> tracks;
final PaginationProps pagination; final PaginationProps pagination;
final bool isLiked; final bool isLiked;
final String? shareUrl; final String? shareUrl;
@ -67,11 +67,12 @@ class TrackPresentationOptions {
this.shareUrl, this.shareUrl,
this.isLiked = false, this.isLiked = false,
this.onHeart, this.onHeart,
}) : assert(collection is AlbumSimple || collection is PlaylistSimple); }) : assert(collection is SpotubeSimpleAlbumObject ||
collection is SpotubeSimplePlaylistObject);
String get collectionId => collection is AlbumSimple String get collectionId => collection is SpotubeSimpleAlbumObject
? (collection as AlbumSimple).id! ? (collection as SpotubeSimpleAlbumObject).id
: (collection as PlaylistSimple).id!; : (collection as SpotubeSimplePlaylistObject).id;
static TrackPresentationOptions of(BuildContext context) { static TrackPresentationOptions of(BuildContext context) {
return Data.of<TrackPresentationOptions>(context); return Data.of<TrackPresentationOptions>(context);

View File

@ -1,14 +1,16 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/pages/library/user_local_tracks/user_local_tracks.dart'; import 'package:spotube/pages/library/user_local_tracks/user_local_tracks.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
import 'package:spotube/provider/metadata_plugin/tracks/album.dart';
import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
class PresentationState { class PresentationState {
final List<Track> selectedTracks; final List<SpotubeTrackObject> selectedTracks;
final List<Track> presentationTracks; final List<SpotubeTrackObject> presentationTracks;
final SortBy sortBy; final SortBy sortBy;
const PresentationState({ const PresentationState({
@ -18,8 +20,8 @@ class PresentationState {
}); });
PresentationState copyWith({ PresentationState copyWith({
List<Track>? selectedTracks, List<SpotubeTrackObject>? selectedTracks,
List<Track>? presentationTracks, List<SpotubeTrackObject>? presentationTracks,
SortBy? sortBy, SortBy? sortBy,
}) { }) {
return PresentationState( return PresentationState(
@ -34,15 +36,15 @@ class PresentationStateNotifier
extends AutoDisposeFamilyNotifier<PresentationState, Object> { extends AutoDisposeFamilyNotifier<PresentationState, Object> {
@override @override
PresentationState build(collection) { PresentationState build(collection) {
if (arg case PlaylistSimple() || AlbumSimple()) { if (arg case SpotubeSimplePlaylistObject() || SpotubeSimpleAlbumObject()) {
if (isSavedTrackPlaylist) { if (isSavedTrackPlaylist) {
ref.listen( ref.listen(
likedTracksProvider, metadataPluginSavedTracksProvider,
(previous, next) { (previous, next) {
next.whenData((value) { next.whenData((value) {
state = state.copyWith( state = state.copyWith(
presentationTracks: ServiceUtils.sortTracks( presentationTracks: ServiceUtils.sortTracks(
value, value.items,
state.sortBy, state.sortBy,
), ),
); );
@ -51,9 +53,11 @@ class PresentationStateNotifier
); );
} else { } else {
ref.listen( ref.listen(
arg is PlaylistSimple arg is SpotubeSimplePlaylistObject
? playlistTracksProvider((arg as PlaylistSimple).id!) ? metadataPluginPlaylistTracksProvider(
: albumTracksProvider((arg as AlbumSimple)), (arg as SpotubeSimplePlaylistObject).id)
: metadataPluginAlbumTracksProvider(
(arg as SpotubeSimpleAlbumObject).id),
(previous, next) { (previous, next) {
next.whenData((value) { next.whenData((value) {
state = state.copyWith( state = state.copyWith(
@ -76,36 +80,39 @@ class PresentationStateNotifier
} }
bool get isSavedTrackPlaylist => bool get isSavedTrackPlaylist =>
arg is PlaylistSimple && arg is SpotubeSimplePlaylistObject &&
(arg as PlaylistSimple).id == "user-liked-tracks"; (arg as SpotubeSimplePlaylistObject).id == "user-liked-tracks";
List<Track> get tracks { List<SpotubeTrackObject> get tracks {
assert( assert(
arg is PlaylistSimple || arg is AlbumSimple, arg is SpotubeSimplePlaylistObject || arg is SpotubeSimpleAlbumObject,
"arg must be PlaylistSimple or AlbumSimple", "arg must be SpotubeSimplePlaylistObject or SpotubeSimpleAlbumObject",
); );
final isPlaylist = arg is PlaylistSimple; final isPlaylist = arg is SpotubeSimplePlaylistObject;
final tracks = switch ((isPlaylist, isSavedTrackPlaylist)) { final tracks = switch ((isPlaylist, isSavedTrackPlaylist)) {
(true, true) => ref.read(likedTracksProvider).asData?.value, (true, true) =>
ref.read(metadataPluginSavedTracksProvider).asData?.value.items,
(true, false) => ref (true, false) => ref
.read(playlistTracksProvider((arg as PlaylistSimple).id!)) .read(metadataPluginPlaylistTracksProvider(
(arg as SpotubeSimplePlaylistObject).id))
.asData .asData
?.value ?.value
.items, .items,
_ => ref _ => ref
.read(albumTracksProvider((arg as AlbumSimple))) .read(metadataPluginAlbumTracksProvider(
(arg as SpotubeSimpleAlbumObject).id))
.asData .asData
?.value ?.value
.items, .items,
} ?? } ??
[]; <SpotubeFullTrackObject>[];
return tracks; return tracks;
} }
void selectTrack(Track track) { void selectTrack(SpotubeTrackObject track) {
if (state.selectedTracks.any((e) => e.id == track.id)) { if (state.selectedTracks.any((e) => e.id == track.id)) {
return; return;
} }
@ -121,7 +128,7 @@ class PresentationStateNotifier
); );
} }
void deselectTrack(Track track) { void deselectTrack(SpotubeTrackObject track) {
state = state.copyWith( state = state.copyWith(
selectedTracks: state.selectedTracks.where((e) => e != track).toList(), selectedTracks: state.selectedTracks.where((e) => e != track).toList(),
); );
@ -141,7 +148,7 @@ class PresentationStateNotifier
state = state.copyWith( state = state.copyWith(
presentationTracks: ServiceUtils.sortTracks( presentationTracks: ServiceUtils.sortTracks(
tracks tracks
.map((e) => (weightedRatio(e.name!, query), e)) .map((e) => (weightedRatio(e.name, query), e))
.sorted((a, b) => b.$1.compareTo(a.$1)) .sorted((a, b) => b.$1.compareTo(a.$1))
.where((e) => e.$1 > 50) .where((e) => e.$1 > 50)
.map((e) => e.$2) .map((e) => e.$2)

View File

@ -3,8 +3,6 @@ import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/env.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/heart_button/heart_button.dart'; import 'package:spotube/components/heart_button/heart_button.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
@ -14,7 +12,6 @@ import 'package:spotube/components/track_presentation/use_is_user_playlist.dart'
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/modules/playlist/playlist_create_dialog.dart'; import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
import 'package:spotube/provider/spotify/spotify.dart';
class TrackPresentationTopSection extends HookConsumerWidget { class TrackPresentationTopSection extends HookConsumerWidget {
const TrackPresentationTopSection({super.key}); const TrackPresentationTopSection({super.key});
@ -26,25 +23,10 @@ class TrackPresentationTopSection extends HookConsumerWidget {
final scale = context.theme.scaling; final scale = context.theme.scaling;
final isUserPlaylist = useIsUserPlaylist(ref, options.collectionId); final isUserPlaylist = useIsUserPlaylist(ref, options.collectionId);
final playlistImage = (options.collection is PlaylistSimple && final decorationImage = DecorationImage(
(options.collection as PlaylistSimple).owner?.displayName == image: UniversalImage.imageProvider(options.image),
"Spotify" && fit: BoxFit.cover,
Env.disableSpotifyImages) );
? ref.watch(playlistImageProvider(options.collectionId))
: null;
final decorationImage = playlistImage != null
? DecorationImage(
image: AssetImage(playlistImage.src),
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(
playlistImage.color,
playlistImage.colorBlendMode,
),
)
: DecorationImage(
image: UniversalImage.imageProvider(options.image),
fit: BoxFit.cover,
);
final imageDimension = mediaQuery.mdAndUp ? 200 : 120; final imageDimension = mediaQuery.mdAndUp ? 200 : 120;
@ -116,7 +98,7 @@ class TrackPresentationTopSection extends HookConsumerWidget {
builder: (context) { builder: (context) {
return PlaylistCreateDialog( return PlaylistCreateDialog(
playlistId: options.collectionId, playlistId: options.collectionId,
trackIds: options.tracks.map((e) => e.id!).toList(), trackIds: options.tracks.map((e) => e.id).toList(),
); );
}, },
); );

View File

@ -2,11 +2,11 @@ import 'dart:math';
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';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/dialogs/select_device_dialog.dart'; import 'package:spotube/components/dialogs/select_device_dialog.dart';
import 'package:spotube/components/track_presentation/presentation_props.dart'; import 'package:spotube/components/track_presentation/presentation_props.dart';
import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/history/history.dart';
@ -45,14 +45,14 @@ UseActionCallbacks useActionCallbacks(WidgetRef ref) {
final allTracks = await options.pagination.onFetchAll(); final allTracks = await options.pagination.onFetchAll();
final remotePlayback = ref.read(connectProvider.notifier); final remotePlayback = ref.read(connectProvider.notifier);
await remotePlayback.load( await remotePlayback.load(
options.collection is AlbumSimple options.collection is SpotubeSimpleAlbumObject
? WebSocketLoadEventData.album( ? WebSocketLoadEventData.album(
tracks: allTracks, tracks: allTracks,
collection: options.collection as AlbumSimple, collection: options.collection as SpotubeSimpleAlbumObject,
initialIndex: Random().nextInt(allTracks.length)) initialIndex: Random().nextInt(allTracks.length))
: WebSocketLoadEventData.playlist( : WebSocketLoadEventData.playlist(
tracks: allTracks, tracks: allTracks,
collection: options.collection as PlaylistSimple, collection: options.collection as SpotubeSimplePlaylistObject,
initialIndex: Random().nextInt(allTracks.length), initialIndex: Random().nextInt(allTracks.length),
), ),
); );
@ -65,10 +65,12 @@ UseActionCallbacks useActionCallbacks(WidgetRef ref) {
); );
await audioPlayer.setShuffle(true); await audioPlayer.setShuffle(true);
playlistNotifier.addCollection(options.collectionId); playlistNotifier.addCollection(options.collectionId);
if (options.collection is AlbumSimple) { if (options.collection is SpotubeSimpleAlbumObject) {
historyNotifier.addAlbums([options.collection as AlbumSimple]); historyNotifier
.addAlbums([options.collection as SpotubeSimpleAlbumObject]);
} else { } else {
historyNotifier.addPlaylists([options.collection as PlaylistSimple]); historyNotifier.addPlaylists(
[options.collection as SpotubeSimplePlaylistObject]);
} }
final allTracks = await options.pagination.onFetchAll(); final allTracks = await options.pagination.onFetchAll();
@ -96,23 +98,25 @@ UseActionCallbacks useActionCallbacks(WidgetRef ref) {
final allTracks = await options.pagination.onFetchAll(); final allTracks = await options.pagination.onFetchAll();
final remotePlayback = ref.read(connectProvider.notifier); final remotePlayback = ref.read(connectProvider.notifier);
await remotePlayback.load( await remotePlayback.load(
options.collection is AlbumSimple options.collection is SpotubeSimpleAlbumObject
? WebSocketLoadEventData.album( ? WebSocketLoadEventData.album(
tracks: allTracks, tracks: allTracks,
collection: options.collection as AlbumSimple, collection: options.collection as SpotubeSimpleAlbumObject,
) )
: WebSocketLoadEventData.playlist( : WebSocketLoadEventData.playlist(
tracks: allTracks, tracks: allTracks,
collection: options.collection as PlaylistSimple, collection: options.collection as SpotubeSimplePlaylistObject,
), ),
); );
} else { } else {
await playlistNotifier.load(initialTracks, autoPlay: true); await playlistNotifier.load(initialTracks, autoPlay: true);
playlistNotifier.addCollection(options.collectionId); playlistNotifier.addCollection(options.collectionId);
if (options.collection is AlbumSimple) { if (options.collection is SpotubeSimpleAlbumObject) {
historyNotifier.addAlbums([options.collection as AlbumSimple]); historyNotifier
.addAlbums([options.collection as SpotubeSimpleAlbumObject]);
} else { } else {
historyNotifier.addPlaylists([options.collection as PlaylistSimple]); historyNotifier.addPlaylists(
[options.collection as SpotubeSimplePlaylistObject]);
} }
final allTracks = await options.pagination.onFetchAll(); final allTracks = await options.pagination.onFetchAll();

View File

@ -1,17 +1,18 @@
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';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
import 'package:spotube/provider/metadata_plugin/user.dart';
bool useIsUserPlaylist(WidgetRef ref, String playlistId) { bool useIsUserPlaylist(WidgetRef ref, String playlistId) {
final userPlaylistsQuery = ref.watch(favoritePlaylistsProvider); final userPlaylistsQuery = ref.watch(metadataPluginSavedPlaylistsProvider);
final me = ref.watch(meProvider); final me = ref.watch(metadataPluginUserProvider);
return useMemoized( return useMemoized(
() => () =>
userPlaylistsQuery.asData?.value.items.any((e) => userPlaylistsQuery.asData?.value.items.any((e) =>
e.id == playlistId && e.id == playlistId &&
me.asData?.value != null && me.asData?.value != null &&
e.owner?.id == me.asData?.value.id) ?? e.owner.id == me.asData?.value?.id) ??
false, false,
[userPlaylistsQuery.asData?.value, playlistId, me.asData?.value], [userPlaylistsQuery.asData?.value, playlistId, me.asData?.value],
); );

View File

@ -1,18 +1,19 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/dialogs/select_device_dialog.dart'; import 'package:spotube/components/dialogs/select_device_dialog.dart';
import 'package:spotube/components/track_presentation/presentation_props.dart'; import 'package:spotube/components/track_presentation/presentation_props.dart';
import 'package:spotube/components/track_presentation/presentation_state.dart'; import 'package:spotube/components/track_presentation/presentation_state.dart';
import 'package:spotube/extensions/list.dart'; import 'package:spotube/extensions/list.dart';
import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/history/history.dart';
Future<void> Function(Track track, int index) useTrackTilePlayCallback( Future<void> Function(SpotubeTrackObject track, int index)
useTrackTilePlayCallback(
WidgetRef ref, WidgetRef ref,
) { ) {
final context = useContext(); final context = useContext();
@ -26,7 +27,8 @@ Future<void> Function(Track track, int index) useTrackTilePlayCallback(
[playlist.collections, options.collectionId], [playlist.collections, options.collectionId],
); );
final onTapTrackTile = useCallback((Track track, int index) async { final onTapTrackTile =
useCallback((SpotubeTrackObject track, int index) async {
final state = ref.read(presentationStateProvider(options.collection)); final state = ref.read(presentationStateProvider(options.collection));
final notifier = final notifier =
ref.read(presentationStateProvider(options.collection).notifier); ref.read(presentationStateProvider(options.collection).notifier);
@ -52,15 +54,15 @@ Future<void> Function(Track track, int index) useTrackTilePlayCallback(
} else { } else {
final tracks = await options.pagination.onFetchAll(); final tracks = await options.pagination.onFetchAll();
await remotePlayback.load( await remotePlayback.load(
options.collection is AlbumSimple options.collection is SpotubeSimpleAlbumObject
? WebSocketLoadEventData.album( ? WebSocketLoadEventData.album(
tracks: tracks, tracks: tracks,
collection: options.collection as AlbumSimple, collection: options.collection as SpotubeSimpleAlbumObject,
initialIndex: index, initialIndex: index,
) )
: WebSocketLoadEventData.playlist( : WebSocketLoadEventData.playlist(
tracks: tracks, tracks: tracks,
collection: options.collection as PlaylistSimple, collection: options.collection as SpotubeSimplePlaylistObject,
initialIndex: index, initialIndex: index,
), ),
); );
@ -76,10 +78,12 @@ Future<void> Function(Track track, int index) useTrackTilePlayCallback(
autoPlay: true, autoPlay: true,
); );
playlistNotifier.addCollection(options.collectionId); playlistNotifier.addCollection(options.collectionId);
if (options.collection is AlbumSimple) { if (options.collection is SpotubeSimpleAlbumObject) {
historyNotifier.addAlbums([options.collection as AlbumSimple]); historyNotifier
.addAlbums([options.collection as SpotubeSimpleAlbumObject]);
} else { } else {
historyNotifier.addPlaylists([options.collection as PlaylistSimple]); historyNotifier.addPlaylists(
[options.collection as SpotubeSimplePlaylistObject]);
} }
} }
} }

View File

@ -8,7 +8,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotify/spotify.dart' hide Offset;
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
@ -21,15 +20,18 @@ import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.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';
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart';
import 'package:spotube/provider/metadata_plugin/user.dart';
import 'package:spotube/services/metadata/endpoints/error.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@ -50,8 +52,9 @@ enum TrackOptionValue {
startRadio, startRadio,
} }
/// [track] must be a [SpotubeFullTrackObject] or [SpotubeLocalTrackObject]
class TrackOptions extends HookConsumerWidget { class TrackOptions extends HookConsumerWidget {
final Track track; final SpotubeTrackObject track;
final bool userPlaylist; final bool userPlaylist;
final String? playlistId; final String? playlistId;
final ObjectRef<ValueChanged<RelativeRect>?>? showMenuCbRef; final ObjectRef<ValueChanged<RelativeRect>?>? showMenuCbRef;
@ -63,9 +66,12 @@ class TrackOptions extends HookConsumerWidget {
this.userPlaylist = false, this.userPlaylist = false,
this.playlistId, this.playlistId,
this.icon, this.icon,
}); }) : assert(
track is SpotubeFullTrackObject || track is SpotubeLocalTrackObject,
"Track must be a SpotubeFullTrackObject, SpotubeLocalTrackObject",
);
void actionShare(BuildContext context, Track track) { void actionShare(BuildContext context, SpotubeTrackObject track) {
final data = "https://open.spotify.com/track/${track.id}"; final data = "https://open.spotify.com/track/${track.id}";
Clipboard.setData(ClipboardData(text: data)).then((_) { Clipboard.setData(ClipboardData(text: data)).then((_) {
if (context.mounted) { if (context.mounted) {
@ -87,7 +93,7 @@ class TrackOptions extends HookConsumerWidget {
void actionAddToPlaylist( void actionAddToPlaylist(
BuildContext context, BuildContext context,
Track track, SpotubeTrackObject track,
) { ) {
/// showDialog doesn't work for some reason. So we have to /// showDialog doesn't work for some reason. So we have to
/// manually push a Dialog Route in the Navigator to get it working /// manually push a Dialog Route in the Navigator to get it working
@ -105,32 +111,32 @@ class TrackOptions extends HookConsumerWidget {
void actionStartRadio( void actionStartRadio(
BuildContext context, BuildContext context,
WidgetRef ref, WidgetRef ref,
Track track, SpotubeTrackObject track,
) async { ) async {
final playback = ref.read(audioPlayerProvider.notifier); final playback = ref.read(audioPlayerProvider.notifier);
final playlist = ref.read(audioPlayerProvider); final playlist = ref.read(audioPlayerProvider);
final spotify = ref.read(spotifyProvider);
final query = "${track.name} Radio"; final query = "${track.name} Radio";
final pages = await spotify.invoke( final metadataPlugin = await ref.read(metadataPluginProvider.future);
(api) => api.search.get(query, types: [SearchType.playlist]).first(),
);
final radios = pages if (metadataPlugin == null) {
.expand((e) => e.items?.cast<PlaylistSimple>().toList() ?? []) throw MetadataPluginException.noDefaultPlugin(
.toList(); "No default metadata plugin set",
);
}
final artists = track.artists!.map((e) => e.name); final pages = await metadataPlugin.search.playlists(query);
final radio = radios.firstWhere( final artists = track.artists.map((e) => e.name);
final radio = pages.items.firstWhere(
(e) { (e) {
final validPlaylists = final validPlaylists = artists.where((a) => e.description.contains(a));
artists.where((a) => e.description!.contains(a!));
return e.name == "${track.name} Radio" && return e.name == "${track.name} Radio" &&
(validPlaylists.length >= 2 || (validPlaylists.length >= 2 ||
validPlaylists.length == artists.length) && validPlaylists.length == artists.length) &&
e.owner?.displayName == "Spotify"; e.owner.name == "Spotify";
}, },
orElse: () => radios.first, orElse: () => pages.items.first,
); );
bool replaceQueue = false; bool replaceQueue = false;
@ -154,10 +160,10 @@ class TrackOptions extends HookConsumerWidget {
} else { } else {
await playback.addTrack(track); await playback.addTrack(track);
} }
await ref.read(metadataPluginPlaylistTracksProvider(radio.id).future);
final tracks = await spotify.invoke( final tracks = await ref
(api) => api.playlists.getTracksByPlaylistId(radio.id!).all(), .read(metadataPluginPlaylistTracksProvider(radio.id).notifier)
); .fetchAll();
await playback.addTracks( await playback.addTracks(
tracks.toList() tracks.toList()
@ -179,7 +185,7 @@ class TrackOptions extends HookConsumerWidget {
ref.watch(downloadManagerProvider); ref.watch(downloadManagerProvider);
final downloadManager = ref.watch(downloadManagerProvider.notifier); final downloadManager = ref.watch(downloadManagerProvider.notifier);
final blacklist = ref.watch(blacklistProvider); final blacklist = ref.watch(blacklistProvider);
final me = ref.watch(meProvider); final me = ref.watch(metadataPluginUserProvider);
final favorites = useTrackToggleLike(track, ref); final favorites = useTrackToggleLike(track, ref);
@ -192,23 +198,32 @@ class TrackOptions extends HookConsumerWidget {
final removingTrack = useState<String?>(null); final removingTrack = useState<String?>(null);
final favoritePlaylistsNotifier = final favoritePlaylistsNotifier =
ref.watch(favoritePlaylistsProvider.notifier); ref.watch(metadataPluginSavedPlaylistsProvider.notifier);
final isInQueue = useMemoized(() { final isInDownloadQueue = useMemoized(() {
if (playlist.activeTrack == null) return false; if (playlist.activeTrack == null ||
return downloadManager.isActive(playlist.activeTrack!); playlist.activeTrack! is SpotubeLocalTrackObject) {
return false;
}
return downloadManager.isActive(
playlist.activeTrack! as SpotubeFullTrackObject,
);
}, [ }, [
playlist.activeTrack, playlist.activeTrack,
downloadManager, downloadManager,
]); ]);
final progressNotifier = useMemoized(() { final progressNotifier = useMemoized(() {
final spotubeTrack = downloadManager.mapToSourcedTrack(track); if (track is! SpotubeFullTrackObject) {
if (spotubeTrack == null) return null; return throw Exception(
return downloadManager.getProgressNotifier(spotubeTrack); "Invalid usage of `progressNotifierFuture`. Track must be a SpotubeFullTrackObject to get download progress",
);
}
return downloadManager
.getProgressNotifier(track as SpotubeFullTrackObject);
}); });
final isLocalTrack = track is LocalTrack; final isLocalTrack = track is SpotubeLocalTrackObject;
final adaptivePopSheetList = AdaptivePopSheetList<TrackOptionValue>( final adaptivePopSheetList = AdaptivePopSheetList<TrackOptionValue>(
tooltip: context.l10n.more_actions, tooltip: context.l10n.more_actions,
@ -220,7 +235,7 @@ class TrackOptions extends HookConsumerWidget {
// ); // );
break; break;
case TrackOptionValue.delete: case TrackOptionValue.delete:
await File((track as LocalTrack).path).delete(); await File((track as SpotubeLocalTrackObject).path).delete();
ref.invalidate(localTracksProvider); ref.invalidate(localTracksProvider);
break; break;
case TrackOptionValue.addToQueue: case TrackOptionValue.addToQueue:
@ -232,7 +247,7 @@ class TrackOptions extends HookConsumerWidget {
builder: (context, overlay) { builder: (context, overlay) {
return SurfaceCard( return SurfaceCard(
child: Text( child: Text(
context.l10n.added_track_to_queue(track.name!), context.l10n.added_track_to_queue(track.name),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
); );
@ -250,7 +265,7 @@ class TrackOptions extends HookConsumerWidget {
builder: (context, overlay) { builder: (context, overlay) {
return SurfaceCard( return SurfaceCard(
child: Text( child: Text(
context.l10n.track_will_play_next(track.name!), context.l10n.track_will_play_next(track.name),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
); );
@ -259,7 +274,7 @@ class TrackOptions extends HookConsumerWidget {
} }
break; break;
case TrackOptionValue.removeFromQueue: case TrackOptionValue.removeFromQueue:
playback.removeTrack(track.id!); playback.removeTrack(track.id);
if (context.mounted) { if (context.mounted) {
showToast( showToast(
@ -269,7 +284,7 @@ class TrackOptions extends HookConsumerWidget {
return SurfaceCard( return SurfaceCard(
child: Text( child: Text(
context.l10n.removed_track_from_queue( context.l10n.removed_track_from_queue(
track.name!, track.name,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@ -285,19 +300,19 @@ class TrackOptions extends HookConsumerWidget {
actionAddToPlaylist(context, track); actionAddToPlaylist(context, track);
break; break;
case TrackOptionValue.removeFromPlaylist: case TrackOptionValue.removeFromPlaylist:
removingTrack.value = track.uri; removingTrack.value = track.externalUri;
favoritePlaylistsNotifier favoritePlaylistsNotifier
.removeTracks(playlistId ?? "", [track.id!]); .removeTracks(playlistId ?? "", [track.id]);
break; break;
case TrackOptionValue.blacklist: case TrackOptionValue.blacklist:
if (isBlackListed == null) break; if (isBlackListed == null) break;
if (isBlackListed == true) { if (isBlackListed == true) {
await ref.read(blacklistProvider.notifier).remove(track.id!); await ref.read(blacklistProvider.notifier).remove(track.id);
} else { } else {
await ref.read(blacklistProvider.notifier).add( await ref.read(blacklistProvider.notifier).add(
BlacklistTableCompanion.insert( BlacklistTableCompanion.insert(
name: track.name!, name: track.name,
elementId: track.id!, elementId: track.id,
elementType: BlacklistedType.track, elementType: BlacklistedType.track,
), ),
); );
@ -311,16 +326,19 @@ class TrackOptions extends HookConsumerWidget {
await launchUrlString(url); await launchUrlString(url);
break; break;
case TrackOptionValue.details: case TrackOptionValue.details:
if (track is! SpotubeFullTrackObject) break;
showDialog( showDialog(
context: context, context: context,
builder: (context) => ConstrainedBox( builder: (context) => ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400), constraints: const BoxConstraints(maxWidth: 400),
child: TrackDetailsDialog(track: track), child:
TrackDetailsDialog(track: track as SpotubeFullTrackObject),
), ),
); );
break; break;
case TrackOptionValue.download: case TrackOptionValue.download:
await downloadManager.addToQueue(track); if (track is! SpotubeFullTrackObject) break;
await downloadManager.addToQueue(track as SpotubeFullTrackObject);
break; break;
case TrackOptionValue.startRadio: case TrackOptionValue.startRadio:
actionStartRadio(context, ref, track); actionStartRadio(context, ref, track);
@ -336,23 +354,23 @@ class TrackOptions extends HookConsumerWidget {
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
child: UniversalImage( child: UniversalImage(
path: track.album!.images path: track.album.images
.asUrlString(placeholder: ImagePlaceholder.albumArt), .asUrlString(placeholder: ImagePlaceholder.albumArt),
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
), ),
title: Text( title: Text(
track.name!, track.name,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
).semiBold(), ).semiBold(),
subtitle: Align( subtitle: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: ArtistLink( child: ArtistLink(
artists: track.artists!, artists: track.artists,
onOverflowArtistClick: () => context.navigateTo( onOverflowArtistClick: () => context.navigateTo(
TrackRoute(trackId: track.id!), TrackRoute(trackId: track.id),
), ),
), ),
), ),
@ -375,7 +393,7 @@ class TrackOptions extends HookConsumerWidget {
children: [ children: [
Text(context.l10n.go_to_album), Text(context.l10n.go_to_album),
Text( Text(
track.album!.name!, track.album.name,
style: context.theme.typography.xSmall, style: context.theme.typography.xSmall,
), ),
], ],
@ -435,12 +453,12 @@ class TrackOptions extends HookConsumerWidget {
if (!isLocalTrack) if (!isLocalTrack)
AdaptiveMenuButton( AdaptiveMenuButton(
value: TrackOptionValue.download, value: TrackOptionValue.download,
enabled: !isInQueue, enabled: !isInDownloadQueue,
leading: isInQueue leading: isInDownloadQueue
? HookBuilder(builder: (context) { ? HookBuilder(builder: (context) {
final progress = useListenable(progressNotifier!); final progress = useListenable(progressNotifier);
return CircularProgressIndicator( return CircularProgressIndicator(
value: progress.value, value: progress?.value,
); );
}) })
: const Icon(SpotubeIcons.download), : const Icon(SpotubeIcons.download),

View File

@ -7,7 +7,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer; import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/hover_builder.dart'; import 'package:spotube/components/hover_builder.dart';
@ -16,11 +15,9 @@ import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/components/links/link_text.dart'; import 'package:spotube/components/links/link_text.dart';
import 'package:spotube/components/track_tile/track_options.dart'; import 'package:spotube/components/track_tile/track_options.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/duration.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/audio_player/querying_track_info.dart';
import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
@ -29,7 +26,7 @@ import 'package:spotube/utils/platform.dart';
class TrackTile extends HookConsumerWidget { class TrackTile extends HookConsumerWidget {
/// [index] will not be shown if null /// [index] will not be shown if null
final int? index; final int? index;
final Track track; final SpotubeTrackObject track;
final bool selected; final bool selected;
final ValueChanged<bool?>? onChanged; final ValueChanged<bool?>? onChanged;
final Future<void> Function()? onTap; final Future<void> Function()? onTap;
@ -151,7 +148,7 @@ class TrackTile extends HookConsumerWidget {
image: DecorationImage( image: DecorationImage(
fit: BoxFit.cover, fit: BoxFit.cover,
image: UniversalImage.imageProvider( image: UniversalImage.imageProvider(
(track.album?.images).asUrlString( (track.album.images).asUrlString(
placeholder: ImagePlaceholder.albumArt, placeholder: ImagePlaceholder.albumArt,
), ),
), ),
@ -217,8 +214,8 @@ class TrackTile extends HookConsumerWidget {
Expanded( Expanded(
flex: 6, flex: 6,
child: switch (track) { child: switch (track) {
LocalTrack() => Text( SpotubeLocalTrackObject() => Text(
track.name!, track.name,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -233,10 +230,10 @@ class TrackTile extends HookConsumerWidget {
), ),
onPressed: () { onPressed: () {
context context
.navigateTo(TrackRoute(trackId: track.id!)); .navigateTo(TrackRoute(trackId: track.id));
}, },
child: Text( child: Text(
track.name!, track.name,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -251,22 +248,22 @@ class TrackTile extends HookConsumerWidget {
Expanded( Expanded(
flex: 4, flex: 4,
child: switch (track) { child: switch (track) {
LocalTrack() => Text( SpotubeLocalTrackObject() => Text(
track.album!.name!, track.album.name,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
_ => Align( _ => Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
/* child: LinkText( child: LinkText(
track.album!.name!, track.album.name,
AlbumRoute( AlbumRoute(
album: track.album!, album: track.album,
id: track.album!.id!, id: track.album.id,
), ),
push: true, push: true,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), */ ),
) )
}, },
), ),
@ -275,18 +272,18 @@ class TrackTile extends HookConsumerWidget {
), ),
subtitle: Align( subtitle: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: track is LocalTrack child: track is SpotubeLocalTrackObject
? Text( ? Text(
track.artists?.asString() ?? '', track.artists.asString(),
) )
: ClipRect( : ClipRect(
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 40), constraints: const BoxConstraints(maxHeight: 40),
child: ArtistLink( child: ArtistLink(
artists: track.artists ?? [], artists: track.artists,
onOverflowArtistClick: () { onOverflowArtistClick: () {
context.navigateTo( context.navigateTo(
TrackRoute(trackId: track.id!), TrackRoute(trackId: track.id),
); );
}, },
), ),
@ -298,7 +295,7 @@ class TrackTile extends HookConsumerWidget {
children: [ children: [
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
Duration(milliseconds: track.durationMs ?? 0) Duration(milliseconds: track.durationMs)
.toHumanReadableString(padZero: false), .toHumanReadableString(padZero: false),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,

View File

@ -1,21 +0,0 @@
import 'package:spotify/spotify.dart';
extension AlbumExtensions on AlbumSimple {
Album toAlbum() {
Album album = Album();
album.albumType = albumType;
album.artists = artists;
album.availableMarkets = availableMarkets;
album.externalUrls = externalUrls;
album.href = href;
album.id = id;
album.images = images;
album.name = name;
album.releaseDate = releaseDate;
album.releaseDatePrecision = releaseDatePrecision;
album.tracks = tracks;
album.type = type;
album.uri = uri;
return album;
}
}

View File

@ -1,7 +0,0 @@
import 'package:spotify/spotify.dart';
extension ArtistExtension on List<ArtistSimple> {
String asString() {
return map((e) => e.name?.replaceAll(",", " ")).join(", ");
}
}

View File

@ -1,34 +0,0 @@
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/utils/primitive_utils.dart';
import 'package:collection/collection.dart';
enum ImagePlaceholder {
albumArt,
artist,
collection,
online,
}
extension SpotifyImageExtensions on List<Image>? {
String asUrlString({
int index = 1,
required ImagePlaceholder placeholder,
}) {
final String placeholderUrl = {
ImagePlaceholder.albumArt: Assets.albumPlaceholder.path,
ImagePlaceholder.artist: Assets.userPlaceholder.path,
ImagePlaceholder.collection: Assets.placeholder.path,
ImagePlaceholder.online:
"https://avatars.dicebear.com/api/bottts/${PrimitiveUtils.uuid.v4()}.png",
}[placeholder]!;
final sortedImage = this?.sorted((a, b) => a.width!.compareTo(b.width!));
return sortedImage != null && sortedImage.isNotEmpty
? sortedImage[
index > sortedImage.length - 1 ? sortedImage.length - 1 : index]
.url!
: placeholderUrl;
}
}

View File

@ -1,107 +0,0 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:metadata_god/metadata_god.dart';
import 'package:path/path.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart';
extension TrackExtensions on Track {
Track fromFile(
File file, {
Metadata? metadata,
String? art,
}) {
album = Album()
..name = metadata?.album ?? "Unknown"
..images = [if (art != null) Image()..url = art]
..genres = [if (metadata?.genre != null) metadata!.genre!]
..artists = [
Artist()
..name = metadata?.albumArtist ?? "Unknown"
..id = metadata?.albumArtist ?? "Unknown"
..type = "artist",
]
..id = metadata?.album
..releaseDate = metadata?.year?.toString();
artists = [
Artist()
..name = metadata?.artist ?? "Unknown"
..id = metadata?.artist ?? "Unknown"
];
id = metadata?.title ?? basenameWithoutExtension(file.path);
name = metadata?.title ?? basenameWithoutExtension(file.path);
type = "track";
uri = file.path;
durationMs = (metadata?.durationMs?.toInt() ?? 0);
return this;
}
Metadata toMetadata({
required int fileLength,
Uint8List? imageBytes,
}) {
return Metadata(
title: name,
artist: artists?.map((a) => a.name).join(", "),
album: album?.name,
albumArtist: artists?.map((a) => a.name).join(", "),
year: album?.releaseDate != null
? int.tryParse(album!.releaseDate!.split("-").first) ?? 1969
: 1969,
trackNumber: trackNumber,
discNumber: discNumber,
durationMs: durationMs?.toDouble() ?? 0.0,
fileSize: BigInt.from(fileLength),
trackTotal: album?.tracks?.length ?? 0,
picture: imageBytes != null
? Picture(
data: imageBytes,
// Spotify images are always JPEGs
mimeType: 'image/jpeg',
)
: null,
);
}
}
extension IterableTrackSimpleExtensions on Iterable<TrackSimple> {
Future<List<Track>> asTracks(AlbumSimple album, ref) async {
try {
final spotify = ref.read(spotifyProvider);
final tracks = await spotify.invoke((api) =>
api.tracks.list(map((trackSimple) => trackSimple.id!).toList()));
return tracks.toList();
} catch (e, stack) {
// Ignore errors and create the track locally
AppLogger.reportError(e, stack);
List<Track> tracks = [];
for (final trackSimple in this) {
Track track = Track();
track.album = album;
track.name = trackSimple.name;
track.artists = trackSimple.artists;
track.availableMarkets = trackSimple.availableMarkets;
track.discNumber = trackSimple.discNumber;
track.durationMs = trackSimple.durationMs;
track.explicit = trackSimple.explicit;
track.externalUrls = trackSimple.externalUrls;
track.href = trackSimple.href;
track.id = trackSimple.id;
track.isPlayable = trackSimple.isPlayable;
track.linkedFrom = trackSimple.linkedFrom;
track.previewUrl = trackSimple.previewUrl;
track.trackNumber = trackSimple.trackNumber;
track.type = trackSimple.type;
track.uri = trackSimple.uri;
tracks.add(track);
}
return tracks;
}
}
}

View File

@ -5,7 +5,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/routes.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart'; import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
@ -14,93 +13,95 @@ import 'package:spotube/utils/platform.dart';
final appLinks = AppLinks(); final appLinks = AppLinks();
final linkStream = appLinks.stringLinkStream.asBroadcastStream(); final linkStream = appLinks.stringLinkStream.asBroadcastStream();
@Deprecated(
"Deeplinking is deprecated. Later a custom API for metadata provider will be created.")
void useDeepLinking(WidgetRef ref, AppRouter router) { void useDeepLinking(WidgetRef ref, AppRouter router) {
// single instance no worries // // single instance no worries
final spotify = ref.watch(spotifyProvider); // final spotify = ref.watch(spotifyProvider);
useEffect(() { // useEffect(() {
void uriListener(List<SharedFile> files) async { // void uriListener(List<SharedFile> files) async {
for (final file in files) { // for (final file in files) {
if (file.type != SharedMediaType.URL) continue; // if (file.type != SharedMediaType.URL) continue;
final url = Uri.parse(file.value!); // final url = Uri.parse(file.value!);
if (url.pathSegments.length != 2) continue; // if (url.pathSegments.length != 2) continue;
switch (url.pathSegments.first) { // switch (url.pathSegments.first) {
case "album": // case "album":
final album = await spotify.invoke((api) { // final album = await spotify.invoke((api) {
return api.albums.get(url.pathSegments.last); // return api.albums.get(url.pathSegments.last);
}); // });
// router.navigate( // // router.navigate(
// AlbumRoute(id: album.id!, album: album), // // AlbumRoute(id: album.id!, album: album),
// ); // // );
break; // break;
case "artist": // case "artist":
router.navigate(ArtistRoute(artistId: url.pathSegments.last)); // router.navigate(ArtistRoute(artistId: url.pathSegments.last));
break; // break;
case "playlist": // case "playlist":
final playlist = await spotify.invoke((api) { // final playlist = await spotify.invoke((api) {
return api.playlists.get(url.pathSegments.last); // return api.playlists.get(url.pathSegments.last);
}); // });
// router // // router
// .navigate(PlaylistRoute(id: playlist.id!, playlist: playlist)); // // .navigate(PlaylistRoute(id: playlist.id!, playlist: playlist));
break; // break;
case "track": // case "track":
router.navigate(TrackRoute(trackId: url.pathSegments.last)); // router.navigate(TrackRoute(trackId: url.pathSegments.last));
break; // break;
default: // default:
break; // break;
} // }
} // }
} // }
StreamSubscription? mediaStream; // StreamSubscription? mediaStream;
if (kIsMobile) { // if (kIsMobile) {
FlutterSharingIntent.instance.getInitialSharing().then(uriListener); // FlutterSharingIntent.instance.getInitialSharing().then(uriListener);
mediaStream = // mediaStream =
FlutterSharingIntent.instance.getMediaStream().listen(uriListener); // FlutterSharingIntent.instance.getMediaStream().listen(uriListener);
} // }
final subscription = linkStream.listen((uri) async { // final subscription = linkStream.listen((uri) async {
try { // try {
final startSegment = uri.split(":").take(2).join(":"); // final startSegment = uri.split(":").take(2).join(":");
final endSegment = uri.split(":").last; // final endSegment = uri.split(":").last;
switch (startSegment) { // switch (startSegment) {
case "spotify:album": // case "spotify:album":
final album = await spotify.invoke((api) { // final album = await spotify.invoke((api) {
return api.albums.get(endSegment); // return api.albums.get(endSegment);
}); // });
// await router.navigate( // // await router.navigate(
// AlbumRoute(id: album.id!, album: album), // // AlbumRoute(id: album.id!, album: album),
// ); // // );
break; // break;
case "spotify:artist": // case "spotify:artist":
await router.navigate(ArtistRoute(artistId: endSegment)); // await router.navigate(ArtistRoute(artistId: endSegment));
break; // break;
case "spotify:track": // case "spotify:track":
await router.navigate(TrackRoute(trackId: endSegment)); // await router.navigate(TrackRoute(trackId: endSegment));
break; // break;
case "spotify:playlist": // case "spotify:playlist":
final playlist = await spotify.invoke((api) { // final playlist = await spotify.invoke((api) {
return api.playlists.get(endSegment); // return api.playlists.get(endSegment);
}); // });
// await router.navigate( // // await router.navigate(
// PlaylistRoute(id: playlist.id!, playlist: playlist), // // PlaylistRoute(id: playlist.id!, playlist: playlist),
// ); // // );
break; // break;
default: // default:
break; // break;
} // }
} catch (e, stack) { // } catch (e, stack) {
AppLogger.reportError(e, stack); // AppLogger.reportError(e, stack);
} // }
}); // });
return () { // return () {
mediaStream?.cancel(); // mediaStream?.cancel();
subscription.cancel(); // subscription.cancel();
}; // };
}, [spotify]); // }, [spotify]);
} }

View File

@ -2,10 +2,8 @@ import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';

View File

@ -10,9 +10,9 @@ import 'package:media_kit/media_kit.dart' hide Track;
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' show ThemeMode, Colors; import 'package:shadcn_flutter/shadcn_flutter.dart' show ThemeMode, Colors;
import 'package:spotify/spotify.dart' hide Playlist;
import 'package:spotube/models/database/database.steps.dart'; import 'package:spotube/models/database/database.steps.dart';
import 'package:spotube/models/lyrics.dart'; import 'package:spotube/models/lyrics.dart';
import 'package:spotube/models/metadata/market.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/services/kv_store/encrypted_kv_store.dart'; import 'package:spotube/services/kv_store/encrypted_kv_store.dart';
import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart';

View File

@ -3,8 +3,8 @@ import 'package:drift/internal/versioned_schema.dart' as i0;
import 'package:drift/drift.dart' as i1; import 'package:drift/drift.dart' as i1;
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/metadata/market.dart';
import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/enums.dart';
// GENERATED BY drift_dev, DO NOT MODIFY. // GENERATED BY drift_dev, DO NOT MODIFY.

View File

@ -16,10 +16,12 @@ class HistoryTable extends Table {
} }
extension HistoryItemParseExtension on HistoryTableData { extension HistoryItemParseExtension on HistoryTableData {
PlaylistSimple? get playlist => SpotubeSimplePlaylistObject? get playlist => type == HistoryEntryType.playlist
type == HistoryEntryType.playlist ? PlaylistSimple.fromJson(data) : null; ? SpotubeSimplePlaylistObject.fromJson(data)
AlbumSimple? get album => : null;
type == HistoryEntryType.album ? AlbumSimple.fromJson(data) : null; SpotubeSimpleAlbumObject? get album => type == HistoryEntryType.album
Track? get track => ? SpotubeSimpleAlbumObject.fromJson(data)
type == HistoryEntryType.track ? Track.fromJson(data) : null; : null;
SpotubeTrackObject? get track =>
type == HistoryEntryType.track ? SpotubeTrackObject.fromJson(data) : null;
} }

View File

@ -1,44 +0,0 @@
import 'package:spotify/spotify.dart';
class LocalTrack extends Track {
final String path;
LocalTrack.fromTrack({
required Track track,
required this.path,
}) : super() {
album = track.album;
artists = track.artists;
availableMarkets = track.availableMarkets;
discNumber = track.discNumber;
durationMs = track.durationMs;
explicit = track.explicit;
externalIds = track.externalIds;
externalUrls = track.externalUrls;
href = track.href;
id = track.id;
isPlayable = track.isPlayable;
linkedFrom = track.linkedFrom;
name = track.name;
popularity = track.popularity;
previewUrl = track.previewUrl;
trackNumber = track.trackNumber;
type = track.type;
uri = track.uri;
}
factory LocalTrack.fromJson(Map<String, dynamic> json) {
return LocalTrack.fromTrack(
track: Track.fromJson(json),
path: json['path'],
);
}
@override
Map<String, dynamic> toJson() {
return {
...super.toJson(),
'path': path,
};
}
}

View File

@ -0,0 +1,252 @@
enum Market {
AD,
AE,
AF,
AG,
AI,
AL,
AM,
AO,
AQ,
AR,
AS,
AT,
AU,
AW,
AX,
AZ,
BA,
BB,
BD,
BE,
BF,
BG,
BH,
BI,
BJ,
BL,
BM,
BN,
BO,
BQ,
BR,
BS,
BT,
BV,
BW,
BY,
BZ,
CA,
CC,
CD,
CF,
CG,
CH,
CI,
CK,
CL,
CM,
CN,
CO,
CR,
CU,
CV,
CW,
CX,
CY,
CZ,
DE,
DJ,
DK,
DM,
DO,
DZ,
EC,
EE,
EG,
EH,
ER,
ES,
ET,
FI,
FJ,
FK,
FM,
FO,
FR,
GA,
GB,
GD,
GE,
GF,
GG,
GH,
GI,
GL,
GM,
GN,
GP,
GQ,
GR,
GS,
GT,
GU,
GW,
GY,
HK,
HM,
HN,
HR,
HT,
HU,
ID,
IE,
IL,
IM,
IN,
IO,
IQ,
IR,
IS,
IT,
JE,
JM,
JO,
JP,
KE,
KG,
KH,
KI,
KM,
KN,
KP,
KR,
KW,
KY,
KZ,
LA,
LB,
LC,
LI,
LK,
LR,
LS,
LT,
LU,
LV,
LY,
MA,
MC,
MD,
ME,
MF,
MG,
MH,
MK,
ML,
MM,
MN,
MO,
MP,
MQ,
MR,
MS,
MT,
MU,
MV,
MW,
MX,
MY,
MZ,
NA,
NC,
NE,
NF,
NG,
NI,
NL,
NO,
NP,
NR,
NU,
NZ,
OM,
PA,
PE,
PF,
PG,
PH,
PK,
PL,
PM,
PN,
PR,
PS,
PT,
PW,
PY,
QA,
RE,
RO,
RS,
RU,
RW,
SA,
SB,
SC,
SD,
SE,
SG,
SH,
SI,
SJ,
SK,
SL,
SM,
SN,
SO,
SR,
SS,
ST,
SV,
SX,
SY,
SZ,
TC,
TD,
TF,
TG,
TH,
TJ,
TK,
TL,
TM,
TN,
TO,
TR,
TT,
TV,
TW,
TZ,
UA,
UG,
UM,
US,
UY,
UZ,
VA,
VC,
VE,
VG,
VI,
VN,
VU,
WF,
WS,
XK,
YE,
YT,
ZA,
ZM,
ZW,
}

View File

@ -1,11 +1,12 @@
library metadata_objects; library metadata_objects;
import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:media_kit/media_kit.dart';
import 'package:metadata_god/metadata_god.dart'; import 'package:metadata_god/metadata_god.dart';
import 'package:path/path.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';

View File

@ -2571,8 +2571,7 @@ mixin _$SpotubeSearchResponseObject {
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
List<SpotubeSimplePlaylistObject> get playlists => List<SpotubeSimplePlaylistObject> get playlists =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
List<SpotubeSimpleTrackObject> get tracks => List<SpotubeFullTrackObject> get tracks => throw _privateConstructorUsedError;
throw _privateConstructorUsedError;
/// Serializes this SpotubeSearchResponseObject to a JSON map. /// Serializes this SpotubeSearchResponseObject to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@ -2596,7 +2595,7 @@ abstract class $SpotubeSearchResponseObjectCopyWith<$Res> {
{List<SpotubeSimpleAlbumObject> albums, {List<SpotubeSimpleAlbumObject> albums,
List<SpotubeFullArtistObject> artists, List<SpotubeFullArtistObject> artists,
List<SpotubeSimplePlaylistObject> playlists, List<SpotubeSimplePlaylistObject> playlists,
List<SpotubeSimpleTrackObject> tracks}); List<SpotubeFullTrackObject> tracks});
} }
/// @nodoc /// @nodoc
@ -2636,7 +2635,7 @@ class _$SpotubeSearchResponseObjectCopyWithImpl<$Res,
tracks: null == tracks tracks: null == tracks
? _value.tracks ? _value.tracks
: tracks // ignore: cast_nullable_to_non_nullable : tracks // ignore: cast_nullable_to_non_nullable
as List<SpotubeSimpleTrackObject>, as List<SpotubeFullTrackObject>,
) as $Val); ) as $Val);
} }
} }
@ -2654,7 +2653,7 @@ abstract class _$$SpotubeSearchResponseObjectImplCopyWith<$Res>
{List<SpotubeSimpleAlbumObject> albums, {List<SpotubeSimpleAlbumObject> albums,
List<SpotubeFullArtistObject> artists, List<SpotubeFullArtistObject> artists,
List<SpotubeSimplePlaylistObject> playlists, List<SpotubeSimplePlaylistObject> playlists,
List<SpotubeSimpleTrackObject> tracks}); List<SpotubeFullTrackObject> tracks});
} }
/// @nodoc /// @nodoc
@ -2693,7 +2692,7 @@ class __$$SpotubeSearchResponseObjectImplCopyWithImpl<$Res>
tracks: null == tracks tracks: null == tracks
? _value._tracks ? _value._tracks
: tracks // ignore: cast_nullable_to_non_nullable : tracks // ignore: cast_nullable_to_non_nullable
as List<SpotubeSimpleTrackObject>, as List<SpotubeFullTrackObject>,
)); ));
} }
} }
@ -2706,7 +2705,7 @@ class _$SpotubeSearchResponseObjectImpl
{required final List<SpotubeSimpleAlbumObject> albums, {required final List<SpotubeSimpleAlbumObject> albums,
required final List<SpotubeFullArtistObject> artists, required final List<SpotubeFullArtistObject> artists,
required final List<SpotubeSimplePlaylistObject> playlists, required final List<SpotubeSimplePlaylistObject> playlists,
required final List<SpotubeSimpleTrackObject> tracks}) required final List<SpotubeFullTrackObject> tracks})
: _albums = albums, : _albums = albums,
_artists = artists, _artists = artists,
_playlists = playlists, _playlists = playlists,
@ -2740,9 +2739,9 @@ class _$SpotubeSearchResponseObjectImpl
return EqualUnmodifiableListView(_playlists); return EqualUnmodifiableListView(_playlists);
} }
final List<SpotubeSimpleTrackObject> _tracks; final List<SpotubeFullTrackObject> _tracks;
@override @override
List<SpotubeSimpleTrackObject> get tracks { List<SpotubeFullTrackObject> get tracks {
if (_tracks is EqualUnmodifiableListView) return _tracks; if (_tracks is EqualUnmodifiableListView) return _tracks;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_tracks); return EqualUnmodifiableListView(_tracks);
@ -2797,7 +2796,7 @@ abstract class _SpotubeSearchResponseObject
{required final List<SpotubeSimpleAlbumObject> albums, {required final List<SpotubeSimpleAlbumObject> albums,
required final List<SpotubeFullArtistObject> artists, required final List<SpotubeFullArtistObject> artists,
required final List<SpotubeSimplePlaylistObject> playlists, required final List<SpotubeSimplePlaylistObject> playlists,
required final List<SpotubeSimpleTrackObject> tracks}) = required final List<SpotubeFullTrackObject> tracks}) =
_$SpotubeSearchResponseObjectImpl; _$SpotubeSearchResponseObjectImpl;
factory _SpotubeSearchResponseObject.fromJson(Map<String, dynamic> json) = factory _SpotubeSearchResponseObject.fromJson(Map<String, dynamic> json) =
@ -2810,7 +2809,7 @@ abstract class _SpotubeSearchResponseObject
@override @override
List<SpotubeSimplePlaylistObject> get playlists; List<SpotubeSimplePlaylistObject> get playlists;
@override @override
List<SpotubeSimpleTrackObject> get tracks; List<SpotubeFullTrackObject> get tracks;
/// Create a copy of SpotubeSearchResponseObject /// Create a copy of SpotubeSearchResponseObject
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -2826,8 +2825,6 @@ SpotubeTrackObject _$SpotubeTrackObjectFromJson(Map<String, dynamic> json) {
return SpotubeLocalTrackObject.fromJson(json); return SpotubeLocalTrackObject.fromJson(json);
case 'full': case 'full':
return SpotubeFullTrackObject.fromJson(json); return SpotubeFullTrackObject.fromJson(json);
case 'simple':
return SpotubeSimpleTrackObject.fromJson(json);
default: default:
throw CheckedFromJsonException(json, 'runtimeType', 'SpotubeTrackObject', throw CheckedFromJsonException(json, 'runtimeType', 'SpotubeTrackObject',
@ -2842,7 +2839,7 @@ mixin _$SpotubeTrackObject {
String get externalUri => throw _privateConstructorUsedError; String get externalUri => throw _privateConstructorUsedError;
List<SpotubeSimpleArtistObject> get artists => List<SpotubeSimpleArtistObject> get artists =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
SpotubeSimpleAlbumObject? get album => throw _privateConstructorUsedError; SpotubeSimpleAlbumObject get album => throw _privateConstructorUsedError;
int get durationMs => throw _privateConstructorUsedError; int get durationMs => throw _privateConstructorUsedError;
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
@ -2865,15 +2862,6 @@ mixin _$SpotubeTrackObject {
String isrc, String isrc,
bool explicit) bool explicit)
full, full,
required TResult Function(
String id,
String name,
String externalUri,
int durationMs,
bool explicit,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject? album)
simple,
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@optionalTypeArgs @optionalTypeArgs
@ -2897,15 +2885,6 @@ mixin _$SpotubeTrackObject {
String isrc, String isrc,
bool explicit)? bool explicit)?
full, full,
TResult? Function(
String id,
String name,
String externalUri,
int durationMs,
bool explicit,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject? album)?
simple,
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@optionalTypeArgs @optionalTypeArgs
@ -2929,15 +2908,6 @@ mixin _$SpotubeTrackObject {
String isrc, String isrc,
bool explicit)? bool explicit)?
full, full,
TResult Function(
String id,
String name,
String externalUri,
int durationMs,
bool explicit,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject? album)?
simple,
required TResult orElse(), required TResult orElse(),
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@ -2945,21 +2915,18 @@ mixin _$SpotubeTrackObject {
TResult map<TResult extends Object?>({ TResult map<TResult extends Object?>({
required TResult Function(SpotubeLocalTrackObject value) local, required TResult Function(SpotubeLocalTrackObject value) local,
required TResult Function(SpotubeFullTrackObject value) full, required TResult Function(SpotubeFullTrackObject value) full,
required TResult Function(SpotubeSimpleTrackObject value) simple,
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@optionalTypeArgs @optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({ TResult? mapOrNull<TResult extends Object?>({
TResult? Function(SpotubeLocalTrackObject value)? local, TResult? Function(SpotubeLocalTrackObject value)? local,
TResult? Function(SpotubeFullTrackObject value)? full, TResult? Function(SpotubeFullTrackObject value)? full,
TResult? Function(SpotubeSimpleTrackObject value)? simple,
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@optionalTypeArgs @optionalTypeArgs
TResult maybeMap<TResult extends Object?>({ TResult maybeMap<TResult extends Object?>({
TResult Function(SpotubeLocalTrackObject value)? local, TResult Function(SpotubeLocalTrackObject value)? local,
TResult Function(SpotubeFullTrackObject value)? full, TResult Function(SpotubeFullTrackObject value)? full,
TResult Function(SpotubeSimpleTrackObject value)? simple,
required TResult orElse(), required TResult orElse(),
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@ -2988,7 +2955,7 @@ abstract class $SpotubeTrackObjectCopyWith<$Res> {
SpotubeSimpleAlbumObject album, SpotubeSimpleAlbumObject album,
int durationMs}); int durationMs});
$SpotubeSimpleAlbumObjectCopyWith<$Res>? get album; $SpotubeSimpleAlbumObjectCopyWith<$Res> get album;
} }
/// @nodoc /// @nodoc
@ -3031,7 +2998,7 @@ class _$SpotubeTrackObjectCopyWithImpl<$Res, $Val extends SpotubeTrackObject>
: artists // ignore: cast_nullable_to_non_nullable : artists // ignore: cast_nullable_to_non_nullable
as List<SpotubeSimpleArtistObject>, as List<SpotubeSimpleArtistObject>,
album: null == album album: null == album
? _value.album! ? _value.album
: album // ignore: cast_nullable_to_non_nullable : album // ignore: cast_nullable_to_non_nullable
as SpotubeSimpleAlbumObject, as SpotubeSimpleAlbumObject,
durationMs: null == durationMs durationMs: null == durationMs
@ -3045,12 +3012,8 @@ class _$SpotubeTrackObjectCopyWithImpl<$Res, $Val extends SpotubeTrackObject>
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$SpotubeSimpleAlbumObjectCopyWith<$Res>? get album { $SpotubeSimpleAlbumObjectCopyWith<$Res> get album {
if (_value.album == null) { return $SpotubeSimpleAlbumObjectCopyWith<$Res>(_value.album, (value) {
return null;
}
return $SpotubeSimpleAlbumObjectCopyWith<$Res>(_value.album!, (value) {
return _then(_value.copyWith(album: value) as $Val); return _then(_value.copyWith(album: value) as $Val);
}); });
} }
@ -3132,16 +3095,6 @@ class __$$SpotubeLocalTrackObjectImplCopyWithImpl<$Res>
as String, as String,
)); ));
} }
/// Create a copy of SpotubeTrackObject
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SpotubeSimpleAlbumObjectCopyWith<$Res> get album {
return $SpotubeSimpleAlbumObjectCopyWith<$Res>(_value.album, (value) {
return _then(_value.copyWith(album: value));
});
}
} }
/// @nodoc /// @nodoc
@ -3244,15 +3197,6 @@ class _$SpotubeLocalTrackObjectImpl implements SpotubeLocalTrackObject {
String isrc, String isrc,
bool explicit) bool explicit)
full, full,
required TResult Function(
String id,
String name,
String externalUri,
int durationMs,
bool explicit,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject? album)
simple,
}) { }) {
return local(id, name, externalUri, artists, album, durationMs, path); return local(id, name, externalUri, artists, album, durationMs, path);
} }
@ -3279,15 +3223,6 @@ class _$SpotubeLocalTrackObjectImpl implements SpotubeLocalTrackObject {
String isrc, String isrc,
bool explicit)? bool explicit)?
full, full,
TResult? Function(
String id,
String name,
String externalUri,
int durationMs,
bool explicit,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject? album)?
simple,
}) { }) {
return local?.call(id, name, externalUri, artists, album, durationMs, path); return local?.call(id, name, externalUri, artists, album, durationMs, path);
} }
@ -3314,15 +3249,6 @@ class _$SpotubeLocalTrackObjectImpl implements SpotubeLocalTrackObject {
String isrc, String isrc,
bool explicit)? bool explicit)?
full, full,
TResult Function(
String id,
String name,
String externalUri,
int durationMs,
bool explicit,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject? album)?
simple,
required TResult orElse(), required TResult orElse(),
}) { }) {
if (local != null) { if (local != null) {
@ -3336,7 +3262,6 @@ class _$SpotubeLocalTrackObjectImpl implements SpotubeLocalTrackObject {
TResult map<TResult extends Object?>({ TResult map<TResult extends Object?>({
required TResult Function(SpotubeLocalTrackObject value) local, required TResult Function(SpotubeLocalTrackObject value) local,
required TResult Function(SpotubeFullTrackObject value) full, required TResult Function(SpotubeFullTrackObject value) full,
required TResult Function(SpotubeSimpleTrackObject value) simple,
}) { }) {
return local(this); return local(this);
} }
@ -3346,7 +3271,6 @@ class _$SpotubeLocalTrackObjectImpl implements SpotubeLocalTrackObject {
TResult? mapOrNull<TResult extends Object?>({ TResult? mapOrNull<TResult extends Object?>({
TResult? Function(SpotubeLocalTrackObject value)? local, TResult? Function(SpotubeLocalTrackObject value)? local,
TResult? Function(SpotubeFullTrackObject value)? full, TResult? Function(SpotubeFullTrackObject value)? full,
TResult? Function(SpotubeSimpleTrackObject value)? simple,
}) { }) {
return local?.call(this); return local?.call(this);
} }
@ -3356,7 +3280,6 @@ class _$SpotubeLocalTrackObjectImpl implements SpotubeLocalTrackObject {
TResult maybeMap<TResult extends Object?>({ TResult maybeMap<TResult extends Object?>({
TResult Function(SpotubeLocalTrackObject value)? local, TResult Function(SpotubeLocalTrackObject value)? local,
TResult Function(SpotubeFullTrackObject value)? full, TResult Function(SpotubeFullTrackObject value)? full,
TResult Function(SpotubeSimpleTrackObject value)? simple,
required TResult orElse(), required TResult orElse(),
}) { }) {
if (local != null) { if (local != null) {
@ -3489,16 +3412,6 @@ class __$$SpotubeFullTrackObjectImplCopyWithImpl<$Res>
as bool, as bool,
)); ));
} }
/// Create a copy of SpotubeTrackObject
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SpotubeSimpleAlbumObjectCopyWith<$Res> get album {
return $SpotubeSimpleAlbumObjectCopyWith<$Res>(_value.album, (value) {
return _then(_value.copyWith(album: value));
});
}
} }
/// @nodoc /// @nodoc
@ -3614,15 +3527,6 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject {
String isrc, String isrc,
bool explicit) bool explicit)
full, full,
required TResult Function(
String id,
String name,
String externalUri,
int durationMs,
bool explicit,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject? album)
simple,
}) { }) {
return full( return full(
id, name, externalUri, artists, album, durationMs, isrc, explicit); id, name, externalUri, artists, album, durationMs, isrc, explicit);
@ -3650,15 +3554,6 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject {
String isrc, String isrc,
bool explicit)? bool explicit)?
full, full,
TResult? Function(
String id,
String name,
String externalUri,
int durationMs,
bool explicit,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject? album)?
simple,
}) { }) {
return full?.call( return full?.call(
id, name, externalUri, artists, album, durationMs, isrc, explicit); id, name, externalUri, artists, album, durationMs, isrc, explicit);
@ -3686,15 +3581,6 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject {
String isrc, String isrc,
bool explicit)? bool explicit)?
full, full,
TResult Function(
String id,
String name,
String externalUri,
int durationMs,
bool explicit,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject? album)?
simple,
required TResult orElse(), required TResult orElse(),
}) { }) {
if (full != null) { if (full != null) {
@ -3709,7 +3595,6 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject {
TResult map<TResult extends Object?>({ TResult map<TResult extends Object?>({
required TResult Function(SpotubeLocalTrackObject value) local, required TResult Function(SpotubeLocalTrackObject value) local,
required TResult Function(SpotubeFullTrackObject value) full, required TResult Function(SpotubeFullTrackObject value) full,
required TResult Function(SpotubeSimpleTrackObject value) simple,
}) { }) {
return full(this); return full(this);
} }
@ -3719,7 +3604,6 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject {
TResult? mapOrNull<TResult extends Object?>({ TResult? mapOrNull<TResult extends Object?>({
TResult? Function(SpotubeLocalTrackObject value)? local, TResult? Function(SpotubeLocalTrackObject value)? local,
TResult? Function(SpotubeFullTrackObject value)? full, TResult? Function(SpotubeFullTrackObject value)? full,
TResult? Function(SpotubeSimpleTrackObject value)? simple,
}) { }) {
return full?.call(this); return full?.call(this);
} }
@ -3729,7 +3613,6 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject {
TResult maybeMap<TResult extends Object?>({ TResult maybeMap<TResult extends Object?>({
TResult Function(SpotubeLocalTrackObject value)? local, TResult Function(SpotubeLocalTrackObject value)? local,
TResult Function(SpotubeFullTrackObject value)? full, TResult Function(SpotubeFullTrackObject value)? full,
TResult Function(SpotubeSimpleTrackObject value)? simple,
required TResult orElse(), required TResult orElse(),
}) { }) {
if (full != null) { if (full != null) {
@ -3783,358 +3666,6 @@ abstract class SpotubeFullTrackObject implements SpotubeTrackObject {
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
} }
/// @nodoc
abstract class _$$SpotubeSimpleTrackObjectImplCopyWith<$Res>
implements $SpotubeTrackObjectCopyWith<$Res> {
factory _$$SpotubeSimpleTrackObjectImplCopyWith(
_$SpotubeSimpleTrackObjectImpl value,
$Res Function(_$SpotubeSimpleTrackObjectImpl) then) =
__$$SpotubeSimpleTrackObjectImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String id,
String name,
String externalUri,
int durationMs,
bool explicit,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject? album});
@override
$SpotubeSimpleAlbumObjectCopyWith<$Res>? get album;
}
/// @nodoc
class __$$SpotubeSimpleTrackObjectImplCopyWithImpl<$Res>
extends _$SpotubeTrackObjectCopyWithImpl<$Res,
_$SpotubeSimpleTrackObjectImpl>
implements _$$SpotubeSimpleTrackObjectImplCopyWith<$Res> {
__$$SpotubeSimpleTrackObjectImplCopyWithImpl(
_$SpotubeSimpleTrackObjectImpl _value,
$Res Function(_$SpotubeSimpleTrackObjectImpl) _then)
: super(_value, _then);
/// Create a copy of SpotubeTrackObject
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? externalUri = null,
Object? durationMs = null,
Object? explicit = null,
Object? artists = null,
Object? album = freezed,
}) {
return _then(_$SpotubeSimpleTrackObjectImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
externalUri: null == externalUri
? _value.externalUri
: externalUri // ignore: cast_nullable_to_non_nullable
as String,
durationMs: null == durationMs
? _value.durationMs
: durationMs // ignore: cast_nullable_to_non_nullable
as int,
explicit: null == explicit
? _value.explicit
: explicit // ignore: cast_nullable_to_non_nullable
as bool,
artists: null == artists
? _value._artists
: artists // ignore: cast_nullable_to_non_nullable
as List<SpotubeSimpleArtistObject>,
album: freezed == album
? _value.album
: album // ignore: cast_nullable_to_non_nullable
as SpotubeSimpleAlbumObject?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$SpotubeSimpleTrackObjectImpl implements SpotubeSimpleTrackObject {
_$SpotubeSimpleTrackObjectImpl(
{required this.id,
required this.name,
required this.externalUri,
required this.durationMs,
required this.explicit,
final List<SpotubeSimpleArtistObject> artists = const [],
this.album,
final String? $type})
: _artists = artists,
$type = $type ?? 'simple';
factory _$SpotubeSimpleTrackObjectImpl.fromJson(Map<String, dynamic> json) =>
_$$SpotubeSimpleTrackObjectImplFromJson(json);
@override
final String id;
@override
final String name;
@override
final String externalUri;
@override
final int durationMs;
@override
final bool explicit;
final List<SpotubeSimpleArtistObject> _artists;
@override
@JsonKey()
List<SpotubeSimpleArtistObject> get artists {
if (_artists is EqualUnmodifiableListView) return _artists;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_artists);
}
@override
final SpotubeSimpleAlbumObject? album;
@JsonKey(name: 'runtimeType')
final String $type;
@override
String toString() {
return 'SpotubeTrackObject.simple(id: $id, name: $name, externalUri: $externalUri, durationMs: $durationMs, explicit: $explicit, artists: $artists, album: $album)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SpotubeSimpleTrackObjectImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.externalUri, externalUri) ||
other.externalUri == externalUri) &&
(identical(other.durationMs, durationMs) ||
other.durationMs == durationMs) &&
(identical(other.explicit, explicit) ||
other.explicit == explicit) &&
const DeepCollectionEquality().equals(other._artists, _artists) &&
(identical(other.album, album) || other.album == album));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
name,
externalUri,
durationMs,
explicit,
const DeepCollectionEquality().hash(_artists),
album);
/// Create a copy of SpotubeTrackObject
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$SpotubeSimpleTrackObjectImplCopyWith<_$SpotubeSimpleTrackObjectImpl>
get copyWith => __$$SpotubeSimpleTrackObjectImplCopyWithImpl<
_$SpotubeSimpleTrackObjectImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(
String id,
String name,
String externalUri,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject album,
int durationMs,
String path)
local,
required TResult Function(
String id,
String name,
String externalUri,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject album,
int durationMs,
String isrc,
bool explicit)
full,
required TResult Function(
String id,
String name,
String externalUri,
int durationMs,
bool explicit,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject? album)
simple,
}) {
return simple(id, name, externalUri, durationMs, explicit, artists, album);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(
String id,
String name,
String externalUri,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject album,
int durationMs,
String path)?
local,
TResult? Function(
String id,
String name,
String externalUri,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject album,
int durationMs,
String isrc,
bool explicit)?
full,
TResult? Function(
String id,
String name,
String externalUri,
int durationMs,
bool explicit,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject? album)?
simple,
}) {
return simple?.call(
id, name, externalUri, durationMs, explicit, artists, album);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(
String id,
String name,
String externalUri,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject album,
int durationMs,
String path)?
local,
TResult Function(
String id,
String name,
String externalUri,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject album,
int durationMs,
String isrc,
bool explicit)?
full,
TResult Function(
String id,
String name,
String externalUri,
int durationMs,
bool explicit,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject? album)?
simple,
required TResult orElse(),
}) {
if (simple != null) {
return simple(
id, name, externalUri, durationMs, explicit, artists, album);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(SpotubeLocalTrackObject value) local,
required TResult Function(SpotubeFullTrackObject value) full,
required TResult Function(SpotubeSimpleTrackObject value) simple,
}) {
return simple(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(SpotubeLocalTrackObject value)? local,
TResult? Function(SpotubeFullTrackObject value)? full,
TResult? Function(SpotubeSimpleTrackObject value)? simple,
}) {
return simple?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(SpotubeLocalTrackObject value)? local,
TResult Function(SpotubeFullTrackObject value)? full,
TResult Function(SpotubeSimpleTrackObject value)? simple,
required TResult orElse(),
}) {
if (simple != null) {
return simple(this);
}
return orElse();
}
@override
Map<String, dynamic> toJson() {
return _$$SpotubeSimpleTrackObjectImplToJson(
this,
);
}
}
abstract class SpotubeSimpleTrackObject implements SpotubeTrackObject {
factory SpotubeSimpleTrackObject(
{required final String id,
required final String name,
required final String externalUri,
required final int durationMs,
required final bool explicit,
final List<SpotubeSimpleArtistObject> artists,
final SpotubeSimpleAlbumObject? album}) = _$SpotubeSimpleTrackObjectImpl;
factory SpotubeSimpleTrackObject.fromJson(Map<String, dynamic> json) =
_$SpotubeSimpleTrackObjectImpl.fromJson;
@override
String get id;
@override
String get name;
@override
String get externalUri;
@override
int get durationMs;
bool get explicit;
@override
List<SpotubeSimpleArtistObject> get artists;
@override
SpotubeSimpleAlbumObject? get album;
/// Create a copy of SpotubeTrackObject
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SpotubeSimpleTrackObjectImplCopyWith<_$SpotubeSimpleTrackObjectImpl>
get copyWith => throw _privateConstructorUsedError;
}
SpotubeUserObject _$SpotubeUserObjectFromJson(Map<String, dynamic> json) { SpotubeUserObject _$SpotubeUserObjectFromJson(Map<String, dynamic> json) {
return _SpotubeUserObject.fromJson(json); return _SpotubeUserObject.fromJson(json);
} }

View File

@ -273,7 +273,7 @@ _$SpotubeSearchResponseObjectImpl _$$SpotubeSearchResponseObjectImplFromJson(
Map<String, dynamic>.from(e as Map))) Map<String, dynamic>.from(e as Map)))
.toList(), .toList(),
tracks: (json['tracks'] as List<dynamic>) tracks: (json['tracks'] as List<dynamic>)
.map((e) => SpotubeSimpleTrackObject.fromJson( .map((e) => SpotubeFullTrackObject.fromJson(
Map<String, dynamic>.from(e as Map))) Map<String, dynamic>.from(e as Map)))
.toList(), .toList(),
); );
@ -350,39 +350,6 @@ Map<String, dynamic> _$$SpotubeFullTrackObjectImplToJson(
'runtimeType': instance.$type, 'runtimeType': instance.$type,
}; };
_$SpotubeSimpleTrackObjectImpl _$$SpotubeSimpleTrackObjectImplFromJson(
Map json) =>
_$SpotubeSimpleTrackObjectImpl(
id: json['id'] as String,
name: json['name'] as String,
externalUri: json['externalUri'] as String,
durationMs: (json['durationMs'] as num).toInt(),
explicit: json['explicit'] as bool,
artists: (json['artists'] as List<dynamic>?)
?.map((e) => SpotubeSimpleArtistObject.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList() ??
const [],
album: json['album'] == null
? null
: SpotubeSimpleAlbumObject.fromJson(
Map<String, dynamic>.from(json['album'] as Map)),
$type: json['runtimeType'] as String?,
);
Map<String, dynamic> _$$SpotubeSimpleTrackObjectImplToJson(
_$SpotubeSimpleTrackObjectImpl instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'externalUri': instance.externalUri,
'durationMs': instance.durationMs,
'explicit': instance.explicit,
'artists': instance.artists.map((e) => e.toJson()).toList(),
'album': instance.album?.toJson(),
'runtimeType': instance.$type,
};
_$SpotubeUserObjectImpl _$$SpotubeUserObjectImplFromJson(Map json) => _$SpotubeUserObjectImpl _$$SpotubeUserObjectImplFromJson(Map json) =>
_$SpotubeUserObjectImpl( _$SpotubeUserObjectImpl(
id: json['id'] as String, id: json['id'] as String,

View File

@ -6,7 +6,7 @@ class SpotubeSearchResponseObject with _$SpotubeSearchResponseObject {
required List<SpotubeSimpleAlbumObject> albums, required List<SpotubeSimpleAlbumObject> albums,
required List<SpotubeFullArtistObject> artists, required List<SpotubeFullArtistObject> artists,
required List<SpotubeSimplePlaylistObject> playlists, required List<SpotubeSimplePlaylistObject> playlists,
required List<SpotubeSimpleTrackObject> tracks, required List<SpotubeFullTrackObject> tracks,
}) = _SpotubeSearchResponseObject; }) = _SpotubeSearchResponseObject;
factory SpotubeSearchResponseObject.fromJson(Map<String, dynamic> json) => factory SpotubeSearchResponseObject.fromJson(Map<String, dynamic> json) =>

View File

@ -23,23 +23,54 @@ class SpotubeTrackObject with _$SpotubeTrackObject {
required bool explicit, required bool explicit,
}) = SpotubeFullTrackObject; }) = SpotubeFullTrackObject;
factory SpotubeTrackObject.simple({ factory SpotubeTrackObject.localTrackFromFile(
required String id, File file, {
required String name, Metadata? metadata,
required String externalUri, String? art,
required int durationMs, }) {
required bool explicit, return SpotubeLocalTrackObject(
@Default([]) List<SpotubeSimpleArtistObject> artists, id: file.absolute.path,
SpotubeSimpleAlbumObject? album, name: metadata?.title ?? basenameWithoutExtension(file.path),
}) = SpotubeSimpleTrackObject; externalUri: "file://${file.absolute.path}",
artists: metadata?.artist?.split(",").map((a) {
return SpotubeSimpleArtistObject(
id: a.trim(),
name: a.trim(),
externalUri: "file://${file.absolute.path}",
);
}).toList() ??
[
SpotubeSimpleArtistObject(
id: "unknown",
name: "Unknown Artist",
externalUri: "file://${file.absolute.path}",
),
],
album: SpotubeSimpleAlbumObject(
albumType: SpotubeAlbumType.album,
id: metadata?.album ?? "unknown",
name: metadata?.album ?? "Unknown Album",
externalUri: "file://${file.absolute.path}",
artists: [
SpotubeSimpleArtistObject(
id: metadata?.albumArtist ?? "unknown",
name: metadata?.albumArtist ?? "Unknown Artist",
externalUri: "file://${file.absolute.path}",
),
],
releaseDate:
metadata?.year != null ? "${metadata!.year}-01-01" : "1970-01-01",
),
durationMs: metadata?.durationMs?.toInt() ?? 0,
path: file.path,
);
}
factory SpotubeTrackObject.fromJson(Map<String, dynamic> json) => factory SpotubeTrackObject.fromJson(Map<String, dynamic> json) =>
_$SpotubeTrackObjectFromJson( _$SpotubeTrackObjectFromJson(
json.containsKey("isrc") json.containsKey("path")
? {...json, "runtimeType": "full"} ? {...json, "runtimeType": "local"}
: json.containsKey("path") : {...json, "runtimeType": "full"},
? {...json, "runtimeType": "local"}
: {...json, "runtimeType": "simple"},
); );
} }

View File

@ -1,247 +0,0 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:spotify/spotify.dart';
part 'home_feed.freezed.dart';
part 'home_feed.g.dart';
@freezed
class SpotifySectionPlaylist with _$SpotifySectionPlaylist {
const SpotifySectionPlaylist._();
const factory SpotifySectionPlaylist({
required String description,
required String format,
required List<SpotifySectionItemImage> images,
required String name,
required String owner,
required String uri,
}) = _SpotifySectionPlaylist;
factory SpotifySectionPlaylist.fromJson(Map<String, dynamic> json) =>
_$SpotifySectionPlaylistFromJson(json);
String get id => uri.split(":").last;
Playlist get asPlaylist {
return Playlist()
..id = id
..name = name
..description = description
..collaborative = false
..images = images.map((e) => e.asImage).toList()
..owner = (User()..displayName = owner)
..uri = uri
..type = "playlist";
}
}
@freezed
class SpotifySectionArtist with _$SpotifySectionArtist {
const SpotifySectionArtist._();
const factory SpotifySectionArtist({
required String name,
required String uri,
required List<SpotifySectionItemImage> images,
}) = _SpotifySectionArtist;
factory SpotifySectionArtist.fromJson(Map<String, dynamic> json) =>
_$SpotifySectionArtistFromJson(json);
String get id => uri.split(":").last;
Artist get asArtist {
return Artist()
..id = id
..name = name
..images = images.map((e) => e.asImage).toList()
..type = "artist"
..uri = uri;
}
}
@freezed
class SpotifySectionAlbum with _$SpotifySectionAlbum {
const SpotifySectionAlbum._();
const factory SpotifySectionAlbum({
required List<SpotifySectionAlbumArtist> artists,
required List<SpotifySectionItemImage> images,
required String name,
required String uri,
}) = _SpotifySectionAlbum;
factory SpotifySectionAlbum.fromJson(Map<String, dynamic> json) =>
_$SpotifySectionAlbumFromJson(json);
String get id => uri.split(":").last;
Album get asAlbum {
return Album()
..id = id
..name = name
..artists = artists.map((a) => a.asArtist).toList()
..albumType = AlbumType.album
..images = images.map((e) => e.asImage).toList()
..uri = uri;
}
}
@freezed
class SpotifySectionAlbumArtist with _$SpotifySectionAlbumArtist {
const SpotifySectionAlbumArtist._();
const factory SpotifySectionAlbumArtist({
required String name,
required String uri,
}) = _SpotifySectionAlbumArtist;
factory SpotifySectionAlbumArtist.fromJson(Map<String, dynamic> json) =>
_$SpotifySectionAlbumArtistFromJson(json);
String get id => uri.split(":").last;
Artist get asArtist {
return Artist()
..id = id
..name = name
..type = "artist"
..uri = uri;
}
}
@freezed
class SpotifySectionItemImage with _$SpotifySectionItemImage {
const SpotifySectionItemImage._();
const factory SpotifySectionItemImage({
required num? height,
required String url,
required num? width,
}) = _SpotifySectionItemImage;
factory SpotifySectionItemImage.fromJson(Map<String, dynamic> json) =>
_$SpotifySectionItemImageFromJson(json);
Image get asImage {
return Image()
..height = height?.toInt()
..width = width?.toInt()
..url = url;
}
}
@freezed
class SpotifyHomeFeedSectionItem with _$SpotifyHomeFeedSectionItem {
factory SpotifyHomeFeedSectionItem({
required String typename,
SpotifySectionPlaylist? playlist,
SpotifySectionArtist? artist,
SpotifySectionAlbum? album,
}) = _SpotifyHomeFeedSectionItem;
factory SpotifyHomeFeedSectionItem.fromJson(Map<String, dynamic> json) =>
_$SpotifyHomeFeedSectionItemFromJson(json);
}
@freezed
class SpotifyHomeFeedSection with _$SpotifyHomeFeedSection {
factory SpotifyHomeFeedSection({
required String typename,
String? title,
required String uri,
required List<SpotifyHomeFeedSectionItem> items,
}) = _SpotifyHomeFeedSection;
factory SpotifyHomeFeedSection.fromJson(Map<String, dynamic> json) =>
_$SpotifyHomeFeedSectionFromJson(json);
}
@freezed
class SpotifyHomeFeed with _$SpotifyHomeFeed {
factory SpotifyHomeFeed({
required String greeting,
required List<SpotifyHomeFeedSection> sections,
}) = _SpotifyHomeFeed;
factory SpotifyHomeFeed.fromJson(Map<String, dynamic> json) =>
_$SpotifyHomeFeedFromJson(json);
}
Map<String, dynamic> transformSectionItemTypeJsonMap(
Map<String, dynamic> json) {
final data = json["content"]["data"];
final objType = json["content"]["data"]["__typename"];
return {
"typename": json["content"]["__typename"],
if (objType == "Playlist")
"playlist": {
"name": data["name"],
"description": data["description"],
"format": data["format"],
"images": (data["images"]["items"] as List)
.expand((j) => j["sources"] as dynamic)
.toList()
.cast<Map<String, dynamic>>(),
"owner": data["ownerV2"]["data"]["name"],
"uri": data["uri"]
},
if (objType == "Artist")
"artist": {
"name": data["profile"]["name"],
"uri": data["uri"],
"images": data["visuals"]["avatarImage"]["sources"],
},
if (objType == "Album")
"album": {
"name": data["name"],
"uri": data["uri"],
"images": data["coverArt"]["sources"],
"artists": data["artists"]["items"]
.map(
(artist) => {
"name": artist["profile"]["name"],
"uri": artist["uri"],
},
)
.toList()
},
};
}
Map<String, dynamic> transformSectionItemJsonMap(Map<String, dynamic> json) {
return {
"typename": json["data"]["__typename"],
"title": json["data"]?["title"]?["text"],
"uri": json["uri"],
"items": (json["sectionItems"]["items"] as List)
.map(
(data) =>
transformSectionItemTypeJsonMap(data as Map<String, dynamic>)
as dynamic,
)
.where(
(w) =>
w["playlist"] != null ||
w["artist"] != null ||
w["album"] != null,
)
.toList()
.cast<Map<String, dynamic>>()
};
}
Map<String, dynamic> transformHomeFeedJsonMap(Map<String, dynamic> json) {
return {
"greeting": json["data"]["home"]["greeting"]["text"],
"sections":
(json["data"]["home"]["sectionContainer"]["sections"]["items"] as List)
.map(
(item) =>
transformSectionItemJsonMap(item as Map<String, dynamic>)
as dynamic,
)
.toList()
.cast<Map<String, dynamic>>()
};
}

File diff suppressed because it is too large Load Diff

View File

@ -1,165 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'home_feed.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$SpotifySectionPlaylistImpl _$$SpotifySectionPlaylistImplFromJson(Map json) =>
_$SpotifySectionPlaylistImpl(
description: json['description'] as String,
format: json['format'] as String,
images: (json['images'] as List<dynamic>)
.map((e) => SpotifySectionItemImage.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(),
name: json['name'] as String,
owner: json['owner'] as String,
uri: json['uri'] as String,
);
Map<String, dynamic> _$$SpotifySectionPlaylistImplToJson(
_$SpotifySectionPlaylistImpl instance) =>
<String, dynamic>{
'description': instance.description,
'format': instance.format,
'images': instance.images.map((e) => e.toJson()).toList(),
'name': instance.name,
'owner': instance.owner,
'uri': instance.uri,
};
_$SpotifySectionArtistImpl _$$SpotifySectionArtistImplFromJson(Map json) =>
_$SpotifySectionArtistImpl(
name: json['name'] as String,
uri: json['uri'] as String,
images: (json['images'] as List<dynamic>)
.map((e) => SpotifySectionItemImage.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(),
);
Map<String, dynamic> _$$SpotifySectionArtistImplToJson(
_$SpotifySectionArtistImpl instance) =>
<String, dynamic>{
'name': instance.name,
'uri': instance.uri,
'images': instance.images.map((e) => e.toJson()).toList(),
};
_$SpotifySectionAlbumImpl _$$SpotifySectionAlbumImplFromJson(Map json) =>
_$SpotifySectionAlbumImpl(
artists: (json['artists'] as List<dynamic>)
.map((e) => SpotifySectionAlbumArtist.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(),
images: (json['images'] as List<dynamic>)
.map((e) => SpotifySectionItemImage.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(),
name: json['name'] as String,
uri: json['uri'] as String,
);
Map<String, dynamic> _$$SpotifySectionAlbumImplToJson(
_$SpotifySectionAlbumImpl instance) =>
<String, dynamic>{
'artists': instance.artists.map((e) => e.toJson()).toList(),
'images': instance.images.map((e) => e.toJson()).toList(),
'name': instance.name,
'uri': instance.uri,
};
_$SpotifySectionAlbumArtistImpl _$$SpotifySectionAlbumArtistImplFromJson(
Map json) =>
_$SpotifySectionAlbumArtistImpl(
name: json['name'] as String,
uri: json['uri'] as String,
);
Map<String, dynamic> _$$SpotifySectionAlbumArtistImplToJson(
_$SpotifySectionAlbumArtistImpl instance) =>
<String, dynamic>{
'name': instance.name,
'uri': instance.uri,
};
_$SpotifySectionItemImageImpl _$$SpotifySectionItemImageImplFromJson(
Map json) =>
_$SpotifySectionItemImageImpl(
height: json['height'] as num?,
url: json['url'] as String,
width: json['width'] as num?,
);
Map<String, dynamic> _$$SpotifySectionItemImageImplToJson(
_$SpotifySectionItemImageImpl instance) =>
<String, dynamic>{
'height': instance.height,
'url': instance.url,
'width': instance.width,
};
_$SpotifyHomeFeedSectionItemImpl _$$SpotifyHomeFeedSectionItemImplFromJson(
Map json) =>
_$SpotifyHomeFeedSectionItemImpl(
typename: json['typename'] as String,
playlist: json['playlist'] == null
? null
: SpotifySectionPlaylist.fromJson(
Map<String, dynamic>.from(json['playlist'] as Map)),
artist: json['artist'] == null
? null
: SpotifySectionArtist.fromJson(
Map<String, dynamic>.from(json['artist'] as Map)),
album: json['album'] == null
? null
: SpotifySectionAlbum.fromJson(
Map<String, dynamic>.from(json['album'] as Map)),
);
Map<String, dynamic> _$$SpotifyHomeFeedSectionItemImplToJson(
_$SpotifyHomeFeedSectionItemImpl instance) =>
<String, dynamic>{
'typename': instance.typename,
'playlist': instance.playlist?.toJson(),
'artist': instance.artist?.toJson(),
'album': instance.album?.toJson(),
};
_$SpotifyHomeFeedSectionImpl _$$SpotifyHomeFeedSectionImplFromJson(Map json) =>
_$SpotifyHomeFeedSectionImpl(
typename: json['typename'] as String,
title: json['title'] as String?,
uri: json['uri'] as String,
items: (json['items'] as List<dynamic>)
.map((e) => SpotifyHomeFeedSectionItem.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(),
);
Map<String, dynamic> _$$SpotifyHomeFeedSectionImplToJson(
_$SpotifyHomeFeedSectionImpl instance) =>
<String, dynamic>{
'typename': instance.typename,
'title': instance.title,
'uri': instance.uri,
'items': instance.items.map((e) => e.toJson()).toList(),
};
_$SpotifyHomeFeedImpl _$$SpotifyHomeFeedImplFromJson(Map json) =>
_$SpotifyHomeFeedImpl(
greeting: json['greeting'] as String,
sections: (json['sections'] as List<dynamic>)
.map((e) => SpotifyHomeFeedSection.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(),
);
Map<String, dynamic> _$$SpotifyHomeFeedImplToJson(
_$SpotifyHomeFeedImpl instance) =>
<String, dynamic>{
'greeting': instance.greeting,
'sections': instance.sections.map((e) => e.toJson()).toList(),
};

View File

@ -1,40 +0,0 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'recommendation_seeds.freezed.dart';
part 'recommendation_seeds.g.dart';
@freezed
class GeneratePlaylistProviderInput with _$GeneratePlaylistProviderInput {
factory GeneratePlaylistProviderInput({
Iterable<String>? seedArtists,
Iterable<String>? seedGenres,
Iterable<String>? seedTracks,
required int limit,
RecommendationSeeds? max,
RecommendationSeeds? min,
RecommendationSeeds? target,
}) = _GeneratePlaylistProviderInput;
}
@freezed
class RecommendationSeeds with _$RecommendationSeeds {
factory RecommendationSeeds({
num? acousticness,
num? danceability,
@JsonKey(name: "duration_ms") num? durationMs,
num? energy,
num? instrumentalness,
num? key,
num? liveness,
num? loudness,
num? mode,
num? popularity,
num? speechiness,
num? tempo,
@JsonKey(name: "time_signature") num? timeSignature,
num? valence,
}) = _RecommendationSeeds;
factory RecommendationSeeds.fromJson(Map<String, dynamic> json) =>
_$RecommendationSeedsFromJson(json);
}

View File

@ -1,786 +0,0 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'recommendation_seeds.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$GeneratePlaylistProviderInput {
Iterable<String>? get seedArtists => throw _privateConstructorUsedError;
Iterable<String>? get seedGenres => throw _privateConstructorUsedError;
Iterable<String>? get seedTracks => throw _privateConstructorUsedError;
int get limit => throw _privateConstructorUsedError;
RecommendationSeeds? get max => throw _privateConstructorUsedError;
RecommendationSeeds? get min => throw _privateConstructorUsedError;
RecommendationSeeds? get target => throw _privateConstructorUsedError;
/// Create a copy of GeneratePlaylistProviderInput
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$GeneratePlaylistProviderInputCopyWith<GeneratePlaylistProviderInput>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $GeneratePlaylistProviderInputCopyWith<$Res> {
factory $GeneratePlaylistProviderInputCopyWith(
GeneratePlaylistProviderInput value,
$Res Function(GeneratePlaylistProviderInput) then) =
_$GeneratePlaylistProviderInputCopyWithImpl<$Res,
GeneratePlaylistProviderInput>;
@useResult
$Res call(
{Iterable<String>? seedArtists,
Iterable<String>? seedGenres,
Iterable<String>? seedTracks,
int limit,
RecommendationSeeds? max,
RecommendationSeeds? min,
RecommendationSeeds? target});
$RecommendationSeedsCopyWith<$Res>? get max;
$RecommendationSeedsCopyWith<$Res>? get min;
$RecommendationSeedsCopyWith<$Res>? get target;
}
/// @nodoc
class _$GeneratePlaylistProviderInputCopyWithImpl<$Res,
$Val extends GeneratePlaylistProviderInput>
implements $GeneratePlaylistProviderInputCopyWith<$Res> {
_$GeneratePlaylistProviderInputCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of GeneratePlaylistProviderInput
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? seedArtists = freezed,
Object? seedGenres = freezed,
Object? seedTracks = freezed,
Object? limit = null,
Object? max = freezed,
Object? min = freezed,
Object? target = freezed,
}) {
return _then(_value.copyWith(
seedArtists: freezed == seedArtists
? _value.seedArtists
: seedArtists // ignore: cast_nullable_to_non_nullable
as Iterable<String>?,
seedGenres: freezed == seedGenres
? _value.seedGenres
: seedGenres // ignore: cast_nullable_to_non_nullable
as Iterable<String>?,
seedTracks: freezed == seedTracks
? _value.seedTracks
: seedTracks // ignore: cast_nullable_to_non_nullable
as Iterable<String>?,
limit: null == limit
? _value.limit
: limit // ignore: cast_nullable_to_non_nullable
as int,
max: freezed == max
? _value.max
: max // ignore: cast_nullable_to_non_nullable
as RecommendationSeeds?,
min: freezed == min
? _value.min
: min // ignore: cast_nullable_to_non_nullable
as RecommendationSeeds?,
target: freezed == target
? _value.target
: target // ignore: cast_nullable_to_non_nullable
as RecommendationSeeds?,
) as $Val);
}
/// Create a copy of GeneratePlaylistProviderInput
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$RecommendationSeedsCopyWith<$Res>? get max {
if (_value.max == null) {
return null;
}
return $RecommendationSeedsCopyWith<$Res>(_value.max!, (value) {
return _then(_value.copyWith(max: value) as $Val);
});
}
/// Create a copy of GeneratePlaylistProviderInput
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$RecommendationSeedsCopyWith<$Res>? get min {
if (_value.min == null) {
return null;
}
return $RecommendationSeedsCopyWith<$Res>(_value.min!, (value) {
return _then(_value.copyWith(min: value) as $Val);
});
}
/// Create a copy of GeneratePlaylistProviderInput
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$RecommendationSeedsCopyWith<$Res>? get target {
if (_value.target == null) {
return null;
}
return $RecommendationSeedsCopyWith<$Res>(_value.target!, (value) {
return _then(_value.copyWith(target: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$GeneratePlaylistProviderInputImplCopyWith<$Res>
implements $GeneratePlaylistProviderInputCopyWith<$Res> {
factory _$$GeneratePlaylistProviderInputImplCopyWith(
_$GeneratePlaylistProviderInputImpl value,
$Res Function(_$GeneratePlaylistProviderInputImpl) then) =
__$$GeneratePlaylistProviderInputImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{Iterable<String>? seedArtists,
Iterable<String>? seedGenres,
Iterable<String>? seedTracks,
int limit,
RecommendationSeeds? max,
RecommendationSeeds? min,
RecommendationSeeds? target});
@override
$RecommendationSeedsCopyWith<$Res>? get max;
@override
$RecommendationSeedsCopyWith<$Res>? get min;
@override
$RecommendationSeedsCopyWith<$Res>? get target;
}
/// @nodoc
class __$$GeneratePlaylistProviderInputImplCopyWithImpl<$Res>
extends _$GeneratePlaylistProviderInputCopyWithImpl<$Res,
_$GeneratePlaylistProviderInputImpl>
implements _$$GeneratePlaylistProviderInputImplCopyWith<$Res> {
__$$GeneratePlaylistProviderInputImplCopyWithImpl(
_$GeneratePlaylistProviderInputImpl _value,
$Res Function(_$GeneratePlaylistProviderInputImpl) _then)
: super(_value, _then);
/// Create a copy of GeneratePlaylistProviderInput
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? seedArtists = freezed,
Object? seedGenres = freezed,
Object? seedTracks = freezed,
Object? limit = null,
Object? max = freezed,
Object? min = freezed,
Object? target = freezed,
}) {
return _then(_$GeneratePlaylistProviderInputImpl(
seedArtists: freezed == seedArtists
? _value.seedArtists
: seedArtists // ignore: cast_nullable_to_non_nullable
as Iterable<String>?,
seedGenres: freezed == seedGenres
? _value.seedGenres
: seedGenres // ignore: cast_nullable_to_non_nullable
as Iterable<String>?,
seedTracks: freezed == seedTracks
? _value.seedTracks
: seedTracks // ignore: cast_nullable_to_non_nullable
as Iterable<String>?,
limit: null == limit
? _value.limit
: limit // ignore: cast_nullable_to_non_nullable
as int,
max: freezed == max
? _value.max
: max // ignore: cast_nullable_to_non_nullable
as RecommendationSeeds?,
min: freezed == min
? _value.min
: min // ignore: cast_nullable_to_non_nullable
as RecommendationSeeds?,
target: freezed == target
? _value.target
: target // ignore: cast_nullable_to_non_nullable
as RecommendationSeeds?,
));
}
}
/// @nodoc
class _$GeneratePlaylistProviderInputImpl
implements _GeneratePlaylistProviderInput {
_$GeneratePlaylistProviderInputImpl(
{this.seedArtists,
this.seedGenres,
this.seedTracks,
required this.limit,
this.max,
this.min,
this.target});
@override
final Iterable<String>? seedArtists;
@override
final Iterable<String>? seedGenres;
@override
final Iterable<String>? seedTracks;
@override
final int limit;
@override
final RecommendationSeeds? max;
@override
final RecommendationSeeds? min;
@override
final RecommendationSeeds? target;
@override
String toString() {
return 'GeneratePlaylistProviderInput(seedArtists: $seedArtists, seedGenres: $seedGenres, seedTracks: $seedTracks, limit: $limit, max: $max, min: $min, target: $target)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$GeneratePlaylistProviderInputImpl &&
const DeepCollectionEquality()
.equals(other.seedArtists, seedArtists) &&
const DeepCollectionEquality()
.equals(other.seedGenres, seedGenres) &&
const DeepCollectionEquality()
.equals(other.seedTracks, seedTracks) &&
(identical(other.limit, limit) || other.limit == limit) &&
(identical(other.max, max) || other.max == max) &&
(identical(other.min, min) || other.min == min) &&
(identical(other.target, target) || other.target == target));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(seedArtists),
const DeepCollectionEquality().hash(seedGenres),
const DeepCollectionEquality().hash(seedTracks),
limit,
max,
min,
target);
/// Create a copy of GeneratePlaylistProviderInput
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$GeneratePlaylistProviderInputImplCopyWith<
_$GeneratePlaylistProviderInputImpl>
get copyWith => __$$GeneratePlaylistProviderInputImplCopyWithImpl<
_$GeneratePlaylistProviderInputImpl>(this, _$identity);
}
abstract class _GeneratePlaylistProviderInput
implements GeneratePlaylistProviderInput {
factory _GeneratePlaylistProviderInput(
{final Iterable<String>? seedArtists,
final Iterable<String>? seedGenres,
final Iterable<String>? seedTracks,
required final int limit,
final RecommendationSeeds? max,
final RecommendationSeeds? min,
final RecommendationSeeds? target}) = _$GeneratePlaylistProviderInputImpl;
@override
Iterable<String>? get seedArtists;
@override
Iterable<String>? get seedGenres;
@override
Iterable<String>? get seedTracks;
@override
int get limit;
@override
RecommendationSeeds? get max;
@override
RecommendationSeeds? get min;
@override
RecommendationSeeds? get target;
/// Create a copy of GeneratePlaylistProviderInput
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$GeneratePlaylistProviderInputImplCopyWith<
_$GeneratePlaylistProviderInputImpl>
get copyWith => throw _privateConstructorUsedError;
}
RecommendationSeeds _$RecommendationSeedsFromJson(Map<String, dynamic> json) {
return _RecommendationSeeds.fromJson(json);
}
/// @nodoc
mixin _$RecommendationSeeds {
num? get acousticness => throw _privateConstructorUsedError;
num? get danceability => throw _privateConstructorUsedError;
@JsonKey(name: "duration_ms")
num? get durationMs => throw _privateConstructorUsedError;
num? get energy => throw _privateConstructorUsedError;
num? get instrumentalness => throw _privateConstructorUsedError;
num? get key => throw _privateConstructorUsedError;
num? get liveness => throw _privateConstructorUsedError;
num? get loudness => throw _privateConstructorUsedError;
num? get mode => throw _privateConstructorUsedError;
num? get popularity => throw _privateConstructorUsedError;
num? get speechiness => throw _privateConstructorUsedError;
num? get tempo => throw _privateConstructorUsedError;
@JsonKey(name: "time_signature")
num? get timeSignature => throw _privateConstructorUsedError;
num? get valence => throw _privateConstructorUsedError;
/// Serializes this RecommendationSeeds to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of RecommendationSeeds
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$RecommendationSeedsCopyWith<RecommendationSeeds> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $RecommendationSeedsCopyWith<$Res> {
factory $RecommendationSeedsCopyWith(
RecommendationSeeds value, $Res Function(RecommendationSeeds) then) =
_$RecommendationSeedsCopyWithImpl<$Res, RecommendationSeeds>;
@useResult
$Res call(
{num? acousticness,
num? danceability,
@JsonKey(name: "duration_ms") num? durationMs,
num? energy,
num? instrumentalness,
num? key,
num? liveness,
num? loudness,
num? mode,
num? popularity,
num? speechiness,
num? tempo,
@JsonKey(name: "time_signature") num? timeSignature,
num? valence});
}
/// @nodoc
class _$RecommendationSeedsCopyWithImpl<$Res, $Val extends RecommendationSeeds>
implements $RecommendationSeedsCopyWith<$Res> {
_$RecommendationSeedsCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of RecommendationSeeds
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? acousticness = freezed,
Object? danceability = freezed,
Object? durationMs = freezed,
Object? energy = freezed,
Object? instrumentalness = freezed,
Object? key = freezed,
Object? liveness = freezed,
Object? loudness = freezed,
Object? mode = freezed,
Object? popularity = freezed,
Object? speechiness = freezed,
Object? tempo = freezed,
Object? timeSignature = freezed,
Object? valence = freezed,
}) {
return _then(_value.copyWith(
acousticness: freezed == acousticness
? _value.acousticness
: acousticness // ignore: cast_nullable_to_non_nullable
as num?,
danceability: freezed == danceability
? _value.danceability
: danceability // ignore: cast_nullable_to_non_nullable
as num?,
durationMs: freezed == durationMs
? _value.durationMs
: durationMs // ignore: cast_nullable_to_non_nullable
as num?,
energy: freezed == energy
? _value.energy
: energy // ignore: cast_nullable_to_non_nullable
as num?,
instrumentalness: freezed == instrumentalness
? _value.instrumentalness
: instrumentalness // ignore: cast_nullable_to_non_nullable
as num?,
key: freezed == key
? _value.key
: key // ignore: cast_nullable_to_non_nullable
as num?,
liveness: freezed == liveness
? _value.liveness
: liveness // ignore: cast_nullable_to_non_nullable
as num?,
loudness: freezed == loudness
? _value.loudness
: loudness // ignore: cast_nullable_to_non_nullable
as num?,
mode: freezed == mode
? _value.mode
: mode // ignore: cast_nullable_to_non_nullable
as num?,
popularity: freezed == popularity
? _value.popularity
: popularity // ignore: cast_nullable_to_non_nullable
as num?,
speechiness: freezed == speechiness
? _value.speechiness
: speechiness // ignore: cast_nullable_to_non_nullable
as num?,
tempo: freezed == tempo
? _value.tempo
: tempo // ignore: cast_nullable_to_non_nullable
as num?,
timeSignature: freezed == timeSignature
? _value.timeSignature
: timeSignature // ignore: cast_nullable_to_non_nullable
as num?,
valence: freezed == valence
? _value.valence
: valence // ignore: cast_nullable_to_non_nullable
as num?,
) as $Val);
}
}
/// @nodoc
abstract class _$$RecommendationSeedsImplCopyWith<$Res>
implements $RecommendationSeedsCopyWith<$Res> {
factory _$$RecommendationSeedsImplCopyWith(_$RecommendationSeedsImpl value,
$Res Function(_$RecommendationSeedsImpl) then) =
__$$RecommendationSeedsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{num? acousticness,
num? danceability,
@JsonKey(name: "duration_ms") num? durationMs,
num? energy,
num? instrumentalness,
num? key,
num? liveness,
num? loudness,
num? mode,
num? popularity,
num? speechiness,
num? tempo,
@JsonKey(name: "time_signature") num? timeSignature,
num? valence});
}
/// @nodoc
class __$$RecommendationSeedsImplCopyWithImpl<$Res>
extends _$RecommendationSeedsCopyWithImpl<$Res, _$RecommendationSeedsImpl>
implements _$$RecommendationSeedsImplCopyWith<$Res> {
__$$RecommendationSeedsImplCopyWithImpl(_$RecommendationSeedsImpl _value,
$Res Function(_$RecommendationSeedsImpl) _then)
: super(_value, _then);
/// Create a copy of RecommendationSeeds
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? acousticness = freezed,
Object? danceability = freezed,
Object? durationMs = freezed,
Object? energy = freezed,
Object? instrumentalness = freezed,
Object? key = freezed,
Object? liveness = freezed,
Object? loudness = freezed,
Object? mode = freezed,
Object? popularity = freezed,
Object? speechiness = freezed,
Object? tempo = freezed,
Object? timeSignature = freezed,
Object? valence = freezed,
}) {
return _then(_$RecommendationSeedsImpl(
acousticness: freezed == acousticness
? _value.acousticness
: acousticness // ignore: cast_nullable_to_non_nullable
as num?,
danceability: freezed == danceability
? _value.danceability
: danceability // ignore: cast_nullable_to_non_nullable
as num?,
durationMs: freezed == durationMs
? _value.durationMs
: durationMs // ignore: cast_nullable_to_non_nullable
as num?,
energy: freezed == energy
? _value.energy
: energy // ignore: cast_nullable_to_non_nullable
as num?,
instrumentalness: freezed == instrumentalness
? _value.instrumentalness
: instrumentalness // ignore: cast_nullable_to_non_nullable
as num?,
key: freezed == key
? _value.key
: key // ignore: cast_nullable_to_non_nullable
as num?,
liveness: freezed == liveness
? _value.liveness
: liveness // ignore: cast_nullable_to_non_nullable
as num?,
loudness: freezed == loudness
? _value.loudness
: loudness // ignore: cast_nullable_to_non_nullable
as num?,
mode: freezed == mode
? _value.mode
: mode // ignore: cast_nullable_to_non_nullable
as num?,
popularity: freezed == popularity
? _value.popularity
: popularity // ignore: cast_nullable_to_non_nullable
as num?,
speechiness: freezed == speechiness
? _value.speechiness
: speechiness // ignore: cast_nullable_to_non_nullable
as num?,
tempo: freezed == tempo
? _value.tempo
: tempo // ignore: cast_nullable_to_non_nullable
as num?,
timeSignature: freezed == timeSignature
? _value.timeSignature
: timeSignature // ignore: cast_nullable_to_non_nullable
as num?,
valence: freezed == valence
? _value.valence
: valence // ignore: cast_nullable_to_non_nullable
as num?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$RecommendationSeedsImpl implements _RecommendationSeeds {
_$RecommendationSeedsImpl(
{this.acousticness,
this.danceability,
@JsonKey(name: "duration_ms") this.durationMs,
this.energy,
this.instrumentalness,
this.key,
this.liveness,
this.loudness,
this.mode,
this.popularity,
this.speechiness,
this.tempo,
@JsonKey(name: "time_signature") this.timeSignature,
this.valence});
factory _$RecommendationSeedsImpl.fromJson(Map<String, dynamic> json) =>
_$$RecommendationSeedsImplFromJson(json);
@override
final num? acousticness;
@override
final num? danceability;
@override
@JsonKey(name: "duration_ms")
final num? durationMs;
@override
final num? energy;
@override
final num? instrumentalness;
@override
final num? key;
@override
final num? liveness;
@override
final num? loudness;
@override
final num? mode;
@override
final num? popularity;
@override
final num? speechiness;
@override
final num? tempo;
@override
@JsonKey(name: "time_signature")
final num? timeSignature;
@override
final num? valence;
@override
String toString() {
return 'RecommendationSeeds(acousticness: $acousticness, danceability: $danceability, durationMs: $durationMs, energy: $energy, instrumentalness: $instrumentalness, key: $key, liveness: $liveness, loudness: $loudness, mode: $mode, popularity: $popularity, speechiness: $speechiness, tempo: $tempo, timeSignature: $timeSignature, valence: $valence)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$RecommendationSeedsImpl &&
(identical(other.acousticness, acousticness) ||
other.acousticness == acousticness) &&
(identical(other.danceability, danceability) ||
other.danceability == danceability) &&
(identical(other.durationMs, durationMs) ||
other.durationMs == durationMs) &&
(identical(other.energy, energy) || other.energy == energy) &&
(identical(other.instrumentalness, instrumentalness) ||
other.instrumentalness == instrumentalness) &&
(identical(other.key, key) || other.key == key) &&
(identical(other.liveness, liveness) ||
other.liveness == liveness) &&
(identical(other.loudness, loudness) ||
other.loudness == loudness) &&
(identical(other.mode, mode) || other.mode == mode) &&
(identical(other.popularity, popularity) ||
other.popularity == popularity) &&
(identical(other.speechiness, speechiness) ||
other.speechiness == speechiness) &&
(identical(other.tempo, tempo) || other.tempo == tempo) &&
(identical(other.timeSignature, timeSignature) ||
other.timeSignature == timeSignature) &&
(identical(other.valence, valence) || other.valence == valence));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
acousticness,
danceability,
durationMs,
energy,
instrumentalness,
key,
liveness,
loudness,
mode,
popularity,
speechiness,
tempo,
timeSignature,
valence);
/// Create a copy of RecommendationSeeds
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$RecommendationSeedsImplCopyWith<_$RecommendationSeedsImpl> get copyWith =>
__$$RecommendationSeedsImplCopyWithImpl<_$RecommendationSeedsImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$RecommendationSeedsImplToJson(
this,
);
}
}
abstract class _RecommendationSeeds implements RecommendationSeeds {
factory _RecommendationSeeds(
{final num? acousticness,
final num? danceability,
@JsonKey(name: "duration_ms") final num? durationMs,
final num? energy,
final num? instrumentalness,
final num? key,
final num? liveness,
final num? loudness,
final num? mode,
final num? popularity,
final num? speechiness,
final num? tempo,
@JsonKey(name: "time_signature") final num? timeSignature,
final num? valence}) = _$RecommendationSeedsImpl;
factory _RecommendationSeeds.fromJson(Map<String, dynamic> json) =
_$RecommendationSeedsImpl.fromJson;
@override
num? get acousticness;
@override
num? get danceability;
@override
@JsonKey(name: "duration_ms")
num? get durationMs;
@override
num? get energy;
@override
num? get instrumentalness;
@override
num? get key;
@override
num? get liveness;
@override
num? get loudness;
@override
num? get mode;
@override
num? get popularity;
@override
num? get speechiness;
@override
num? get tempo;
@override
@JsonKey(name: "time_signature")
num? get timeSignature;
@override
num? get valence;
/// Create a copy of RecommendationSeeds
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$RecommendationSeedsImplCopyWith<_$RecommendationSeedsImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -1,44 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'recommendation_seeds.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$RecommendationSeedsImpl _$$RecommendationSeedsImplFromJson(Map json) =>
_$RecommendationSeedsImpl(
acousticness: json['acousticness'] as num?,
danceability: json['danceability'] as num?,
durationMs: json['duration_ms'] as num?,
energy: json['energy'] as num?,
instrumentalness: json['instrumentalness'] as num?,
key: json['key'] as num?,
liveness: json['liveness'] as num?,
loudness: json['loudness'] as num?,
mode: json['mode'] as num?,
popularity: json['popularity'] as num?,
speechiness: json['speechiness'] as num?,
tempo: json['tempo'] as num?,
timeSignature: json['time_signature'] as num?,
valence: json['valence'] as num?,
);
Map<String, dynamic> _$$RecommendationSeedsImplToJson(
_$RecommendationSeedsImpl instance) =>
<String, dynamic>{
'acousticness': instance.acousticness,
'danceability': instance.danceability,
'duration_ms': instance.durationMs,
'energy': instance.energy,
'instrumentalness': instance.instrumentalness,
'key': instance.key,
'liveness': instance.liveness,
'loudness': instance.loudness,
'mode': instance.mode,
'popularity': instance.popularity,
'speechiness': instance.speechiness,
'tempo': instance.tempo,
'time_signature': instance.timeSignature,
'valence': instance.valence,
};

View File

@ -1,111 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
part 'spotify_friends.g.dart';
@JsonSerializable(createToJson: false)
class SpotifyFriend {
final String uri;
final String name;
final String imageUrl;
const SpotifyFriend({
required this.uri,
required this.name,
required this.imageUrl,
});
factory SpotifyFriend.fromJson(Map<String, dynamic> json) =>
_$SpotifyFriendFromJson(json);
String get id => uri.split(":").last;
}
@JsonSerializable(createToJson: false)
class SpotifyActivityArtist {
final String uri;
final String name;
const SpotifyActivityArtist({required this.uri, required this.name});
factory SpotifyActivityArtist.fromJson(Map<String, dynamic> json) =>
_$SpotifyActivityArtistFromJson(json);
String get id => uri.split(":").last;
}
@JsonSerializable(createToJson: false)
class SpotifyActivityAlbum {
final String uri;
final String name;
const SpotifyActivityAlbum({required this.uri, required this.name});
factory SpotifyActivityAlbum.fromJson(Map<String, dynamic> json) =>
_$SpotifyActivityAlbumFromJson(json);
String get id => uri.split(":").last;
}
@JsonSerializable(createToJson: false)
class SpotifyActivityContext {
final String uri;
final String name;
final num index;
const SpotifyActivityContext({
required this.uri,
required this.name,
required this.index,
});
factory SpotifyActivityContext.fromJson(Map<String, dynamic> json) =>
_$SpotifyActivityContextFromJson(json);
String get id => uri.split(":").last;
String get path => uri.split(":").skip(1).join("/");
}
@JsonSerializable(createToJson: false)
class SpotifyActivityTrack {
final String uri;
final String name;
final String imageUrl;
final SpotifyActivityArtist artist;
final SpotifyActivityAlbum album;
final SpotifyActivityContext context;
const SpotifyActivityTrack({
required this.uri,
required this.name,
required this.imageUrl,
required this.artist,
required this.album,
required this.context,
});
factory SpotifyActivityTrack.fromJson(Map<String, dynamic> json) =>
_$SpotifyActivityTrackFromJson(json);
String get id => uri.split(":").last;
}
@JsonSerializable(createToJson: false)
class SpotifyFriendActivity {
SpotifyFriend user;
SpotifyActivityTrack track;
SpotifyFriendActivity({required this.user, required this.track});
factory SpotifyFriendActivity.fromJson(Map<String, dynamic> json) =>
_$SpotifyFriendActivityFromJson(json);
}
@JsonSerializable(createToJson: false)
class SpotifyFriends {
List<SpotifyFriendActivity> friends;
SpotifyFriends({required this.friends});
factory SpotifyFriends.fromJson(Map<String, dynamic> json) =>
_$SpotifyFriendsFromJson(json);
}

View File

@ -1,60 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'spotify_friends.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
SpotifyFriend _$SpotifyFriendFromJson(Map json) => SpotifyFriend(
uri: json['uri'] as String,
name: json['name'] as String,
imageUrl: json['imageUrl'] as String,
);
SpotifyActivityArtist _$SpotifyActivityArtistFromJson(Map json) =>
SpotifyActivityArtist(
uri: json['uri'] as String,
name: json['name'] as String,
);
SpotifyActivityAlbum _$SpotifyActivityAlbumFromJson(Map json) =>
SpotifyActivityAlbum(
uri: json['uri'] as String,
name: json['name'] as String,
);
SpotifyActivityContext _$SpotifyActivityContextFromJson(Map json) =>
SpotifyActivityContext(
uri: json['uri'] as String,
name: json['name'] as String,
index: json['index'] as num,
);
SpotifyActivityTrack _$SpotifyActivityTrackFromJson(Map json) =>
SpotifyActivityTrack(
uri: json['uri'] as String,
name: json['name'] as String,
imageUrl: json['imageUrl'] as String,
artist: SpotifyActivityArtist.fromJson(
Map<String, dynamic>.from(json['artist'] as Map)),
album: SpotifyActivityAlbum.fromJson(
Map<String, dynamic>.from(json['album'] as Map)),
context: SpotifyActivityContext.fromJson(
Map<String, dynamic>.from(json['context'] as Map)),
);
SpotifyFriendActivity _$SpotifyFriendActivityFromJson(Map json) =>
SpotifyFriendActivity(
user: SpotifyFriend.fromJson(
Map<String, dynamic>.from(json['user'] as Map)),
track: SpotifyActivityTrack.fromJson(
Map<String, dynamic>.from(json['track'] as Map)),
);
SpotifyFriends _$SpotifyFriendsFromJson(Map json) => SpotifyFriends(
friends: (json['friends'] as List<dynamic>)
.map((e) => SpotifyFriendActivity.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(),
);

View File

@ -2,14 +2,11 @@ import 'package:auto_route/auto_route.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';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/dialogs/select_device_dialog.dart'; import 'package:spotube/components/dialogs/select_device_dialog.dart';
import 'package:spotube/components/playbutton_view/playbutton_card.dart'; import 'package:spotube/components/playbutton_view/playbutton_card.dart';
import 'package:spotube/components/playbutton_view/playbutton_tile.dart'; import 'package:spotube/components/playbutton_view/playbutton_tile.dart';
import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/track.dart';
import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/audio_player/querying_track_info.dart';
@ -17,10 +14,9 @@ import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/history/history.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/metadata_plugin/tracks/album.dart'; import 'package:spotube/provider/metadata_plugin/tracks/album.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
extension FormattedAlbumType on AlbumType { extension FormattedAlbumType on SpotubeAlbumType {
String get formatted => name.replaceFirst(name[0], name[0].toUpperCase()); String get formatted => name.replaceFirst(name[0], name[0].toUpperCase());
} }
@ -53,9 +49,11 @@ class AlbumCard extends HookConsumerWidget {
final updating = useState(false); final updating = useState(false);
Future<List<Track>> fetchAllTrack() async { Future<List<SpotubeFullTrackObject>> fetchAllTrack() async {
// return ref.read(metadataPluginAlbumTracksProvider(album).notifier).fetchAll(); await ref.read(metadataPluginAlbumTracksProvider(album.id).future);
return []; return ref
.read(metadataPluginAlbumTracksProvider(album.id).notifier)
.fetchAll();
} }
var imageUrl = album.images.asUrlString( var imageUrl = album.images.asUrlString(
@ -87,13 +85,13 @@ class AlbumCard extends HookConsumerWidget {
await remotePlayback.load( await remotePlayback.load(
WebSocketLoadEventData.album( WebSocketLoadEventData.album(
tracks: fetchedTracks, tracks: fetchedTracks,
// collection: album, collection: album,
), ),
); );
} else { } else {
await playlistNotifier.load(fetchedTracks, autoPlay: true); await playlistNotifier.load(fetchedTracks, autoPlay: true);
playlistNotifier.addCollection(album.id); playlistNotifier.addCollection(album.id);
// historyNotifier.addAlbums([album]); historyNotifier.addAlbums([album]);
} }
} finally { } finally {
updating.value = false; updating.value = false;
@ -112,7 +110,7 @@ class AlbumCard extends HookConsumerWidget {
if (fetchedTracks.isEmpty) return; if (fetchedTracks.isEmpty) return;
playlistNotifier.addTracks(fetchedTracks); playlistNotifier.addTracks(fetchedTracks);
playlistNotifier.addCollection(album.id); playlistNotifier.addCollection(album.id);
// historyNotifier.addAlbums([album]); historyNotifier.addAlbums([album]);
if (context.mounted) { if (context.mounted) {
showToast( showToast(
context: context, context: context,
@ -126,7 +124,7 @@ class AlbumCard extends HookConsumerWidget {
child: Text(context.l10n.undo), child: Text(context.l10n.undo),
onPressed: () { onPressed: () {
playlistNotifier playlistNotifier
.removeTracks(fetchedTracks.map((e) => e.id!)); .removeTracks(fetchedTracks.map((e) => e.id));
}, },
), ),
), ),

View File

@ -4,7 +4,7 @@ import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_pl
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/metadata_plugin/artist/albums.dart'; import 'package:spotube/provider/metadata_plugin/artist/albums.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/metadata_plugin/utils/common.dart';
class ArtistAlbumList extends HookConsumerWidget { class ArtistAlbumList extends HookConsumerWidget {
final String artistId; final String artistId;

View File

@ -3,44 +3,45 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/spotify/spotify.dart';
@Deprecated(
"Later a featured playlists API will be added for metadata plugins.")
class HomeFeaturedSection extends HookConsumerWidget { class HomeFeaturedSection extends HookConsumerWidget {
const HomeFeaturedSection({super.key}); const HomeFeaturedSection({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final featuredPlaylists = ref.watch(featuredPlaylistsProvider); return const SizedBox.shrink();
final featuredPlaylistsNotifier = // final featuredPlaylists = ref.watch(featuredPlaylistsProvider);
ref.watch(featuredPlaylistsProvider.notifier); // final featuredPlaylistsNotifier =
// ref.watch(featuredPlaylistsProvider.notifier);
if (featuredPlaylists.hasError) { // if (featuredPlaylists.hasError) {
return Column( // return Column(
mainAxisSize: MainAxisSize.min, // mainAxisSize: MainAxisSize.min,
children: [ // children: [
Undraw( // Undraw(
illustration: UndrawIllustration.fixingBugs, // illustration: UndrawIllustration.fixingBugs,
height: 200 * context.theme.scaling, // height: 200 * context.theme.scaling,
color: context.theme.colorScheme.primary, // color: context.theme.colorScheme.primary,
), // ),
Text(context.l10n.something_went_wrong).small().muted(), // Text(context.l10n.something_went_wrong).small().muted(),
const Gap(8), // const Gap(8),
], // ],
); // );
} // }
return Skeletonizer( // return Skeletonizer(
enabled: featuredPlaylists.isLoading, // enabled: featuredPlaylists.isLoading,
child: HorizontalPlaybuttonCardView<PlaylistSimple>( // child: HorizontalPlaybuttonCardView<PlaylistSimple>(
items: featuredPlaylists.asData?.value.items ?? [], // items: featuredPlaylists.asData?.value.items ?? [],
title: Text(context.l10n.featured), // title: Text(context.l10n.featured),
isLoadingNextPage: featuredPlaylists.isLoadingNextPage, // isLoadingNextPage: featuredPlaylists.isLoadingNextPage,
hasNextPage: featuredPlaylists.asData?.value.hasMore ?? false, // hasNextPage: featuredPlaylists.asData?.value.hasMore ?? false,
onFetchMore: featuredPlaylistsNotifier.fetchMore, // onFetchMore: featuredPlaylistsNotifier.fetchMore,
), // ),
); // );
} }
} }

View File

@ -1,65 +0,0 @@
import 'dart:ui';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart';
import 'package:spotube/modules/home/sections/friends/friend_item.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart';
class HomePageFriendsSection extends HookConsumerWidget {
const HomePageFriendsSection({super.key});
@override
Widget build(BuildContext context, ref) {
final auth = ref.watch(authenticationProvider);
final friendsQuery = ref.watch(friendsProvider);
final friends =
friendsQuery.asData?.value.friends ?? FakeData.friends.friends;
if (friendsQuery.isLoading ||
friendsQuery.asData?.value.friends.isEmpty == true ||
auth.asData?.value == null) {
return const SizedBox.shrink();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
context.l10n.friends,
style: context.theme.typography.h4,
),
),
SizedBox(
height: 80 * context.theme.scaling,
width: double.infinity,
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: PointerDeviceKind.values.toSet(),
scrollbars: false,
),
child: Skeletonizer(
enabled: friendsQuery.isLoading,
child: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 8),
scrollDirection: Axis.horizontal,
itemCount: friends.length,
separatorBuilder: (context, index) => const Gap(8),
itemBuilder: (context, index) {
return FriendItem(friend: friends[index]);
},
),
),
),
),
],
);
}
}

View File

@ -1,117 +0,0 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/gestures.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/models/spotify_friends.dart';
import 'package:spotube/provider/spotify/spotify.dart';
class FriendItem extends HookConsumerWidget {
final SpotifyFriendActivity friend;
const FriendItem({
super.key,
required this.friend,
});
@override
Widget build(BuildContext context, ref) {
final spotify = ref.watch(spotifyProvider);
return Card(
padding: const EdgeInsets.all(8),
child: Row(
children: [
Avatar(
initials: Avatar.getInitials(friend.user.name),
provider: UniversalImage.imageProvider(
friend.user.imageUrl,
),
),
const Gap(8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
friend.user.name,
style: context.theme.typography.bold,
),
RichText(
text: TextSpan(
style: context.theme.typography.normal.copyWith(
color: context.theme.colorScheme.foreground,
),
children: [
TextSpan(
text: friend.track.name,
recognizer: TapGestureRecognizer()
..onTap = () {
context
.navigateTo(TrackRoute(trackId: friend.track.id));
},
),
const TextSpan(text: ""),
const WidgetSpan(
child: Icon(
SpotubeIcons.artist,
size: 12,
),
),
TextSpan(
text: " ${friend.track.artist.name}",
recognizer: TapGestureRecognizer()
..onTap = () {
context.navigateTo(
ArtistRoute(artistId: friend.track.artist.id),
);
},
),
const TextSpan(text: "\n"),
TextSpan(
text: friend.track.context.name,
recognizer: TapGestureRecognizer()
..onTap = () async {
context.router.navigateNamed(
"/${friend.track.context.path}",
// extra:
// !friend.track.context.path.startsWith("album")
// ? null
// : await spotify.albums
// .get(friend.track.context.id),
);
},
),
const TextSpan(text: ""),
const WidgetSpan(
child: Icon(
SpotubeIcons.album,
size: 12,
),
),
TextSpan(
text: " ${friend.track.album.name}",
recognizer: TapGestureRecognizer()
..onTap = () async {
final album = await spotify.invoke(
(api) => api.albums.get(friend.track.album.id),
);
if (context.mounted) {
// context.navigateTo(
// AlbumRoute(id: album.id!, album: album),
// );
}
},
),
],
),
),
],
),
],
),
);
}
}

View File

@ -1,116 +0,0 @@
import 'dart:math';
import 'dart:ui';
import 'package:auto_route/auto_route.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart' hide Offset;
import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/gradients.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/modules/home/sections/genres/genre_card_playlist_card.dart';
import 'package:spotube/provider/spotify/spotify.dart';
final random = Random();
final gradientState = StateProvider.family(
(ref, String id) => gradients[random.nextInt(gradients.length)],
);
class GenreSectionCard extends HookConsumerWidget {
final Category category;
const GenreSectionCard({
super.key,
required this.category,
});
@override
Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final playlists = category == FakeData.category
? null
: ref.watch(categoryPlaylistsProvider(category.id!));
final playlistsData = playlists?.asData?.value.items.take(8) ??
List.generate(5, (index) => FakeData.playlistSimple);
final randomGradient = ref.watch(gradientState(category.id!));
return Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
borderRadius: theme.borderRadiusXxl,
boxShadow: [
BoxShadow(
color: theme.colorScheme.foreground,
offset: const Offset(0, 5),
blurRadius: 7,
spreadRadius: -5,
),
],
image: DecorationImage(
image: UniversalImage.imageProvider(
category.icons!.first.url!,
),
fit: BoxFit.cover,
),
),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: theme.borderRadiusXxl,
gradient: randomGradient
.withOpacity(theme.brightness == Brightness.dark ? 0.2 : 0.7),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 16,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
category.name!,
style: const TextStyle(color: Colors.white),
).h3(),
Button.link(
onPressed: () {
context.navigateTo(
GenrePlaylistsRoute(
id: category.id!,
category: category,
),
);
},
child: Text(
context.l10n.view_all,
style: const TextStyle(color: Colors.white),
).muted(),
),
],
),
if (playlists?.hasError != true)
Expanded(
child: Skeleton.ignore(
child: Skeletonizer(
enabled: playlists?.isLoading ?? false,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: playlistsData.length,
separatorBuilder: (context, index) => const Gap(12),
itemBuilder: (context, index) {
final playlist = playlistsData.elementAt(index);
return GenreSectionCardPlaylistCard(playlist: playlist);
},
),
),
),
)
],
),
),
);
}
}

View File

@ -1,130 +0,0 @@
import 'package:auto_route/auto_route.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
import 'package:spotify/spotify.dart' hide Image;
import 'package:spotube/collections/env.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/extensions/string.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:stroke_text/stroke_text.dart';
class GenreSectionCardPlaylistCard extends HookConsumerWidget {
final PlaylistSimple playlist;
const GenreSectionCardPlaylistCard({
super.key,
required this.playlist,
});
@override
Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
return Container(
width: 115 * theme.scaling,
decoration: BoxDecoration(
color: theme.colorScheme.background.withAlpha(75),
borderRadius: theme.borderRadiusMd,
),
child: SurfaceBlur(
borderRadius: theme.borderRadiusMd,
surfaceBlur: theme.surfaceBlur,
child: Button(
style: ButtonVariance.secondary.copyWith(
padding: (context, states, value) => const EdgeInsets.all(8),
decoration: (context, states, value) {
final decoration = ButtonVariance.secondary
.decoration(context, states) as BoxDecoration;
if (states.isNotEmpty) {
return decoration;
}
return decoration.copyWith(
color: decoration.color?.withAlpha(180),
);
},
),
onPressed: () {
// context.navigateTo(
// PlaylistRoute(id: playlist.id!, playlist: playlist),
// );
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 5,
children: [
ClipRRect(
borderRadius: theme.borderRadiusSm,
child: playlist.owner?.displayName == "Spotify" &&
Env.disableSpotifyImages
? Consumer(
builder: (context, ref, _) {
final (:src, :color, :colorBlendMode, :placement) =
ref.watch(playlistImageProvider(playlist.id!));
return SizedBox(
height: 100 * theme.scaling,
width: 100 * theme.scaling,
child: Stack(
children: [
Positioned.fill(
child: Image.asset(
src,
color: color,
colorBlendMode: colorBlendMode,
fit: BoxFit.cover,
),
),
Positioned.fill(
top: placement == Alignment.topLeft
? 10
: null,
left: 10,
bottom: placement == Alignment.bottomLeft
? 10
: null,
child: StrokeText(
text: playlist.name!,
strokeColor: Colors.white,
strokeWidth: 3,
textColor: Colors.black,
textStyle: const TextStyle(
fontSize: 16,
fontStyle: FontStyle.italic,
),
),
),
],
),
);
},
)
: UniversalImage(
path: (playlist.images)!.asUrlString(
placeholder: ImagePlaceholder.collection,
index: 1,
),
fit: BoxFit.cover,
height: 100 * theme.scaling,
width: 100 * theme.scaling,
),
),
Text(
playlist.name!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
).semiBold().small(),
if (playlist.description != null)
Text(
playlist.description?.unescapeHtml().cleanHtml() ?? "",
maxLines: 2,
overflow: TextOverflow.ellipsis,
).xSmall().muted(),
],
),
),
),
);
}
}

View File

@ -1,139 +0,0 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/modules/home/sections/genres/genre_card.dart';
import 'package:spotube/provider/spotify/spotify.dart';
class HomeGenresSection extends HookConsumerWidget {
const HomeGenresSection({super.key});
@override
Widget build(BuildContext context, ref) {
final theme = context.theme;
final mediaQuery = MediaQuery.sizeOf(context);
final categoriesQuery = ref.watch(categoriesProvider);
final categories = useMemoized(
() =>
categoriesQuery.asData?.value
.where((c) => (c.icons?.length ?? 0) > 0)
.take(6)
.toList() ??
[
FakeData.category,
],
[categoriesQuery.asData?.value],
);
final controller = useMemoized(() => CarouselController(), []);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
context.l10n.genres,
style: context.theme.typography.h4,
),
Button.link(
onPressed: () {
context.navigateTo(const GenreRoute());
},
child: Text(
context.l10n.browse_all,
).muted(),
),
],
),
),
const Gap(8),
Stack(
children: [
SizedBox(
height: 280 * theme.scaling,
child: Carousel(
controller: controller,
transition: const CarouselTransition.sliding(gap: 24),
sizeConstraint: CarouselSizeConstraint.fixed(
mediaQuery.mdAndUp
? mediaQuery.width * .6
: mediaQuery.width * .95,
),
itemCount: categories.length,
pauseOnHover: true,
direction: Axis.horizontal,
itemBuilder: (context, index) {
final category = categories[index];
return Skeletonizer(
enabled: categoriesQuery.isLoading,
child: GenreSectionCard(category: category),
);
},
),
),
Positioned(
left: 0,
child: Container(
height: 280 * theme.scaling,
width: (mediaQuery.mdAndUp ? 60 : 40) * theme.scaling,
alignment: Alignment.center,
child: IconButton.secondary(
shape: ButtonShape.circle,
size: mediaQuery.mdAndUp
? const ButtonSize(1.3)
: ButtonSize.normal,
icon: const Icon(SpotubeIcons.angleLeft),
onPressed: () {
controller.animatePrevious(
const Duration(seconds: 1),
);
},
),
),
),
Positioned(
right: 0,
child: Container(
height: 280 * theme.scaling,
width: (mediaQuery.mdAndUp ? 60 : 40) * theme.scaling,
alignment: Alignment.center,
child: IconButton.secondary(
shape: ButtonShape.circle,
size: mediaQuery.mdAndUp
? const ButtonSize(1.3)
: ButtonSize.normal,
icon: const Icon(SpotubeIcons.angleRight),
onPressed: () {
controller.animateNext(
const Duration(seconds: 1),
);
},
),
),
),
],
),
const Gap(8),
Center(
child: CarouselDotIndicator(
itemCount: categories.length,
controller: controller,
),
),
],
);
}
}

View File

@ -1,35 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/provider/spotify/spotify.dart';
class HomeMadeForUserSection extends HookConsumerWidget {
const HomeMadeForUserSection({super.key});
@override
Widget build(BuildContext context, ref) {
final madeForUser = ref.watch(viewProvider("made-for-x-hub"));
return SliverList.builder(
itemCount: madeForUser.asData?.value["content"]?["items"]?.length ?? 0,
itemBuilder: (context, index) {
final item = madeForUser.asData?.value["content"]?["items"]?[index];
final playlists = item["content"]?["items"]
?.where((itemL2) => itemL2["type"] == "playlist")
.map((itemL2) => PlaylistSimple.fromJson(itemL2))
.toList()
.cast<PlaylistSimple>() ??
<PlaylistSimple>[];
if (playlists.isEmpty) return const SizedBox.shrink();
return HorizontalPlaybuttonCardView<PlaylistSimple>(
items: playlists,
title: Text(item["name"] ?? ""),
hasNextPage: false,
isLoadingNextPage: false,
onFetchMore: () {},
);
},
);
}
}

View File

@ -1,10 +1,11 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/metadata_plugin/album/releases.dart';
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
class HomeNewReleasesSection extends HookConsumerWidget { class HomeNewReleasesSection extends HookConsumerWidget {
const HomeNewReleasesSection({super.key}); const HomeNewReleasesSection({super.key});
@ -13,10 +14,9 @@ class HomeNewReleasesSection extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(authenticationProvider); final auth = ref.watch(authenticationProvider);
final newReleases = ref.watch(albumReleasesProvider); final newReleases = ref.watch(metadataPluginAlbumReleasesProvider);
final newReleasesNotifier = ref.read(albumReleasesProvider.notifier); final newReleasesNotifier =
ref.read(metadataPluginAlbumReleasesProvider.notifier);
final albums = ref.watch(userArtistAlbumReleasesProvider);
if (auth.asData?.value == null || if (auth.asData?.value == null ||
newReleases.isLoading || newReleases.isLoading ||
@ -24,8 +24,8 @@ class HomeNewReleasesSection extends HookConsumerWidget {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return HorizontalPlaybuttonCardView<Album>( return HorizontalPlaybuttonCardView<SpotubeSimpleAlbumObject>(
items: albums, items: newReleases.asData?.value.items ?? [],
title: Text(context.l10n.new_releases), title: Text(context.l10n.new_releases),
isLoadingNextPage: newReleases.isLoadingNextPage, isLoadingNextPage: newReleases.isLoadingNextPage,
hasNextPage: newReleases.asData?.value.hasMore ?? false, hasNextPage: newReleases.asData?.value.hasMore ?? false,

View File

@ -11,8 +11,8 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/extensions/string.dart'; import 'package:spotube/extensions/string.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
@ -100,7 +100,7 @@ class LocalFolderItem extends HookConsumerWidget {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final track = tracks[index]; final track = tracks[index];
return UniversalImage( return UniversalImage(
path: (track.album?.images).asUrlString( path: track.album.images.asUrlString(
placeholder: ImagePlaceholder.albumArt, placeholder: ImagePlaceholder.albumArt,
), ),
fit: BoxFit.cover, fit: BoxFit.cover,

View File

@ -1,272 +0,0 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
class MultiSelectField<T> extends HookWidget {
final List<T> options;
final List<T> selectedOptions;
final Widget Function(T option, VoidCallback onSelect)? optionBuilder;
final Widget Function(T option)? selectedOptionBuilder;
final ValueChanged<List<T>> onSelected;
final Widget? dialogTitle;
final Object Function(T option) getValueForOption;
final Widget label;
final String? helperText;
final bool enabled;
const MultiSelectField({
super.key,
required this.options,
required this.selectedOptions,
required this.getValueForOption,
required this.label,
this.optionBuilder,
this.selectedOptionBuilder,
required this.onSelected,
this.dialogTitle,
this.helperText,
this.enabled = true,
});
Widget defaultSelectedOptionBuilder(T option) {
return Chip(
label: Text(option.toString()),
onDeleted: () {
onSelected(
selectedOptions.where((e) => e != getValueForOption(option)).toList(),
);
},
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
MaterialButton(
elevation: 0,
focusElevation: 0,
hoverElevation: 0,
disabledElevation: 0,
highlightElevation: 0,
padding: const EdgeInsets.symmetric(vertical: 22),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
side: BorderSide(
color: enabled
? theme.colorScheme.onSurface
: theme.colorScheme.onSurface.withValues(alpha: 0.1),
),
),
mouseCursor: WidgetStateMouseCursor.textable,
onPressed: !enabled
? null
: () async {
final selected = await showDialog<List<T>>(
context: context,
builder: (context) {
return _MultiSelectDialog<T>(
dialogTitle: dialogTitle,
options: options,
getValueForOption: getValueForOption,
optionBuilder: optionBuilder,
initialSelection: selectedOptions,
helperText: helperText,
);
},
);
if (selected != null) {
onSelected(selected);
}
},
child: Container(
alignment: Alignment.centerLeft,
margin: const EdgeInsets.symmetric(horizontal: 10),
child: DefaultTextStyle(
style: theme.textTheme.titleMedium!,
child: label,
),
),
),
if (helperText != null)
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
helperText!,
style: Theme.of(context).textTheme.bodySmall,
),
),
Wrap(
children: [
...selectedOptions.map(
(option) => Padding(
padding: const EdgeInsets.all(4.0),
child: (selectedOptionBuilder ??
defaultSelectedOptionBuilder)(option),
),
),
],
)
],
);
}
}
class _MultiSelectDialog<T> extends HookWidget {
final Widget? dialogTitle;
final List<T> options;
final Widget Function(T option, VoidCallback onSelect)? optionBuilder;
final Object Function(T option) getValueForOption;
final List<T> initialSelection;
final String? helperText;
const _MultiSelectDialog({
super.key,
required this.dialogTitle,
required this.options,
required this.getValueForOption,
this.optionBuilder,
this.initialSelection = const [],
this.helperText,
});
@override
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
final selected = useState(initialSelection.map(getValueForOption));
final searchController = useTextEditingController();
// creates render update
useValueListenable(searchController);
final filteredOptions = useMemoized(
() {
if (searchController.text.isEmpty) {
return options;
}
return options
.map((e) => (
weightedRatio(
getValueForOption(e).toString(), searchController.text),
e
))
.sorted((a, b) => b.$1.compareTo(a.$1))
.where((e) => e.$1 > 50)
.map((e) => e.$2)
.toList();
},
[searchController.text, options, getValueForOption],
);
Widget defaultOptionBuilder(T option, VoidCallback onSelect) {
final isSelected = selected.value.contains(getValueForOption(option));
return ChoiceChip(
label: Text("${!isSelected ? " " : ""}${option.toString()}"),
selected: isSelected,
side: BorderSide.none,
onSelected: (_) {
onSelect();
},
);
}
return AlertDialog(
scrollable: true,
title: dialogTitle ?? Text(context.l10n.select),
contentPadding: mediaQuery.mdAndUp ? null : const EdgeInsets.all(16),
insetPadding: const EdgeInsets.all(16),
actions: [
OutlinedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(context.l10n.cancel),
),
ElevatedButton(
onPressed: () {
Navigator.pop(
context,
options
.where(
(option) =>
selected.value.contains(getValueForOption(option)),
)
.toList(),
);
},
child: Text(context.l10n.done),
),
],
content: SizedBox(
height: mediaQuery.size.height * 0.5,
width: 400,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
autofocus: true,
controller: searchController,
decoration: InputDecoration(
hintText: context.l10n.search,
prefixIcon: const Icon(SpotubeIcons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
),
),
),
const SizedBox(height: 10),
Expanded(
child: SingleChildScrollView(
child: Wrap(
spacing: 5,
runSpacing: 5,
children: [
...filteredOptions.map(
(option) => Padding(
padding: const EdgeInsets.all(4.0),
child: (optionBuilder ?? defaultOptionBuilder)(
option,
() {
final value = getValueForOption(option);
if (selected.value.contains(value)) {
selected.value = selected.value
.where((e) => e != value)
.toList();
} else {
selected.value = [...selected.value, value];
}
},
),
),
),
],
),
),
),
if (helperText != null)
Text(
helperText!,
style: Theme.of(context).textTheme.labelMedium,
),
],
),
),
);
}
}

View File

@ -1,183 +0,0 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
typedef RecommendationAttribute = ({double min, double target, double max});
RecommendationAttribute lowValues(double base) =>
(min: 1 * base, target: 0.3 * base, max: 0.3 * base);
RecommendationAttribute moderateValues(double base) =>
(min: 0.5 * base, target: 1 * base, max: 0.5 * base);
RecommendationAttribute highValues(double base) =>
(min: 0.3 * base, target: 0.3 * base, max: 1 * base);
class RecommendationAttributeDials extends HookWidget {
final Widget title;
final RecommendationAttribute values;
final ValueChanged<RecommendationAttribute> onChanged;
final double base;
const RecommendationAttributeDials({
super.key,
required this.values,
required this.onChanged,
required this.title,
this.base = 1,
});
@override
Widget build(BuildContext context) {
final labelStyle = Theme.of(context).typography.small.copyWith(
fontWeight: FontWeight.w500,
);
final minSlider = Row(
spacing: 5,
children: [
Text(context.l10n.min, style: labelStyle),
Expanded(
child: Slider(
value: SliderValue.single(values.min / base),
min: 0,
max: 1,
onChanged: (value) => onChanged((
min: value.value * base,
target: values.target,
max: values.max,
)),
),
),
],
);
final targetSlider = Row(
spacing: 5,
children: [
Text(context.l10n.target, style: labelStyle),
Expanded(
child: Slider(
value: SliderValue.single(values.target / base),
min: 0,
max: 1,
onChanged: (value) => onChanged((
min: values.min,
target: value.value * base,
max: values.max,
)),
),
),
],
);
final maxSlider = Row(
spacing: 5,
children: [
Text(context.l10n.max, style: labelStyle),
Expanded(
child: Slider(
value: SliderValue.single(values.max / base),
min: 0,
max: 1,
onChanged: (value) => onChanged((
min: values.min,
target: values.target,
max: value.value * base,
)),
),
),
],
);
void onSelected(int index) {
RecommendationAttribute newValues = zeroValues;
switch (index) {
case 0:
newValues = lowValues(base);
break;
case 1:
newValues = moderateValues(base);
break;
case 2:
newValues = highValues(base);
break;
}
if (newValues == values) {
onChanged(zeroValues);
} else {
onChanged(newValues);
}
}
return LayoutBuilder(builder: (context, constrain) {
return Accordion(
items: [
AccordionItem(
trigger: AccordionTrigger(
child: SizedBox(
width: double.infinity,
child: Basic(
title: title.semiBold(),
trailing: Row(
spacing: 5,
children: [
Toggle(
value: values == lowValues(base),
onChanged: (value) => onSelected(0),
style:
const ButtonStyle.outline(size: ButtonSize.small),
child: Text(context.l10n.low),
),
Toggle(
value: values == moderateValues(base),
onChanged: (value) => onSelected(1),
style:
const ButtonStyle.outline(size: ButtonSize.small),
child: Text(context.l10n.moderate),
),
Toggle(
value: values == highValues(base),
onChanged: (value) => onSelected(2),
style:
const ButtonStyle.outline(size: ButtonSize.small),
child: Text(context.l10n.high),
),
],
),
),
),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (constrain.mdAndUp)
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const SizedBox(width: 16),
Expanded(child: minSlider),
Expanded(child: targetSlider),
Expanded(child: maxSlider),
],
)
else
Padding(
padding: const EdgeInsets.only(left: 16),
child: Column(
children: [
minSlider,
targetSlider,
maxSlider,
],
),
),
],
),
),
],
);
});
}
}

View File

@ -1,182 +0,0 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
import 'package:spotube/modules/library/playlist_generate/recommendation_attribute_dials.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
class RecommendationAttributeFields extends HookWidget {
final Widget title;
final RecommendationAttribute values;
final ValueChanged<RecommendationAttribute> onChanged;
final Map<String, RecommendationAttribute>? presets;
const RecommendationAttributeFields({
super.key,
required this.values,
required this.onChanged,
required this.title,
this.presets,
});
@override
Widget build(BuildContext context) {
final minController =
useShadcnTextEditingController(text: values.min.toString());
final targetController =
useShadcnTextEditingController(text: values.target.toString());
final maxController =
useShadcnTextEditingController(text: values.max.toString());
useEffect(() {
listener() {
onChanged((
min: double.tryParse(minController.text) ?? 0,
target: double.tryParse(targetController.text) ?? 0,
max: double.tryParse(maxController.text) ?? 0,
));
}
minController.addListener(listener);
targetController.addListener(listener);
maxController.addListener(listener);
return () {
minController.removeListener(listener);
targetController.removeListener(listener);
maxController.removeListener(listener);
};
}, [values]);
final minField = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 5,
children: [
Text(context.l10n.min).semiBold(),
NumberInput(
controller: minController,
allowDecimals: false,
),
],
);
final targetField = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 5,
children: [
Text(context.l10n.target).semiBold(),
NumberInput(
controller: targetController,
allowDecimals: false,
),
],
);
final maxField = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 5,
children: [
Text(context.l10n.max).semiBold(),
NumberInput(
controller: maxController,
allowDecimals: false,
),
],
);
void onSelected(int index) {
RecommendationAttribute newValues = presets!.values.elementAt(index);
if (newValues == values) {
onChanged(zeroValues);
minController.text = zeroValues.min.toString();
targetController.text = zeroValues.target.toString();
maxController.text = zeroValues.max.toString();
} else {
onChanged(newValues);
minController.text = newValues.min.toString();
targetController.text = newValues.target.toString();
maxController.text = newValues.max.toString();
}
}
return LayoutBuilder(builder: (context, constraints) {
return Accordion(
items: [
AccordionItem(
trigger: AccordionTrigger(
child: SizedBox(
width: double.infinity,
child: Basic(
title: title.semiBold(),
trailing: presets == null
? const SizedBox.shrink()
: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
spacing: 5,
children: [
for (final presetEntry in presets?.entries
.toList() ??
<MapEntry<String, RecommendationAttribute>>[])
Toggle(
value: presetEntry.value == values,
style: const ButtonStyle.outline(
size: ButtonSize.small,
),
onChanged: (value) {
onSelected(
presets!.entries.toList().indexWhere(
(s) => s.key == presetEntry.key),
);
},
child: Text(presetEntry.key),
),
],
),
),
),
),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
if (constraints.mdAndUp)
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const SizedBox(width: 16),
Expanded(child: minField),
const SizedBox(width: 16),
Expanded(child: targetField),
const SizedBox(width: 16),
Expanded(child: maxField),
const SizedBox(width: 16),
],
)
else
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
minField,
const SizedBox(height: 16),
targetField,
const SizedBox(height: 16),
maxField,
],
),
),
const SizedBox(height: 8),
],
),
),
],
);
});
}
}

View File

@ -1,165 +0,0 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart' show Autocomplete;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
enum SelectedItemDisplayType {
wrap,
list,
}
class SeedsMultiAutocomplete<T extends Object> extends HookWidget {
final ValueNotifier<List<T>> seeds;
final FutureOr<List<T>> Function(TextEditingValue textEditingValue)
fetchSeeds;
final Widget Function(T option, ValueChanged<T> onSelected)
autocompleteOptionBuilder;
final Widget Function(T option) selectedSeedBuilder;
final String Function(T option) displayStringForOption;
final bool enabled;
final SelectedItemDisplayType selectedItemDisplayType;
final Widget? placeholder;
final Widget? leading;
final Widget? trailing;
final Widget? label;
const SeedsMultiAutocomplete({
super.key,
required this.seeds,
required this.fetchSeeds,
required this.autocompleteOptionBuilder,
required this.displayStringForOption,
required this.selectedSeedBuilder,
this.enabled = true,
this.selectedItemDisplayType = SelectedItemDisplayType.wrap,
this.placeholder,
this.leading,
this.trailing,
this.label,
});
@override
Widget build(BuildContext context) {
useValueListenable(seeds);
final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
final seedController = useShadcnTextEditingController();
final containerKey = useRef(GlobalKey());
final box =
containerKey.value.currentContext?.findRenderObject() as RenderBox?;
final position = box?.localToGlobal(Offset.zero); //this is global position
final containerYPos = position?.dy ?? 0; //th
final containerHeight = box?.size.height ?? 0;
final listHeight = mediaQuery.size.height -
(containerYPos + containerHeight) -
// bottom player bar height
(mediaQuery.mdAndUp ? 80 : 0);
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (label != null) ...[
label!.semiBold(),
const Gap(8),
],
LayoutBuilder(builder: (context, constrains) {
return Container(
key: containerKey.value,
child: Autocomplete<T>(
optionsBuilder: (textEditingValue) async {
if (textEditingValue.text.isEmpty) return [];
return fetchSeeds(textEditingValue);
},
onSelected: (value) {
seeds.value = [...seeds.value, value];
seedController.clear();
},
optionsViewBuilder: (context, onSelected, options) {
return Align(
alignment: Alignment.topLeft,
child: Container(
constraints: BoxConstraints(
maxWidth: constrains.maxWidth,
),
height: max(listHeight, 0),
child: Card(
child: ListView.builder(
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (context, index) {
final option = options.elementAt(index);
return autocompleteOptionBuilder(option, onSelected);
},
),
),
),
);
},
displayStringForOption: displayStringForOption,
fieldViewBuilder: (
context,
textEditingController,
focusNode,
onFieldSubmitted,
) {
return TextField(
controller: seedController,
onChanged: (value) => textEditingController.text = value,
focusNode: focusNode,
onSubmitted: (_) => onFieldSubmitted(),
enabled: enabled,
features: [
if (leading != null) InputFeature.leading(leading!),
if (trailing != null) InputFeature.trailing(trailing!),
],
placeholder: placeholder,
);
},
),
);
}),
const SizedBox(height: 8),
switch (selectedItemDisplayType) {
SelectedItemDisplayType.wrap => Wrap(
spacing: 4,
runSpacing: 4,
children: seeds.value.map(selectedSeedBuilder).toList(),
),
SelectedItemDisplayType.list => AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: seeds.value.isEmpty
? const SizedBox.shrink()
: Card(
child: Column(
children: [
for (final seed in seeds.value) ...[
selectedSeedBuilder(seed),
if (seeds.value.length > 1 &&
seed != seeds.value.last)
Divider(
color: theme.colorScheme.secondary,
height: 1,
indent: 12,
endIndent: 12,
),
],
],
),
),
),
},
],
);
}
}

View File

@ -1,45 +0,0 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/image.dart';
class SimpleTrackTile extends HookWidget {
final Track track;
final VoidCallback? onDelete;
const SimpleTrackTile({
super.key,
required this.track,
this.onDelete,
});
@override
Widget build(BuildContext context) {
return ButtonTile(
leading: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: UniversalImage(
path: (track.album?.images).asUrlString(
placeholder: ImagePlaceholder.artist,
),
height: 40,
width: 40,
),
),
title: Text(track.name!),
trailing: onDelete == null
? null
: IconButton.ghost(
icon: const Icon(SpotubeIcons.close),
onPressed: onDelete,
),
subtitle: Text(
track.artists?.map((e) => e.name).join(", ") ?? track.album?.name ?? "",
),
style: ButtonVariance.ghost,
);
}
}

View File

@ -2,20 +2,19 @@ import 'package:auto_route/auto_route.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';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/services/download_manager/download_status.dart'; import 'package:spotube/services/download_manager/download_status.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart';
class DownloadItem extends HookConsumerWidget { class DownloadItem extends HookConsumerWidget {
final Track track; final SpotubeFullTrackObject track;
const DownloadItem({ const DownloadItem({
super.key, super.key,
required this.track, required this.track,
@ -29,7 +28,7 @@ class DownloadItem extends HookConsumerWidget {
useEffect(() { useEffect(() {
if (track is! SourcedTrack) return null; if (track is! SourcedTrack) return null;
final notifier = downloadManager.getStatusNotifier(track as SourcedTrack); final notifier = downloadManager.getStatusNotifier(track);
taskStatus.value = notifier?.value; taskStatus.value = notifier?.value;
@ -56,18 +55,18 @@ class DownloadItem extends HookConsumerWidget {
child: UniversalImage( child: UniversalImage(
height: 40, height: 40,
width: 40, width: 40,
path: (track.album?.images).asUrlString( path: track.album.images.asUrlString(
placeholder: ImagePlaceholder.albumArt, placeholder: ImagePlaceholder.albumArt,
), ),
), ),
), ),
), ),
title: Text(track.name ?? ''), title: Text(track.name),
subtitle: ArtistLink( subtitle: ArtistLink(
artists: track.artists ?? <Artist>[], artists: track.artists,
mainAxisAlignment: WrapAlignment.start, mainAxisAlignment: WrapAlignment.start,
onOverflowArtistClick: () { onOverflowArtistClick: () {
context.navigateTo(TrackRoute(trackId: track.id!)); context.navigateTo(TrackRoute(trackId: track.id));
}, },
), ),
trailing: isQueryingSourceInfo trailing: isQueryingSourceInfo
@ -75,8 +74,7 @@ class DownloadItem extends HookConsumerWidget {
: switch (taskStatus.value!) { : switch (taskStatus.value!) {
DownloadStatus.downloading => HookBuilder(builder: (context) { DownloadStatus.downloading => HookBuilder(builder: (context) {
final taskProgress = useListenable(useMemoized( final taskProgress = useListenable(useMemoized(
() => downloadManager () => downloadManager.getProgressNotifier(track),
.getProgressNotifier(track as SourcedTrack),
[track], [track],
)); ));
return Row( return Row(
@ -88,13 +86,13 @@ class DownloadItem extends HookConsumerWidget {
IconButton.ghost( IconButton.ghost(
icon: const Icon(SpotubeIcons.pause), icon: const Icon(SpotubeIcons.pause),
onPressed: () { onPressed: () {
downloadManager.pause(track as SourcedTrack); downloadManager.pause(track);
}), }),
const SizedBox(width: 10), const SizedBox(width: 10),
IconButton.ghost( IconButton.ghost(
icon: const Icon(SpotubeIcons.close), icon: const Icon(SpotubeIcons.close),
onPressed: () { onPressed: () {
downloadManager.cancel(track as SourcedTrack); downloadManager.cancel(track);
}), }),
], ],
); );
@ -105,13 +103,13 @@ class DownloadItem extends HookConsumerWidget {
IconButton.ghost( IconButton.ghost(
icon: const Icon(SpotubeIcons.play), icon: const Icon(SpotubeIcons.play),
onPressed: () { onPressed: () {
downloadManager.resume(track as SourcedTrack); downloadManager.resume(track);
}), }),
const SizedBox(width: 10), const SizedBox(width: 10),
IconButton.ghost( IconButton.ghost(
icon: const Icon(SpotubeIcons.close), icon: const Icon(SpotubeIcons.close),
onPressed: () { onPressed: () {
downloadManager.cancel(track as SourcedTrack); downloadManager.cancel(track);
}) })
], ],
), ),
@ -127,7 +125,7 @@ class DownloadItem extends HookConsumerWidget {
IconButton.ghost( IconButton.ghost(
icon: const Icon(SpotubeIcons.refresh), icon: const Icon(SpotubeIcons.refresh),
onPressed: () { onPressed: () {
downloadManager.retry(track as SourcedTrack); downloadManager.retry(track);
}, },
), ),
], ],
@ -138,7 +136,7 @@ class DownloadItem extends HookConsumerWidget {
DownloadStatus.queued => IconButton.ghost( DownloadStatus.queued => IconButton.ghost(
icon: const Icon(SpotubeIcons.close), icon: const Icon(SpotubeIcons.close),
onPressed: () { onPressed: () {
downloadManager.removeFromQueue(track as SourcedTrack); downloadManager.removeFromQueue(track);
}), }),
}, },
); );

View File

@ -9,6 +9,7 @@ import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/framework/app_pop_scope.dart'; import 'package:spotube/components/framework/app_pop_scope.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/modules/player/player_actions.dart'; import 'package:spotube/modules/player/player_actions.dart';
import 'package:spotube/modules/player/player_controls.dart'; import 'package:spotube/modules/player/player_controls.dart';
import 'package:spotube/modules/player/volume_slider.dart'; import 'package:spotube/modules/player/volume_slider.dart';
@ -16,11 +17,8 @@ import 'package:spotube/components/dialogs/track_details_dialog.dart';
import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/modules/root/spotube_navigation_bar.dart'; import 'package:spotube/modules/root/spotube_navigation_bar.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
@ -47,8 +45,8 @@ class PlayerView extends HookConsumerWidget {
final sourcedCurrentTrack = ref.watch(activeTrackSourcesProvider); final sourcedCurrentTrack = ref.watch(activeTrackSourcesProvider);
final currentActiveTrack = final currentActiveTrack =
ref.watch(audioPlayerProvider.select((s) => s.activeTrack)); ref.watch(audioPlayerProvider.select((s) => s.activeTrack));
final currentTrack = sourcedCurrentTrack ?? currentActiveTrack; final currentActiveTrackSource = sourcedCurrentTrack.asData?.value?.source;
final isLocalTrack = currentTrack is LocalTrack; final isLocalTrack = currentActiveTrack is SpotubeLocalTrackObject;
final mediaQuery = MediaQuery.sizeOf(context); final mediaQuery = MediaQuery.sizeOf(context);
final shouldHide = useState(true); final shouldHide = useState(true);
@ -71,10 +69,10 @@ class PlayerView extends HookConsumerWidget {
}, [mediaQuery.lgAndUp]); }, [mediaQuery.lgAndUp]);
String albumArt = useMemoized( String albumArt = useMemoized(
() => (currentTrack?.album?.images).asUrlString( () => (currentActiveTrack?.album.images).asUrlString(
placeholder: ImagePlaceholder.albumArt, placeholder: ImagePlaceholder.albumArt,
), ),
[currentTrack?.album?.images], [currentActiveTrack?.album.images],
); );
useEffect(() { useEffect(() {
@ -115,7 +113,7 @@ class PlayerView extends HookConsumerWidget {
) )
], ],
trailing: [ trailing: [
if (currentTrack is YoutubeSourcedTrack) if (currentActiveTrackSource is YoutubeSourcedTrack)
TextButton( TextButton(
leading: Assets.logos.songlinkTransparent.image( leading: Assets.logos.songlinkTransparent.image(
width: 20, width: 20,
@ -123,31 +121,34 @@ class PlayerView extends HookConsumerWidget {
color: theme.colorScheme.foreground, color: theme.colorScheme.foreground,
), ),
onPressed: () { onPressed: () {
final url = "https://song.link/s/${currentTrack.id}"; final url =
"https://song.link/s/${currentActiveTrack?.id}";
launchUrlString(url); launchUrlString(url);
}, },
child: Text(context.l10n.song_link), child: Text(context.l10n.song_link),
), ),
Tooltip( if (!isLocalTrack)
tooltip: TooltipContainer( Tooltip(
child: Text(context.l10n.details), tooltip: TooltipContainer(
).call, child: Text(context.l10n.details),
child: IconButton.ghost( ).call,
icon: const Icon(SpotubeIcons.info, size: 18), child: IconButton.ghost(
onPressed: currentTrack == null icon: const Icon(SpotubeIcons.info, size: 18),
? null onPressed: currentActiveTrackSource == null
: () { ? null
showDialog( : () {
context: context, showDialog(
builder: (context) { context: context,
return TrackDetailsDialog( builder: (context) {
track: currentTrack, return TrackDetailsDialog(
); track: currentActiveTrack
}); as SpotubeFullTrackObject,
}, );
), });
) },
),
)
], ],
), ),
), ),
@ -190,7 +191,7 @@ class PlayerView extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
AutoSizeText( AutoSizeText(
currentTrack?.name ?? context.l10n.not_playing, currentActiveTrack?.name ?? context.l10n.not_playing,
style: const TextStyle(fontSize: 22), style: const TextStyle(fontSize: 22),
maxFontSize: 22, maxFontSize: 22,
maxLines: 1, maxLines: 1,
@ -198,13 +199,13 @@ class PlayerView extends HookConsumerWidget {
), ),
if (isLocalTrack) if (isLocalTrack)
Text( Text(
currentTrack.artists?.asString() ?? "", currentActiveTrack.artists.asString(),
style: theme.typography.normal style: theme.typography.normal
.copyWith(fontWeight: FontWeight.bold), .copyWith(fontWeight: FontWeight.bold),
) )
else else
ArtistLink( ArtistLink(
artists: currentTrack?.artists ?? [], artists: currentActiveTrack?.artists ?? [],
textStyle: theme.typography.normal textStyle: theme.typography.normal
.copyWith(fontWeight: FontWeight.bold), .copyWith(fontWeight: FontWeight.bold),
onRouteChange: (route) { onRouteChange: (route) {
@ -212,7 +213,9 @@ class PlayerView extends HookConsumerWidget {
context.router.navigateNamed(route); context.router.navigateNamed(route);
}, },
onOverflowArtistClick: () => context.navigateTo( onOverflowArtistClick: () => context.navigateTo(
TrackRoute(trackId: currentTrack!.id!), TrackRoute(
trackId: currentActiveTrack!.id,
),
), ),
), ),
], ],

View File

@ -8,14 +8,13 @@ import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/modules/player/player_queue.dart'; import 'package:spotube/modules/player/player_queue.dart';
import 'package:spotube/modules/player/sibling_tracks_sheet.dart'; import 'package:spotube/modules/player/sibling_tracks_sheet.dart';
import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart';
import 'package:spotube/components/heart_button/heart_button.dart'; import 'package:spotube/components/heart_button/heart_button.dart';
import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/duration.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
@ -38,12 +37,13 @@ class PlayerActions extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final playlist = ref.watch(audioPlayerProvider); final playlist = ref.watch(audioPlayerProvider);
final isLocalTrack = playlist.activeTrack is LocalTrack; final isLocalTrack = playlist.activeTrack is SpotubeLocalTrackObject;
ref.watch(downloadManagerProvider); ref.watch(downloadManagerProvider);
final downloader = ref.watch(downloadManagerProvider.notifier); final downloader = ref.watch(downloadManagerProvider.notifier);
final isInQueue = useMemoized(() { final isInQueue = useMemoized(() {
if (playlist.activeTrack == null) return false; if (playlist.activeTrack is! SpotubeFullTrackObject) return false;
return downloader.isActive(playlist.activeTrack!); return downloader
.isActive(playlist.activeTrack! as SpotubeFullTrackObject);
}, [ }, [
playlist.activeTrack, playlist.activeTrack,
downloader, downloader,
@ -58,9 +58,9 @@ class PlayerActions extends HookConsumerWidget {
return localTracks.any( return localTracks.any(
(element) => (element) =>
element.name == playlist.activeTrack?.name && element.name == playlist.activeTrack?.name &&
element.album?.name == playlist.activeTrack?.album?.name && element.album?.name == playlist.activeTrack?.album.name &&
element.artists?.asString() == element.artists?.asString() ==
playlist.activeTrack?.artists?.asString(), playlist.activeTrack?.artists.asString(),
) == ) ==
true; true;
}, [localTracks, playlist.activeTrack]); }, [localTracks, playlist.activeTrack]);
@ -168,7 +168,8 @@ class PlayerActions extends HookConsumerWidget {
isDownloaded ? SpotubeIcons.done : SpotubeIcons.download, isDownloaded ? SpotubeIcons.done : SpotubeIcons.download,
), ),
onPressed: playlist.activeTrack != null onPressed: playlist.activeTrack != null
? () => downloader.addToQueue(playlist.activeTrack!) ? () => downloader.addToQueue(
playlist.activeTrack! as SpotubeFullTrackObject)
: null, : null,
), ),
), ),

View File

@ -7,16 +7,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/button/back_button.dart'; import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/components/fallbacks/not_found.dart'; import 'package:spotube/components/fallbacks/not_found.dart';
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/track_tile/track_tile.dart'; import 'package:spotube/components/track_tile/track_tile.dart';
import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart'; import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/audio_player/state.dart';
@ -24,7 +23,7 @@ class PlayerQueue extends HookConsumerWidget {
final bool floating; final bool floating;
final AudioPlayerState playlist; final AudioPlayerState playlist;
final Future<void> Function(Track track) onJump; final Future<void> Function(SpotubeTrackObject track) onJump;
final Future<void> Function(String trackId) onRemove; final Future<void> Function(String trackId) onRemove;
final Future<void> Function(int oldIndex, int newIndex) onReorder; final Future<void> Function(int oldIndex, int newIndex) onReorder;
final Future<void> Function() onStop; final Future<void> Function() onStop;
@ -68,7 +67,7 @@ class PlayerQueue extends HookConsumerWidget {
return tracks return tracks
.map((e) => ( .map((e) => (
weightedRatio( weightedRatio(
'${e.name!} - ${e.artists?.asString() ?? ""}', '${e.name} - ${e.artists.asString()}',
searchText.value, searchText.value,
), ),
e e
@ -161,7 +160,8 @@ class PlayerQueue extends HookConsumerWidget {
const SizedBox(width: 10), const SizedBox(width: 10),
Tooltip( Tooltip(
tooltip: TooltipContainer( tooltip: TooltipContainer(
child: Text(context.l10n.clear_all)).call, child: Text(context.l10n.clear_all))
.call,
child: IconButton.outline( child: IconButton.outline(
icon: const Icon(SpotubeIcons.playlistRemove), icon: const Icon(SpotubeIcons.playlistRemove),
onPressed: () { onPressed: () {
@ -244,7 +244,7 @@ class PlayerQueue extends HookConsumerWidget {
icon: const Icon(SpotubeIcons.angleDown), icon: const Icon(SpotubeIcons.angleDown),
onPressed: () { onPressed: () {
controller.scrollToIndex( controller.scrollToIndex(
playlist.playlist.index, playlist.currentIndex,
preferPosition: AutoScrollPosition.middle, preferPosition: AutoScrollPosition.middle,
); );
}, },

View File

@ -2,21 +2,19 @@ import 'package:auto_route/auto_route.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/components/links/link_text.dart'; import 'package:spotube/components/links/link_text.dart';
import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
class PlayerTrackDetails extends HookConsumerWidget { class PlayerTrackDetails extends HookConsumerWidget {
final Color? color; final Color? color;
final Track? track; final SpotubeTrackObject? track;
const PlayerTrackDetails({super.key, this.color, this.track}); const PlayerTrackDetails({super.key, this.color, this.track});
@override @override
@ -37,7 +35,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
child: UniversalImage( child: UniversalImage(
path: (track?.album?.images) path: (track?.album.images)
.asUrlString(placeholder: ImagePlaceholder.albumArt), .asUrlString(placeholder: ImagePlaceholder.albumArt),
placeholder: Assets.albumPlaceholder.path, placeholder: Assets.albumPlaceholder.path,
), ),
@ -59,7 +57,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
), ),
), ),
Text( Text(
playback.activeTrack?.artists?.asString() ?? "", playback.activeTrack?.artists.asString() ?? "",
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: theme.typography.small.copyWith(color: color), style: theme.typography.small.copyWith(color: color),
) )
@ -84,7 +82,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
context.router.navigateNamed(route); context.router.navigateNamed(route);
}, },
onOverflowArtistClick: () => onOverflowArtistClick: () =>
context.navigateTo(TrackRoute(trackId: track!.id!)), context.navigateTo(TrackRoute(trackId: track!.id)),
) )
], ],
), ),

View File

@ -10,30 +10,27 @@ import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/duration.dart';
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart'; import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
import 'package:spotube/hooks/utils/use_debounce.dart'; import 'package:spotube/hooks/utils/use_debounce.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/audio_player/querying_track_info.dart';
import 'package:spotube/provider/server/active_track_sources.dart'; import 'package:spotube/provider/server/active_track_sources.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/provider/youtube_engine/youtube_engine.dart'; import 'package:spotube/provider/youtube_engine/youtube_engine.dart';
import 'package:spotube/services/sourced_track/models/source_info.dart';
import 'package:spotube/services/sourced_track/models/video_info.dart'; import 'package:spotube/services/sourced_track/models/video_info.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:spotube/services/sourced_track/sources/invidious.dart';
import 'package:spotube/services/sourced_track/sources/jiosaavn.dart'; import 'package:spotube/services/sourced_track/sources/jiosaavn.dart';
import 'package:spotube/services/sourced_track/sources/piped.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart'; import 'package:spotube/services/sourced_track/sources/youtube.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
final sourceInfoToIconMap = { final sourceInfoToIconMap = {
YoutubeSourceInfo: const Icon(SpotubeIcons.youtube, color: Color(0xFFFF0000)), AudioSource.youtube:
JioSaavnSourceInfo: Container( const Icon(SpotubeIcons.youtube, color: Color(0xFFFF0000)),
AudioSource.jiosaavn: Container(
height: 30, height: 30,
width: 30, width: 30,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -44,8 +41,8 @@ final sourceInfoToIconMap = {
), ),
), ),
), ),
PipedSourceInfo: const Icon(SpotubeIcons.piped), AudioSource.piped: const Icon(SpotubeIcons.piped),
InvidiousSourceInfo: Container( AudioSource.invidious: Container(
height: 18, height: 18,
width: 18, width: 18,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -68,25 +65,25 @@ class SiblingTracksSheet extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final playlist = ref.watch(audioPlayerProvider);
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
final preferences = ref.watch(userPreferencesProvider); final preferences = ref.watch(userPreferencesProvider);
final youtubeEngine = ref.watch(youtubeEngineProvider); final youtubeEngine = ref.watch(youtubeEngineProvider);
final isSearching = useState(false); final isSearching = useState(false);
final searchMode = useState(preferences.searchMode); final searchMode = useState(preferences.searchMode);
final activeTrackNotifier = ref.watch(activeTrackSourcesProvider.notifier); final activeTrackSources = ref.watch(activeTrackSourcesProvider);
final activeTrack = final activeTrackNotifier = activeTrackSources.asData?.value?.notifier;
ref.watch(activeTrackSourcesProvider) ?? playlist.activeTrack; final activeTrack = activeTrackSources.asData?.value?.track;
final activeTrackSource = activeTrackSources.asData?.value?.source;
final title = ServiceUtils.getTitle( final title = ServiceUtils.getTitle(
activeTrack?.name ?? "", activeTrack?.name ?? "",
artists: activeTrack?.artists?.map((e) => e.name!).toList() ?? [], artists: activeTrack?.artists.map((e) => e.name).toList() ?? [],
onlyCleanArtist: true, onlyCleanArtist: true,
).trim(); ).trim();
final defaultSearchTerm = final defaultSearchTerm =
"$title - ${activeTrack?.artists?.asString() ?? ""}"; "$title - ${activeTrack?.artists.asString() ?? ""}";
final searchController = useShadcnTextEditingController( final searchController = useShadcnTextEditingController(
text: defaultSearchTerm, text: defaultSearchTerm,
); );
@ -99,7 +96,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
final searchRequest = useMemoized(() async { final searchRequest = useMemoized(() async {
if (searchTerm.trim().isEmpty) { if (searchTerm.trim().isEmpty) {
return <SourceInfo>[]; return <TrackSourceInfo>[];
} }
if (preferences.audioSource == AudioSource.jiosaavn) { if (preferences.audioSource == AudioSource.jiosaavn) {
final resultsJioSaavn = final resultsJioSaavn =
@ -110,7 +107,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
return siblingType.info; return siblingType.info;
})); }));
final activeSourceInfo = (activeTrack! as SourcedTrack).sourceInfo; final activeSourceInfo = activeTrackSource as TrackSourceInfo;
return results return results
..removeWhere((element) => element.id == activeSourceInfo.id) ..removeWhere((element) => element.id == activeSourceInfo.id)
@ -130,7 +127,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
return siblingType.info; return siblingType.info;
}), }),
); );
final activeSourceInfo = (activeTrack! as SourcedTrack).sourceInfo; final activeSourceInfo = activeTrackSource as TrackSourceInfo;
return searchResults return searchResults
..removeWhere((element) => element.id == activeSourceInfo.id) ..removeWhere((element) => element.id == activeSourceInfo.id)
..insert( ..insert(
@ -142,6 +139,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
searchTerm, searchTerm,
searchMode.value, searchMode.value,
activeTrack, activeTrack,
activeTrackSource,
preferences.audioSource, preferences.audioSource,
youtubeEngine, youtubeEngine,
]); ]);
@ -149,25 +147,25 @@ class SiblingTracksSheet extends HookConsumerWidget {
final siblings = useMemoized( final siblings = useMemoized(
() => !isFetchingActiveTrack () => !isFetchingActiveTrack
? [ ? [
(activeTrack as SourcedTrack).sourceInfo, if (activeTrackSource != null) activeTrackSource.info,
...activeTrack.siblings, ...?activeTrackSource?.siblings,
] ]
: <SourceInfo>[], : <TrackSourceInfo>[],
[activeTrack, isFetchingActiveTrack], [activeTrackSource, isFetchingActiveTrack],
); );
final previousActiveTrack = usePrevious(activeTrack); final previousActiveTrack = usePrevious(activeTrack);
useEffect(() { useEffect(() {
/// Populate sibling when active track changes /// Populate sibling when active track changes
if (previousActiveTrack?.id == activeTrack?.id) return; if (previousActiveTrack?.id == activeTrack?.id) return;
if (activeTrack is SourcedTrack && activeTrack.siblings.isEmpty) { if (activeTrackSource != null && activeTrackSource.siblings.isEmpty) {
activeTrackNotifier.populateSibling(); activeTrackNotifier?.copyWithSibling();
} }
return null; return null;
}, [activeTrack, previousActiveTrack]); }, [activeTrack, previousActiveTrack]);
final itemBuilder = useCallback( final itemBuilder = useCallback(
(SourceInfo sourceInfo) { (TrackSourceInfo sourceInfo) {
final icon = sourceInfoToIconMap[sourceInfo.runtimeType]; final icon = sourceInfoToIconMap[sourceInfo.runtimeType];
return ButtonTile( return ButtonTile(
style: ButtonVariance.ghost, style: ButtonVariance.ghost,
@ -182,13 +180,14 @@ class SiblingTracksSheet extends HookConsumerWidget {
height: 60, height: 60,
width: 60, width: 60,
), ),
trailing: Text(sourceInfo.duration.toHumanReadableString()), trailing: Text(Duration(milliseconds: sourceInfo.durationMs)
.toHumanReadableString()),
subtitle: Row( subtitle: Row(
children: [ children: [
if (icon != null) icon, if (icon != null) icon,
Flexible( Flexible(
child: Text( child: Text(
"${sourceInfo.artist}", "${sourceInfo.artists}",
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -197,11 +196,11 @@ class SiblingTracksSheet extends HookConsumerWidget {
), ),
enabled: !isFetchingActiveTrack, enabled: !isFetchingActiveTrack,
selected: !isFetchingActiveTrack && selected: !isFetchingActiveTrack &&
sourceInfo.id == (activeTrack as SourcedTrack).sourceInfo.id, sourceInfo.id == activeTrackSource?.info.id,
onPressed: () { onPressed: () {
if (!isFetchingActiveTrack && if (!isFetchingActiveTrack &&
sourceInfo.id != (activeTrack as SourcedTrack).sourceInfo.id) { sourceInfo.id != activeTrackSource?.info.id) {
activeTrackNotifier.swapSibling(sourceInfo); activeTrackNotifier?.swapWithSibling(sourceInfo);
if (MediaQuery.sizeOf(context).mdAndUp) { if (MediaQuery.sizeOf(context).mdAndUp) {
closeOverlay(context); closeOverlay(context);
} else { } else {
@ -211,7 +210,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
}, },
); );
}, },
[activeTrack, siblings], [activeTrackSource, activeTrackNotifier, siblings],
); );
final scale = context.theme.scaling; final scale = context.theme.scaling;

View File

@ -2,8 +2,6 @@ import 'package:auto_route/auto_route.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';
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer; import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
import 'package:spotify/spotify.dart' hide Offset, Image;
import 'package:spotube/collections/env.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/dialogs/select_device_dialog.dart'; import 'package:spotube/components/dialogs/select_device_dialog.dart';
import 'package:spotube/components/playbutton_view/playbutton_card.dart'; import 'package:spotube/components/playbutton_view/playbutton_card.dart';
@ -15,9 +13,10 @@ import 'package:spotube/provider/audio_player/querying_track_info.dart';
import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/history/history.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart';
import 'package:spotube/provider/metadata_plugin/user.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:stroke_text/stroke_text.dart';
class PlaylistCard extends HookConsumerWidget { class PlaylistCard extends HookConsumerWidget {
final SpotubeSimplePlaylistObject playlist; final SpotubeSimplePlaylistObject playlist;
@ -48,26 +47,30 @@ class PlaylistCard extends HookConsumerWidget {
); );
final updating = useState(false); final updating = useState(false);
final me = ref.watch(meProvider); final me = ref.watch(metadataPluginUserProvider);
Future<List<Track>> fetchInitialTracks() async { Future<List<SpotubeTrackObject>> fetchInitialTracks() async {
if (playlist.id == 'user-liked-tracks') { if (playlist.id == 'user-liked-tracks') {
return await ref.read(likedTracksProvider.future); final tracks = await ref.read(metadataPluginSavedTracksProvider.future);
return tracks.items;
} }
final result = await ref.read(playlistTracksProvider(playlist.id).future); final result = await ref
.read(metadataPluginPlaylistTracksProvider(playlist.id).future);
return result.items; return result.items;
} }
Future<List<Track>> fetchAllTracks() async { Future<List<SpotubeTrackObject>> fetchAllTracks() async {
final initialTracks = await fetchInitialTracks(); final initialTracks = await fetchInitialTracks();
if (playlist.id == 'user-liked-tracks') { if (playlist.id == 'user-liked-tracks') {
return initialTracks; return initialTracks;
} }
return ref.read(playlistTracksProvider(playlist.id).notifier).fetchAll(); return ref
.read(metadataPluginPlaylistTracksProvider(playlist.id).notifier)
.fetchAll();
} }
void onTap() { void onTap() {
@ -94,14 +97,14 @@ class PlaylistCard extends HookConsumerWidget {
final allTracks = await fetchAllTracks(); final allTracks = await fetchAllTracks();
await remotePlayback.load( await remotePlayback.load(
WebSocketLoadEventData.playlist( WebSocketLoadEventData.playlist(
tracks: allTracks, tracks: allTracks as List<SpotubeFullTrackObject>,
// collection: playlist, collection: playlist,
), ),
); );
} else { } else {
await playlistNotifier.load(fetchedInitialTracks, autoPlay: true); await playlistNotifier.load(fetchedInitialTracks, autoPlay: true);
playlistNotifier.addCollection(playlist.id); playlistNotifier.addCollection(playlist.id);
// historyNotifier.addPlaylists([playlist]); historyNotifier.addPlaylists([playlist]);
final allTracks = await fetchAllTracks(); final allTracks = await fetchAllTracks();
@ -126,7 +129,7 @@ class PlaylistCard extends HookConsumerWidget {
playlistNotifier.addTracks(fetchedInitialTracks); playlistNotifier.addTracks(fetchedInitialTracks);
playlistNotifier.addCollection(playlist.id); playlistNotifier.addCollection(playlist.id);
// historyNotifier.addPlaylists([playlist]); historyNotifier.addPlaylists([playlist]);
if (context.mounted) { if (context.mounted) {
showToast( showToast(
context: context, context: context,
@ -141,7 +144,7 @@ class PlaylistCard extends HookConsumerWidget {
child: Text(context.l10n.undo), child: Text(context.l10n.undo),
onPressed: () { onPressed: () {
playlistNotifier playlistNotifier
.removeTracks(fetchedInitialTracks.map((e) => e.id!)); .removeTracks(fetchedInitialTracks.map((e) => e.id));
}, },
), ),
), ),
@ -159,52 +162,15 @@ class PlaylistCard extends HookConsumerWidget {
); );
final isLoading = final isLoading =
(isPlaylistPlaying && isFetchingActiveTrack) || updating.value; (isPlaylistPlaying && isFetchingActiveTrack) || updating.value;
final isOwner = final isOwner = playlist.owner.id == me.asData?.value?.id &&
playlist.owner.id == me.asData?.value.id && me.asData?.value.id != null; me.asData?.value?.id != null;
final image = playlist.owner.name == "Spotify" && Env.disableSpotifyImages
? Consumer(
builder: (context, ref, child) {
final (:color, :colorBlendMode, :src, :placement) =
ref.watch(playlistImageProvider(playlist.id));
return Stack(
children: [
Positioned.fill(
child: Image.asset(
src,
color: color,
colorBlendMode: colorBlendMode,
fit: BoxFit.cover,
),
),
Positioned.fill(
top: placement == Alignment.topLeft ? 10 : null,
left: 10,
bottom: placement == Alignment.bottomLeft ? 10 : null,
child: StrokeText(
text: playlist.name,
strokeColor: Colors.white,
strokeWidth: 3,
textColor: Colors.black,
textStyle: const TextStyle(
fontSize: 16,
fontStyle: FontStyle.italic,
),
),
),
],
);
},
)
: null;
if (_isTile) { if (_isTile) {
return PlaybuttonTile( return PlaybuttonTile(
title: playlist.name, title: playlist.name,
description: playlist.description, description: playlist.description,
image: image, image: null,
imageUrl: image == null ? imageUrl : null, imageUrl: imageUrl,
isPlaying: isPlaylistPlaying, isPlaying: isPlaylistPlaying,
isLoading: isLoading, isLoading: isLoading,
isOwner: isOwner, isOwner: isOwner,
@ -217,8 +183,8 @@ class PlaylistCard extends HookConsumerWidget {
return PlaybuttonCard( return PlaybuttonCard(
title: playlist.name, title: playlist.name,
description: playlist.description, description: playlist.description,
image: image, image: null,
imageUrl: image == null ? imageUrl : null, imageUrl: imageUrl,
isPlaying: isPlaylistPlaying, isPlaying: isPlaylistPlaying,
isLoading: isLoading, isLoading: isLoading,
isOwner: isOwner, isOwner: isOwner,

View File

@ -10,15 +10,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/form/checkbox_form_field.dart'; import 'package:spotube/components/form/checkbox_form_field.dart';
import 'package:spotube/components/form/text_form_field.dart'; import 'package:spotube/components/form/text_form_field.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
import 'package:spotube/provider/metadata_plugin/playlist/playlist.dart';
class PlaylistCreateDialog extends HookConsumerWidget { class PlaylistCreateDialog extends HookConsumerWidget {
/// Track ids to add to the playlist /// Track ids to add to the playlist
@ -32,10 +32,11 @@ class PlaylistCreateDialog extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final userPlaylists = ref.watch(favoritePlaylistsProvider); final userPlaylists = ref.watch(metadataPluginSavedPlaylistsProvider);
final playlist = ref.watch(playlistProvider(playlistId ?? "")); final playlist =
ref.watch(metadataPluginPlaylistProvider(playlistId ?? ""));
final playlistNotifier = final playlistNotifier =
ref.watch(playlistProvider(playlistId ?? "").notifier); ref.watch(metadataPluginPlaylistProvider(playlistId ?? "").notifier);
final isSubmitting = useState(false); final isSubmitting = useState(false);
@ -55,25 +56,54 @@ class PlaylistCreateDialog extends HookConsumerWidget {
final l10n = context.l10n; final l10n = context.l10n;
final theme = Theme.of(context); final theme = Theme.of(context);
useEffect(() {
if (playlist.asData?.value != null) {
formKey.currentState?.patchValue({
'playlistName': playlist.asData!.value.name,
'description': playlist.asData!.value.description,
'public': playlist.asData!.value.public,
'collaborative': playlist.asData!.value.collaborative,
});
}
return;
}, [playlist]);
final onError = useCallback((error) { final onError = useCallback((error) {
if (error is SpotifyError || error is SpotifyException) { // if (error is SpotifyError || error is SpotifyException) {
showToast( // showToast(
context: context, // context: context,
location: ToastLocation.topRight, // location: ToastLocation.topRight,
builder: (context, overlay) { // builder: (context, overlay) {
return SurfaceCard( // return SurfaceCard(
child: Basic( // child: Basic(
title: Text( // title: Text(
l10n.error(error.message ?? l10n.epic_failure), // l10n.error(error.message ?? l10n.epic_failure),
style: theme.typography.normal.copyWith( // style: theme.typography.normal.copyWith(
color: theme.colorScheme.destructive, // color: theme.colorScheme.destructive,
), // ),
// ),
// ),
// );
// },
// );
// }
showToast(
context: context,
location: ToastLocation.topRight,
builder: (context, overlay) {
return SurfaceCard(
child: Basic(
title: Text(
l10n.error(l10n.epic_failure),
style: theme.typography.normal.copyWith(
color: theme.colorScheme.destructive,
), ),
), ),
); ),
}, );
); },
} );
}, [l10n, theme]); }, [l10n, theme]);
Future<void> onCreate() async { Future<void> onCreate() async {
@ -83,7 +113,7 @@ class PlaylistCreateDialog extends HookConsumerWidget {
isSubmitting.value = true; isSubmitting.value = true;
final values = formKey.currentState!.value; final values = formKey.currentState!.value;
final PlaylistInput payload = ( final payload = (
playlistName: values['playlistName'], playlistName: values['playlistName'],
collaborative: values['collaborative'], collaborative: values['collaborative'],
public: values['public'], public: values['public'],
@ -96,9 +126,21 @@ class PlaylistCreateDialog extends HookConsumerWidget {
); );
if (isUpdatingPlaylist) { if (isUpdatingPlaylist) {
await playlistNotifier.modify(payload, onError); await playlistNotifier.modify(
name: payload.playlistName,
description: payload.description,
public: payload.public,
collaborative: payload.collaborative,
onError: onError,
);
} else { } else {
await playlistNotifier.create(payload, onError); await playlistNotifier.create(
name: payload.playlistName,
description: payload.description,
public: payload.public,
collaborative: payload.collaborative,
onError: onError,
);
} }
if (trackIds.isNotEmpty) { if (trackIds.isNotEmpty) {
@ -107,9 +149,12 @@ class PlaylistCreateDialog extends HookConsumerWidget {
} finally { } finally {
isSubmitting.value = false; isSubmitting.value = false;
if (context.mounted && if (context.mounted &&
!ref.read(playlistProvider(playlistId ?? "")).hasError) { !ref
context.router.maybePop<Playlist>( .read(metadataPluginPlaylistProvider(playlistId ?? ""))
await ref.read(playlistProvider(playlistId ?? "").future), .hasError) {
context.router.maybePop<SpotubeFullPlaylistObject>(
await ref
.read(metadataPluginPlaylistProvider(playlistId ?? "").future),
); );
} }
} }
@ -144,8 +189,8 @@ class PlaylistCreateDialog extends HookConsumerWidget {
initialValue: { initialValue: {
'playlistName': updatingPlaylist?.name, 'playlistName': updatingPlaylist?.name,
'description': updatingPlaylist?.description, 'description': updatingPlaylist?.description,
'public': updatingPlaylist?.public ?? false, 'public': playlist.asData?.value.public ?? false,
'collaborative': updatingPlaylist?.collaborative ?? false, 'collaborative': playlist.asData?.value.collaborative ?? false,
}, },
child: ListView( child: ListView(
shrinkWrap: true, shrinkWrap: true,
@ -259,7 +304,7 @@ class PlaylistCreateDialog extends HookConsumerWidget {
class PlaylistCreateDialogButton extends HookConsumerWidget { class PlaylistCreateDialogButton extends HookConsumerWidget {
const PlaylistCreateDialogButton({super.key}); const PlaylistCreateDialogButton({super.key});
showPlaylistDialog(BuildContext context, SpotifyApiWrapper spotify) { showPlaylistDialog(BuildContext context) {
showDialog( showDialog(
context: context, context: context,
alignment: Alignment.center, alignment: Alignment.center,
@ -271,12 +316,10 @@ class PlaylistCreateDialogButton extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final spotify = ref.watch(spotifyProvider);
return Button.secondary( return Button.secondary(
leading: const Icon(SpotubeIcons.addFilled), leading: const Icon(SpotubeIcons.addFilled),
child: Text(context.l10n.playlist), child: Text(context.l10n.playlist),
onPressed: () => showPlaylistDialog(context, spotify), onPressed: () => showPlaylistDialog(context),
); );
} }
} }

View File

@ -8,6 +8,7 @@ import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/modules/player/player_actions.dart'; import 'package:spotube/modules/player/player_actions.dart';
import 'package:spotube/modules/player/player_overlay.dart'; import 'package:spotube/modules/player/player_overlay.dart';
import 'package:spotube/modules/player/player_track_details.dart'; import 'package:spotube/modules/player/player_track_details.dart';
@ -15,7 +16,6 @@ import 'package:spotube/modules/player/player_controls.dart';
import 'package:spotube/modules/player/volume_slider.dart'; import 'package:spotube/modules/player/volume_slider.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
@ -35,13 +35,13 @@ class BottomPlayer extends HookConsumerWidget {
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
String albumArt = useMemoized( String albumArt = useMemoized(
() => playlist.activeTrack?.album?.images?.isNotEmpty == true () => playlist.activeTrack?.album.images.isNotEmpty == true
? (playlist.activeTrack?.album?.images).asUrlString( ? (playlist.activeTrack?.album.images).asUrlString(
index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1, index: (playlist.activeTrack?.album.images.length ?? 1) - 1,
placeholder: ImagePlaceholder.albumArt, placeholder: ImagePlaceholder.albumArt,
) )
: Assets.albumPlaceholder.path, : Assets.albumPlaceholder.path,
[playlist.activeTrack?.album?.images], [playlist.activeTrack?.album.images],
); );
// returning an empty non spacious Container as the overlay will take // returning an empty non spacious Container as the overlay will take
@ -76,7 +76,8 @@ class BottomPlayer extends HookConsumerWidget {
extraActions: [ extraActions: [
Tooltip( Tooltip(
tooltip: tooltip:
TooltipContainer(child: Text(context.l10n.mini_player)).call, TooltipContainer(child: Text(context.l10n.mini_player))
.call,
child: IconButton( child: IconButton(
variance: ButtonVariance.ghost, variance: ButtonVariance.ghost,
icon: const Icon(SpotubeIcons.miniPlayer), icon: const Icon(SpotubeIcons.miniPlayer),

View File

@ -6,13 +6,13 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/modules/connect/connect_device.dart'; import 'package:spotube/modules/connect/connect_device.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/metadata_plugin/user.dart';
class SidebarFooter extends HookConsumerWidget implements NavigationBarItem { class SidebarFooter extends HookConsumerWidget implements NavigationBarItem {
const SidebarFooter({ const SidebarFooter({
@ -25,11 +25,11 @@ class SidebarFooter extends HookConsumerWidget implements NavigationBarItem {
final router = AutoRouter.of(context, watch: true); final router = AutoRouter.of(context, watch: true);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final downloadCount = ref.watch(downloadManagerProvider).$downloadCount; final downloadCount = ref.watch(downloadManagerProvider).$downloadCount;
final userSnapshot = ref.watch(meProvider); final userSnapshot = ref.watch(metadataPluginUserProvider);
final data = userSnapshot.asData?.value; final data = userSnapshot.asData?.value;
final avatarImg = (data?.images).asUrlString( final avatarImg = (data?.images).asUrlString(
index: (data?.images?.length ?? 1) - 1, index: (data?.images.length ?? 1) - 1,
placeholder: ImagePlaceholder.artist, placeholder: ImagePlaceholder.artist,
); );
@ -102,14 +102,13 @@ class SidebarFooter extends HookConsumerWidget implements NavigationBarItem {
child: Row( child: Row(
children: [ children: [
Avatar( Avatar(
initials: initials: Avatar.getInitials(data.name),
Avatar.getInitials(data.displayName ?? "User"),
provider: UniversalImage.imageProvider(avatarImg), provider: UniversalImage.imageProvider(avatarImg),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Flexible( Flexible(
child: Text( child: Text(
data.displayName ?? context.l10n.guest, data.name,
maxLines: 1, maxLines: 1,
softWrap: false, softWrap: false,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,

View File

@ -1,15 +1,14 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/modules/album/album_card.dart'; import 'package:spotube/modules/album/album_card.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/extensions/image.dart';
class StatsAlbumItem extends StatelessWidget { class StatsAlbumItem extends StatelessWidget {
final AlbumSimple album; final SpotubeSimpleAlbumObject album;
final Widget info; final Widget info;
const StatsAlbumItem({super.key, required this.album, required this.info}); const StatsAlbumItem({super.key, required this.album, required this.info});
@ -27,24 +26,24 @@ class StatsAlbumItem extends StatelessWidget {
height: 40, height: 40,
), ),
), ),
title: Text(album.name!), title: Text(album.name),
subtitle: Row( subtitle: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text("${album.albumType?.formatted}"), Text("${album.albumType.formatted}"),
// Flexible( Flexible(
// child: ArtistLink( child: ArtistLink(
// artists: album.artists ?? [], artists: album.artists,
// mainAxisAlignment: WrapAlignment.start, mainAxisAlignment: WrapAlignment.start,
// onOverflowArtistClick: () => onOverflowArtistClick: () =>
// context.navigateTo(AlbumRoute(id: album.id!, album: album)), context.navigateTo(AlbumRoute(id: album.id, album: album)),
// ), ),
// ), ),
], ],
), ),
trailing: info, trailing: info,
onPressed: () { onPressed: () {
// context.navigateTo(AlbumRoute(id: album.id!, album: album)); context.navigateTo(AlbumRoute(id: album.id, album: album));
}, },
); );
} }

View File

@ -1,13 +1,12 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/metadata/metadata.dart';
class StatsArtistItem extends StatelessWidget { class StatsArtistItem extends StatelessWidget {
final Artist artist; final SpotubeSimpleArtistObject artist;
final Widget info; final Widget info;
const StatsArtistItem({ const StatsArtistItem({
super.key, super.key,
@ -19,9 +18,9 @@ class StatsArtistItem extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ButtonTile( return ButtonTile(
style: ButtonVariance.ghost, style: ButtonVariance.ghost,
title: Text(artist.name!), title: Text(artist.name),
leading: Avatar( leading: Avatar(
initials: artist.name!.substring(0, 1), initials: artist.name.substring(0, 1),
provider: UniversalImage.imageProvider( provider: UniversalImage.imageProvider(
(artist.images).asUrlString( (artist.images).asUrlString(
placeholder: ImagePlaceholder.artist, placeholder: ImagePlaceholder.artist,
@ -30,7 +29,7 @@ class StatsArtistItem extends StatelessWidget {
), ),
trailing: info, trailing: info,
onPressed: () { onPressed: () {
context.navigateTo(ArtistRoute(artistId: artist.id!)); context.navigateTo(ArtistRoute(artistId: artist.id));
}, },
); );
} }

View File

@ -1,14 +1,11 @@
import 'package:auto_route/auto_route.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/extensions/string.dart'; import 'package:spotube/extensions/string.dart';
import 'package:spotube/models/metadata/metadata.dart';
class StatsPlaylistItem extends StatelessWidget { class StatsPlaylistItem extends StatelessWidget {
final PlaylistSimple playlist; final SpotubeSimplePlaylistObject playlist;
final Widget info; final Widget info;
const StatsPlaylistItem( const StatsPlaylistItem(
{super.key, required this.playlist, required this.info}); {super.key, required this.playlist, required this.info});
@ -27,9 +24,9 @@ class StatsPlaylistItem extends StatelessWidget {
height: 40, height: 40,
), ),
), ),
title: Text(playlist.name!), title: Text(playlist.name),
subtitle: Text( subtitle: Text(
playlist.description?.unescapeHtml() ?? '', playlist.description.unescapeHtml(),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),

View File

@ -1,14 +1,13 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/metadata/metadata.dart';
class StatsTrackItem extends StatelessWidget { class StatsTrackItem extends StatelessWidget {
final Track track; final SpotubeTrackObject track;
final Widget info; final Widget info;
const StatsTrackItem({ const StatsTrackItem({
super.key, super.key,
@ -23,24 +22,24 @@ class StatsTrackItem extends StatelessWidget {
leading: ClipRRect( leading: ClipRRect(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
child: UniversalImage( child: UniversalImage(
path: (track.album?.images).asUrlString( path: (track.album.images).asUrlString(
placeholder: ImagePlaceholder.albumArt, placeholder: ImagePlaceholder.albumArt,
), ),
width: 40, width: 40,
height: 40, height: 40,
), ),
), ),
title: Text(track.name!), title: Text(track.name),
subtitle: ArtistLink( subtitle: ArtistLink(
artists: track.artists!, artists: track.artists,
mainAxisAlignment: WrapAlignment.start, mainAxisAlignment: WrapAlignment.start,
onOverflowArtistClick: () { onOverflowArtistClick: () {
context.navigateTo(TrackRoute(trackId: track.id!)); context.navigateTo(TrackRoute(trackId: track.id));
}, },
), ),
trailing: info, trailing: info,
onPressed: () { onPressed: () {
context.navigateTo(TrackRoute(trackId: track.id!)); context.navigateTo(TrackRoute(trackId: track.id));
}, },
); );
} }

View File

@ -8,7 +8,7 @@ import 'package:spotube/modules/stats/common/album_item.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top.dart';
import 'package:spotube/provider/history/top/albums.dart'; import 'package:spotube/provider/history/top/albums.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/metadata_plugin/utils/common.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class TopAlbums extends HookConsumerWidget { class TopAlbums extends HookConsumerWidget {

View File

@ -9,7 +9,7 @@ import 'package:spotube/modules/stats/common/artist_item.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top.dart';
import 'package:spotube/provider/history/top/tracks.dart'; import 'package:spotube/provider/history/top/tracks.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/metadata_plugin/utils/common.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class TopArtists extends HookConsumerWidget { class TopArtists extends HookConsumerWidget {
@ -24,14 +24,8 @@ class TopArtists extends HookConsumerWidget {
final topTracksNotifier = final topTracksNotifier =
ref.watch(historyTopTracksProvider(historyDuration).notifier); ref.watch(historyTopTracksProvider(historyDuration).notifier);
final artistsData = useMemoized( final artistsData =
() => topTracks.asData?.value.artists ?? [], useMemoized(() => topTracksNotifier.artists, [topTracks.asData?.value]);
[topTracks.asData?.value],
);
for (final artist in artistsData) {
print("${artist.artist.name} has ${artist.artist.images?.length} images");
}
return Skeletonizer.sliver( return Skeletonizer.sliver(
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,

View File

@ -8,7 +8,7 @@ import 'package:spotube/modules/stats/common/track_item.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top.dart';
import 'package:spotube/provider/history/top/tracks.dart'; import 'package:spotube/provider/history/top/tracks.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/metadata_plugin/utils/common.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class TopTracks extends HookConsumerWidget { class TopTracks extends HookConsumerWidget {

View File

@ -8,7 +8,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/metadata_plugin/library/albums.dart'; import 'package:spotube/provider/metadata_plugin/library/albums.dart';
import 'package:spotube/provider/metadata_plugin/tracks/album.dart'; import 'package:spotube/provider/metadata_plugin/tracks/album.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/metadata_plugin/utils/common.dart';
@RoutePage() @RoutePage()
class AlbumPage extends HookConsumerWidget { class AlbumPage extends HookConsumerWidget {

View File

@ -7,15 +7,17 @@ import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/modules/artist/artist_album_list.dart'; import 'package:spotube/modules/artist/artist_album_list.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/artist/section/footer.dart'; import 'package:spotube/pages/artist/section/footer.dart';
import 'package:spotube/pages/artist/section/header.dart'; import 'package:spotube/pages/artist/section/header.dart';
// import 'package:spotube/pages/artist/section/related_artists.dart'; // import 'package:spotube/pages/artist/section/related_artists.dart';
import 'package:spotube/pages/artist/section/top_tracks.dart'; import 'package:spotube/pages/artist/section/top_tracks.dart';
import 'package:spotube/provider/metadata_plugin/artist/albums.dart';
import 'package:spotube/provider/metadata_plugin/artist/artist.dart'; import 'package:spotube/provider/metadata_plugin/artist/artist.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:spotube/provider/metadata_plugin/artist/top_tracks.dart';
import 'package:spotube/provider/metadata_plugin/artist/wikipedia.dart';
import 'package:spotube/provider/metadata_plugin/library/artists.dart';
@RoutePage() @RoutePage()
class ArtistPage extends HookConsumerWidget { class ArtistPage extends HookConsumerWidget {
@ -30,7 +32,6 @@ class ArtistPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final scrollController = useScrollController(); final scrollController = useScrollController();
final theme = Theme.of(context);
final artistQuery = ref.watch(metadataPluginArtistProvider(artistId)); final artistQuery = ref.watch(metadataPluginArtistProvider(artistId));
@ -46,14 +47,15 @@ class ArtistPage extends HookConsumerWidget {
floatingHeader: true, floatingHeader: true,
child: material.RefreshIndicator.adaptive( child: material.RefreshIndicator.adaptive(
onRefresh: () async { onRefresh: () async {
ref.invalidate(artistProvider(artistId)); ref.invalidate(metadataPluginArtistProvider(artistId));
ref.invalidate(relatedArtistsProvider(artistId)); // ref.invalidate(relatedArtistsProvider(artistId));
ref.invalidate(artistAlbumsProvider(artistId)); ref.invalidate(metadataPluginArtistAlbumsProvider(artistId));
ref.invalidate(artistIsFollowingProvider(artistId)); ref.invalidate(metadataPluginIsSavedArtistProvider(artistId));
ref.invalidate(artistTopTracksProvider(artistId)); ref.invalidate(metadataPluginArtistTopTracksProvider(artistId));
if (artistQuery.hasValue) { if (artistQuery.hasValue) {
ref.invalidate( ref.invalidate(
artistWikipediaSummaryProvider(artistQuery.asData!.value)); artistWikipediaSummaryProvider(artistQuery.asData!.value),
);
} }
}, },
child: Builder(builder: (context) { child: Builder(builder: (context) {

View File

@ -5,8 +5,7 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/metadata_plugin/artist/wikipedia.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class ArtistPageFooter extends ConsumerWidget { class ArtistPageFooter extends ConsumerWidget {

View File

@ -13,7 +13,7 @@ import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/metadata_plugin/artist/artist.dart'; import 'package:spotube/provider/metadata_plugin/artist/artist.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/metadata_plugin/library/artists.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';
class ArtistPageHeader extends HookConsumerWidget { class ArtistPageHeader extends HookConsumerWidget {
@ -31,7 +31,7 @@ class ArtistPageHeader extends HookConsumerWidget {
final auth = ref.watch(authenticationProvider); final auth = ref.watch(authenticationProvider);
ref.watch(blacklistProvider); ref.watch(blacklistProvider);
final blacklistNotifier = ref.watch(blacklistProvider.notifier); final blacklistNotifier = ref.watch(blacklistProvider.notifier);
final isBlackListed = /* blacklistNotifier.containsArtist(artist) */ false; final isBlackListed = blacklistNotifier.containsArtist(artist.id);
final image = artist.images.asUrlString( final image = artist.images.asUrlString(
placeholder: ImagePlaceholder.artist, placeholder: ImagePlaceholder.artist,
@ -45,11 +45,10 @@ class ArtistPageHeader extends HookConsumerWidget {
Consumer( Consumer(
builder: (context, ref, _) { builder: (context, ref, _) {
final isFollowingQuery = ref.watch( final isFollowingQuery = ref.watch(
artistIsFollowingProvider(artist.id!), metadataPluginIsSavedArtistProvider(artist.id),
);
final followingArtistNotifier = ref.watch(
followedArtistsProvider.notifier,
); );
final followingArtistNotifier =
ref.watch(metadataPluginSavedArtistsProvider.notifier);
return switch (isFollowingQuery) { return switch (isFollowingQuery) {
AsyncData(value: final following) => Builder( AsyncData(value: final following) => Builder(
@ -58,7 +57,7 @@ class ArtistPageHeader extends HookConsumerWidget {
return Button.outline( return Button.outline(
onPressed: () async { onPressed: () async {
await followingArtistNotifier await followingArtistNotifier
.removeArtists([artist.id!]); .removeFavorite([artist]);
}, },
child: Text(context.l10n.following), child: Text(context.l10n.following),
); );
@ -66,8 +65,7 @@ class ArtistPageHeader extends HookConsumerWidget {
return Button.primary( return Button.primary(
onPressed: () async { onPressed: () async {
await followingArtistNotifier await followingArtistNotifier.addFavorite([artist]);
.saveArtists([artist.id!]);
}, },
child: Text(context.l10n.follow), child: Text(context.l10n.follow),
); );
@ -96,12 +94,12 @@ class ArtistPageHeader extends HookConsumerWidget {
: ButtonVariance.ghost, : ButtonVariance.ghost,
onPressed: () async { onPressed: () async {
if (isBlackListed) { if (isBlackListed) {
await ref.read(blacklistProvider.notifier).remove(artist.id!); await ref.read(blacklistProvider.notifier).remove(artist.id);
} else { } else {
await ref.read(blacklistProvider.notifier).add( await ref.read(blacklistProvider.notifier).add(
BlacklistTableCompanion.insert( BlacklistTableCompanion.insert(
name: artist.name!, name: artist.name,
elementId: artist.id!, elementId: artist.id,
elementType: BlacklistedType.artist, elementType: BlacklistedType.artist,
), ),
); );
@ -184,7 +182,7 @@ class ArtistPageHeader extends HookConsumerWidget {
const Gap(10), const Gap(10),
Flexible( Flexible(
child: AutoSizeText( child: AutoSizeText(
artist.name!, artist.name,
style: constrains.smAndDown style: constrains.smAndDown
? typography.h4 ? typography.h4
: typography.h3, : typography.h3,

View File

@ -1,7 +1,5 @@
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/modules/artist/artist_card.dart';
import 'package:spotube/provider/spotify/spotify.dart';
@Deprecated("Related artists are no longer supported by Spotube") @Deprecated("Related artists are no longer supported by Spotube")
class ArtistPageRelatedArtists extends ConsumerWidget { class ArtistPageRelatedArtists extends ConsumerWidget {
@ -13,38 +11,39 @@ class ArtistPageRelatedArtists extends ConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final relatedArtists = ref.watch(relatedArtistsProvider(artistId)); return const SizedBox.shrink();
// final relatedArtists = ref.watch(relatedArtistsProvider(artistId));
return switch (relatedArtists) { // return switch (relatedArtists) {
AsyncData(value: final artists) => SliverPadding( // AsyncData(value: final artists) => SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), // padding: const EdgeInsets.symmetric(horizontal: 8.0),
sliver: SliverGrid.builder( // sliver: SliverGrid.builder(
itemCount: artists.length, // itemCount: artists.length,
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( // gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200, // maxCrossAxisExtent: 200,
mainAxisExtent: 250, // mainAxisExtent: 250,
mainAxisSpacing: 10, // mainAxisSpacing: 10,
crossAxisSpacing: 10, // crossAxisSpacing: 10,
childAspectRatio: 0.8, // childAspectRatio: 0.8,
), // ),
itemBuilder: (context, index) { // itemBuilder: (context, index) {
final artist = artists.elementAt(index); // final artist = artists.elementAt(index);
return SizedBox( // return SizedBox(
width: 180, // width: 180,
// child: ArtistCard(artist), // // child: ArtistCard(artist),
); // );
// return ArtistCard(artist); // // return ArtistCard(artist);
}, // },
), // ),
), // ),
AsyncError(:final error) => SliverToBoxAdapter( // AsyncError(:final error) => SliverToBoxAdapter(
child: Center( // child: Center(
child: Text(error.toString()), // child: Text(error.toString()),
), // ),
), // ),
_ => const SliverToBoxAdapter( // _ => const SliverToBoxAdapter(
child: Center(child: CircularProgressIndicator()), // child: Center(child: CircularProgressIndicator()),
), // ),
}; // };
} }
} }

View File

@ -1,16 +1,16 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/dialogs/select_device_dialog.dart'; import 'package:spotube/components/dialogs/select_device_dialog.dart';
import 'package:spotube/components/track_tile/track_tile.dart'; import 'package:spotube/components/track_tile/track_tile.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/metadata_plugin/artist/top_tracks.dart';
class ArtistPageTopTracks extends HookConsumerWidget { class ArtistPageTopTracks extends HookConsumerWidget {
final String artistId; final String artistId;
@ -22,10 +22,11 @@ class ArtistPageTopTracks extends HookConsumerWidget {
final playlist = ref.watch(audioPlayerProvider); final playlist = ref.watch(audioPlayerProvider);
final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final topTracksQuery = ref.watch(artistTopTracksProvider(artistId)); final topTracksQuery =
ref.watch(metadataPluginArtistTopTracksProvider(artistId));
final isPlaylistPlaying = playlist.containsTracks( final isPlaylistPlaying = playlist.containsTracks(
topTracksQuery.asData?.value ?? <Track>[], topTracksQuery.asData?.value.items ?? <SpotubeTrackObject>[],
); );
if (topTracksQuery.hasError) { if (topTracksQuery.hasError) {
@ -36,10 +37,11 @@ class ArtistPageTopTracks extends HookConsumerWidget {
); );
} }
final topTracks = topTracksQuery.asData?.value ?? final topTracks = topTracksQuery.asData?.value.items ??
List.generate(10, (index) => FakeData.track); List.generate(10, (index) => FakeData.track);
void playPlaylist(List<Track> tracks, {Track? currentTrack}) async { void playPlaylist(List<SpotubeFullTrackObject> tracks,
{SpotubeTrackObject? currentTrack}) async {
currentTrack ??= tracks.first; currentTrack ??= tracks.first;
final isRemoteDevice = await showSelectDeviceDialog(context, ref); final isRemoteDevice = await showSelectDeviceDialog(context, ref);
@ -61,7 +63,6 @@ class ArtistPageTopTracks extends HookConsumerWidget {
), ),
); );
} else if (isPlaylistPlaying && } else if (isPlaylistPlaying &&
currentTrack.id != null &&
currentTrack.id != remotePlaylist.activeTrack?.id) { currentTrack.id != remotePlaylist.activeTrack?.id) {
final index = playlist.tracks final index = playlist.tracks
.toList() .toList()
@ -76,7 +77,6 @@ class ArtistPageTopTracks extends HookConsumerWidget {
autoPlay: true, autoPlay: true,
); );
} else if (isPlaylistPlaying && } else if (isPlaylistPlaying &&
currentTrack.id != null &&
currentTrack.id != playlist.activeTrack?.id) { currentTrack.id != playlist.activeTrack?.id) {
await playlistNotifier.jumpToTrack(currentTrack); await playlistNotifier.jumpToTrack(currentTrack);
} }

View File

@ -8,6 +8,7 @@ import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/modules/player/player_queue.dart'; import 'package:spotube/modules/player/player_queue.dart';
import 'package:spotube/modules/player/volume_slider.dart'; import 'package:spotube/modules/player/volume_slider.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
@ -17,7 +18,6 @@ import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/duration.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/provider/connect/clients.dart'; import 'package:spotube/provider/connect/clients.dart';
import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart';
import 'package:media_kit/media_kit.dart' hide Track; import 'package:media_kit/media_kit.dart' hide Track;
@ -120,7 +120,7 @@ class ConnectControlPage extends HookConsumerWidget {
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
child: UniversalImage( child: UniversalImage(
path: (playlist.activeTrack?.album?.images) path: (playlist.activeTrack?.album.images)
.asUrlString( .asUrlString(
placeholder: ImagePlaceholder.albumArt, placeholder: ImagePlaceholder.albumArt,
), ),
@ -140,8 +140,7 @@ class ConnectControlPage extends HookConsumerWidget {
onTap: () { onTap: () {
if (playlist.activeTrack == null) return; if (playlist.activeTrack == null) return;
context.navigateTo( context.navigateTo(
TrackRoute( TrackRoute(trackId: playlist.activeTrack!.id),
trackId: playlist.activeTrack!.id!),
); );
}, },
), ),
@ -152,7 +151,7 @@ class ConnectControlPage extends HookConsumerWidget {
textStyle: typography.normal, textStyle: typography.normal,
mainAxisAlignment: WrapAlignment.start, mainAxisAlignment: WrapAlignment.start,
onOverflowArtistClick: () => context.navigateTo( onOverflowArtistClick: () => context.navigateTo(
TrackRoute(trackId: playlist.activeTrack!.id!), TrackRoute(trackId: playlist.activeTrack!.id),
), ),
), ),
), ),

View File

@ -1,6 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/language_codes.dart'; import 'package:spotube/collections/language_codes.dart';
import 'package:spotube/collections/spotify_markets.dart'; import 'package:spotube/collections/spotify_markets.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
@ -14,7 +13,7 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget {
const GettingStartedPageLanguageRegionSection( const GettingStartedPageLanguageRegionSection(
{super.key, required this.onNext}); {super.key, required this.onNext});
bool filterMarkets(Market item, String query) { bool filterMarkets(dynamic item, String query) {
final market = spotifyMarkets final market = spotifyMarkets
.firstWhere((element) => element.$1 == item) .firstWhere((element) => element.$1 == item)
.$2 .$2
@ -64,7 +63,7 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget {
const Gap(8), const Gap(8),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: Select<Market>( child: Select(
value: preferences.market, value: preferences.market,
onChanged: (value) { onChanged: (value) {
if (value == null) return; if (value == null) return;

View File

@ -1,145 +0,0 @@
import 'package:flutter/material.dart' show CollapseMode, FlexibleSpaceBar;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotify/spotify.dart' hide Offset;
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/components/playbutton_view/playbutton_view.dart';
import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart';
import 'package:spotube/modules/playlist/playlist_card.dart';
import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/utils/platform.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class GenrePlaylistsPage extends HookConsumerWidget {
static const name = "genre_playlists";
final Category category;
final String id;
const GenrePlaylistsPage({
super.key,
@PathParam("categoryId") required this.id,
required this.category,
});
@override
Widget build(BuildContext context, ref) {
final mediaQuery = MediaQuery.of(context);
final playlists = ref.watch(categoryPlaylistsProvider(category.id!));
final playlistsNotifier =
ref.read(categoryPlaylistsProvider(category.id!).notifier);
final scrollController = useScrollController();
useCustomStatusBarColor(
Colors.black,
context.watchRouter.topRoute.name == GenrePlaylistsRoute.name,
noSetBGColor: true,
automaticSystemUiAdjustment: false,
);
return SafeArea(
child: Scaffold(
headers: [
if (kIsDesktop)
const TitleBar(
leading: [
BackButton(),
],
backgroundColor: Colors.transparent,
surfaceOpacity: 0,
surfaceBlur: 0,
)
],
floatingHeader: true,
child: DecoratedBox(
decoration: BoxDecoration(
image: DecorationImage(
image: UniversalImage.imageProvider(category.icons!.first.url!),
alignment: Alignment.topCenter,
fit: BoxFit.cover,
repeat: ImageRepeat.noRepeat,
matchTextDirection: true,
),
),
child: SurfaceCard(
borderRadius: BorderRadius.zero,
padding: EdgeInsets.zero,
child: CustomScrollView(
controller: scrollController,
slivers: [
SliverSafeArea(
bottom: false,
sliver: SliverAppBar(
automaticallyImplyLeading: false,
leading: kIsMobile ? const BackButton() : null,
expandedHeight: mediaQuery.mdAndDown ? 200 : 150,
title: const Text(""),
backgroundColor: Colors.transparent,
flexibleSpace: FlexibleSpaceBar(
centerTitle: kIsDesktop,
title: Text(
category.name!,
style: context.theme.typography.h3.copyWith(
color: Colors.white,
letterSpacing: 3,
shadows: [
Shadow(
offset: const Offset(-1.5, -1.5),
color: Colors.black.withAlpha(138),
),
Shadow(
offset: const Offset(1.5, -1.5),
color: Colors.black.withAlpha(138),
),
Shadow(
offset: const Offset(1.5, 1.5),
color: Colors.black.withAlpha(138),
),
Shadow(
offset: const Offset(-1.5, 1.5),
color: Colors.black.withAlpha(138),
),
],
),
),
collapseMode: CollapseMode.parallax,
),
),
),
const SliverGap(20),
// SliverSafeArea(
// top: false,
// sliver: SliverPadding(
// padding: EdgeInsets.symmetric(
// horizontal: mediaQuery.mdAndDown ? 12 : 24,
// ),
// sliver: PlaybuttonView(
// controller: scrollController,
// itemCount: playlists.asData?.value.items.length ?? 0,
// isLoading: playlists.isLoading,
// hasMore: playlists.asData?.value.hasMore == true,
// onRequestMore: playlistsNotifier.fetchMore,
// listItemBuilder: (context, index) => PlaylistCard.tile(
// playlists.asData!.value.items[index]),
// gridItemBuilder: (context, index) =>
// PlaylistCard(playlists.asData!.value.items[index]),
// ),
// ),
// ),
const SliverGap(20),
],
),
),
),
),
);
}
}

View File

@ -1,106 +0,0 @@
import 'dart:math';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/gradients.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class GenrePage extends HookConsumerWidget {
static const name = "genre";
const GenrePage({super.key});
@override
Widget build(BuildContext context, ref) {
final scrollController = useScrollController();
final categories = ref.watch(categoriesProvider);
final mediaQuery = MediaQuery.of(context);
return SafeArea(
child: Scaffold(
headers: [
TitleBar(
title: Text(context.l10n.explore_genres),
)
],
child: GridView.builder(
padding: const EdgeInsets.all(12),
controller: scrollController,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
childAspectRatio: 9 / 18,
maxCrossAxisExtent: mediaQuery.smAndDown ? 200 : 300,
mainAxisExtent: 200,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: categories.asData!.value.length,
itemBuilder: (context, index) {
final category = categories.asData!.value[index];
final gradient = gradients[Random().nextInt(gradients.length)];
return CardImage(
onPressed: () {
context.navigateTo(
GenrePlaylistsRoute(
id: category.id!,
category: category,
),
);
},
image: Stack(
children: [
Container(
height: 300,
width: 250,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: NetworkImage(category.icons!.first.url!),
fit: BoxFit.cover,
),
gradient: gradient,
),
),
Positioned.fill(
bottom: 10,
child: Align(
alignment: Alignment.bottomCenter,
child: AutoSizeText(
category.name!,
style: context.theme.typography.h3.copyWith(
color: Colors.white,
shadows: [
// stroke shadow
const Shadow(
color: Colors.black,
offset: Offset(1, 1),
blurRadius: 2,
),
],
),
maxLines: 1,
textAlign: TextAlign.center,
maxFontSize: context.theme.typography.h3.fontSize!,
minFontSize: context.theme.typography.large.fontSize!,
),
),
),
],
),
);
},
),
),
);
}
}

View File

@ -13,9 +13,6 @@ import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/connect/connect_device.dart'; import 'package:spotube/modules/connect/connect_device.dart';
import 'package:spotube/modules/home/sections/featured.dart'; import 'package:spotube/modules/home/sections/featured.dart';
import 'package:spotube/modules/home/sections/sections.dart'; import 'package:spotube/modules/home/sections/sections.dart';
import 'package:spotube/modules/home/sections/friends.dart';
import 'package:spotube/modules/home/sections/genres/genres.dart';
import 'package:spotube/modules/home/sections/made_for_user.dart';
import 'package:spotube/modules/home/sections/new_releases.dart'; import 'package:spotube/modules/home/sections/new_releases.dart';
import 'package:spotube/modules/home/sections/recent.dart'; import 'package:spotube/modules/home/sections/recent.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
@ -76,18 +73,18 @@ class HomePage extends HookConsumerWidget {
else if (kIsMacOS) else if (kIsMacOS)
const SliverGap(10), const SliverGap(10),
const SliverGap(10), const SliverGap(10),
// SliverList.builder( SliverList.builder(
// itemCount: 5, itemCount: 3,
// itemBuilder: (context, index) { itemBuilder: (context, index) {
// return switch (index) { return switch (index) {
// 0 => const HomeGenresSection(), // 0 => const HomeGenresSection(),
// 1 => const HomeRecentlyPlayedSection(), 0 => const HomeRecentlyPlayedSection(),
// 2 => const HomeFeaturedSection(), 1 => const HomeFeaturedSection(),
// 3 => const HomePageFriendsSection(), // 3 => const HomePageFriendsSection(),
// _ => const HomeNewReleasesSection() _ => const HomeNewReleasesSection()
// }; };
// }, },
// ), ),
const HomePageBrowseSection(), const HomePageBrowseSection(),
], ],
), ),

View File

@ -1,717 +0,0 @@
import 'package:collection/collection.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotify_markets.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/modules/library/playlist_generate/recommendation_attribute_dials.dart';
import 'package:spotube/modules/library/playlist_generate/recommendation_attribute_fields.dart';
import 'package:spotube/modules/library/playlist_generate/seeds_multi_autocomplete.dart';
import 'package:spotube/modules/library/playlist_generate/simple_track_tile.dart';
import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/models/spotify/recommendation_seeds.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:auto_route/auto_route.dart';
const RecommendationAttribute zeroValues = (min: 0, target: 0, max: 0);
@RoutePage()
class PlaylistGeneratorPage extends HookConsumerWidget {
static const name = "playlist_generator";
const PlaylistGeneratorPage({super.key});
@override
Widget build(BuildContext context, ref) {
final spotify = ref.watch(spotifyProvider);
final theme = Theme.of(context);
final typography = theme.typography;
final preferences = ref.watch(userPreferencesProvider);
final genresCollection = ref.watch(categoryGenresProvider);
final limit = useValueNotifier<int>(10);
final market = useValueNotifier<Market>(preferences.market);
final genres = useState<List<String>>([]);
final artists = useState<List<Artist>>([]);
final tracks = useState<List<Track>>([]);
final enabled =
genres.value.length + artists.value.length + tracks.value.length < 5;
final leftSeedCount =
5 - genres.value.length - artists.value.length - tracks.value.length;
// Dial (int 0-1) attributes
final min = useState<RecommendationSeeds>(RecommendationSeeds());
final max = useState<RecommendationSeeds>(RecommendationSeeds());
final target = useState<RecommendationSeeds>(RecommendationSeeds());
final artistAutoComplete = SeedsMultiAutocomplete<Artist>(
seeds: artists,
enabled: enabled,
label: Text(context.l10n.artists),
placeholder: Text(context.l10n.select_up_to_count_type(
leftSeedCount,
context.l10n.artists,
)),
fetchSeeds: (textEditingValue) => spotify.invoke(
(api) => api.search
.get(
textEditingValue.text,
types: [SearchType.artist],
)
.first(6)
.then(
(v) => List.castFrom<dynamic, Artist>(
v.expand((e) => e.items ?? []).toList(),
)
.where(
(element) =>
artists.value.none((artist) => element.id == artist.id),
)
.toList(),
),
),
autocompleteOptionBuilder: (option, onSelected) => ButtonTile(
leading: Avatar(
initials: "O",
provider: UniversalImage.imageProvider(
option.images.asUrlString(
placeholder: ImagePlaceholder.artist,
),
),
),
title: Text(option.name!),
subtitle: option.genres?.isNotEmpty != true
? null
: Wrap(
spacing: 4,
runSpacing: 4,
children: option.genres!.mapIndexed(
(index, genre) {
return Chip(
style: ButtonVariance.secondary,
child: Text(genre),
);
},
).toList(),
),
onPressed: () => onSelected(option),
style: ButtonVariance.ghost,
),
displayStringForOption: (option) => option.name!,
selectedSeedBuilder: (artist) => OutlineBadge(
leading: Avatar(
initials: artist.name!.substring(0, 1),
size: 30,
provider: UniversalImage.imageProvider(
artist.images.asUrlString(
placeholder: ImagePlaceholder.artist,
),
),
),
trailing: IconButton.ghost(
icon: const Icon(SpotubeIcons.close),
onPressed: () {
artists.value = [
...artists.value
..removeWhere((element) => element.id == artist.id)
];
},
),
child: Text(artist.name!),
),
);
final tracksAutocomplete = SeedsMultiAutocomplete<Track>(
seeds: tracks,
enabled: enabled,
selectedItemDisplayType: SelectedItemDisplayType.list,
label: Text(context.l10n.tracks),
placeholder: Text(context.l10n.select_up_to_count_type(
leftSeedCount,
context.l10n.tracks,
)),
fetchSeeds: (textEditingValue) => spotify.invoke(
(api) => api.search
.get(
textEditingValue.text,
types: [SearchType.track],
)
.first(6)
.then(
(v) => List.castFrom<dynamic, Track>(
v.expand((e) => e.items ?? []).toList(),
)
.where(
(element) =>
tracks.value.none((track) => element.id == track.id),
)
.toList(),
),
),
autocompleteOptionBuilder: (option, onSelected) => ButtonTile(
leading: Avatar(
initials: option.name!.substring(0, 1),
provider: UniversalImage.imageProvider(
(option.album?.images).asUrlString(
placeholder: ImagePlaceholder.artist,
),
),
),
title: Text(option.name!),
subtitle: Text(
option.artists?.map((e) => e.name).join(", ") ??
option.album?.name ??
"",
),
onPressed: () => onSelected(option),
style: ButtonVariance.ghost,
),
displayStringForOption: (option) => option.name!,
selectedSeedBuilder: (option) => SimpleTrackTile(
track: option,
onDelete: () {
tracks.value = [
...tracks.value..removeWhere((element) => element.id == option.id)
];
},
),
);
final genreSelector = MultiSelect<String>(
value: genres.value,
onChanged: (value) {
if (!enabled) return;
genres.value = value?.toList() ?? [];
},
itemBuilder: (context, item) => Text(item),
popoverAlignment: Alignment.bottomCenter,
popupConstraints: BoxConstraints(
maxHeight: MediaQuery.sizeOf(context).height * .8,
),
placeholder: Text(
context.l10n.select_up_to_count_type(
leftSeedCount,
context.l10n.genre,
),
),
popup: SelectPopup.builder(
searchPlaceholder: Text(context.l10n.select_genres),
builder: (context, searchQuery) {
final filteredGenres = searchQuery?.isNotEmpty != true
? genresCollection.asData?.value ?? []
: genresCollection.asData?.value
.where(
(item) => item
.toLowerCase()
.contains(searchQuery!.toLowerCase()),
)
.toList() ??
[];
return SelectItemBuilder(
childCount: filteredGenres.length,
builder: (context, index) {
final option = filteredGenres[index];
return SelectItemButton(
value: option,
child: Text(option),
);
},
);
},
).call,
);
final countrySelector = ValueListenableBuilder(
valueListenable: market,
builder: (context, value, _) {
return Select<Market>(
placeholder: Text(context.l10n.country),
value: market.value,
onChanged: (value) {
market.value = value!;
},
popupConstraints: BoxConstraints(
maxHeight: MediaQuery.sizeOf(context).height * .8,
),
popoverAlignment: Alignment.bottomCenter,
itemBuilder: (context, value) => Text(value.name),
popup: SelectPopup.builder(
searchPlaceholder: Text(context.l10n.search),
builder: (context, searchQuery) {
final filteredMarkets = searchQuery == null || searchQuery.isEmpty
? spotifyMarkets
: spotifyMarkets
.where(
(item) => item.$1.name
.toLowerCase()
.contains(searchQuery.toLowerCase()),
)
.toList();
return SelectItemBuilder(
childCount: filteredMarkets.length,
builder: (context, index) {
return SelectItemButton(
value: filteredMarkets[index].$1,
child: Text(filteredMarkets[index].$2),
);
},
);
},
).call,
);
},
);
final controller = useScrollController();
return SafeArea(
bottom: false,
child: Scaffold(
headers: [
TitleBar(
leading: const [BackButton()],
title: Text(context.l10n.generate),
)
],
child: Scrollbar(
controller: controller,
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: Breakpoints.lg),
child: SafeArea(
child: LayoutBuilder(builder: (context, constrains) {
return ScrollConfiguration(
behavior: ScrollConfiguration.of(context)
.copyWith(scrollbars: false),
child: ListView(
controller: controller,
padding: const EdgeInsets.all(16),
children: [
ValueListenableBuilder(
valueListenable: limit,
builder: (context, value, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.number_of_tracks_generate,
style: typography.semiBold,
),
Row(
spacing: 5,
children: [
Container(
width: 40,
height: 40,
alignment: Alignment.center,
decoration: BoxDecoration(
color: theme.colorScheme.primary
.withAlpha(25),
shape: BoxShape.circle,
),
child: Text(
value.round().toString(),
style: typography.large.copyWith(
color: theme.colorScheme.primary,
),
),
),
Expanded(
child: Slider(
value: SliderValue.single(
value.toDouble()),
min: 10,
max: 100,
divisions: 9,
onChanged: (value) {
limit.value = value.value.round();
},
),
)
],
)
],
);
},
),
const SizedBox(height: 16),
if (constrains.mdAndUp)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: countrySelector,
),
const SizedBox(width: 16),
Expanded(
child: genreSelector,
),
],
)
else ...[
countrySelector,
const SizedBox(height: 16),
genreSelector,
],
const SizedBox(height: 16),
if (constrains.mdAndUp)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: artistAutoComplete,
),
const SizedBox(width: 16),
Expanded(
child: tracksAutocomplete,
),
],
)
else ...[
artistAutoComplete,
const SizedBox(height: 16),
tracksAutocomplete,
],
const SizedBox(height: 16),
RecommendationAttributeDials(
title: Text(context.l10n.acousticness),
values: (
target: target.value.acousticness?.toDouble() ?? 0,
min: min.value.acousticness?.toDouble() ?? 0,
max: max.value.acousticness?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
acousticness: value.target,
);
min.value = min.value.copyWith(
acousticness: value.min,
);
max.value = max.value.copyWith(
acousticness: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.danceability),
values: (
target: target.value.danceability?.toDouble() ?? 0,
min: min.value.danceability?.toDouble() ?? 0,
max: max.value.danceability?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
danceability: value.target,
);
min.value = min.value.copyWith(
danceability: value.min,
);
max.value = max.value.copyWith(
danceability: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.energy),
values: (
target: target.value.energy?.toDouble() ?? 0,
min: min.value.energy?.toDouble() ?? 0,
max: max.value.energy?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
energy: value.target,
);
min.value = min.value.copyWith(
energy: value.min,
);
max.value = max.value.copyWith(
energy: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.instrumentalness),
values: (
target:
target.value.instrumentalness?.toDouble() ?? 0,
min: min.value.instrumentalness?.toDouble() ?? 0,
max: max.value.instrumentalness?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
instrumentalness: value.target,
);
min.value = min.value.copyWith(
instrumentalness: value.min,
);
max.value = max.value.copyWith(
instrumentalness: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.liveness),
values: (
target: target.value.liveness?.toDouble() ?? 0,
min: min.value.liveness?.toDouble() ?? 0,
max: max.value.liveness?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
liveness: value.target,
);
min.value = min.value.copyWith(
liveness: value.min,
);
max.value = max.value.copyWith(
liveness: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.loudness),
values: (
target: target.value.loudness?.toDouble() ?? 0,
min: min.value.loudness?.toDouble() ?? 0,
max: max.value.loudness?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
loudness: value.target,
);
min.value = min.value.copyWith(
loudness: value.min,
);
max.value = max.value.copyWith(
loudness: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.speechiness),
values: (
target: target.value.speechiness?.toDouble() ?? 0,
min: min.value.speechiness?.toDouble() ?? 0,
max: max.value.speechiness?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
speechiness: value.target,
);
min.value = min.value.copyWith(
speechiness: value.min,
);
max.value = max.value.copyWith(
speechiness: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.valence),
values: (
target: target.value.valence?.toDouble() ?? 0,
min: min.value.valence?.toDouble() ?? 0,
max: max.value.valence?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
valence: value.target,
);
min.value = min.value.copyWith(
valence: value.min,
);
max.value = max.value.copyWith(
valence: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.popularity),
base: 100,
values: (
target: target.value.popularity?.toDouble() ?? 0,
min: min.value.popularity?.toDouble() ?? 0,
max: max.value.popularity?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
popularity: value.target,
);
min.value = min.value.copyWith(
popularity: value.min,
);
max.value = max.value.copyWith(
popularity: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.key),
base: 11,
values: (
target: target.value.key?.toDouble() ?? 0,
min: min.value.key?.toDouble() ?? 0,
max: max.value.key?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
key: value.target,
);
min.value = min.value.copyWith(
key: value.min,
);
max.value = max.value.copyWith(
key: value.max,
);
},
),
RecommendationAttributeFields(
title: Text(context.l10n.duration),
values: (
max: (max.value.durationMs ?? 0) / 1000,
target: (target.value.durationMs ?? 0) / 1000,
min: (min.value.durationMs ?? 0) / 1000,
),
onChanged: (value) {
target.value = target.value.copyWith(
durationMs: (value.target * 1000).toInt(),
);
min.value = min.value.copyWith(
durationMs: (value.min * 1000).toInt(),
);
max.value = max.value.copyWith(
durationMs: (value.max * 1000).toInt(),
);
},
presets: {
context.l10n.short: (min: 50, target: 90, max: 120),
context.l10n.medium: (
min: 120,
target: 180,
max: 200
),
context.l10n.long: (min: 480, target: 560, max: 640)
},
),
RecommendationAttributeFields(
title: Text(context.l10n.tempo),
values: (
max: max.value.tempo?.toDouble() ?? 0,
target: target.value.tempo?.toDouble() ?? 0,
min: min.value.tempo?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
tempo: value.target,
);
min.value = min.value.copyWith(
tempo: value.min,
);
max.value = max.value.copyWith(
tempo: value.max,
);
},
),
RecommendationAttributeFields(
title: Text(context.l10n.mode),
values: (
max: max.value.mode?.toDouble() ?? 0,
target: target.value.mode?.toDouble() ?? 0,
min: min.value.mode?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
mode: value.target,
);
min.value = min.value.copyWith(
mode: value.min,
);
max.value = max.value.copyWith(
mode: value.max,
);
},
),
RecommendationAttributeFields(
title: Text(context.l10n.time_signature),
values: (
max: max.value.timeSignature?.toDouble() ?? 0,
target: target.value.timeSignature?.toDouble() ?? 0,
min: min.value.timeSignature?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
timeSignature: value.target,
);
min.value = min.value.copyWith(
timeSignature: value.min,
);
max.value = max.value.copyWith(
timeSignature: value.max,
);
},
),
const Gap(20),
Center(
child: Button.primary(
leading: const Icon(SpotubeIcons.magic),
onPressed: artists.value.isEmpty &&
tracks.value.isEmpty &&
genres.value.isEmpty
? null
: () {
final routeState =
GeneratePlaylistProviderInput(
seedArtists: artists.value
.map((a) => a.id!)
.toList(),
seedTracks: tracks.value
.map((t) => t.id!)
.toList(),
seedGenres: genres.value,
limit: limit.value,
max: max.value,
min: min.value,
target: target.value,
);
context.navigateTo(
PlaylistGenerateResultRoute(
state: routeState,
),
);
},
child: Text(context.l10n.generate),
),
),
],
),
);
}),
),
),
),
),
),
);
}
}

View File

@ -1,272 +0,0 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/modules/library/playlist_generate/simple_track_tile.dart';
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/spotify/recommendation_seeds.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart';
@RoutePage()
class PlaylistGenerateResultPage extends HookConsumerWidget {
static const name = "playlist_generate_result";
final GeneratePlaylistProviderInput state;
const PlaylistGenerateResultPage({
super.key,
required this.state,
});
@override
Widget build(BuildContext context, ref) {
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final generatedPlaylist = ref.watch(generatePlaylistProvider(state));
final selectedTracks = useState<List<String>>(
generatedPlaylist.asData?.value.map((e) => e.id!).toList() ?? [],
);
useEffect(() {
if (generatedPlaylist.asData?.value != null) {
selectedTracks.value =
generatedPlaylist.asData!.value.map((e) => e.id!).toList();
}
return null;
}, [generatedPlaylist.asData?.value]);
final isAllTrackSelected = selectedTracks.value.length ==
(generatedPlaylist.asData?.value.length ?? 0);
return SafeArea(
bottom: false,
child: Scaffold(
headers: const [
TitleBar(leading: [BackButton()])
],
child: generatedPlaylist.isLoading
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const CircularProgressIndicator(),
Text(context.l10n.generating_playlist),
],
),
)
: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView(
children: [
GridView(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
mainAxisExtent: 32,
),
shrinkWrap: true,
children: [
Button.primary(
leading: const Icon(SpotubeIcons.play),
onPressed: selectedTracks.value.isEmpty
? null
: () async {
await playlistNotifier.load(
generatedPlaylist.asData!.value
.where(
(e) => selectedTracks.value
.contains(e.id!),
)
.toList(),
autoPlay: true,
);
},
child: Text(context.l10n.play),
),
Button.primary(
leading: const Icon(SpotubeIcons.queueAdd),
onPressed: selectedTracks.value.isEmpty
? null
: () async {
await playlistNotifier.addTracks(
generatedPlaylist.asData!.value.where(
(e) =>
selectedTracks.value.contains(e.id!),
),
);
if (context.mounted) {
showToast(
context: context,
location: ToastLocation.topRight,
builder: (context, overlay) {
return SurfaceCard(
child: Text(
context.l10n.add_count_to_queue(
selectedTracks.value.length,
),
),
);
},
);
}
},
child: Text(context.l10n.add_to_queue),
),
Button.primary(
leading: const Icon(SpotubeIcons.addFilled),
onPressed: selectedTracks.value.isEmpty
? null
: () async {
final playlist = await showDialog<Playlist>(
context: context,
builder: (context) => PlaylistCreateDialog(
trackIds: selectedTracks.value,
),
);
// if (playlist != null && context.mounted) {
// context.navigateTo(
// PlaylistRoute(
// id: playlist.id!,
// playlist: playlist,
// ),
// );
// }
},
child: Text(context.l10n.create_a_playlist),
),
Button.primary(
leading: const Icon(SpotubeIcons.playlistAdd),
onPressed: selectedTracks.value.isEmpty
? null
: () async {
final hasAdded = await showDialog<bool>(
context: context,
builder: (context) =>
PlaylistAddTrackDialog(
openFromPlaylist: null,
tracks: selectedTracks.value
.map(
(e) => generatedPlaylist
.asData!.value
.firstWhere(
(element) => element.id == e,
),
)
.toList(),
),
);
if (context.mounted && hasAdded == true) {
showToast(
context: context,
location: ToastLocation.topRight,
builder: (context, overlay) {
return SurfaceCard(
child: Text(
context.l10n.add_count_to_playlist(
selectedTracks.value.length,
),
),
);
},
);
}
},
child: Text(context.l10n.add_to_playlist),
)
],
),
const SizedBox(height: 16),
if (generatedPlaylist.asData?.value != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
context.l10n.selected_count_tracks(
selectedTracks.value.length,
),
),
Button.secondary(
onPressed: () {
if (isAllTrackSelected) {
selectedTracks.value = [];
} else {
selectedTracks.value = generatedPlaylist
.asData?.value
.map((e) => e.id!)
.toList() ??
[];
}
},
leading: const Icon(SpotubeIcons.selectionCheck),
child: Text(
isAllTrackSelected
? context.l10n.deselect_all
: context.l10n.select_all,
),
),
],
),
const SizedBox(height: 8),
SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final track
in generatedPlaylist.asData?.value ?? [])
Row(
spacing: 5,
children: [
Checkbox(
state: selectedTracks.value.contains(track.id)
? CheckboxState.checked
: CheckboxState.unchecked,
onChanged: (value) {
if (value == CheckboxState.checked) {
selectedTracks.value.add(track.id!);
} else {
selectedTracks.value.remove(track.id);
}
selectedTracks.value =
selectedTracks.value.toList();
},
),
Expanded(
child: GestureDetector(
onTap: () {
selectedTracks.value.contains(track.id)
? selectedTracks.value
.remove(track.id)
: selectedTracks.value.add(track.id!);
selectedTracks.value =
selectedTracks.value.toList();
},
child: SimpleTrackTile(track: track),
),
),
],
)
],
),
),
],
),
),
),
);
}
}

View File

@ -17,7 +17,6 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/metadata_plugin/auth.dart'; import 'package:spotube/provider/metadata_plugin/auth.dart';
import 'package:spotube/provider/metadata_plugin/library/albums.dart'; import 'package:spotube/provider/metadata_plugin/library/albums.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
@RoutePage() @RoutePage()
@ -61,7 +60,7 @@ class UserAlbumsPage extends HookConsumerWidget {
child: Scaffold( child: Scaffold(
child: material.RefreshIndicator.adaptive( child: material.RefreshIndicator.adaptive(
onRefresh: () async { onRefresh: () async {
ref.invalidate(favoriteAlbumsProvider); ref.invalidate(metadataPluginSavedAlbumsProvider);
}, },
child: InterScrollbar( child: InterScrollbar(
controller: controller, controller: controller,

View File

@ -19,7 +19,6 @@ import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/metadata_plugin/auth.dart'; import 'package:spotube/provider/metadata_plugin/auth.dart';
import 'package:spotube/provider/metadata_plugin/library/artists.dart'; import 'package:spotube/provider/metadata_plugin/library/artists.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
@RoutePage() @RoutePage()
@ -65,7 +64,7 @@ class UserArtistsPage extends HookConsumerWidget {
child: Scaffold( child: Scaffold(
child: material.RefreshIndicator.adaptive( child: material.RefreshIndicator.adaptive(
onRefresh: () async { onRefresh: () async {
ref.invalidate(followedArtistsProvider); ref.invalidate(metadataPluginSavedArtistsProvider);
}, },
child: InterScrollbar( child: InterScrollbar(
controller: controller, controller: controller,

View File

@ -16,10 +16,7 @@ class UserDownloadsPage extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final downloadManager = ref.watch(downloadManagerProvider); final downloadManager = ref.watch(downloadManagerProvider);
final history = [ final history = downloadManager.$backHistory;
...downloadManager.$history,
...downloadManager.$backHistory,
];
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -51,7 +48,7 @@ class UserDownloadsPage extends HookConsumerWidget {
child: ListView.builder( child: ListView.builder(
itemCount: history.length, itemCount: history.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return DownloadItem(track: history[index]); return DownloadItem(track: history.elementAt(index));
}, },
), ),
), ),

View File

@ -17,6 +17,7 @@ import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/string.dart'; import 'package:spotube/extensions/string.dart';
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart'; import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/modules/library/local_folder/cache_export_dialog.dart'; import 'package:spotube/modules/library/local_folder/cache_export_dialog.dart';
import 'package:spotube/pages/library/user_local_tracks/user_local_tracks.dart'; import 'package:spotube/pages/library/user_local_tracks/user_local_tracks.dart';
import 'package:spotube/components/expandable_search/expandable_search.dart'; import 'package:spotube/components/expandable_search/expandable_search.dart';
@ -24,9 +25,7 @@ import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/components/track_presentation/sort_tracks_dropdown.dart'; import 'package:spotube/components/track_presentation/sort_tracks_dropdown.dart';
import 'package:spotube/components/track_tile/track_tile.dart'; import 'package:spotube/components/track_tile/track_tile.dart';
import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
@ -49,8 +48,8 @@ class LocalLibraryPage extends HookConsumerWidget {
Future<void> playLocalTracks( Future<void> playLocalTracks(
WidgetRef ref, WidgetRef ref,
List<LocalTrack> tracks, { List<SpotubeLocalTrackObject> tracks, {
LocalTrack? currentTrack, SpotubeLocalTrackObject? currentTrack,
}) async { }) async {
final playlist = ref.read(audioPlayerProvider); final playlist = ref.read(audioPlayerProvider);
final playback = ref.read(audioPlayerProvider.notifier); final playback = ref.read(audioPlayerProvider.notifier);
@ -64,7 +63,6 @@ class LocalLibraryPage extends HookConsumerWidget {
autoPlay: true, autoPlay: true,
); );
} else if (isPlaylistPlaying && } else if (isPlaylistPlaying &&
currentTrack.id != null &&
currentTrack.id != playlist.activeTrack?.id) { currentTrack.id != playlist.activeTrack?.id) {
await playback.jumpToTrack(currentTrack); await playback.jumpToTrack(currentTrack);
} }
@ -296,7 +294,8 @@ class LocalLibraryPage extends HookConsumerWidget {
data: (tracks) { data: (tracks) {
final sortedTracks = useMemoized(() { final sortedTracks = useMemoized(() {
return ServiceUtils.sortTracks( return ServiceUtils.sortTracks(
tracks[location] ?? <LocalTrack>[], tracks[location] ??
<SpotubeLocalTrackObject>[],
sortBy.value); sortBy.value);
}, [sortBy.value, tracks]); }, [sortBy.value, tracks]);
@ -307,7 +306,7 @@ class LocalLibraryPage extends HookConsumerWidget {
return sortedTracks return sortedTracks
.map((e) => ( .map((e) => (
weightedRatio( weightedRatio(
"${e.name} - ${e.artists?.asString() ?? ""}", "${e.name} - ${e.artists.asString()}",
searchController.text, searchController.text,
), ),
e, e,

View File

@ -6,13 +6,13 @@ import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/hooks/utils/use_palette_color.dart'; import 'package:spotube/hooks/utils/use_palette_color.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/pages/lyrics/plain_lyrics.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart';
import 'package:spotube/pages/lyrics/synced_lyrics.dart'; import 'package:spotube/pages/lyrics/synced_lyrics.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/lyrics/synced.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
@RoutePage() @RoutePage()
@ -25,11 +25,11 @@ class LyricsPage extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final playlist = ref.watch(audioPlayerProvider); final playlist = ref.watch(audioPlayerProvider);
String albumArt = useMemoized( String albumArt = useMemoized(
() => (playlist.activeTrack?.album?.images).asUrlString( () => (playlist.activeTrack?.album.images).asUrlString(
index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1, index: (playlist.activeTrack?.album.images.length ?? 1) - 1,
placeholder: ImagePlaceholder.albumArt, placeholder: ImagePlaceholder.albumArt,
), ),
[playlist.activeTrack?.album?.images], [playlist.activeTrack?.album.images],
); );
final palette = usePaletteColor(albumArt, ref); final palette = usePaletteColor(albumArt, ref);
final selectedIndex = useState(0); final selectedIndex = useState(0);

View File

@ -5,14 +5,14 @@ import 'package:palette_generator/palette_generator.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/modules/lyrics/zoom_controls.dart'; import 'package:spotube/modules/lyrics/zoom_controls.dart';
import 'package:spotube/components/shimmers/shimmer_lyrics.dart'; import 'package:spotube/components/shimmers/shimmer_lyrics.dart';
import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/lyrics/synced.dart';
class PlainLyrics extends HookConsumerWidget { class PlainLyrics extends HookConsumerWidget {
final PaletteColor palette; final PaletteColor palette;
@ -52,7 +52,7 @@ class PlainLyrics extends HookConsumerWidget {
), ),
Center( Center(
child: Text( child: Text(
playlist.activeTrack?.artists?.asString() ?? "", playlist.activeTrack?.artists.asString() ?? "",
style: (mediaQuery.mdAndUp ? typography.h4 : typography.large) style: (mediaQuery.mdAndUp ? typography.h4 : typography.large)
.copyWith( .copyWith(
color: palette.bodyTextColor, color: palette.bodyTextColor,

View File

@ -5,16 +5,16 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:palette_generator/palette_generator.dart'; import 'package:palette_generator/palette_generator.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/modules/lyrics/zoom_controls.dart'; import 'package:spotube/modules/lyrics/zoom_controls.dart';
import 'package:spotube/components/shimmers/shimmer_lyrics.dart'; import 'package:spotube/components/shimmers/shimmer_lyrics.dart';
import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart'; import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart';
import 'package:spotube/modules/lyrics/use_synced_lyrics.dart'; import 'package:spotube/modules/lyrics/use_synced_lyrics.dart';
import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/lyrics/synced.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
@ -117,7 +117,7 @@ class SyncedLyrics extends HookConsumerWidget {
bottom: PreferredSize( bottom: PreferredSize(
preferredSize: const Size.fromHeight(40), preferredSize: const Size.fromHeight(40),
child: Text( child: Text(
playlist.activeTrack?.artists?.asString() ?? "", playlist.activeTrack?.artists.asString() ?? "",
style: style:
mediaQuery.mdAndUp ? typography.h4 : typography.x2Large, mediaQuery.mdAndUp ? typography.h4 : typography.x2Large,
), ),

View File

@ -5,8 +5,8 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/button/back_button.dart'; import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/hooks/utils/use_palette_color.dart'; import 'package:spotube/hooks/utils/use_palette_color.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/pages/lyrics/plain_lyrics.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart';
import 'package:spotube/pages/lyrics/synced_lyrics.dart'; import 'package:spotube/pages/lyrics/synced_lyrics.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
@ -19,11 +19,11 @@ class PlayerLyricsPage extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final playlist = ref.watch(audioPlayerProvider); final playlist = ref.watch(audioPlayerProvider);
String albumArt = useMemoized( String albumArt = useMemoized(
() => (playlist.activeTrack?.album?.images).asUrlString( () => (playlist.activeTrack?.album.images).asUrlString(
index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1, index: (playlist.activeTrack?.album.images.length ?? 1) - 1,
placeholder: ImagePlaceholder.albumArt, placeholder: ImagePlaceholder.albumArt,
), ),
[playlist.activeTrack?.album?.images], [playlist.activeTrack?.album.images],
); );
final selectedIndex = useState(0); final selectedIndex = useState(0);
final palette = usePaletteColor(albumArt, ref); final palette = usePaletteColor(albumArt, ref);

View File

@ -1,12 +1,11 @@
import 'package:flutter/material.dart' as material; import 'package:flutter/material.dart' as material;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/track_presentation/presentation_props.dart'; import 'package:spotube/components/track_presentation/presentation_props.dart';
import 'package:spotube/components/track_presentation/track_presentation.dart'; import 'package:spotube/components/track_presentation/track_presentation.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/pages/playlist/playlist.dart'; import 'package:spotube/pages/playlist/playlist.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
@RoutePage() @RoutePage()
@ -21,12 +20,12 @@ class LikedPlaylistPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final likedTracks = ref.watch(likedTracksProvider); final likedTracks = ref.watch(metadataPluginSavedTracksProvider);
final tracks = likedTracks.asData?.value ?? <Track>[]; final tracks = likedTracks.asData?.value.items ?? [];
return material.RefreshIndicator.adaptive( return material.RefreshIndicator.adaptive(
onRefresh: () async { onRefresh: () async {
ref.invalidate(likedTracksProvider); ref.invalidate(metadataPluginSavedTracksProvider);
}, },
child: TrackPresentation( child: TrackPresentation(
options: TrackPresentationOptions( options: TrackPresentationOptions(
@ -40,7 +39,7 @@ class LikedPlaylistPage extends HookConsumerWidget {
return tracks.toList(); return tracks.toList();
}, },
onRefresh: () async { onRefresh: () async {
ref.invalidate(likedTracksProvider); ref.invalidate(metadataPluginSavedTracksProvider);
}, },
), ),
title: playlist.name, title: playlist.name,

View File

@ -9,8 +9,9 @@ import 'package:spotube/components/track_presentation/use_is_user_playlist.dart'
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/metadata_plugin/library/playlists.dart'; import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart';
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
@RoutePage() @RoutePage()
class PlaylistPage extends HookConsumerWidget { class PlaylistPage extends HookConsumerWidget {
@ -30,8 +31,8 @@ class PlaylistPage extends HookConsumerWidget {
.watch( .watch(
metadataPluginSavedPlaylistsProvider.select( metadataPluginSavedPlaylistsProvider.select(
(value) => value.whenData( (value) => value.whenData(
(value) => (value.items as List<SpotubeSimplePlaylistObject>) (value) =>
.firstWhereOrNull((s) => s.id == _playlist.id), value.items.firstWhereOrNull((s) => s.id == _playlist.id),
), ),
), ),
) )
@ -39,11 +40,11 @@ class PlaylistPage extends HookConsumerWidget {
?.value ?? ?.value ??
_playlist; _playlist;
final tracks = ref.watch(playlistTracksProvider(playlist.id)); final tracks = ref.watch(metadataPluginPlaylistTracksProvider(playlist.id));
final tracksNotifier = final tracksNotifier =
ref.watch(playlistTracksProvider(playlist.id).notifier); ref.watch(metadataPluginPlaylistTracksProvider(playlist.id).notifier);
final isFavoritePlaylist = final isFavoritePlaylist =
ref.watch(isFavoritePlaylistProvider(playlist.id)); ref.watch(metadataPluginIsSavedPlaylistProvider(playlist.id));
final favoritePlaylistsNotifier = final favoritePlaylistsNotifier =
ref.watch(metadataPluginSavedPlaylistsProvider.notifier); ref.watch(metadataPluginSavedPlaylistsProvider.notifier);
@ -52,9 +53,9 @@ class PlaylistPage extends HookConsumerWidget {
return material.RefreshIndicator.adaptive( return material.RefreshIndicator.adaptive(
onRefresh: () async { onRefresh: () async {
ref.invalidate(playlistTracksProvider(playlist.id)); ref.invalidate(metadataPluginPlaylistTracksProvider(playlist.id));
ref.invalidate(isFavoritePlaylistProvider(playlist.id)); ref.invalidate(metadataPluginSavedPlaylistsProvider);
ref.invalidate(favoritePlaylistsProvider); ref.invalidate(metadataPluginIsSavedPlaylistProvider(playlist.id));
}, },
child: TrackPresentation( child: TrackPresentation(
options: TrackPresentationOptions( options: TrackPresentationOptions(
@ -67,7 +68,7 @@ class PlaylistPage extends HookConsumerWidget {
isLoading: tracks.isLoading || tracks.isLoadingNextPage, isLoading: tracks.isLoading || tracks.isLoadingNextPage,
onFetchMore: tracksNotifier.fetchMore, onFetchMore: tracksNotifier.fetchMore,
onRefresh: () async { onRefresh: () async {
ref.invalidate(playlistTracksProvider(playlist.id)); ref.invalidate(metadataPluginPlaylistTracksProvider(playlist.id));
}, },
onFetchAll: () async { onFetchAll: () async {
return await tracksNotifier.fetchAll(); return await tracksNotifier.fetchAll();

View File

@ -1,16 +1,14 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:sliver_tools/sliver_tools.dart'; import 'package:sliver_tools/sliver_tools.dart';
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/spotify_markets.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/metadata_plugin/user.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
@ -22,22 +20,22 @@ class ProfilePage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final me = ref.watch(meProvider); final me = ref.watch(metadataPluginUserProvider);
final meData = me.asData?.value ?? FakeData.user; final meData = me.asData?.value ?? FakeData.user;
final userProperties = useMemoized( // final userProperties = useMemoized(
() => { // () => {
context.l10n.email: meData.email ?? "N/A", // context.l10n.email: meData.email ?? "N/A",
context.l10n.profile_followers: // context.l10n.profile_followers:
meData.followers?.total.toString() ?? "N/A", // meData.followers?.total.toString() ?? "N/A",
context.l10n.birthday: meData.birthdate ?? context.l10n.not_born, // context.l10n.birthday: meData.birthdate ?? context.l10n.not_born,
context.l10n.country: spotifyMarkets // context.l10n.country: spotifyMarkets
.firstWhere((market) => market.$1 == meData.country) // .firstWhere((market) => market.$1 == meData.country)
.$2, // .$2,
context.l10n.subscription: meData.product ?? context.l10n.hacker, // context.l10n.subscription: meData.product ?? context.l10n.hacker,
}, // },
[meData], // [meData],
); // );
return SafeArea( return SafeArea(
child: Scaffold( child: Scaffold(
@ -72,7 +70,7 @@ class ProfilePage extends HookConsumerWidget {
const SliverGap(10), const SliverGap(10),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Text( child: Text(
meData.displayName ?? context.l10n.no_name, meData.name,
textAlign: TextAlign.center, textAlign: TextAlign.center,
).h4(), ).h4(),
), ),
@ -97,42 +95,42 @@ class ProfilePage extends HookConsumerWidget {
), ),
), ),
), ),
SliverCrossAxisConstrained( // SliverCrossAxisConstrained(
maxCrossAxisExtent: 500, // maxCrossAxisExtent: 500,
child: SliverToBoxAdapter( // child: SliverToBoxAdapter(
child: Card( // child: Card(
child: Padding( // child: Padding(
padding: const EdgeInsets.all(8.0), // padding: const EdgeInsets.all(8.0),
child: Table( // child: Table(
columnWidths: const { // columnWidths: const {
0: FixedTableSize(120), // 0: FixedTableSize(120),
}, // },
defaultRowHeight: const FixedTableSize(40), // defaultRowHeight: const FixedTableSize(40),
rows: [ // rows: [
for (final MapEntry(:key, :value) // for (final MapEntry(:key, :value)
in userProperties.entries) // in userProperties.entries)
TableRow( // TableRow(
cells: [ // cells: [
TableCell( // TableCell(
child: Padding( // child: Padding(
padding: const EdgeInsets.all(6), // padding: const EdgeInsets.all(6),
child: Text(key).large(), // child: Text(key).large(),
), // ),
), // ),
TableCell( // TableCell(
child: Padding( // child: Padding(
padding: const EdgeInsets.all(6), // padding: const EdgeInsets.all(6),
child: Text(value), // child: Text(value),
), // ),
), // ),
], // ],
) // )
], // ],
), // ),
), // ),
), // ),
), // ),
), // ),
const SliverGap(200), const SliverGap(200),
], ],
), ),

View File

@ -19,10 +19,13 @@ import 'package:spotube/pages/search/sections/artists.dart';
import 'package:spotube/pages/search/sections/playlists.dart'; import 'package:spotube/pages/search/sections/playlists.dart';
import 'package:spotube/provider/metadata_plugin/auth.dart'; import 'package:spotube/provider/metadata_plugin/auth.dart';
import 'package:spotube/provider/metadata_plugin/search/all.dart'; import 'package:spotube/provider/metadata_plugin/search/all.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
final searchTermStateProvider = StateProvider<String>((ref) {
return "";
});
@RoutePage() @RoutePage()
class SearchPage extends HookConsumerWidget { class SearchPage extends HookConsumerWidget {
static const name = "search"; static const name = "search";

View File

@ -3,8 +3,8 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/search/search.dart';
import 'package:spotube/provider/metadata_plugin/search/all.dart'; import 'package:spotube/provider/metadata_plugin/search/all.dart';
import 'package:spotube/provider/spotify/spotify.dart';
class SearchAlbumsSection extends HookConsumerWidget { class SearchAlbumsSection extends HookConsumerWidget {
const SearchAlbumsSection({ const SearchAlbumsSection({

View File

@ -3,8 +3,8 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/search/search.dart';
import 'package:spotube/provider/metadata_plugin/search/all.dart'; import 'package:spotube/provider/metadata_plugin/search/all.dart';
import 'package:spotube/provider/spotify/spotify.dart';
class SearchArtistsSection extends HookConsumerWidget { class SearchArtistsSection extends HookConsumerWidget {
const SearchArtistsSection({ const SearchArtistsSection({

Some files were not shown because too many files have changed in this diff Show More