diff --git a/lib/collections/fake.dart b/lib/collections/fake.dart index 09bb8087..7d201ae2 100644 --- a/lib/collections/fake.dart +++ b/lib/collections/fake.dart @@ -1,19 +1,13 @@ -import 'package:spotify/spotify.dart'; import 'package:spotube/models/database/database.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'; abstract class FakeData { - static final Image image = Image() - ..height = 1 - ..width = 1 - ..url = "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg"; - - static final Followers followers = Followers() - ..href = "text" - ..total = 1; + static final SpotubeImageObject image = SpotubeImageObject( + height: 100, + width: 100, + url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg", + ); static final SpotubeFullArtistObject artist = SpotubeFullArtistObject( id: "1", @@ -30,43 +24,26 @@ abstract class FakeData { ], ); - static final externalIds = ExternalIds() - ..isrc = "text" - ..ean = "text" - ..upc = "text"; + static final SpotubeFullAlbumObject album = SpotubeFullAlbumObject( + id: "1", + name: "A good album", + 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 Album album = Album() - ..id = "1" - ..genres = ["genre"] - ..label = "label" - ..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 SpotubeSimpleArtistObject artistSimple = + SpotubeSimpleArtistObject( + id: "1", + name: "What an artist", + externalUri: "https://example.com", + images: null, + ); static final SpotubeSimpleAlbumObject albumSimple = SpotubeSimpleAlbumObject( albumType: SpotubeAlbumType.album, @@ -84,163 +61,51 @@ abstract class FakeData { ], ); - static final Track track = Track() - ..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( + static final SpotubeFullTrackObject track = SpotubeTrackObject.full( id: "1", - name: "A Track Name", - artists: [], - album: albumSimple, + name: "A good track", externalUri: "https://example.com", - durationMs: 50000, + album: albumSimple, + durationMs: 3 * 60 * 1000, // 3 minutes + isrc: "USUM72112345", 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() - ..id = "1" - ..type = "type" - ..uri = "uri" - ..externalUrls = {"spotify": "text"} - ..href = "text"; + static final SpotubeFullPlaylistObject playlist = SpotubeFullPlaylistObject( + id: "1", + name: "A good playlist", + description: "A very good playlist description", + externalUri: "https://example.com", + collaborative: false, + public: true, + owner: user, + images: [image], + collaborators: [user]); - static final Paging paging = Paging() - ..href = "text" - ..itemsNative = [track.toJson()] - ..limit = 1 - ..next = "text" - ..offset = 1 - ..previous = "text" - ..total = 1; - - 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 SpotubeSimplePlaylistObject playlistSimple = + SpotubeSimplePlaylistObject( + id: "1", + name: "A good playlist", + description: "A very good playlist description", + externalUri: "https://example.com", + owner: user, + images: [image], ); - static final feedSection = SpotifyHomeFeedSection( - typename: "HomeGenericSectionData", - uri: "spotify:section:lol", - title: "Dummy", - items: [ - for (int i = 0; i < 10; i++) - SpotifyHomeFeedSectionItem( - 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 final SpotubeBrowseSectionObject browseSection = + SpotubeBrowseSectionObject( + id: "section-id", + title: "Browse Section", + browseMore: true, + externalUri: "https://example.com/browse/section", + items: [playlistSimple, playlistSimple, playlistSimple]); static const historySummary = PlaybackHistorySummary( albums: 1, diff --git a/lib/collections/spotify_markets.dart b/lib/collections/spotify_markets.dart index 514b3f0b..c4788022 100644 --- a/lib/collections/spotify_markets.dart +++ b/lib/collections/spotify_markets.dart @@ -1,6 +1,6 @@ // Country Codes contributed by momobobe -import 'package:spotify/spotify.dart'; +import 'package:spotube/models/metadata/market.dart'; final spotifyMarkets = [ (Market.AL, "Albania (AL)"), diff --git a/lib/components/dialogs/playlist_add_track_dialog.dart b/lib/components/dialogs/playlist_add_track_dialog.dart index 5098bf9d..8bdd24bd 100644 --- a/lib/components/dialogs/playlist_add_track_dialog.dart +++ b/lib/components/dialogs/playlist_add_track_dialog.dart @@ -1,18 +1,18 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.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/components/image/universal_image.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/extensions/image.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'; class PlaylistAddTrackDialog extends HookConsumerWidget { /// The id of the playlist this dialog was opened from final String? openFromPlaylist; - final List tracks; + final List tracks; const PlaylistAddTrackDialog({ required this.tracks, required this.openFromPlaylist, @@ -22,24 +22,23 @@ class PlaylistAddTrackDialog extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final typography = Theme.of(context).typography; - final userPlaylists = ref.watch(favoritePlaylistsProvider); + final userPlaylists = ref.watch(metadataPluginSavedPlaylistsProvider); final favoritePlaylistsNotifier = - ref.watch(favoritePlaylistsProvider.notifier); + ref.watch(metadataPluginSavedPlaylistsProvider.notifier); - final me = ref.watch(meProvider); + final me = ref.watch(metadataPluginUserProvider); final filteredPlaylists = useMemoized( () => userPlaylists.asData?.value.items .where( (playlist) => - playlist.owner?.id != null && - playlist.owner!.id == me.asData?.value.id && + playlist.owner.id == me.asData?.value?.id && playlist.id != openFromPlaylist, ) .toList() ?? [], - [userPlaylists.asData?.value, me.asData?.value.id, openFromPlaylist], + [userPlaylists.asData?.value, me.asData?.value?.id, openFromPlaylist], ); final playlistsCheck = useState({}); @@ -60,7 +59,7 @@ class PlaylistAddTrackDialog extends HookConsumerWidget { selectedPlaylists.map( (playlistId) => favoritePlaylistsNotifier.addTracks( playlistId, - tracks.map((e) => e.id!).toList(), + tracks.map((e) => e.id).toList(), ), ), ).then((_) => context.mounted ? Navigator.pop(context, true) : null); @@ -109,8 +108,7 @@ class PlaylistAddTrackDialog extends HookConsumerWidget { }, ), leading: Avatar( - initials: - Avatar.getInitials(playlist.name ?? "Playlist"), + initials: Avatar.getInitials(playlist.name), provider: UniversalImage.imageProvider( playlist.images.asUrlString( placeholder: ImagePlaceholder.collection, @@ -124,20 +122,20 @@ class PlaylistAddTrackDialog extends HookConsumerWidget { onChanged: (val) { playlistsCheck.value = { ...playlistsCheck.value, - playlist.id!: val == CheckboxState.checked, + playlist.id: val == CheckboxState.checked, }; }, ), onPressed: () { playlistsCheck.value = { ...playlistsCheck.value, - playlist.id!: + playlist.id: !(playlistsCheck.value[playlist.id] ?? false), }; }, child: Padding( padding: const EdgeInsets.only(left: 8.0), - child: Text(playlist.name!), + child: Text(playlist.name), ), ); }, diff --git a/lib/components/dialogs/replace_downloaded_dialog.dart b/lib/components/dialogs/replace_downloaded_dialog.dart index 3a0f3a1d..6634a039 100644 --- a/lib/components/dialogs/replace_downloaded_dialog.dart +++ b/lib/components/dialogs/replace_downloaded_dialog.dart @@ -1,13 +1,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/models/metadata/metadata.dart'; final replaceDownloadedFileState = StateProvider((ref) => null); class ReplaceDownloadedDialog extends ConsumerWidget { - final Track track; + final SpotubeTrackObject track; const ReplaceDownloadedDialog({required this.track, super.key}); @override @@ -16,7 +16,7 @@ class ReplaceDownloadedDialog extends ConsumerWidget { final replaceAll = ref.watch(replaceDownloadedFileState); return AlertDialog( - title: Text(context.l10n.track_exists(track.name ?? "")), + title: Text(context.l10n.track_exists(track.name)), content: RadioGroup( value: groupValue, onChanged: (value) { diff --git a/lib/components/dialogs/track_details_dialog.dart b/lib/components/dialogs/track_details_dialog.dart index 7237afc6..f916aefb 100644 --- a/lib/components/dialogs/track_details_dialog.dart +++ b/lib/components/dialogs/track_details_dialog.dart @@ -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:spotify/spotify.dart'; -import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/links/artist_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/context.dart'; -import 'package:spotube/services/sourced_track/sourced_track.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 { - final Track track; +class TrackDetailsDialog extends HookConsumerWidget { + final SpotubeFullTrackObject track; const TrackDetailsDialog({ super.key, required this.track, }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, ref) { final theme = Theme.of(context); final mediaQuery = MediaQuery.of(context); + final sourcedTrack = + ref.read(trackSourcesProvider(TrackSourceQuery.fromTrack(track))); final detailsMap = { - context.l10n.title: track.name!, + context.l10n.title: track.name, context.l10n.artist: ArtistLink( - artists: track.artists ?? [], + artists: track.artists, mainAxisAlignment: WrapAlignment.start, textStyle: const TextStyle(color: Colors.blue), hideOverflowArtist: false, @@ -37,17 +39,15 @@ class TrackDetailsDialog extends HookWidget { // overflow: TextOverflow.ellipsis, // style: const TextStyle(color: Colors.blue), // ), - context.l10n.duration: (track is SourcedTrack - ? (track as SourcedTrack).sourceInfo.duration - : track.duration!) - .toHumanReadableString(), - if (track.album!.releaseDate != null) - context.l10n.released: track.album!.releaseDate, - context.l10n.popularity: track.popularity?.toString() ?? "0", + context.l10n.duration: sourcedTrack.asData != null + ? Duration(milliseconds: sourcedTrack.asData!.value.info.durationMs) + .toHumanReadableString() + : Duration(milliseconds: track.durationMs).toHumanReadableString(), + if (track.album.releaseDate != null) + context.l10n.released: track.album.releaseDate, }; - final sourceInfo = - track is SourcedTrack ? (track as SourcedTrack).sourceInfo : null; + final sourceInfo = sourcedTrack.asData?.value.info; final ytTracksDetailsMap = sourceInfo == null ? {} @@ -58,12 +58,7 @@ class TrackDetailsDialog extends HookWidget { maxLines: 2, overflow: TextOverflow.ellipsis, ), - context.l10n.channel: Hyperlink( - sourceInfo.artist, - sourceInfo.artistUrl, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), + context.l10n.channel: Text(sourceInfo.artists), context.l10n.streamUrl: Hyperlink( (track as SourcedTrack).url, (track as SourcedTrack).url, diff --git a/lib/components/heart_button/heart_button.dart b/lib/components/heart_button/heart_button.dart index 275d5db1..fa9508f0 100644 --- a/lib/components/heart_button/heart_button.dart +++ b/lib/components/heart_button/heart_button.dart @@ -1,11 +1,12 @@ import 'package:hooks_riverpod/hooks_riverpod.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/extensions/context.dart'; +import 'package:spotube/models/metadata/metadata.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 { final bool isLiked; @@ -63,7 +64,7 @@ class HeartButton extends HookConsumerWidget { } class TrackHeartButton extends HookConsumerWidget { - final Track track; + final SpotubeTrackObject track; const TrackHeartButton({ super.key, required this.track, @@ -71,8 +72,8 @@ class TrackHeartButton extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final savedTracks = ref.watch(likedTracksProvider); - final me = ref.watch(meProvider); + final savedTracks = ref.watch(metadataPluginSavedTracksProvider); + final me = ref.watch(metadataPluginUserProvider); final (:isLiked, :toggleTrackLike) = useTrackToggleLike(track, ref); if (me.isLoading) { diff --git a/lib/components/links/artist_link.dart b/lib/components/links/artist_link.dart index 9467cb38..dc093345 100644 --- a/lib/components/links/artist_link.dart +++ b/lib/components/links/artist_link.dart @@ -1,12 +1,12 @@ import 'package:auto_route/auto_route.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/components/links/anchor_button.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/models/metadata/metadata.dart'; class ArtistLink extends StatelessWidget { - final List artists; + final List artists; final WrapCrossAlignment crossAxisAlignment; final WrapAlignment mainAxisAlignment; final TextStyle textStyle; @@ -38,19 +38,16 @@ class ArtistLink extends StatelessWidget { .entries .map( (artist) => Builder(builder: (context) { - if (artist.value.name == null) { - return Text("Spotify", style: textStyle); - } return AnchorButton( (artist.key != artists.length - 1) ? "${artist.value.name}, " - : artist.value.name!, + : artist.value.name, onTap: () { if (onRouteChange != null) { onRouteChange?.call("/artist/${artist.value.id}"); } else { context - .navigateTo(ArtistRoute(artistId: artist.value.id!)); + .navigateTo(ArtistRoute(artistId: artist.value.id)); } }, overflow: TextOverflow.ellipsis, diff --git a/lib/components/track_presentation/presentation_actions.dart b/lib/components/track_presentation/presentation_actions.dart index bbeb90a5..197d3dca 100644 --- a/lib/components/track_presentation/presentation_actions.dart +++ b/lib/components/track_presentation/presentation_actions.dart @@ -1,7 +1,6 @@ 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'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.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/extensions/context.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/history/history.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; @@ -76,9 +76,11 @@ class TrackPresentationActionsSection extends HookConsumerWidget { Future actionDownloadTracks({ required BuildContext context, - required List tracks, + required List tracks, required String action, }) async { + final fullTrackObjects = + tracks.whereType().toList(); final confirmed = audioSource == AudioSource.piped || (await showDialog( context: context, @@ -88,10 +90,10 @@ class TrackPresentationActionsSection extends HookConsumerWidget { ) ?? false); if (confirmed != true) return; - downloader.batchAddToQueue(tracks); + downloader.batchAddToQueue(fullTrackObjects); notifier.deselectAllTracks(); if (!context.mounted) return; - showToastForAction(context, action, tracks.length); + showToastForAction(context, action, fullTrackObjects.length); } return AdaptivePopSheetList( @@ -143,11 +145,12 @@ class TrackPresentationActionsSection extends HookConsumerWidget { { playlistNotifier.addTracksAtFirst(tracks); playlistNotifier.addCollection(options.collectionId); - if (options.collection is AlbumSimple) { - historyNotifier.addAlbums([options.collection as AlbumSimple]); + if (options.collection is SpotubeSimpleAlbumObject) { + historyNotifier.addAlbums( + [options.collection as SpotubeSimpleAlbumObject]); } else { - historyNotifier - .addPlaylists([options.collection as PlaylistSimple]); + historyNotifier.addPlaylists( + [options.collection as SpotubeSimplePlaylistObject]); } notifier.deselectAllTracks(); if (!context.mounted) return; @@ -158,11 +161,12 @@ class TrackPresentationActionsSection extends HookConsumerWidget { { playlistNotifier.addTracks(tracks); playlistNotifier.addCollection(options.collectionId); - if (options.collection is AlbumSimple) { - historyNotifier.addAlbums([options.collection as AlbumSimple]); + if (options.collection is SpotubeSimpleAlbumObject) { + historyNotifier.addAlbums( + [options.collection as SpotubeSimpleAlbumObject]); } else { - historyNotifier - .addPlaylists([options.collection as PlaylistSimple]); + historyNotifier.addPlaylists( + [options.collection as SpotubeSimplePlaylistObject]); } notifier.deselectAllTracks(); if (!context.mounted) return; diff --git a/lib/components/track_presentation/presentation_props.dart b/lib/components/track_presentation/presentation_props.dart index 144cf0e8..72f65c71 100644 --- a/lib/components/track_presentation/presentation_props.dart +++ b/lib/components/track_presentation/presentation_props.dart @@ -1,14 +1,14 @@ import 'dart:async'; import 'package:shadcn_flutter/shadcn_flutter.dart'; -import 'package:spotify/spotify.dart'; +import 'package:spotube/models/metadata/metadata.dart'; class PaginationProps { final bool hasNextPage; final bool isLoading; final VoidCallback onFetchMore; final Future Function() onRefresh; - final Future> Function() onFetchAll; + final Future> Function() onFetchAll; const PaginationProps({ required this.hasNextPage, @@ -46,7 +46,7 @@ class TrackPresentationOptions { final String? ownerImage; final String image; final String routePath; - final List tracks; + final List tracks; final PaginationProps pagination; final bool isLiked; final String? shareUrl; @@ -67,11 +67,12 @@ class TrackPresentationOptions { this.shareUrl, this.isLiked = false, this.onHeart, - }) : assert(collection is AlbumSimple || collection is PlaylistSimple); + }) : assert(collection is SpotubeSimpleAlbumObject || + collection is SpotubeSimplePlaylistObject); - String get collectionId => collection is AlbumSimple - ? (collection as AlbumSimple).id! - : (collection as PlaylistSimple).id!; + String get collectionId => collection is SpotubeSimpleAlbumObject + ? (collection as SpotubeSimpleAlbumObject).id + : (collection as SpotubeSimplePlaylistObject).id; static TrackPresentationOptions of(BuildContext context) { return Data.of(context); diff --git a/lib/components/track_presentation/presentation_state.dart b/lib/components/track_presentation/presentation_state.dart index d3428861..32b7353a 100644 --- a/lib/components/track_presentation/presentation_state.dart +++ b/lib/components/track_presentation/presentation_state.dart @@ -1,14 +1,16 @@ import 'package:collection/collection.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.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/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'; class PresentationState { - final List selectedTracks; - final List presentationTracks; + final List selectedTracks; + final List presentationTracks; final SortBy sortBy; const PresentationState({ @@ -18,8 +20,8 @@ class PresentationState { }); PresentationState copyWith({ - List? selectedTracks, - List? presentationTracks, + List? selectedTracks, + List? presentationTracks, SortBy? sortBy, }) { return PresentationState( @@ -34,15 +36,15 @@ class PresentationStateNotifier extends AutoDisposeFamilyNotifier { @override PresentationState build(collection) { - if (arg case PlaylistSimple() || AlbumSimple()) { + if (arg case SpotubeSimplePlaylistObject() || SpotubeSimpleAlbumObject()) { if (isSavedTrackPlaylist) { ref.listen( - likedTracksProvider, + metadataPluginSavedTracksProvider, (previous, next) { next.whenData((value) { state = state.copyWith( presentationTracks: ServiceUtils.sortTracks( - value, + value.items, state.sortBy, ), ); @@ -51,9 +53,11 @@ class PresentationStateNotifier ); } else { ref.listen( - arg is PlaylistSimple - ? playlistTracksProvider((arg as PlaylistSimple).id!) - : albumTracksProvider((arg as AlbumSimple)), + arg is SpotubeSimplePlaylistObject + ? metadataPluginPlaylistTracksProvider( + (arg as SpotubeSimplePlaylistObject).id) + : metadataPluginAlbumTracksProvider( + (arg as SpotubeSimpleAlbumObject).id), (previous, next) { next.whenData((value) { state = state.copyWith( @@ -76,36 +80,39 @@ class PresentationStateNotifier } bool get isSavedTrackPlaylist => - arg is PlaylistSimple && - (arg as PlaylistSimple).id == "user-liked-tracks"; + arg is SpotubeSimplePlaylistObject && + (arg as SpotubeSimplePlaylistObject).id == "user-liked-tracks"; - List get tracks { + List get tracks { assert( - arg is PlaylistSimple || arg is AlbumSimple, - "arg must be PlaylistSimple or AlbumSimple", + arg is SpotubeSimplePlaylistObject || arg is SpotubeSimpleAlbumObject, + "arg must be SpotubeSimplePlaylistObject or SpotubeSimpleAlbumObject", ); - final isPlaylist = arg is PlaylistSimple; + final isPlaylist = arg is SpotubeSimplePlaylistObject; final tracks = switch ((isPlaylist, isSavedTrackPlaylist)) { - (true, true) => ref.read(likedTracksProvider).asData?.value, + (true, true) => + ref.read(metadataPluginSavedTracksProvider).asData?.value.items, (true, false) => ref - .read(playlistTracksProvider((arg as PlaylistSimple).id!)) + .read(metadataPluginPlaylistTracksProvider( + (arg as SpotubeSimplePlaylistObject).id)) .asData ?.value .items, _ => ref - .read(albumTracksProvider((arg as AlbumSimple))) + .read(metadataPluginAlbumTracksProvider( + (arg as SpotubeSimpleAlbumObject).id)) .asData ?.value .items, } ?? - []; + []; return tracks; } - void selectTrack(Track track) { + void selectTrack(SpotubeTrackObject track) { if (state.selectedTracks.any((e) => e.id == track.id)) { return; } @@ -121,7 +128,7 @@ class PresentationStateNotifier ); } - void deselectTrack(Track track) { + void deselectTrack(SpotubeTrackObject track) { state = state.copyWith( selectedTracks: state.selectedTracks.where((e) => e != track).toList(), ); @@ -141,7 +148,7 @@ class PresentationStateNotifier state = state.copyWith( presentationTracks: ServiceUtils.sortTracks( tracks - .map((e) => (weightedRatio(e.name!, query), e)) + .map((e) => (weightedRatio(e.name, query), e)) .sorted((a, b) => b.$1.compareTo(a.$1)) .where((e) => e.$1 > 50) .map((e) => e.$2) diff --git a/lib/components/track_presentation/presentation_top.dart b/lib/components/track_presentation/presentation_top.dart index 5935fa13..ff7f5167 100644 --- a/lib/components/track_presentation/presentation_top.dart +++ b/lib/components/track_presentation/presentation_top.dart @@ -3,8 +3,6 @@ import 'package:flutter/services.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'; -import 'package:spotube/collections/env.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/heart_button/heart_button.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/context.dart'; import 'package:spotube/modules/playlist/playlist_create_dialog.dart'; -import 'package:spotube/provider/spotify/spotify.dart'; class TrackPresentationTopSection extends HookConsumerWidget { const TrackPresentationTopSection({super.key}); @@ -26,25 +23,10 @@ class TrackPresentationTopSection extends HookConsumerWidget { final scale = context.theme.scaling; final isUserPlaylist = useIsUserPlaylist(ref, options.collectionId); - final playlistImage = (options.collection is PlaylistSimple && - (options.collection as PlaylistSimple).owner?.displayName == - "Spotify" && - 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 decorationImage = DecorationImage( + image: UniversalImage.imageProvider(options.image), + fit: BoxFit.cover, + ); final imageDimension = mediaQuery.mdAndUp ? 200 : 120; @@ -116,7 +98,7 @@ class TrackPresentationTopSection extends HookConsumerWidget { builder: (context) { return PlaylistCreateDialog( playlistId: options.collectionId, - trackIds: options.tracks.map((e) => e.id!).toList(), + trackIds: options.tracks.map((e) => e.id).toList(), ); }, ); diff --git a/lib/components/track_presentation/use_action_callbacks.dart b/lib/components/track_presentation/use_action_callbacks.dart index 0012594a..ff49f4a4 100644 --- a/lib/components/track_presentation/use_action_callbacks.dart +++ b/lib/components/track_presentation/use_action_callbacks.dart @@ -2,11 +2,11 @@ import 'dart:math'; import 'package:flutter_hooks/flutter_hooks.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/track_presentation/presentation_props.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/connect/connect.dart'; import 'package:spotube/provider/history/history.dart'; @@ -45,14 +45,14 @@ UseActionCallbacks useActionCallbacks(WidgetRef ref) { final allTracks = await options.pagination.onFetchAll(); final remotePlayback = ref.read(connectProvider.notifier); await remotePlayback.load( - options.collection is AlbumSimple + options.collection is SpotubeSimpleAlbumObject ? WebSocketLoadEventData.album( tracks: allTracks, - collection: options.collection as AlbumSimple, + collection: options.collection as SpotubeSimpleAlbumObject, initialIndex: Random().nextInt(allTracks.length)) : WebSocketLoadEventData.playlist( tracks: allTracks, - collection: options.collection as PlaylistSimple, + collection: options.collection as SpotubeSimplePlaylistObject, initialIndex: Random().nextInt(allTracks.length), ), ); @@ -65,10 +65,12 @@ UseActionCallbacks useActionCallbacks(WidgetRef ref) { ); await audioPlayer.setShuffle(true); playlistNotifier.addCollection(options.collectionId); - if (options.collection is AlbumSimple) { - historyNotifier.addAlbums([options.collection as AlbumSimple]); + if (options.collection is SpotubeSimpleAlbumObject) { + historyNotifier + .addAlbums([options.collection as SpotubeSimpleAlbumObject]); } else { - historyNotifier.addPlaylists([options.collection as PlaylistSimple]); + historyNotifier.addPlaylists( + [options.collection as SpotubeSimplePlaylistObject]); } final allTracks = await options.pagination.onFetchAll(); @@ -96,23 +98,25 @@ UseActionCallbacks useActionCallbacks(WidgetRef ref) { final allTracks = await options.pagination.onFetchAll(); final remotePlayback = ref.read(connectProvider.notifier); await remotePlayback.load( - options.collection is AlbumSimple + options.collection is SpotubeSimpleAlbumObject ? WebSocketLoadEventData.album( tracks: allTracks, - collection: options.collection as AlbumSimple, + collection: options.collection as SpotubeSimpleAlbumObject, ) : WebSocketLoadEventData.playlist( tracks: allTracks, - collection: options.collection as PlaylistSimple, + collection: options.collection as SpotubeSimplePlaylistObject, ), ); } else { await playlistNotifier.load(initialTracks, autoPlay: true); playlistNotifier.addCollection(options.collectionId); - if (options.collection is AlbumSimple) { - historyNotifier.addAlbums([options.collection as AlbumSimple]); + if (options.collection is SpotubeSimpleAlbumObject) { + historyNotifier + .addAlbums([options.collection as SpotubeSimpleAlbumObject]); } else { - historyNotifier.addPlaylists([options.collection as PlaylistSimple]); + historyNotifier.addPlaylists( + [options.collection as SpotubeSimplePlaylistObject]); } final allTracks = await options.pagination.onFetchAll(); diff --git a/lib/components/track_presentation/use_is_user_playlist.dart b/lib/components/track_presentation/use_is_user_playlist.dart index 2f87ccc8..18426118 100644 --- a/lib/components/track_presentation/use_is_user_playlist.dart +++ b/lib/components/track_presentation/use_is_user_playlist.dart @@ -1,17 +1,18 @@ import 'package:flutter_hooks/flutter_hooks.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) { - final userPlaylistsQuery = ref.watch(favoritePlaylistsProvider); - final me = ref.watch(meProvider); + final userPlaylistsQuery = ref.watch(metadataPluginSavedPlaylistsProvider); + final me = ref.watch(metadataPluginUserProvider); return useMemoized( () => userPlaylistsQuery.asData?.value.items.any((e) => e.id == playlistId && me.asData?.value != null && - e.owner?.id == me.asData?.value.id) ?? + e.owner.id == me.asData?.value?.id) ?? false, [userPlaylistsQuery.asData?.value, playlistId, me.asData?.value], ); diff --git a/lib/components/track_presentation/use_track_tile_play_callback.dart b/lib/components/track_presentation/use_track_tile_play_callback.dart index b519f781..99f44f1e 100644 --- a/lib/components/track_presentation/use_track_tile_play_callback.dart +++ b/lib/components/track_presentation/use_track_tile_play_callback.dart @@ -1,18 +1,19 @@ import 'package:hooks_riverpod/hooks_riverpod.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/track_presentation/presentation_props.dart'; import 'package:spotube/components/track_presentation/presentation_state.dart'; import 'package:spotube/extensions/list.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/connect/connect.dart'; import 'package:spotube/provider/history/history.dart'; -Future Function(Track track, int index) useTrackTilePlayCallback( +Future Function(SpotubeTrackObject track, int index) + useTrackTilePlayCallback( WidgetRef ref, ) { final context = useContext(); @@ -26,7 +27,8 @@ Future Function(Track track, int index) useTrackTilePlayCallback( [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 notifier = ref.read(presentationStateProvider(options.collection).notifier); @@ -52,15 +54,15 @@ Future Function(Track track, int index) useTrackTilePlayCallback( } else { final tracks = await options.pagination.onFetchAll(); await remotePlayback.load( - options.collection is AlbumSimple + options.collection is SpotubeSimpleAlbumObject ? WebSocketLoadEventData.album( tracks: tracks, - collection: options.collection as AlbumSimple, + collection: options.collection as SpotubeSimpleAlbumObject, initialIndex: index, ) : WebSocketLoadEventData.playlist( tracks: tracks, - collection: options.collection as PlaylistSimple, + collection: options.collection as SpotubeSimplePlaylistObject, initialIndex: index, ), ); @@ -76,10 +78,12 @@ Future Function(Track track, int index) useTrackTilePlayCallback( autoPlay: true, ); playlistNotifier.addCollection(options.collectionId); - if (options.collection is AlbumSimple) { - historyNotifier.addAlbums([options.collection as AlbumSimple]); + if (options.collection is SpotubeSimpleAlbumObject) { + historyNotifier + .addAlbums([options.collection as SpotubeSimpleAlbumObject]); } else { - historyNotifier.addPlaylists([options.collection as PlaylistSimple]); + historyNotifier.addPlaylists( + [options.collection as SpotubeSimplePlaylistObject]); } } } diff --git a/lib/components/track_tile/track_options.dart b/lib/components/track_tile/track_options.dart index 0136c419..d9e31d78 100644 --- a/lib/components/track_tile/track_options.dart +++ b/lib/components/track_tile/track_options.dart @@ -8,7 +8,6 @@ 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/assets.gen.dart'; import 'package:spotube/collections/routes.gr.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/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/extensions/image.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/blacklist_provider.dart'; import 'package:spotube/provider/download_manager_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/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'; @@ -50,8 +52,9 @@ enum TrackOptionValue { startRadio, } +/// [track] must be a [SpotubeFullTrackObject] or [SpotubeLocalTrackObject] class TrackOptions extends HookConsumerWidget { - final Track track; + final SpotubeTrackObject track; final bool userPlaylist; final String? playlistId; final ObjectRef?>? showMenuCbRef; @@ -63,9 +66,12 @@ class TrackOptions extends HookConsumerWidget { this.userPlaylist = false, this.playlistId, 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}"; Clipboard.setData(ClipboardData(text: data)).then((_) { if (context.mounted) { @@ -87,7 +93,7 @@ class TrackOptions extends HookConsumerWidget { void actionAddToPlaylist( BuildContext context, - Track track, + SpotubeTrackObject track, ) { /// showDialog doesn't work for some reason. So we have to /// manually push a Dialog Route in the Navigator to get it working @@ -105,32 +111,32 @@ class TrackOptions extends HookConsumerWidget { void actionStartRadio( BuildContext context, WidgetRef ref, - Track track, + SpotubeTrackObject track, ) async { final playback = ref.read(audioPlayerProvider.notifier); final playlist = ref.read(audioPlayerProvider); - final spotify = ref.read(spotifyProvider); final query = "${track.name} Radio"; - final pages = await spotify.invoke( - (api) => api.search.get(query, types: [SearchType.playlist]).first(), - ); + final metadataPlugin = await ref.read(metadataPluginProvider.future); - final radios = pages - .expand((e) => e.items?.cast().toList() ?? []) - .toList(); + if (metadataPlugin == null) { + throw MetadataPluginException.noDefaultPlugin( + "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) { - final validPlaylists = - artists.where((a) => e.description!.contains(a!)); + final validPlaylists = artists.where((a) => e.description.contains(a)); return e.name == "${track.name} Radio" && (validPlaylists.length >= 2 || validPlaylists.length == artists.length) && - e.owner?.displayName == "Spotify"; + e.owner.name == "Spotify"; }, - orElse: () => radios.first, + orElse: () => pages.items.first, ); bool replaceQueue = false; @@ -154,10 +160,10 @@ class TrackOptions extends HookConsumerWidget { } else { await playback.addTrack(track); } - - final tracks = await spotify.invoke( - (api) => api.playlists.getTracksByPlaylistId(radio.id!).all(), - ); + await ref.read(metadataPluginPlaylistTracksProvider(radio.id).future); + final tracks = await ref + .read(metadataPluginPlaylistTracksProvider(radio.id).notifier) + .fetchAll(); await playback.addTracks( tracks.toList() @@ -179,7 +185,7 @@ class TrackOptions extends HookConsumerWidget { ref.watch(downloadManagerProvider); final downloadManager = ref.watch(downloadManagerProvider.notifier); final blacklist = ref.watch(blacklistProvider); - final me = ref.watch(meProvider); + final me = ref.watch(metadataPluginUserProvider); final favorites = useTrackToggleLike(track, ref); @@ -192,23 +198,32 @@ class TrackOptions extends HookConsumerWidget { final removingTrack = useState(null); final favoritePlaylistsNotifier = - ref.watch(favoritePlaylistsProvider.notifier); + ref.watch(metadataPluginSavedPlaylistsProvider.notifier); - final isInQueue = useMemoized(() { - if (playlist.activeTrack == null) return false; - return downloadManager.isActive(playlist.activeTrack!); + final isInDownloadQueue = useMemoized(() { + if (playlist.activeTrack == null || + playlist.activeTrack! is SpotubeLocalTrackObject) { + return false; + } + return downloadManager.isActive( + playlist.activeTrack! as SpotubeFullTrackObject, + ); }, [ playlist.activeTrack, downloadManager, ]); final progressNotifier = useMemoized(() { - final spotubeTrack = downloadManager.mapToSourcedTrack(track); - if (spotubeTrack == null) return null; - return downloadManager.getProgressNotifier(spotubeTrack); + if (track is! SpotubeFullTrackObject) { + return throw Exception( + "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( tooltip: context.l10n.more_actions, @@ -220,7 +235,7 @@ class TrackOptions extends HookConsumerWidget { // ); break; case TrackOptionValue.delete: - await File((track as LocalTrack).path).delete(); + await File((track as SpotubeLocalTrackObject).path).delete(); ref.invalidate(localTracksProvider); break; case TrackOptionValue.addToQueue: @@ -232,7 +247,7 @@ class TrackOptions extends HookConsumerWidget { builder: (context, overlay) { return SurfaceCard( child: Text( - context.l10n.added_track_to_queue(track.name!), + context.l10n.added_track_to_queue(track.name), textAlign: TextAlign.center, ), ); @@ -250,7 +265,7 @@ class TrackOptions extends HookConsumerWidget { builder: (context, overlay) { return SurfaceCard( child: Text( - context.l10n.track_will_play_next(track.name!), + context.l10n.track_will_play_next(track.name), textAlign: TextAlign.center, ), ); @@ -259,7 +274,7 @@ class TrackOptions extends HookConsumerWidget { } break; case TrackOptionValue.removeFromQueue: - playback.removeTrack(track.id!); + playback.removeTrack(track.id); if (context.mounted) { showToast( @@ -269,7 +284,7 @@ class TrackOptions extends HookConsumerWidget { return SurfaceCard( child: Text( context.l10n.removed_track_from_queue( - track.name!, + track.name, ), textAlign: TextAlign.center, ), @@ -285,19 +300,19 @@ class TrackOptions extends HookConsumerWidget { actionAddToPlaylist(context, track); break; case TrackOptionValue.removeFromPlaylist: - removingTrack.value = track.uri; + removingTrack.value = track.externalUri; favoritePlaylistsNotifier - .removeTracks(playlistId ?? "", [track.id!]); + .removeTracks(playlistId ?? "", [track.id]); break; case TrackOptionValue.blacklist: if (isBlackListed == null) break; if (isBlackListed == true) { - await ref.read(blacklistProvider.notifier).remove(track.id!); + await ref.read(blacklistProvider.notifier).remove(track.id); } else { await ref.read(blacklistProvider.notifier).add( BlacklistTableCompanion.insert( - name: track.name!, - elementId: track.id!, + name: track.name, + elementId: track.id, elementType: BlacklistedType.track, ), ); @@ -311,16 +326,19 @@ class TrackOptions extends HookConsumerWidget { await launchUrlString(url); break; case TrackOptionValue.details: + if (track is! SpotubeFullTrackObject) break; showDialog( context: context, builder: (context) => ConstrainedBox( constraints: const BoxConstraints(maxWidth: 400), - child: TrackDetailsDialog(track: track), + child: + TrackDetailsDialog(track: track as SpotubeFullTrackObject), ), ); break; case TrackOptionValue.download: - await downloadManager.addToQueue(track); + if (track is! SpotubeFullTrackObject) break; + await downloadManager.addToQueue(track as SpotubeFullTrackObject); break; case TrackOptionValue.startRadio: actionStartRadio(context, ref, track); @@ -336,23 +354,23 @@ class TrackOptions extends HookConsumerWidget { child: ClipRRect( borderRadius: BorderRadius.circular(10), child: UniversalImage( - path: track.album!.images + path: track.album.images .asUrlString(placeholder: ImagePlaceholder.albumArt), fit: BoxFit.cover, ), ), ), title: Text( - track.name!, + track.name, maxLines: 1, overflow: TextOverflow.ellipsis, ).semiBold(), subtitle: Align( alignment: Alignment.centerLeft, child: ArtistLink( - artists: track.artists!, + artists: track.artists, onOverflowArtistClick: () => context.navigateTo( - TrackRoute(trackId: track.id!), + TrackRoute(trackId: track.id), ), ), ), @@ -375,7 +393,7 @@ class TrackOptions extends HookConsumerWidget { children: [ Text(context.l10n.go_to_album), Text( - track.album!.name!, + track.album.name, style: context.theme.typography.xSmall, ), ], @@ -435,12 +453,12 @@ class TrackOptions extends HookConsumerWidget { if (!isLocalTrack) AdaptiveMenuButton( value: TrackOptionValue.download, - enabled: !isInQueue, - leading: isInQueue + enabled: !isInDownloadQueue, + leading: isInDownloadQueue ? HookBuilder(builder: (context) { - final progress = useListenable(progressNotifier!); + final progress = useListenable(progressNotifier); return CircularProgressIndicator( - value: progress.value, + value: progress?.value, ); }) : const Icon(SpotubeIcons.download), diff --git a/lib/components/track_tile/track_tile.dart b/lib/components/track_tile/track_tile.dart index 9d9045c5..a3207353 100644 --- a/lib/components/track_tile/track_tile.dart +++ b/lib/components/track_tile/track_tile.dart @@ -7,7 +7,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer; import 'package:skeletonizer/skeletonizer.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/spotube_icons.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/track_tile/track_options.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/duration.dart'; -import 'package:spotube/extensions/image.dart'; -import 'package:spotube/models/local_track.dart'; +import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/blacklist_provider.dart'; @@ -29,7 +26,7 @@ import 'package:spotube/utils/platform.dart'; class TrackTile extends HookConsumerWidget { /// [index] will not be shown if null final int? index; - final Track track; + final SpotubeTrackObject track; final bool selected; final ValueChanged? onChanged; final Future Function()? onTap; @@ -151,7 +148,7 @@ class TrackTile extends HookConsumerWidget { image: DecorationImage( fit: BoxFit.cover, image: UniversalImage.imageProvider( - (track.album?.images).asUrlString( + (track.album.images).asUrlString( placeholder: ImagePlaceholder.albumArt, ), ), @@ -217,8 +214,8 @@ class TrackTile extends HookConsumerWidget { Expanded( flex: 6, child: switch (track) { - LocalTrack() => Text( - track.name!, + SpotubeLocalTrackObject() => Text( + track.name, maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -233,10 +230,10 @@ class TrackTile extends HookConsumerWidget { ), onPressed: () { context - .navigateTo(TrackRoute(trackId: track.id!)); + .navigateTo(TrackRoute(trackId: track.id)); }, child: Text( - track.name!, + track.name, maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -251,22 +248,22 @@ class TrackTile extends HookConsumerWidget { Expanded( flex: 4, child: switch (track) { - LocalTrack() => Text( - track.album!.name!, + SpotubeLocalTrackObject() => Text( + track.album.name, maxLines: 1, overflow: TextOverflow.ellipsis, ), _ => Align( alignment: Alignment.centerLeft, - /* child: LinkText( - track.album!.name!, + child: LinkText( + track.album.name, AlbumRoute( - album: track.album!, - id: track.album!.id!, + album: track.album, + id: track.album.id, ), push: true, overflow: TextOverflow.ellipsis, - ), */ + ), ) }, ), @@ -275,18 +272,18 @@ class TrackTile extends HookConsumerWidget { ), subtitle: Align( alignment: Alignment.centerLeft, - child: track is LocalTrack + child: track is SpotubeLocalTrackObject ? Text( - track.artists?.asString() ?? '', + track.artists.asString(), ) : ClipRect( child: ConstrainedBox( constraints: const BoxConstraints(maxHeight: 40), child: ArtistLink( - artists: track.artists ?? [], + artists: track.artists, onOverflowArtistClick: () { context.navigateTo( - TrackRoute(trackId: track.id!), + TrackRoute(trackId: track.id), ); }, ), @@ -298,7 +295,7 @@ class TrackTile extends HookConsumerWidget { children: [ const SizedBox(width: 8), Text( - Duration(milliseconds: track.durationMs ?? 0) + Duration(milliseconds: track.durationMs) .toHumanReadableString(padZero: false), maxLines: 1, overflow: TextOverflow.ellipsis, diff --git a/lib/extensions/album_simple.dart b/lib/extensions/album_simple.dart deleted file mode 100644 index 5678390c..00000000 --- a/lib/extensions/album_simple.dart +++ /dev/null @@ -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; - } -} diff --git a/lib/extensions/artist_simple.dart b/lib/extensions/artist_simple.dart deleted file mode 100644 index 7997355d..00000000 --- a/lib/extensions/artist_simple.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:spotify/spotify.dart'; - -extension ArtistExtension on List { - String asString() { - return map((e) => e.name?.replaceAll(",", " ")).join(", "); - } -} diff --git a/lib/extensions/image.dart b/lib/extensions/image.dart deleted file mode 100644 index ee78653a..00000000 --- a/lib/extensions/image.dart +++ /dev/null @@ -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? { - 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; - } -} diff --git a/lib/extensions/track.dart b/lib/extensions/track.dart deleted file mode 100644 index bfe1f639..00000000 --- a/lib/extensions/track.dart +++ /dev/null @@ -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 { - Future> 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 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; - } - } -} diff --git a/lib/hooks/configurators/use_deep_linking.dart b/lib/hooks/configurators/use_deep_linking.dart index fa23091e..aaa4111c 100644 --- a/lib/hooks/configurators/use_deep_linking.dart +++ b/lib/hooks/configurators/use_deep_linking.dart @@ -5,7 +5,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/routes.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/model/sharing_file.dart'; import 'package:spotube/services/logger/logger.dart'; @@ -14,93 +13,95 @@ import 'package:spotube/utils/platform.dart'; final appLinks = AppLinks(); 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) { - // single instance no worries - final spotify = ref.watch(spotifyProvider); + // // single instance no worries + // final spotify = ref.watch(spotifyProvider); - useEffect(() { - void uriListener(List files) async { - for (final file in files) { - if (file.type != SharedMediaType.URL) continue; - final url = Uri.parse(file.value!); - if (url.pathSegments.length != 2) continue; + // useEffect(() { + // void uriListener(List files) async { + // for (final file in files) { + // if (file.type != SharedMediaType.URL) continue; + // final url = Uri.parse(file.value!); + // if (url.pathSegments.length != 2) continue; - switch (url.pathSegments.first) { - case "album": - final album = await spotify.invoke((api) { - return api.albums.get(url.pathSegments.last); - }); - // router.navigate( - // AlbumRoute(id: album.id!, album: album), - // ); - break; - case "artist": - router.navigate(ArtistRoute(artistId: url.pathSegments.last)); - break; - case "playlist": - final playlist = await spotify.invoke((api) { - return api.playlists.get(url.pathSegments.last); - }); - // router - // .navigate(PlaylistRoute(id: playlist.id!, playlist: playlist)); - break; - case "track": - router.navigate(TrackRoute(trackId: url.pathSegments.last)); - break; - default: - break; - } - } - } + // switch (url.pathSegments.first) { + // case "album": + // final album = await spotify.invoke((api) { + // return api.albums.get(url.pathSegments.last); + // }); + // // router.navigate( + // // AlbumRoute(id: album.id!, album: album), + // // ); + // break; + // case "artist": + // router.navigate(ArtistRoute(artistId: url.pathSegments.last)); + // break; + // case "playlist": + // final playlist = await spotify.invoke((api) { + // return api.playlists.get(url.pathSegments.last); + // }); + // // router + // // .navigate(PlaylistRoute(id: playlist.id!, playlist: playlist)); + // break; + // case "track": + // router.navigate(TrackRoute(trackId: url.pathSegments.last)); + // break; + // default: + // break; + // } + // } + // } - StreamSubscription? mediaStream; + // StreamSubscription? mediaStream; - if (kIsMobile) { - FlutterSharingIntent.instance.getInitialSharing().then(uriListener); + // if (kIsMobile) { + // FlutterSharingIntent.instance.getInitialSharing().then(uriListener); - mediaStream = - FlutterSharingIntent.instance.getMediaStream().listen(uriListener); - } + // mediaStream = + // FlutterSharingIntent.instance.getMediaStream().listen(uriListener); + // } - final subscription = linkStream.listen((uri) async { - try { - final startSegment = uri.split(":").take(2).join(":"); - final endSegment = uri.split(":").last; + // final subscription = linkStream.listen((uri) async { + // try { + // final startSegment = uri.split(":").take(2).join(":"); + // final endSegment = uri.split(":").last; - switch (startSegment) { - case "spotify:album": - final album = await spotify.invoke((api) { - return api.albums.get(endSegment); - }); - // await router.navigate( - // AlbumRoute(id: album.id!, album: album), - // ); - break; - case "spotify:artist": - await router.navigate(ArtistRoute(artistId: endSegment)); - break; - case "spotify:track": - await router.navigate(TrackRoute(trackId: endSegment)); - break; - case "spotify:playlist": - final playlist = await spotify.invoke((api) { - return api.playlists.get(endSegment); - }); - // await router.navigate( - // PlaylistRoute(id: playlist.id!, playlist: playlist), - // ); - break; - default: - break; - } - } catch (e, stack) { - AppLogger.reportError(e, stack); - } - }); + // switch (startSegment) { + // case "spotify:album": + // final album = await spotify.invoke((api) { + // return api.albums.get(endSegment); + // }); + // // await router.navigate( + // // AlbumRoute(id: album.id!, album: album), + // // ); + // break; + // case "spotify:artist": + // await router.navigate(ArtistRoute(artistId: endSegment)); + // break; + // case "spotify:track": + // await router.navigate(TrackRoute(trackId: endSegment)); + // break; + // case "spotify:playlist": + // final playlist = await spotify.invoke((api) { + // return api.playlists.get(endSegment); + // }); + // // await router.navigate( + // // PlaylistRoute(id: playlist.id!, playlist: playlist), + // // ); + // break; + // default: + // break; + // } + // } catch (e, stack) { + // AppLogger.reportError(e, stack); + // } + // }); - return () { - mediaStream?.cancel(); - subscription.cancel(); - }; - }, [spotify]); + // return () { + // mediaStream?.cancel(); + // subscription.cancel(); + // }; + // }, [spotify]); } diff --git a/lib/hooks/configurators/use_endless_playback.dart b/lib/hooks/configurators/use_endless_playback.dart index 96628442..81a66168 100644 --- a/lib/hooks/configurators/use_endless_playback.dart +++ b/lib/hooks/configurators/use_endless_playback.dart @@ -2,10 +2,8 @@ import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/provider/authentication/authentication.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/services/audio_player/audio_player.dart'; diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 8186fd92..51b5b880 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -10,9 +10,9 @@ import 'package:media_kit/media_kit.dart' hide Track; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; 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/lyrics.dart'; +import 'package:spotube/models/metadata/market.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/services/kv_store/encrypted_kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; diff --git a/lib/models/database/database.steps.dart b/lib/models/database/database.steps.dart index beccaef8..ef277bc5 100644 --- a/lib/models/database/database.steps.dart +++ b/lib/models/database/database.steps.dart @@ -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'; // ignore_for_file: type=lint,unused_import import 'package:flutter/material.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/models/database/database.dart'; +import 'package:spotube/models/metadata/market.dart'; import 'package:spotube/services/sourced_track/enums.dart'; // GENERATED BY drift_dev, DO NOT MODIFY. diff --git a/lib/models/database/tables/history.dart b/lib/models/database/tables/history.dart index 23c16f17..2faeba9a 100644 --- a/lib/models/database/tables/history.dart +++ b/lib/models/database/tables/history.dart @@ -16,10 +16,12 @@ class HistoryTable extends Table { } extension HistoryItemParseExtension on HistoryTableData { - PlaylistSimple? get playlist => - type == HistoryEntryType.playlist ? PlaylistSimple.fromJson(data) : null; - AlbumSimple? get album => - type == HistoryEntryType.album ? AlbumSimple.fromJson(data) : null; - Track? get track => - type == HistoryEntryType.track ? Track.fromJson(data) : null; + SpotubeSimplePlaylistObject? get playlist => type == HistoryEntryType.playlist + ? SpotubeSimplePlaylistObject.fromJson(data) + : null; + SpotubeSimpleAlbumObject? get album => type == HistoryEntryType.album + ? SpotubeSimpleAlbumObject.fromJson(data) + : null; + SpotubeTrackObject? get track => + type == HistoryEntryType.track ? SpotubeTrackObject.fromJson(data) : null; } diff --git a/lib/models/local_track.dart b/lib/models/local_track.dart deleted file mode 100644 index def3b64f..00000000 --- a/lib/models/local_track.dart +++ /dev/null @@ -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 json) { - return LocalTrack.fromTrack( - track: Track.fromJson(json), - path: json['path'], - ); - } - - @override - Map toJson() { - return { - ...super.toJson(), - 'path': path, - }; - } -} diff --git a/lib/models/metadata/market.dart b/lib/models/metadata/market.dart new file mode 100644 index 00000000..caaef957 --- /dev/null +++ b/lib/models/metadata/market.dart @@ -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, +} diff --git a/lib/models/metadata/metadata.dart b/lib/models/metadata/metadata.dart index d7aebdb3..65a4d91f 100644 --- a/lib/models/metadata/metadata.dart +++ b/lib/models/metadata/metadata.dart @@ -1,11 +1,12 @@ library metadata_objects; +import 'dart:io'; import 'dart:typed_data'; import 'package:collection/collection.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:media_kit/media_kit.dart'; import 'package:metadata_god/metadata_god.dart'; +import 'package:path/path.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/utils/primitive_utils.dart'; diff --git a/lib/models/metadata/metadata.freezed.dart b/lib/models/metadata/metadata.freezed.dart index 45ce4e84..636a092f 100644 --- a/lib/models/metadata/metadata.freezed.dart +++ b/lib/models/metadata/metadata.freezed.dart @@ -2571,8 +2571,7 @@ mixin _$SpotubeSearchResponseObject { throw _privateConstructorUsedError; List get playlists => throw _privateConstructorUsedError; - List get tracks => - throw _privateConstructorUsedError; + List get tracks => throw _privateConstructorUsedError; /// Serializes this SpotubeSearchResponseObject to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -2596,7 +2595,7 @@ abstract class $SpotubeSearchResponseObjectCopyWith<$Res> { {List albums, List artists, List playlists, - List tracks}); + List tracks}); } /// @nodoc @@ -2636,7 +2635,7 @@ class _$SpotubeSearchResponseObjectCopyWithImpl<$Res, tracks: null == tracks ? _value.tracks : tracks // ignore: cast_nullable_to_non_nullable - as List, + as List, ) as $Val); } } @@ -2654,7 +2653,7 @@ abstract class _$$SpotubeSearchResponseObjectImplCopyWith<$Res> {List albums, List artists, List playlists, - List tracks}); + List tracks}); } /// @nodoc @@ -2693,7 +2692,7 @@ class __$$SpotubeSearchResponseObjectImplCopyWithImpl<$Res> tracks: null == tracks ? _value._tracks : tracks // ignore: cast_nullable_to_non_nullable - as List, + as List, )); } } @@ -2706,7 +2705,7 @@ class _$SpotubeSearchResponseObjectImpl {required final List albums, required final List artists, required final List playlists, - required final List tracks}) + required final List tracks}) : _albums = albums, _artists = artists, _playlists = playlists, @@ -2740,9 +2739,9 @@ class _$SpotubeSearchResponseObjectImpl return EqualUnmodifiableListView(_playlists); } - final List _tracks; + final List _tracks; @override - List get tracks { + List get tracks { if (_tracks is EqualUnmodifiableListView) return _tracks; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(_tracks); @@ -2797,7 +2796,7 @@ abstract class _SpotubeSearchResponseObject {required final List albums, required final List artists, required final List playlists, - required final List tracks}) = + required final List tracks}) = _$SpotubeSearchResponseObjectImpl; factory _SpotubeSearchResponseObject.fromJson(Map json) = @@ -2810,7 +2809,7 @@ abstract class _SpotubeSearchResponseObject @override List get playlists; @override - List get tracks; + List get tracks; /// Create a copy of SpotubeSearchResponseObject /// with the given fields replaced by the non-null parameter values. @@ -2826,8 +2825,6 @@ SpotubeTrackObject _$SpotubeTrackObjectFromJson(Map json) { return SpotubeLocalTrackObject.fromJson(json); case 'full': return SpotubeFullTrackObject.fromJson(json); - case 'simple': - return SpotubeSimpleTrackObject.fromJson(json); default: throw CheckedFromJsonException(json, 'runtimeType', 'SpotubeTrackObject', @@ -2842,7 +2839,7 @@ mixin _$SpotubeTrackObject { String get externalUri => throw _privateConstructorUsedError; List get artists => throw _privateConstructorUsedError; - SpotubeSimpleAlbumObject? get album => throw _privateConstructorUsedError; + SpotubeSimpleAlbumObject get album => throw _privateConstructorUsedError; int get durationMs => throw _privateConstructorUsedError; @optionalTypeArgs TResult when({ @@ -2865,15 +2862,6 @@ mixin _$SpotubeTrackObject { String isrc, bool explicit) full, - required TResult Function( - String id, - String name, - String externalUri, - int durationMs, - bool explicit, - List artists, - SpotubeSimpleAlbumObject? album) - simple, }) => throw _privateConstructorUsedError; @optionalTypeArgs @@ -2897,15 +2885,6 @@ mixin _$SpotubeTrackObject { String isrc, bool explicit)? full, - TResult? Function( - String id, - String name, - String externalUri, - int durationMs, - bool explicit, - List artists, - SpotubeSimpleAlbumObject? album)? - simple, }) => throw _privateConstructorUsedError; @optionalTypeArgs @@ -2929,15 +2908,6 @@ mixin _$SpotubeTrackObject { String isrc, bool explicit)? full, - TResult Function( - String id, - String name, - String externalUri, - int durationMs, - bool explicit, - List artists, - SpotubeSimpleAlbumObject? album)? - simple, required TResult orElse(), }) => throw _privateConstructorUsedError; @@ -2945,21 +2915,18 @@ mixin _$SpotubeTrackObject { TResult map({ required TResult Function(SpotubeLocalTrackObject value) local, required TResult Function(SpotubeFullTrackObject value) full, - required TResult Function(SpotubeSimpleTrackObject value) simple, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? mapOrNull({ TResult? Function(SpotubeLocalTrackObject value)? local, TResult? Function(SpotubeFullTrackObject value)? full, - TResult? Function(SpotubeSimpleTrackObject value)? simple, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeMap({ TResult Function(SpotubeLocalTrackObject value)? local, TResult Function(SpotubeFullTrackObject value)? full, - TResult Function(SpotubeSimpleTrackObject value)? simple, required TResult orElse(), }) => throw _privateConstructorUsedError; @@ -2988,7 +2955,7 @@ abstract class $SpotubeTrackObjectCopyWith<$Res> { SpotubeSimpleAlbumObject album, int durationMs}); - $SpotubeSimpleAlbumObjectCopyWith<$Res>? get album; + $SpotubeSimpleAlbumObjectCopyWith<$Res> get album; } /// @nodoc @@ -3031,7 +2998,7 @@ class _$SpotubeTrackObjectCopyWithImpl<$Res, $Val extends SpotubeTrackObject> : artists // ignore: cast_nullable_to_non_nullable as List, album: null == album - ? _value.album! + ? _value.album : album // ignore: cast_nullable_to_non_nullable as SpotubeSimpleAlbumObject, durationMs: null == durationMs @@ -3045,12 +3012,8 @@ class _$SpotubeTrackObjectCopyWithImpl<$Res, $Val extends SpotubeTrackObject> /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') - $SpotubeSimpleAlbumObjectCopyWith<$Res>? get album { - if (_value.album == null) { - return null; - } - - return $SpotubeSimpleAlbumObjectCopyWith<$Res>(_value.album!, (value) { + $SpotubeSimpleAlbumObjectCopyWith<$Res> get album { + return $SpotubeSimpleAlbumObjectCopyWith<$Res>(_value.album, (value) { return _then(_value.copyWith(album: value) as $Val); }); } @@ -3132,16 +3095,6 @@ class __$$SpotubeLocalTrackObjectImplCopyWithImpl<$Res> 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 @@ -3244,15 +3197,6 @@ class _$SpotubeLocalTrackObjectImpl implements SpotubeLocalTrackObject { String isrc, bool explicit) full, - required TResult Function( - String id, - String name, - String externalUri, - int durationMs, - bool explicit, - List artists, - SpotubeSimpleAlbumObject? album) - simple, }) { return local(id, name, externalUri, artists, album, durationMs, path); } @@ -3279,15 +3223,6 @@ class _$SpotubeLocalTrackObjectImpl implements SpotubeLocalTrackObject { String isrc, bool explicit)? full, - TResult? Function( - String id, - String name, - String externalUri, - int durationMs, - bool explicit, - List artists, - SpotubeSimpleAlbumObject? album)? - simple, }) { return local?.call(id, name, externalUri, artists, album, durationMs, path); } @@ -3314,15 +3249,6 @@ class _$SpotubeLocalTrackObjectImpl implements SpotubeLocalTrackObject { String isrc, bool explicit)? full, - TResult Function( - String id, - String name, - String externalUri, - int durationMs, - bool explicit, - List artists, - SpotubeSimpleAlbumObject? album)? - simple, required TResult orElse(), }) { if (local != null) { @@ -3336,7 +3262,6 @@ class _$SpotubeLocalTrackObjectImpl implements SpotubeLocalTrackObject { TResult map({ required TResult Function(SpotubeLocalTrackObject value) local, required TResult Function(SpotubeFullTrackObject value) full, - required TResult Function(SpotubeSimpleTrackObject value) simple, }) { return local(this); } @@ -3346,7 +3271,6 @@ class _$SpotubeLocalTrackObjectImpl implements SpotubeLocalTrackObject { TResult? mapOrNull({ TResult? Function(SpotubeLocalTrackObject value)? local, TResult? Function(SpotubeFullTrackObject value)? full, - TResult? Function(SpotubeSimpleTrackObject value)? simple, }) { return local?.call(this); } @@ -3356,7 +3280,6 @@ class _$SpotubeLocalTrackObjectImpl implements SpotubeLocalTrackObject { TResult maybeMap({ TResult Function(SpotubeLocalTrackObject value)? local, TResult Function(SpotubeFullTrackObject value)? full, - TResult Function(SpotubeSimpleTrackObject value)? simple, required TResult orElse(), }) { if (local != null) { @@ -3489,16 +3412,6 @@ class __$$SpotubeFullTrackObjectImplCopyWithImpl<$Res> 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 @@ -3614,15 +3527,6 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject { String isrc, bool explicit) full, - required TResult Function( - String id, - String name, - String externalUri, - int durationMs, - bool explicit, - List artists, - SpotubeSimpleAlbumObject? album) - simple, }) { return full( id, name, externalUri, artists, album, durationMs, isrc, explicit); @@ -3650,15 +3554,6 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject { String isrc, bool explicit)? full, - TResult? Function( - String id, - String name, - String externalUri, - int durationMs, - bool explicit, - List artists, - SpotubeSimpleAlbumObject? album)? - simple, }) { return full?.call( id, name, externalUri, artists, album, durationMs, isrc, explicit); @@ -3686,15 +3581,6 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject { String isrc, bool explicit)? full, - TResult Function( - String id, - String name, - String externalUri, - int durationMs, - bool explicit, - List artists, - SpotubeSimpleAlbumObject? album)? - simple, required TResult orElse(), }) { if (full != null) { @@ -3709,7 +3595,6 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject { TResult map({ required TResult Function(SpotubeLocalTrackObject value) local, required TResult Function(SpotubeFullTrackObject value) full, - required TResult Function(SpotubeSimpleTrackObject value) simple, }) { return full(this); } @@ -3719,7 +3604,6 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject { TResult? mapOrNull({ TResult? Function(SpotubeLocalTrackObject value)? local, TResult? Function(SpotubeFullTrackObject value)? full, - TResult? Function(SpotubeSimpleTrackObject value)? simple, }) { return full?.call(this); } @@ -3729,7 +3613,6 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject { TResult maybeMap({ TResult Function(SpotubeLocalTrackObject value)? local, TResult Function(SpotubeFullTrackObject value)? full, - TResult Function(SpotubeSimpleTrackObject value)? simple, required TResult orElse(), }) { if (full != null) { @@ -3783,358 +3666,6 @@ abstract class SpotubeFullTrackObject implements SpotubeTrackObject { 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 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, - 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 artists = const [], - this.album, - final String? $type}) - : _artists = artists, - $type = $type ?? 'simple'; - - factory _$SpotubeSimpleTrackObjectImpl.fromJson(Map 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 _artists; - @override - @JsonKey() - List 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({ - required TResult Function( - String id, - String name, - String externalUri, - List artists, - SpotubeSimpleAlbumObject album, - int durationMs, - String path) - local, - required TResult Function( - String id, - String name, - String externalUri, - List artists, - SpotubeSimpleAlbumObject album, - int durationMs, - String isrc, - bool explicit) - full, - required TResult Function( - String id, - String name, - String externalUri, - int durationMs, - bool explicit, - List artists, - SpotubeSimpleAlbumObject? album) - simple, - }) { - return simple(id, name, externalUri, durationMs, explicit, artists, album); - } - - @override - @optionalTypeArgs - TResult? whenOrNull({ - TResult? Function( - String id, - String name, - String externalUri, - List artists, - SpotubeSimpleAlbumObject album, - int durationMs, - String path)? - local, - TResult? Function( - String id, - String name, - String externalUri, - List artists, - SpotubeSimpleAlbumObject album, - int durationMs, - String isrc, - bool explicit)? - full, - TResult? Function( - String id, - String name, - String externalUri, - int durationMs, - bool explicit, - List artists, - SpotubeSimpleAlbumObject? album)? - simple, - }) { - return simple?.call( - id, name, externalUri, durationMs, explicit, artists, album); - } - - @override - @optionalTypeArgs - TResult maybeWhen({ - TResult Function( - String id, - String name, - String externalUri, - List artists, - SpotubeSimpleAlbumObject album, - int durationMs, - String path)? - local, - TResult Function( - String id, - String name, - String externalUri, - List artists, - SpotubeSimpleAlbumObject album, - int durationMs, - String isrc, - bool explicit)? - full, - TResult Function( - String id, - String name, - String externalUri, - int durationMs, - bool explicit, - List 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({ - 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? Function(SpotubeLocalTrackObject value)? local, - TResult? Function(SpotubeFullTrackObject value)? full, - TResult? Function(SpotubeSimpleTrackObject value)? simple, - }) { - return simple?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap({ - 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 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 artists, - final SpotubeSimpleAlbumObject? album}) = _$SpotubeSimpleTrackObjectImpl; - - factory SpotubeSimpleTrackObject.fromJson(Map json) = - _$SpotubeSimpleTrackObjectImpl.fromJson; - - @override - String get id; - @override - String get name; - @override - String get externalUri; - @override - int get durationMs; - bool get explicit; - @override - List 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 json) { return _SpotubeUserObject.fromJson(json); } diff --git a/lib/models/metadata/metadata.g.dart b/lib/models/metadata/metadata.g.dart index 3303e324..75e2ec18 100644 --- a/lib/models/metadata/metadata.g.dart +++ b/lib/models/metadata/metadata.g.dart @@ -273,7 +273,7 @@ _$SpotubeSearchResponseObjectImpl _$$SpotubeSearchResponseObjectImplFromJson( Map.from(e as Map))) .toList(), tracks: (json['tracks'] as List) - .map((e) => SpotubeSimpleTrackObject.fromJson( + .map((e) => SpotubeFullTrackObject.fromJson( Map.from(e as Map))) .toList(), ); @@ -350,39 +350,6 @@ Map _$$SpotubeFullTrackObjectImplToJson( '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?) - ?.map((e) => SpotubeSimpleArtistObject.fromJson( - Map.from(e as Map))) - .toList() ?? - const [], - album: json['album'] == null - ? null - : SpotubeSimpleAlbumObject.fromJson( - Map.from(json['album'] as Map)), - $type: json['runtimeType'] as String?, - ); - -Map _$$SpotubeSimpleTrackObjectImplToJson( - _$SpotubeSimpleTrackObjectImpl instance) => - { - '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( id: json['id'] as String, diff --git a/lib/models/metadata/search.dart b/lib/models/metadata/search.dart index 4918c898..b39f063a 100644 --- a/lib/models/metadata/search.dart +++ b/lib/models/metadata/search.dart @@ -6,7 +6,7 @@ class SpotubeSearchResponseObject with _$SpotubeSearchResponseObject { required List albums, required List artists, required List playlists, - required List tracks, + required List tracks, }) = _SpotubeSearchResponseObject; factory SpotubeSearchResponseObject.fromJson(Map json) => diff --git a/lib/models/metadata/track.dart b/lib/models/metadata/track.dart index b7cf1a3e..9520daab 100644 --- a/lib/models/metadata/track.dart +++ b/lib/models/metadata/track.dart @@ -23,23 +23,54 @@ class SpotubeTrackObject with _$SpotubeTrackObject { required bool explicit, }) = SpotubeFullTrackObject; - factory SpotubeTrackObject.simple({ - required String id, - required String name, - required String externalUri, - required int durationMs, - required bool explicit, - @Default([]) List artists, - SpotubeSimpleAlbumObject? album, - }) = SpotubeSimpleTrackObject; + factory SpotubeTrackObject.localTrackFromFile( + File file, { + Metadata? metadata, + String? art, + }) { + return SpotubeLocalTrackObject( + id: file.absolute.path, + name: metadata?.title ?? basenameWithoutExtension(file.path), + 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 json) => _$SpotubeTrackObjectFromJson( - json.containsKey("isrc") - ? {...json, "runtimeType": "full"} - : json.containsKey("path") - ? {...json, "runtimeType": "local"} - : {...json, "runtimeType": "simple"}, + json.containsKey("path") + ? {...json, "runtimeType": "local"} + : {...json, "runtimeType": "full"}, ); } diff --git a/lib/models/spotify/home_feed.dart b/lib/models/spotify/home_feed.dart deleted file mode 100644 index ad764304..00000000 --- a/lib/models/spotify/home_feed.dart +++ /dev/null @@ -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 images, - required String name, - required String owner, - required String uri, - }) = _SpotifySectionPlaylist; - - factory SpotifySectionPlaylist.fromJson(Map 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 images, - }) = _SpotifySectionArtist; - - factory SpotifySectionArtist.fromJson(Map 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 artists, - required List images, - required String name, - required String uri, - }) = _SpotifySectionAlbum; - - factory SpotifySectionAlbum.fromJson(Map 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 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 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 json) => - _$SpotifyHomeFeedSectionItemFromJson(json); -} - -@freezed -class SpotifyHomeFeedSection with _$SpotifyHomeFeedSection { - factory SpotifyHomeFeedSection({ - required String typename, - String? title, - required String uri, - required List items, - }) = _SpotifyHomeFeedSection; - - factory SpotifyHomeFeedSection.fromJson(Map json) => - _$SpotifyHomeFeedSectionFromJson(json); -} - -@freezed -class SpotifyHomeFeed with _$SpotifyHomeFeed { - factory SpotifyHomeFeed({ - required String greeting, - required List sections, - }) = _SpotifyHomeFeed; - - factory SpotifyHomeFeed.fromJson(Map json) => - _$SpotifyHomeFeedFromJson(json); -} - -Map transformSectionItemTypeJsonMap( - Map 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>(), - "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 transformSectionItemJsonMap(Map 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) - as dynamic, - ) - .where( - (w) => - w["playlist"] != null || - w["artist"] != null || - w["album"] != null, - ) - .toList() - .cast>() - }; -} - -Map transformHomeFeedJsonMap(Map json) { - return { - "greeting": json["data"]["home"]["greeting"]["text"], - "sections": - (json["data"]["home"]["sectionContainer"]["sections"]["items"] as List) - .map( - (item) => - transformSectionItemJsonMap(item as Map) - as dynamic, - ) - .toList() - .cast>() - }; -} diff --git a/lib/models/spotify/home_feed.freezed.dart b/lib/models/spotify/home_feed.freezed.dart deleted file mode 100644 index 5076da29..00000000 --- a/lib/models/spotify/home_feed.freezed.dart +++ /dev/null @@ -1,1776 +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 'home_feed.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(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'); - -SpotifySectionPlaylist _$SpotifySectionPlaylistFromJson( - Map json) { - return _SpotifySectionPlaylist.fromJson(json); -} - -/// @nodoc -mixin _$SpotifySectionPlaylist { - String get description => throw _privateConstructorUsedError; - String get format => throw _privateConstructorUsedError; - List get images => - throw _privateConstructorUsedError; - String get name => throw _privateConstructorUsedError; - String get owner => throw _privateConstructorUsedError; - String get uri => throw _privateConstructorUsedError; - - /// Serializes this SpotifySectionPlaylist to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of SpotifySectionPlaylist - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $SpotifySectionPlaylistCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $SpotifySectionPlaylistCopyWith<$Res> { - factory $SpotifySectionPlaylistCopyWith(SpotifySectionPlaylist value, - $Res Function(SpotifySectionPlaylist) then) = - _$SpotifySectionPlaylistCopyWithImpl<$Res, SpotifySectionPlaylist>; - @useResult - $Res call( - {String description, - String format, - List images, - String name, - String owner, - String uri}); -} - -/// @nodoc -class _$SpotifySectionPlaylistCopyWithImpl<$Res, - $Val extends SpotifySectionPlaylist> - implements $SpotifySectionPlaylistCopyWith<$Res> { - _$SpotifySectionPlaylistCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of SpotifySectionPlaylist - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? description = null, - Object? format = null, - Object? images = null, - Object? name = null, - Object? owner = null, - Object? uri = null, - }) { - return _then(_value.copyWith( - description: null == description - ? _value.description - : description // ignore: cast_nullable_to_non_nullable - as String, - format: null == format - ? _value.format - : format // ignore: cast_nullable_to_non_nullable - as String, - images: null == images - ? _value.images - : images // ignore: cast_nullable_to_non_nullable - as List, - name: null == name - ? _value.name - : name // ignore: cast_nullable_to_non_nullable - as String, - owner: null == owner - ? _value.owner - : owner // ignore: cast_nullable_to_non_nullable - as String, - uri: null == uri - ? _value.uri - : uri // ignore: cast_nullable_to_non_nullable - as String, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$SpotifySectionPlaylistImplCopyWith<$Res> - implements $SpotifySectionPlaylistCopyWith<$Res> { - factory _$$SpotifySectionPlaylistImplCopyWith( - _$SpotifySectionPlaylistImpl value, - $Res Function(_$SpotifySectionPlaylistImpl) then) = - __$$SpotifySectionPlaylistImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {String description, - String format, - List images, - String name, - String owner, - String uri}); -} - -/// @nodoc -class __$$SpotifySectionPlaylistImplCopyWithImpl<$Res> - extends _$SpotifySectionPlaylistCopyWithImpl<$Res, - _$SpotifySectionPlaylistImpl> - implements _$$SpotifySectionPlaylistImplCopyWith<$Res> { - __$$SpotifySectionPlaylistImplCopyWithImpl( - _$SpotifySectionPlaylistImpl _value, - $Res Function(_$SpotifySectionPlaylistImpl) _then) - : super(_value, _then); - - /// Create a copy of SpotifySectionPlaylist - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? description = null, - Object? format = null, - Object? images = null, - Object? name = null, - Object? owner = null, - Object? uri = null, - }) { - return _then(_$SpotifySectionPlaylistImpl( - description: null == description - ? _value.description - : description // ignore: cast_nullable_to_non_nullable - as String, - format: null == format - ? _value.format - : format // ignore: cast_nullable_to_non_nullable - as String, - images: null == images - ? _value._images - : images // ignore: cast_nullable_to_non_nullable - as List, - name: null == name - ? _value.name - : name // ignore: cast_nullable_to_non_nullable - as String, - owner: null == owner - ? _value.owner - : owner // ignore: cast_nullable_to_non_nullable - as String, - uri: null == uri - ? _value.uri - : uri // ignore: cast_nullable_to_non_nullable - as String, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$SpotifySectionPlaylistImpl extends _SpotifySectionPlaylist { - const _$SpotifySectionPlaylistImpl( - {required this.description, - required this.format, - required final List images, - required this.name, - required this.owner, - required this.uri}) - : _images = images, - super._(); - - factory _$SpotifySectionPlaylistImpl.fromJson(Map json) => - _$$SpotifySectionPlaylistImplFromJson(json); - - @override - final String description; - @override - final String format; - final List _images; - @override - List get images { - if (_images is EqualUnmodifiableListView) return _images; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_images); - } - - @override - final String name; - @override - final String owner; - @override - final String uri; - - @override - String toString() { - return 'SpotifySectionPlaylist(description: $description, format: $format, images: $images, name: $name, owner: $owner, uri: $uri)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$SpotifySectionPlaylistImpl && - (identical(other.description, description) || - other.description == description) && - (identical(other.format, format) || other.format == format) && - const DeepCollectionEquality().equals(other._images, _images) && - (identical(other.name, name) || other.name == name) && - (identical(other.owner, owner) || other.owner == owner) && - (identical(other.uri, uri) || other.uri == uri)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash(runtimeType, description, format, - const DeepCollectionEquality().hash(_images), name, owner, uri); - - /// Create a copy of SpotifySectionPlaylist - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$SpotifySectionPlaylistImplCopyWith<_$SpotifySectionPlaylistImpl> - get copyWith => __$$SpotifySectionPlaylistImplCopyWithImpl< - _$SpotifySectionPlaylistImpl>(this, _$identity); - - @override - Map toJson() { - return _$$SpotifySectionPlaylistImplToJson( - this, - ); - } -} - -abstract class _SpotifySectionPlaylist extends SpotifySectionPlaylist { - const factory _SpotifySectionPlaylist( - {required final String description, - required final String format, - required final List images, - required final String name, - required final String owner, - required final String uri}) = _$SpotifySectionPlaylistImpl; - const _SpotifySectionPlaylist._() : super._(); - - factory _SpotifySectionPlaylist.fromJson(Map json) = - _$SpotifySectionPlaylistImpl.fromJson; - - @override - String get description; - @override - String get format; - @override - List get images; - @override - String get name; - @override - String get owner; - @override - String get uri; - - /// Create a copy of SpotifySectionPlaylist - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$SpotifySectionPlaylistImplCopyWith<_$SpotifySectionPlaylistImpl> - get copyWith => throw _privateConstructorUsedError; -} - -SpotifySectionArtist _$SpotifySectionArtistFromJson(Map json) { - return _SpotifySectionArtist.fromJson(json); -} - -/// @nodoc -mixin _$SpotifySectionArtist { - String get name => throw _privateConstructorUsedError; - String get uri => throw _privateConstructorUsedError; - List get images => - throw _privateConstructorUsedError; - - /// Serializes this SpotifySectionArtist to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of SpotifySectionArtist - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $SpotifySectionArtistCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $SpotifySectionArtistCopyWith<$Res> { - factory $SpotifySectionArtistCopyWith(SpotifySectionArtist value, - $Res Function(SpotifySectionArtist) then) = - _$SpotifySectionArtistCopyWithImpl<$Res, SpotifySectionArtist>; - @useResult - $Res call({String name, String uri, List images}); -} - -/// @nodoc -class _$SpotifySectionArtistCopyWithImpl<$Res, - $Val extends SpotifySectionArtist> - implements $SpotifySectionArtistCopyWith<$Res> { - _$SpotifySectionArtistCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of SpotifySectionArtist - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? name = null, - Object? uri = null, - Object? images = null, - }) { - return _then(_value.copyWith( - name: null == name - ? _value.name - : name // ignore: cast_nullable_to_non_nullable - as String, - uri: null == uri - ? _value.uri - : uri // ignore: cast_nullable_to_non_nullable - as String, - images: null == images - ? _value.images - : images // ignore: cast_nullable_to_non_nullable - as List, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$SpotifySectionArtistImplCopyWith<$Res> - implements $SpotifySectionArtistCopyWith<$Res> { - factory _$$SpotifySectionArtistImplCopyWith(_$SpotifySectionArtistImpl value, - $Res Function(_$SpotifySectionArtistImpl) then) = - __$$SpotifySectionArtistImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({String name, String uri, List images}); -} - -/// @nodoc -class __$$SpotifySectionArtistImplCopyWithImpl<$Res> - extends _$SpotifySectionArtistCopyWithImpl<$Res, _$SpotifySectionArtistImpl> - implements _$$SpotifySectionArtistImplCopyWith<$Res> { - __$$SpotifySectionArtistImplCopyWithImpl(_$SpotifySectionArtistImpl _value, - $Res Function(_$SpotifySectionArtistImpl) _then) - : super(_value, _then); - - /// Create a copy of SpotifySectionArtist - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? name = null, - Object? uri = null, - Object? images = null, - }) { - return _then(_$SpotifySectionArtistImpl( - name: null == name - ? _value.name - : name // ignore: cast_nullable_to_non_nullable - as String, - uri: null == uri - ? _value.uri - : uri // ignore: cast_nullable_to_non_nullable - as String, - images: null == images - ? _value._images - : images // ignore: cast_nullable_to_non_nullable - as List, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$SpotifySectionArtistImpl extends _SpotifySectionArtist { - const _$SpotifySectionArtistImpl( - {required this.name, - required this.uri, - required final List images}) - : _images = images, - super._(); - - factory _$SpotifySectionArtistImpl.fromJson(Map json) => - _$$SpotifySectionArtistImplFromJson(json); - - @override - final String name; - @override - final String uri; - final List _images; - @override - List get images { - if (_images is EqualUnmodifiableListView) return _images; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_images); - } - - @override - String toString() { - return 'SpotifySectionArtist(name: $name, uri: $uri, images: $images)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$SpotifySectionArtistImpl && - (identical(other.name, name) || other.name == name) && - (identical(other.uri, uri) || other.uri == uri) && - const DeepCollectionEquality().equals(other._images, _images)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash( - runtimeType, name, uri, const DeepCollectionEquality().hash(_images)); - - /// Create a copy of SpotifySectionArtist - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$SpotifySectionArtistImplCopyWith<_$SpotifySectionArtistImpl> - get copyWith => - __$$SpotifySectionArtistImplCopyWithImpl<_$SpotifySectionArtistImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$SpotifySectionArtistImplToJson( - this, - ); - } -} - -abstract class _SpotifySectionArtist extends SpotifySectionArtist { - const factory _SpotifySectionArtist( - {required final String name, - required final String uri, - required final List images}) = - _$SpotifySectionArtistImpl; - const _SpotifySectionArtist._() : super._(); - - factory _SpotifySectionArtist.fromJson(Map json) = - _$SpotifySectionArtistImpl.fromJson; - - @override - String get name; - @override - String get uri; - @override - List get images; - - /// Create a copy of SpotifySectionArtist - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$SpotifySectionArtistImplCopyWith<_$SpotifySectionArtistImpl> - get copyWith => throw _privateConstructorUsedError; -} - -SpotifySectionAlbum _$SpotifySectionAlbumFromJson(Map json) { - return _SpotifySectionAlbum.fromJson(json); -} - -/// @nodoc -mixin _$SpotifySectionAlbum { - List get artists => - throw _privateConstructorUsedError; - List get images => - throw _privateConstructorUsedError; - String get name => throw _privateConstructorUsedError; - String get uri => throw _privateConstructorUsedError; - - /// Serializes this SpotifySectionAlbum to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of SpotifySectionAlbum - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $SpotifySectionAlbumCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $SpotifySectionAlbumCopyWith<$Res> { - factory $SpotifySectionAlbumCopyWith( - SpotifySectionAlbum value, $Res Function(SpotifySectionAlbum) then) = - _$SpotifySectionAlbumCopyWithImpl<$Res, SpotifySectionAlbum>; - @useResult - $Res call( - {List artists, - List images, - String name, - String uri}); -} - -/// @nodoc -class _$SpotifySectionAlbumCopyWithImpl<$Res, $Val extends SpotifySectionAlbum> - implements $SpotifySectionAlbumCopyWith<$Res> { - _$SpotifySectionAlbumCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of SpotifySectionAlbum - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? artists = null, - Object? images = null, - Object? name = null, - Object? uri = null, - }) { - return _then(_value.copyWith( - artists: null == artists - ? _value.artists - : artists // ignore: cast_nullable_to_non_nullable - as List, - images: null == images - ? _value.images - : images // ignore: cast_nullable_to_non_nullable - as List, - name: null == name - ? _value.name - : name // ignore: cast_nullable_to_non_nullable - as String, - uri: null == uri - ? _value.uri - : uri // ignore: cast_nullable_to_non_nullable - as String, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$SpotifySectionAlbumImplCopyWith<$Res> - implements $SpotifySectionAlbumCopyWith<$Res> { - factory _$$SpotifySectionAlbumImplCopyWith(_$SpotifySectionAlbumImpl value, - $Res Function(_$SpotifySectionAlbumImpl) then) = - __$$SpotifySectionAlbumImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {List artists, - List images, - String name, - String uri}); -} - -/// @nodoc -class __$$SpotifySectionAlbumImplCopyWithImpl<$Res> - extends _$SpotifySectionAlbumCopyWithImpl<$Res, _$SpotifySectionAlbumImpl> - implements _$$SpotifySectionAlbumImplCopyWith<$Res> { - __$$SpotifySectionAlbumImplCopyWithImpl(_$SpotifySectionAlbumImpl _value, - $Res Function(_$SpotifySectionAlbumImpl) _then) - : super(_value, _then); - - /// Create a copy of SpotifySectionAlbum - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? artists = null, - Object? images = null, - Object? name = null, - Object? uri = null, - }) { - return _then(_$SpotifySectionAlbumImpl( - artists: null == artists - ? _value._artists - : artists // ignore: cast_nullable_to_non_nullable - as List, - images: null == images - ? _value._images - : images // ignore: cast_nullable_to_non_nullable - as List, - name: null == name - ? _value.name - : name // ignore: cast_nullable_to_non_nullable - as String, - uri: null == uri - ? _value.uri - : uri // ignore: cast_nullable_to_non_nullable - as String, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$SpotifySectionAlbumImpl extends _SpotifySectionAlbum { - const _$SpotifySectionAlbumImpl( - {required final List artists, - required final List images, - required this.name, - required this.uri}) - : _artists = artists, - _images = images, - super._(); - - factory _$SpotifySectionAlbumImpl.fromJson(Map json) => - _$$SpotifySectionAlbumImplFromJson(json); - - final List _artists; - @override - List get artists { - if (_artists is EqualUnmodifiableListView) return _artists; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_artists); - } - - final List _images; - @override - List get images { - if (_images is EqualUnmodifiableListView) return _images; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_images); - } - - @override - final String name; - @override - final String uri; - - @override - String toString() { - return 'SpotifySectionAlbum(artists: $artists, images: $images, name: $name, uri: $uri)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$SpotifySectionAlbumImpl && - const DeepCollectionEquality().equals(other._artists, _artists) && - const DeepCollectionEquality().equals(other._images, _images) && - (identical(other.name, name) || other.name == name) && - (identical(other.uri, uri) || other.uri == uri)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash( - runtimeType, - const DeepCollectionEquality().hash(_artists), - const DeepCollectionEquality().hash(_images), - name, - uri); - - /// Create a copy of SpotifySectionAlbum - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$SpotifySectionAlbumImplCopyWith<_$SpotifySectionAlbumImpl> get copyWith => - __$$SpotifySectionAlbumImplCopyWithImpl<_$SpotifySectionAlbumImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$SpotifySectionAlbumImplToJson( - this, - ); - } -} - -abstract class _SpotifySectionAlbum extends SpotifySectionAlbum { - const factory _SpotifySectionAlbum( - {required final List artists, - required final List images, - required final String name, - required final String uri}) = _$SpotifySectionAlbumImpl; - const _SpotifySectionAlbum._() : super._(); - - factory _SpotifySectionAlbum.fromJson(Map json) = - _$SpotifySectionAlbumImpl.fromJson; - - @override - List get artists; - @override - List get images; - @override - String get name; - @override - String get uri; - - /// Create a copy of SpotifySectionAlbum - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$SpotifySectionAlbumImplCopyWith<_$SpotifySectionAlbumImpl> get copyWith => - throw _privateConstructorUsedError; -} - -SpotifySectionAlbumArtist _$SpotifySectionAlbumArtistFromJson( - Map json) { - return _SpotifySectionAlbumArtist.fromJson(json); -} - -/// @nodoc -mixin _$SpotifySectionAlbumArtist { - String get name => throw _privateConstructorUsedError; - String get uri => throw _privateConstructorUsedError; - - /// Serializes this SpotifySectionAlbumArtist to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of SpotifySectionAlbumArtist - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $SpotifySectionAlbumArtistCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $SpotifySectionAlbumArtistCopyWith<$Res> { - factory $SpotifySectionAlbumArtistCopyWith(SpotifySectionAlbumArtist value, - $Res Function(SpotifySectionAlbumArtist) then) = - _$SpotifySectionAlbumArtistCopyWithImpl<$Res, SpotifySectionAlbumArtist>; - @useResult - $Res call({String name, String uri}); -} - -/// @nodoc -class _$SpotifySectionAlbumArtistCopyWithImpl<$Res, - $Val extends SpotifySectionAlbumArtist> - implements $SpotifySectionAlbumArtistCopyWith<$Res> { - _$SpotifySectionAlbumArtistCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of SpotifySectionAlbumArtist - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? name = null, - Object? uri = null, - }) { - return _then(_value.copyWith( - name: null == name - ? _value.name - : name // ignore: cast_nullable_to_non_nullable - as String, - uri: null == uri - ? _value.uri - : uri // ignore: cast_nullable_to_non_nullable - as String, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$SpotifySectionAlbumArtistImplCopyWith<$Res> - implements $SpotifySectionAlbumArtistCopyWith<$Res> { - factory _$$SpotifySectionAlbumArtistImplCopyWith( - _$SpotifySectionAlbumArtistImpl value, - $Res Function(_$SpotifySectionAlbumArtistImpl) then) = - __$$SpotifySectionAlbumArtistImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({String name, String uri}); -} - -/// @nodoc -class __$$SpotifySectionAlbumArtistImplCopyWithImpl<$Res> - extends _$SpotifySectionAlbumArtistCopyWithImpl<$Res, - _$SpotifySectionAlbumArtistImpl> - implements _$$SpotifySectionAlbumArtistImplCopyWith<$Res> { - __$$SpotifySectionAlbumArtistImplCopyWithImpl( - _$SpotifySectionAlbumArtistImpl _value, - $Res Function(_$SpotifySectionAlbumArtistImpl) _then) - : super(_value, _then); - - /// Create a copy of SpotifySectionAlbumArtist - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? name = null, - Object? uri = null, - }) { - return _then(_$SpotifySectionAlbumArtistImpl( - name: null == name - ? _value.name - : name // ignore: cast_nullable_to_non_nullable - as String, - uri: null == uri - ? _value.uri - : uri // ignore: cast_nullable_to_non_nullable - as String, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$SpotifySectionAlbumArtistImpl extends _SpotifySectionAlbumArtist { - const _$SpotifySectionAlbumArtistImpl({required this.name, required this.uri}) - : super._(); - - factory _$SpotifySectionAlbumArtistImpl.fromJson(Map json) => - _$$SpotifySectionAlbumArtistImplFromJson(json); - - @override - final String name; - @override - final String uri; - - @override - String toString() { - return 'SpotifySectionAlbumArtist(name: $name, uri: $uri)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$SpotifySectionAlbumArtistImpl && - (identical(other.name, name) || other.name == name) && - (identical(other.uri, uri) || other.uri == uri)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash(runtimeType, name, uri); - - /// Create a copy of SpotifySectionAlbumArtist - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$SpotifySectionAlbumArtistImplCopyWith<_$SpotifySectionAlbumArtistImpl> - get copyWith => __$$SpotifySectionAlbumArtistImplCopyWithImpl< - _$SpotifySectionAlbumArtistImpl>(this, _$identity); - - @override - Map toJson() { - return _$$SpotifySectionAlbumArtistImplToJson( - this, - ); - } -} - -abstract class _SpotifySectionAlbumArtist extends SpotifySectionAlbumArtist { - const factory _SpotifySectionAlbumArtist( - {required final String name, - required final String uri}) = _$SpotifySectionAlbumArtistImpl; - const _SpotifySectionAlbumArtist._() : super._(); - - factory _SpotifySectionAlbumArtist.fromJson(Map json) = - _$SpotifySectionAlbumArtistImpl.fromJson; - - @override - String get name; - @override - String get uri; - - /// Create a copy of SpotifySectionAlbumArtist - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$SpotifySectionAlbumArtistImplCopyWith<_$SpotifySectionAlbumArtistImpl> - get copyWith => throw _privateConstructorUsedError; -} - -SpotifySectionItemImage _$SpotifySectionItemImageFromJson( - Map json) { - return _SpotifySectionItemImage.fromJson(json); -} - -/// @nodoc -mixin _$SpotifySectionItemImage { - num? get height => throw _privateConstructorUsedError; - String get url => throw _privateConstructorUsedError; - num? get width => throw _privateConstructorUsedError; - - /// Serializes this SpotifySectionItemImage to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of SpotifySectionItemImage - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $SpotifySectionItemImageCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $SpotifySectionItemImageCopyWith<$Res> { - factory $SpotifySectionItemImageCopyWith(SpotifySectionItemImage value, - $Res Function(SpotifySectionItemImage) then) = - _$SpotifySectionItemImageCopyWithImpl<$Res, SpotifySectionItemImage>; - @useResult - $Res call({num? height, String url, num? width}); -} - -/// @nodoc -class _$SpotifySectionItemImageCopyWithImpl<$Res, - $Val extends SpotifySectionItemImage> - implements $SpotifySectionItemImageCopyWith<$Res> { - _$SpotifySectionItemImageCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of SpotifySectionItemImage - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? height = freezed, - Object? url = null, - Object? width = freezed, - }) { - return _then(_value.copyWith( - height: freezed == height - ? _value.height - : height // ignore: cast_nullable_to_non_nullable - as num?, - url: null == url - ? _value.url - : url // ignore: cast_nullable_to_non_nullable - as String, - width: freezed == width - ? _value.width - : width // ignore: cast_nullable_to_non_nullable - as num?, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$SpotifySectionItemImageImplCopyWith<$Res> - implements $SpotifySectionItemImageCopyWith<$Res> { - factory _$$SpotifySectionItemImageImplCopyWith( - _$SpotifySectionItemImageImpl value, - $Res Function(_$SpotifySectionItemImageImpl) then) = - __$$SpotifySectionItemImageImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({num? height, String url, num? width}); -} - -/// @nodoc -class __$$SpotifySectionItemImageImplCopyWithImpl<$Res> - extends _$SpotifySectionItemImageCopyWithImpl<$Res, - _$SpotifySectionItemImageImpl> - implements _$$SpotifySectionItemImageImplCopyWith<$Res> { - __$$SpotifySectionItemImageImplCopyWithImpl( - _$SpotifySectionItemImageImpl _value, - $Res Function(_$SpotifySectionItemImageImpl) _then) - : super(_value, _then); - - /// Create a copy of SpotifySectionItemImage - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? height = freezed, - Object? url = null, - Object? width = freezed, - }) { - return _then(_$SpotifySectionItemImageImpl( - height: freezed == height - ? _value.height - : height // ignore: cast_nullable_to_non_nullable - as num?, - url: null == url - ? _value.url - : url // ignore: cast_nullable_to_non_nullable - as String, - width: freezed == width - ? _value.width - : width // ignore: cast_nullable_to_non_nullable - as num?, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$SpotifySectionItemImageImpl extends _SpotifySectionItemImage { - const _$SpotifySectionItemImageImpl( - {required this.height, required this.url, required this.width}) - : super._(); - - factory _$SpotifySectionItemImageImpl.fromJson(Map json) => - _$$SpotifySectionItemImageImplFromJson(json); - - @override - final num? height; - @override - final String url; - @override - final num? width; - - @override - String toString() { - return 'SpotifySectionItemImage(height: $height, url: $url, width: $width)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$SpotifySectionItemImageImpl && - (identical(other.height, height) || other.height == height) && - (identical(other.url, url) || other.url == url) && - (identical(other.width, width) || other.width == width)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash(runtimeType, height, url, width); - - /// Create a copy of SpotifySectionItemImage - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$SpotifySectionItemImageImplCopyWith<_$SpotifySectionItemImageImpl> - get copyWith => __$$SpotifySectionItemImageImplCopyWithImpl< - _$SpotifySectionItemImageImpl>(this, _$identity); - - @override - Map toJson() { - return _$$SpotifySectionItemImageImplToJson( - this, - ); - } -} - -abstract class _SpotifySectionItemImage extends SpotifySectionItemImage { - const factory _SpotifySectionItemImage( - {required final num? height, - required final String url, - required final num? width}) = _$SpotifySectionItemImageImpl; - const _SpotifySectionItemImage._() : super._(); - - factory _SpotifySectionItemImage.fromJson(Map json) = - _$SpotifySectionItemImageImpl.fromJson; - - @override - num? get height; - @override - String get url; - @override - num? get width; - - /// Create a copy of SpotifySectionItemImage - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$SpotifySectionItemImageImplCopyWith<_$SpotifySectionItemImageImpl> - get copyWith => throw _privateConstructorUsedError; -} - -SpotifyHomeFeedSectionItem _$SpotifyHomeFeedSectionItemFromJson( - Map json) { - return _SpotifyHomeFeedSectionItem.fromJson(json); -} - -/// @nodoc -mixin _$SpotifyHomeFeedSectionItem { - String get typename => throw _privateConstructorUsedError; - SpotifySectionPlaylist? get playlist => throw _privateConstructorUsedError; - SpotifySectionArtist? get artist => throw _privateConstructorUsedError; - SpotifySectionAlbum? get album => throw _privateConstructorUsedError; - - /// Serializes this SpotifyHomeFeedSectionItem to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of SpotifyHomeFeedSectionItem - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $SpotifyHomeFeedSectionItemCopyWith - get copyWith => throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $SpotifyHomeFeedSectionItemCopyWith<$Res> { - factory $SpotifyHomeFeedSectionItemCopyWith(SpotifyHomeFeedSectionItem value, - $Res Function(SpotifyHomeFeedSectionItem) then) = - _$SpotifyHomeFeedSectionItemCopyWithImpl<$Res, - SpotifyHomeFeedSectionItem>; - @useResult - $Res call( - {String typename, - SpotifySectionPlaylist? playlist, - SpotifySectionArtist? artist, - SpotifySectionAlbum? album}); - - $SpotifySectionPlaylistCopyWith<$Res>? get playlist; - $SpotifySectionArtistCopyWith<$Res>? get artist; - $SpotifySectionAlbumCopyWith<$Res>? get album; -} - -/// @nodoc -class _$SpotifyHomeFeedSectionItemCopyWithImpl<$Res, - $Val extends SpotifyHomeFeedSectionItem> - implements $SpotifyHomeFeedSectionItemCopyWith<$Res> { - _$SpotifyHomeFeedSectionItemCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of SpotifyHomeFeedSectionItem - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? typename = null, - Object? playlist = freezed, - Object? artist = freezed, - Object? album = freezed, - }) { - return _then(_value.copyWith( - typename: null == typename - ? _value.typename - : typename // ignore: cast_nullable_to_non_nullable - as String, - playlist: freezed == playlist - ? _value.playlist - : playlist // ignore: cast_nullable_to_non_nullable - as SpotifySectionPlaylist?, - artist: freezed == artist - ? _value.artist - : artist // ignore: cast_nullable_to_non_nullable - as SpotifySectionArtist?, - album: freezed == album - ? _value.album - : album // ignore: cast_nullable_to_non_nullable - as SpotifySectionAlbum?, - ) as $Val); - } - - /// Create a copy of SpotifyHomeFeedSectionItem - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $SpotifySectionPlaylistCopyWith<$Res>? get playlist { - if (_value.playlist == null) { - return null; - } - - return $SpotifySectionPlaylistCopyWith<$Res>(_value.playlist!, (value) { - return _then(_value.copyWith(playlist: value) as $Val); - }); - } - - /// Create a copy of SpotifyHomeFeedSectionItem - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $SpotifySectionArtistCopyWith<$Res>? get artist { - if (_value.artist == null) { - return null; - } - - return $SpotifySectionArtistCopyWith<$Res>(_value.artist!, (value) { - return _then(_value.copyWith(artist: value) as $Val); - }); - } - - /// Create a copy of SpotifyHomeFeedSectionItem - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $SpotifySectionAlbumCopyWith<$Res>? get album { - if (_value.album == null) { - return null; - } - - return $SpotifySectionAlbumCopyWith<$Res>(_value.album!, (value) { - return _then(_value.copyWith(album: value) as $Val); - }); - } -} - -/// @nodoc -abstract class _$$SpotifyHomeFeedSectionItemImplCopyWith<$Res> - implements $SpotifyHomeFeedSectionItemCopyWith<$Res> { - factory _$$SpotifyHomeFeedSectionItemImplCopyWith( - _$SpotifyHomeFeedSectionItemImpl value, - $Res Function(_$SpotifyHomeFeedSectionItemImpl) then) = - __$$SpotifyHomeFeedSectionItemImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {String typename, - SpotifySectionPlaylist? playlist, - SpotifySectionArtist? artist, - SpotifySectionAlbum? album}); - - @override - $SpotifySectionPlaylistCopyWith<$Res>? get playlist; - @override - $SpotifySectionArtistCopyWith<$Res>? get artist; - @override - $SpotifySectionAlbumCopyWith<$Res>? get album; -} - -/// @nodoc -class __$$SpotifyHomeFeedSectionItemImplCopyWithImpl<$Res> - extends _$SpotifyHomeFeedSectionItemCopyWithImpl<$Res, - _$SpotifyHomeFeedSectionItemImpl> - implements _$$SpotifyHomeFeedSectionItemImplCopyWith<$Res> { - __$$SpotifyHomeFeedSectionItemImplCopyWithImpl( - _$SpotifyHomeFeedSectionItemImpl _value, - $Res Function(_$SpotifyHomeFeedSectionItemImpl) _then) - : super(_value, _then); - - /// Create a copy of SpotifyHomeFeedSectionItem - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? typename = null, - Object? playlist = freezed, - Object? artist = freezed, - Object? album = freezed, - }) { - return _then(_$SpotifyHomeFeedSectionItemImpl( - typename: null == typename - ? _value.typename - : typename // ignore: cast_nullable_to_non_nullable - as String, - playlist: freezed == playlist - ? _value.playlist - : playlist // ignore: cast_nullable_to_non_nullable - as SpotifySectionPlaylist?, - artist: freezed == artist - ? _value.artist - : artist // ignore: cast_nullable_to_non_nullable - as SpotifySectionArtist?, - album: freezed == album - ? _value.album - : album // ignore: cast_nullable_to_non_nullable - as SpotifySectionAlbum?, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$SpotifyHomeFeedSectionItemImpl implements _SpotifyHomeFeedSectionItem { - _$SpotifyHomeFeedSectionItemImpl( - {required this.typename, this.playlist, this.artist, this.album}); - - factory _$SpotifyHomeFeedSectionItemImpl.fromJson( - Map json) => - _$$SpotifyHomeFeedSectionItemImplFromJson(json); - - @override - final String typename; - @override - final SpotifySectionPlaylist? playlist; - @override - final SpotifySectionArtist? artist; - @override - final SpotifySectionAlbum? album; - - @override - String toString() { - return 'SpotifyHomeFeedSectionItem(typename: $typename, playlist: $playlist, artist: $artist, album: $album)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$SpotifyHomeFeedSectionItemImpl && - (identical(other.typename, typename) || - other.typename == typename) && - (identical(other.playlist, playlist) || - other.playlist == playlist) && - (identical(other.artist, artist) || other.artist == artist) && - (identical(other.album, album) || other.album == album)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => - Object.hash(runtimeType, typename, playlist, artist, album); - - /// Create a copy of SpotifyHomeFeedSectionItem - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$SpotifyHomeFeedSectionItemImplCopyWith<_$SpotifyHomeFeedSectionItemImpl> - get copyWith => __$$SpotifyHomeFeedSectionItemImplCopyWithImpl< - _$SpotifyHomeFeedSectionItemImpl>(this, _$identity); - - @override - Map toJson() { - return _$$SpotifyHomeFeedSectionItemImplToJson( - this, - ); - } -} - -abstract class _SpotifyHomeFeedSectionItem - implements SpotifyHomeFeedSectionItem { - factory _SpotifyHomeFeedSectionItem( - {required final String typename, - final SpotifySectionPlaylist? playlist, - final SpotifySectionArtist? artist, - final SpotifySectionAlbum? album}) = _$SpotifyHomeFeedSectionItemImpl; - - factory _SpotifyHomeFeedSectionItem.fromJson(Map json) = - _$SpotifyHomeFeedSectionItemImpl.fromJson; - - @override - String get typename; - @override - SpotifySectionPlaylist? get playlist; - @override - SpotifySectionArtist? get artist; - @override - SpotifySectionAlbum? get album; - - /// Create a copy of SpotifyHomeFeedSectionItem - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$SpotifyHomeFeedSectionItemImplCopyWith<_$SpotifyHomeFeedSectionItemImpl> - get copyWith => throw _privateConstructorUsedError; -} - -SpotifyHomeFeedSection _$SpotifyHomeFeedSectionFromJson( - Map json) { - return _SpotifyHomeFeedSection.fromJson(json); -} - -/// @nodoc -mixin _$SpotifyHomeFeedSection { - String get typename => throw _privateConstructorUsedError; - String? get title => throw _privateConstructorUsedError; - String get uri => throw _privateConstructorUsedError; - List get items => - throw _privateConstructorUsedError; - - /// Serializes this SpotifyHomeFeedSection to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of SpotifyHomeFeedSection - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $SpotifyHomeFeedSectionCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $SpotifyHomeFeedSectionCopyWith<$Res> { - factory $SpotifyHomeFeedSectionCopyWith(SpotifyHomeFeedSection value, - $Res Function(SpotifyHomeFeedSection) then) = - _$SpotifyHomeFeedSectionCopyWithImpl<$Res, SpotifyHomeFeedSection>; - @useResult - $Res call( - {String typename, - String? title, - String uri, - List items}); -} - -/// @nodoc -class _$SpotifyHomeFeedSectionCopyWithImpl<$Res, - $Val extends SpotifyHomeFeedSection> - implements $SpotifyHomeFeedSectionCopyWith<$Res> { - _$SpotifyHomeFeedSectionCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of SpotifyHomeFeedSection - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? typename = null, - Object? title = freezed, - Object? uri = null, - Object? items = null, - }) { - return _then(_value.copyWith( - typename: null == typename - ? _value.typename - : typename // ignore: cast_nullable_to_non_nullable - as String, - title: freezed == title - ? _value.title - : title // ignore: cast_nullable_to_non_nullable - as String?, - uri: null == uri - ? _value.uri - : uri // ignore: cast_nullable_to_non_nullable - as String, - items: null == items - ? _value.items - : items // ignore: cast_nullable_to_non_nullable - as List, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$SpotifyHomeFeedSectionImplCopyWith<$Res> - implements $SpotifyHomeFeedSectionCopyWith<$Res> { - factory _$$SpotifyHomeFeedSectionImplCopyWith( - _$SpotifyHomeFeedSectionImpl value, - $Res Function(_$SpotifyHomeFeedSectionImpl) then) = - __$$SpotifyHomeFeedSectionImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {String typename, - String? title, - String uri, - List items}); -} - -/// @nodoc -class __$$SpotifyHomeFeedSectionImplCopyWithImpl<$Res> - extends _$SpotifyHomeFeedSectionCopyWithImpl<$Res, - _$SpotifyHomeFeedSectionImpl> - implements _$$SpotifyHomeFeedSectionImplCopyWith<$Res> { - __$$SpotifyHomeFeedSectionImplCopyWithImpl( - _$SpotifyHomeFeedSectionImpl _value, - $Res Function(_$SpotifyHomeFeedSectionImpl) _then) - : super(_value, _then); - - /// Create a copy of SpotifyHomeFeedSection - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? typename = null, - Object? title = freezed, - Object? uri = null, - Object? items = null, - }) { - return _then(_$SpotifyHomeFeedSectionImpl( - typename: null == typename - ? _value.typename - : typename // ignore: cast_nullable_to_non_nullable - as String, - title: freezed == title - ? _value.title - : title // ignore: cast_nullable_to_non_nullable - as String?, - uri: null == uri - ? _value.uri - : uri // ignore: cast_nullable_to_non_nullable - as String, - items: null == items - ? _value._items - : items // ignore: cast_nullable_to_non_nullable - as List, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$SpotifyHomeFeedSectionImpl implements _SpotifyHomeFeedSection { - _$SpotifyHomeFeedSectionImpl( - {required this.typename, - this.title, - required this.uri, - required final List items}) - : _items = items; - - factory _$SpotifyHomeFeedSectionImpl.fromJson(Map json) => - _$$SpotifyHomeFeedSectionImplFromJson(json); - - @override - final String typename; - @override - final String? title; - @override - final String uri; - final List _items; - @override - List get items { - if (_items is EqualUnmodifiableListView) return _items; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_items); - } - - @override - String toString() { - return 'SpotifyHomeFeedSection(typename: $typename, title: $title, uri: $uri, items: $items)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$SpotifyHomeFeedSectionImpl && - (identical(other.typename, typename) || - other.typename == typename) && - (identical(other.title, title) || other.title == title) && - (identical(other.uri, uri) || other.uri == uri) && - const DeepCollectionEquality().equals(other._items, _items)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash(runtimeType, typename, title, uri, - const DeepCollectionEquality().hash(_items)); - - /// Create a copy of SpotifyHomeFeedSection - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$SpotifyHomeFeedSectionImplCopyWith<_$SpotifyHomeFeedSectionImpl> - get copyWith => __$$SpotifyHomeFeedSectionImplCopyWithImpl< - _$SpotifyHomeFeedSectionImpl>(this, _$identity); - - @override - Map toJson() { - return _$$SpotifyHomeFeedSectionImplToJson( - this, - ); - } -} - -abstract class _SpotifyHomeFeedSection implements SpotifyHomeFeedSection { - factory _SpotifyHomeFeedSection( - {required final String typename, - final String? title, - required final String uri, - required final List items}) = - _$SpotifyHomeFeedSectionImpl; - - factory _SpotifyHomeFeedSection.fromJson(Map json) = - _$SpotifyHomeFeedSectionImpl.fromJson; - - @override - String get typename; - @override - String? get title; - @override - String get uri; - @override - List get items; - - /// Create a copy of SpotifyHomeFeedSection - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$SpotifyHomeFeedSectionImplCopyWith<_$SpotifyHomeFeedSectionImpl> - get copyWith => throw _privateConstructorUsedError; -} - -SpotifyHomeFeed _$SpotifyHomeFeedFromJson(Map json) { - return _SpotifyHomeFeed.fromJson(json); -} - -/// @nodoc -mixin _$SpotifyHomeFeed { - String get greeting => throw _privateConstructorUsedError; - List get sections => - throw _privateConstructorUsedError; - - /// Serializes this SpotifyHomeFeed to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of SpotifyHomeFeed - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $SpotifyHomeFeedCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $SpotifyHomeFeedCopyWith<$Res> { - factory $SpotifyHomeFeedCopyWith( - SpotifyHomeFeed value, $Res Function(SpotifyHomeFeed) then) = - _$SpotifyHomeFeedCopyWithImpl<$Res, SpotifyHomeFeed>; - @useResult - $Res call({String greeting, List sections}); -} - -/// @nodoc -class _$SpotifyHomeFeedCopyWithImpl<$Res, $Val extends SpotifyHomeFeed> - implements $SpotifyHomeFeedCopyWith<$Res> { - _$SpotifyHomeFeedCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of SpotifyHomeFeed - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? greeting = null, - Object? sections = null, - }) { - return _then(_value.copyWith( - greeting: null == greeting - ? _value.greeting - : greeting // ignore: cast_nullable_to_non_nullable - as String, - sections: null == sections - ? _value.sections - : sections // ignore: cast_nullable_to_non_nullable - as List, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$SpotifyHomeFeedImplCopyWith<$Res> - implements $SpotifyHomeFeedCopyWith<$Res> { - factory _$$SpotifyHomeFeedImplCopyWith(_$SpotifyHomeFeedImpl value, - $Res Function(_$SpotifyHomeFeedImpl) then) = - __$$SpotifyHomeFeedImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({String greeting, List sections}); -} - -/// @nodoc -class __$$SpotifyHomeFeedImplCopyWithImpl<$Res> - extends _$SpotifyHomeFeedCopyWithImpl<$Res, _$SpotifyHomeFeedImpl> - implements _$$SpotifyHomeFeedImplCopyWith<$Res> { - __$$SpotifyHomeFeedImplCopyWithImpl( - _$SpotifyHomeFeedImpl _value, $Res Function(_$SpotifyHomeFeedImpl) _then) - : super(_value, _then); - - /// Create a copy of SpotifyHomeFeed - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? greeting = null, - Object? sections = null, - }) { - return _then(_$SpotifyHomeFeedImpl( - greeting: null == greeting - ? _value.greeting - : greeting // ignore: cast_nullable_to_non_nullable - as String, - sections: null == sections - ? _value._sections - : sections // ignore: cast_nullable_to_non_nullable - as List, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$SpotifyHomeFeedImpl implements _SpotifyHomeFeed { - _$SpotifyHomeFeedImpl( - {required this.greeting, - required final List sections}) - : _sections = sections; - - factory _$SpotifyHomeFeedImpl.fromJson(Map json) => - _$$SpotifyHomeFeedImplFromJson(json); - - @override - final String greeting; - final List _sections; - @override - List get sections { - if (_sections is EqualUnmodifiableListView) return _sections; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_sections); - } - - @override - String toString() { - return 'SpotifyHomeFeed(greeting: $greeting, sections: $sections)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$SpotifyHomeFeedImpl && - (identical(other.greeting, greeting) || - other.greeting == greeting) && - const DeepCollectionEquality().equals(other._sections, _sections)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash( - runtimeType, greeting, const DeepCollectionEquality().hash(_sections)); - - /// Create a copy of SpotifyHomeFeed - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$SpotifyHomeFeedImplCopyWith<_$SpotifyHomeFeedImpl> get copyWith => - __$$SpotifyHomeFeedImplCopyWithImpl<_$SpotifyHomeFeedImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$SpotifyHomeFeedImplToJson( - this, - ); - } -} - -abstract class _SpotifyHomeFeed implements SpotifyHomeFeed { - factory _SpotifyHomeFeed( - {required final String greeting, - required final List sections}) = - _$SpotifyHomeFeedImpl; - - factory _SpotifyHomeFeed.fromJson(Map json) = - _$SpotifyHomeFeedImpl.fromJson; - - @override - String get greeting; - @override - List get sections; - - /// Create a copy of SpotifyHomeFeed - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$SpotifyHomeFeedImplCopyWith<_$SpotifyHomeFeedImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/lib/models/spotify/home_feed.g.dart b/lib/models/spotify/home_feed.g.dart deleted file mode 100644 index fceb3db4..00000000 --- a/lib/models/spotify/home_feed.g.dart +++ /dev/null @@ -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) - .map((e) => SpotifySectionItemImage.fromJson( - Map.from(e as Map))) - .toList(), - name: json['name'] as String, - owner: json['owner'] as String, - uri: json['uri'] as String, - ); - -Map _$$SpotifySectionPlaylistImplToJson( - _$SpotifySectionPlaylistImpl instance) => - { - '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) - .map((e) => SpotifySectionItemImage.fromJson( - Map.from(e as Map))) - .toList(), - ); - -Map _$$SpotifySectionArtistImplToJson( - _$SpotifySectionArtistImpl instance) => - { - 'name': instance.name, - 'uri': instance.uri, - 'images': instance.images.map((e) => e.toJson()).toList(), - }; - -_$SpotifySectionAlbumImpl _$$SpotifySectionAlbumImplFromJson(Map json) => - _$SpotifySectionAlbumImpl( - artists: (json['artists'] as List) - .map((e) => SpotifySectionAlbumArtist.fromJson( - Map.from(e as Map))) - .toList(), - images: (json['images'] as List) - .map((e) => SpotifySectionItemImage.fromJson( - Map.from(e as Map))) - .toList(), - name: json['name'] as String, - uri: json['uri'] as String, - ); - -Map _$$SpotifySectionAlbumImplToJson( - _$SpotifySectionAlbumImpl instance) => - { - '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 _$$SpotifySectionAlbumArtistImplToJson( - _$SpotifySectionAlbumArtistImpl instance) => - { - '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 _$$SpotifySectionItemImageImplToJson( - _$SpotifySectionItemImageImpl instance) => - { - '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.from(json['playlist'] as Map)), - artist: json['artist'] == null - ? null - : SpotifySectionArtist.fromJson( - Map.from(json['artist'] as Map)), - album: json['album'] == null - ? null - : SpotifySectionAlbum.fromJson( - Map.from(json['album'] as Map)), - ); - -Map _$$SpotifyHomeFeedSectionItemImplToJson( - _$SpotifyHomeFeedSectionItemImpl instance) => - { - '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) - .map((e) => SpotifyHomeFeedSectionItem.fromJson( - Map.from(e as Map))) - .toList(), - ); - -Map _$$SpotifyHomeFeedSectionImplToJson( - _$SpotifyHomeFeedSectionImpl instance) => - { - '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) - .map((e) => SpotifyHomeFeedSection.fromJson( - Map.from(e as Map))) - .toList(), - ); - -Map _$$SpotifyHomeFeedImplToJson( - _$SpotifyHomeFeedImpl instance) => - { - 'greeting': instance.greeting, - 'sections': instance.sections.map((e) => e.toJson()).toList(), - }; diff --git a/lib/models/spotify/recommendation_seeds.dart b/lib/models/spotify/recommendation_seeds.dart deleted file mode 100644 index 0d874ad6..00000000 --- a/lib/models/spotify/recommendation_seeds.dart +++ /dev/null @@ -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? seedArtists, - Iterable? seedGenres, - Iterable? 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 json) => - _$RecommendationSeedsFromJson(json); -} diff --git a/lib/models/spotify/recommendation_seeds.freezed.dart b/lib/models/spotify/recommendation_seeds.freezed.dart deleted file mode 100644 index c55a4134..00000000 --- a/lib/models/spotify/recommendation_seeds.freezed.dart +++ /dev/null @@ -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 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? get seedArtists => throw _privateConstructorUsedError; - Iterable? get seedGenres => throw _privateConstructorUsedError; - Iterable? 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 - get copyWith => throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $GeneratePlaylistProviderInputCopyWith<$Res> { - factory $GeneratePlaylistProviderInputCopyWith( - GeneratePlaylistProviderInput value, - $Res Function(GeneratePlaylistProviderInput) then) = - _$GeneratePlaylistProviderInputCopyWithImpl<$Res, - GeneratePlaylistProviderInput>; - @useResult - $Res call( - {Iterable? seedArtists, - Iterable? seedGenres, - Iterable? 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?, - seedGenres: freezed == seedGenres - ? _value.seedGenres - : seedGenres // ignore: cast_nullable_to_non_nullable - as Iterable?, - seedTracks: freezed == seedTracks - ? _value.seedTracks - : seedTracks // ignore: cast_nullable_to_non_nullable - as Iterable?, - 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? seedArtists, - Iterable? seedGenres, - Iterable? 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?, - seedGenres: freezed == seedGenres - ? _value.seedGenres - : seedGenres // ignore: cast_nullable_to_non_nullable - as Iterable?, - seedTracks: freezed == seedTracks - ? _value.seedTracks - : seedTracks // ignore: cast_nullable_to_non_nullable - as Iterable?, - 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? seedArtists; - @override - final Iterable? seedGenres; - @override - final Iterable? 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? seedArtists, - final Iterable? seedGenres, - final Iterable? seedTracks, - required final int limit, - final RecommendationSeeds? max, - final RecommendationSeeds? min, - final RecommendationSeeds? target}) = _$GeneratePlaylistProviderInputImpl; - - @override - Iterable? get seedArtists; - @override - Iterable? get seedGenres; - @override - Iterable? 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 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 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 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 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 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 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; -} diff --git a/lib/models/spotify/recommendation_seeds.g.dart b/lib/models/spotify/recommendation_seeds.g.dart deleted file mode 100644 index accb2ed1..00000000 --- a/lib/models/spotify/recommendation_seeds.g.dart +++ /dev/null @@ -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 _$$RecommendationSeedsImplToJson( - _$RecommendationSeedsImpl instance) => - { - '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, - }; diff --git a/lib/models/spotify_friends.dart b/lib/models/spotify_friends.dart deleted file mode 100644 index b386fb81..00000000 --- a/lib/models/spotify_friends.dart +++ /dev/null @@ -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 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 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 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 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 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 json) => - _$SpotifyFriendActivityFromJson(json); -} - -@JsonSerializable(createToJson: false) -class SpotifyFriends { - List friends; - - SpotifyFriends({required this.friends}); - - factory SpotifyFriends.fromJson(Map json) => - _$SpotifyFriendsFromJson(json); -} diff --git a/lib/models/spotify_friends.g.dart b/lib/models/spotify_friends.g.dart deleted file mode 100644 index a1248429..00000000 --- a/lib/models/spotify_friends.g.dart +++ /dev/null @@ -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.from(json['artist'] as Map)), - album: SpotifyActivityAlbum.fromJson( - Map.from(json['album'] as Map)), - context: SpotifyActivityContext.fromJson( - Map.from(json['context'] as Map)), - ); - -SpotifyFriendActivity _$SpotifyFriendActivityFromJson(Map json) => - SpotifyFriendActivity( - user: SpotifyFriend.fromJson( - Map.from(json['user'] as Map)), - track: SpotifyActivityTrack.fromJson( - Map.from(json['track'] as Map)), - ); - -SpotifyFriends _$SpotifyFriendsFromJson(Map json) => SpotifyFriends( - friends: (json['friends'] as List) - .map((e) => SpotifyFriendActivity.fromJson( - Map.from(e as Map))) - .toList(), - ); diff --git a/lib/modules/album/album_card.dart b/lib/modules/album/album_card.dart index 0ea07f51..b4809aed 100644 --- a/lib/modules/album/album_card.dart +++ b/lib/modules/album/album_card.dart @@ -2,14 +2,11 @@ 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/components/dialogs/select_device_dialog.dart'; import 'package:spotube/components/playbutton_view/playbutton_card.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/track.dart'; import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/models/metadata/metadata.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/audio_player/audio_player.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'; -extension FormattedAlbumType on AlbumType { +extension FormattedAlbumType on SpotubeAlbumType { String get formatted => name.replaceFirst(name[0], name[0].toUpperCase()); } @@ -53,9 +49,11 @@ class AlbumCard extends HookConsumerWidget { final updating = useState(false); - Future> fetchAllTrack() async { - // return ref.read(metadataPluginAlbumTracksProvider(album).notifier).fetchAll(); - return []; + Future> fetchAllTrack() async { + await ref.read(metadataPluginAlbumTracksProvider(album.id).future); + return ref + .read(metadataPluginAlbumTracksProvider(album.id).notifier) + .fetchAll(); } var imageUrl = album.images.asUrlString( @@ -87,13 +85,13 @@ class AlbumCard extends HookConsumerWidget { await remotePlayback.load( WebSocketLoadEventData.album( tracks: fetchedTracks, - // collection: album, + collection: album, ), ); } else { await playlistNotifier.load(fetchedTracks, autoPlay: true); playlistNotifier.addCollection(album.id); - // historyNotifier.addAlbums([album]); + historyNotifier.addAlbums([album]); } } finally { updating.value = false; @@ -112,7 +110,7 @@ class AlbumCard extends HookConsumerWidget { if (fetchedTracks.isEmpty) return; playlistNotifier.addTracks(fetchedTracks); playlistNotifier.addCollection(album.id); - // historyNotifier.addAlbums([album]); + historyNotifier.addAlbums([album]); if (context.mounted) { showToast( context: context, @@ -126,7 +124,7 @@ class AlbumCard extends HookConsumerWidget { child: Text(context.l10n.undo), onPressed: () { playlistNotifier - .removeTracks(fetchedTracks.map((e) => e.id!)); + .removeTracks(fetchedTracks.map((e) => e.id)); }, ), ), diff --git a/lib/modules/artist/artist_album_list.dart b/lib/modules/artist/artist_album_list.dart index c5c0defd..8d228905 100644 --- a/lib/modules/artist/artist_album_list.dart +++ b/lib/modules/artist/artist_album_list.dart @@ -4,7 +4,7 @@ import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_pl import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/metadata/metadata.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 { final String artistId; diff --git a/lib/modules/home/sections/featured.dart b/lib/modules/home/sections/featured.dart index a339bd43..c65ebf89 100644 --- a/lib/modules/home/sections/featured.dart +++ b/lib/modules/home/sections/featured.dart @@ -3,44 +3,45 @@ 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:spotify/spotify.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.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 { const HomeFeaturedSection({super.key}); @override Widget build(BuildContext context, ref) { - final featuredPlaylists = ref.watch(featuredPlaylistsProvider); - final featuredPlaylistsNotifier = - ref.watch(featuredPlaylistsProvider.notifier); + return const SizedBox.shrink(); + // final featuredPlaylists = ref.watch(featuredPlaylistsProvider); + // final featuredPlaylistsNotifier = + // ref.watch(featuredPlaylistsProvider.notifier); - if (featuredPlaylists.hasError) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Undraw( - illustration: UndrawIllustration.fixingBugs, - height: 200 * context.theme.scaling, - color: context.theme.colorScheme.primary, - ), - Text(context.l10n.something_went_wrong).small().muted(), - const Gap(8), - ], - ); - } + // if (featuredPlaylists.hasError) { + // return Column( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Undraw( + // illustration: UndrawIllustration.fixingBugs, + // height: 200 * context.theme.scaling, + // color: context.theme.colorScheme.primary, + // ), + // Text(context.l10n.something_went_wrong).small().muted(), + // const Gap(8), + // ], + // ); + // } - return Skeletonizer( - enabled: featuredPlaylists.isLoading, - child: HorizontalPlaybuttonCardView( - items: featuredPlaylists.asData?.value.items ?? [], - title: Text(context.l10n.featured), - isLoadingNextPage: featuredPlaylists.isLoadingNextPage, - hasNextPage: featuredPlaylists.asData?.value.hasMore ?? false, - onFetchMore: featuredPlaylistsNotifier.fetchMore, - ), - ); + // return Skeletonizer( + // enabled: featuredPlaylists.isLoading, + // child: HorizontalPlaybuttonCardView( + // items: featuredPlaylists.asData?.value.items ?? [], + // title: Text(context.l10n.featured), + // isLoadingNextPage: featuredPlaylists.isLoadingNextPage, + // hasNextPage: featuredPlaylists.asData?.value.hasMore ?? false, + // onFetchMore: featuredPlaylistsNotifier.fetchMore, + // ), + // ); } } diff --git a/lib/modules/home/sections/friends.dart b/lib/modules/home/sections/friends.dart deleted file mode 100644 index 5c9c2178..00000000 --- a/lib/modules/home/sections/friends.dart +++ /dev/null @@ -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]); - }, - ), - ), - ), - ), - ], - ); - } -} diff --git a/lib/modules/home/sections/friends/friend_item.dart b/lib/modules/home/sections/friends/friend_item.dart deleted file mode 100644 index 00617404..00000000 --- a/lib/modules/home/sections/friends/friend_item.dart +++ /dev/null @@ -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), - // ); - } - }, - ), - ], - ), - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/modules/home/sections/genres/genre_card.dart b/lib/modules/home/sections/genres/genre_card.dart deleted file mode 100644 index 8133f0db..00000000 --- a/lib/modules/home/sections/genres/genre_card.dart +++ /dev/null @@ -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); - }, - ), - ), - ), - ) - ], - ), - ), - ); - } -} diff --git a/lib/modules/home/sections/genres/genre_card_playlist_card.dart b/lib/modules/home/sections/genres/genre_card_playlist_card.dart deleted file mode 100644 index 7bae4503..00000000 --- a/lib/modules/home/sections/genres/genre_card_playlist_card.dart +++ /dev/null @@ -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(), - ], - ), - ), - ), - ); - } -} diff --git a/lib/modules/home/sections/genres/genres.dart b/lib/modules/home/sections/genres/genres.dart deleted file mode 100644 index c9f3f9b2..00000000 --- a/lib/modules/home/sections/genres/genres.dart +++ /dev/null @@ -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, - ), - ), - ], - ); - } -} diff --git a/lib/modules/home/sections/made_for_user.dart b/lib/modules/home/sections/made_for_user.dart deleted file mode 100644 index 4fd025d5..00000000 --- a/lib/modules/home/sections/made_for_user.dart +++ /dev/null @@ -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() ?? - []; - if (playlists.isEmpty) return const SizedBox.shrink(); - return HorizontalPlaybuttonCardView( - items: playlists, - title: Text(item["name"] ?? ""), - hasNextPage: false, - isLoadingNextPage: false, - onFetchMore: () {}, - ); - }, - ); - } -} diff --git a/lib/modules/home/sections/new_releases.dart b/lib/modules/home/sections/new_releases.dart index 2ebbbee0..31072954 100644 --- a/lib/modules/home/sections/new_releases.dart +++ b/lib/modules/home/sections/new_releases.dart @@ -1,10 +1,11 @@ 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/extensions/context.dart'; +import 'package:spotube/models/metadata/metadata.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 { const HomeNewReleasesSection({super.key}); @@ -13,10 +14,9 @@ class HomeNewReleasesSection extends HookConsumerWidget { Widget build(BuildContext context, ref) { final auth = ref.watch(authenticationProvider); - final newReleases = ref.watch(albumReleasesProvider); - final newReleasesNotifier = ref.read(albumReleasesProvider.notifier); - - final albums = ref.watch(userArtistAlbumReleasesProvider); + final newReleases = ref.watch(metadataPluginAlbumReleasesProvider); + final newReleasesNotifier = + ref.read(metadataPluginAlbumReleasesProvider.notifier); if (auth.asData?.value == null || newReleases.isLoading || @@ -24,8 +24,8 @@ class HomeNewReleasesSection extends HookConsumerWidget { return const SizedBox.shrink(); } - return HorizontalPlaybuttonCardView( - items: albums, + return HorizontalPlaybuttonCardView( + items: newReleases.asData?.value.items ?? [], title: Text(context.l10n.new_releases), isLoadingNextPage: newReleases.isLoadingNextPage, hasNextPage: newReleases.asData?.value.hasMore ?? false, diff --git a/lib/modules/library/local_folder/local_folder_item.dart b/lib/modules/library/local_folder/local_folder_item.dart index 78f1aa14..12dcdbbe 100644 --- a/lib/modules/library/local_folder/local_folder_item.dart +++ b/lib/modules/library/local_folder/local_folder_item.dart @@ -11,8 +11,8 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/extensions/image.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/user_preferences/user_preferences_provider.dart'; @@ -100,7 +100,7 @@ class LocalFolderItem extends HookConsumerWidget { itemBuilder: (context, index) { final track = tracks[index]; return UniversalImage( - path: (track.album?.images).asUrlString( + path: track.album.images.asUrlString( placeholder: ImagePlaceholder.albumArt, ), fit: BoxFit.cover, diff --git a/lib/modules/library/playlist_generate/multi_select_field.dart b/lib/modules/library/playlist_generate/multi_select_field.dart deleted file mode 100644 index 00a09c95..00000000 --- a/lib/modules/library/playlist_generate/multi_select_field.dart +++ /dev/null @@ -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 extends HookWidget { - final List options; - final List selectedOptions; - - final Widget Function(T option, VoidCallback onSelect)? optionBuilder; - final Widget Function(T option)? selectedOptionBuilder; - final ValueChanged> 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>( - context: context, - builder: (context) { - return _MultiSelectDialog( - 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 extends HookWidget { - final Widget? dialogTitle; - final List options; - final Widget Function(T option, VoidCallback onSelect)? optionBuilder; - final Object Function(T option) getValueForOption; - final List 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, - ), - ], - ), - ), - ); - } -} diff --git a/lib/modules/library/playlist_generate/recommendation_attribute_dials.dart b/lib/modules/library/playlist_generate/recommendation_attribute_dials.dart deleted file mode 100644 index 564bfb55..00000000 --- a/lib/modules/library/playlist_generate/recommendation_attribute_dials.dart +++ /dev/null @@ -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 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, - ], - ), - ), - ], - ), - ), - ], - ); - }); - } -} diff --git a/lib/modules/library/playlist_generate/recommendation_attribute_fields.dart b/lib/modules/library/playlist_generate/recommendation_attribute_fields.dart deleted file mode 100644 index b616b080..00000000 --- a/lib/modules/library/playlist_generate/recommendation_attribute_fields.dart +++ /dev/null @@ -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 onChanged; - final Map? 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() ?? - >[]) - 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), - ], - ), - ), - ], - ); - }); - } -} diff --git a/lib/modules/library/playlist_generate/seeds_multi_autocomplete.dart b/lib/modules/library/playlist_generate/seeds_multi_autocomplete.dart deleted file mode 100644 index 812d9367..00000000 --- a/lib/modules/library/playlist_generate/seeds_multi_autocomplete.dart +++ /dev/null @@ -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 extends HookWidget { - final ValueNotifier> seeds; - - final FutureOr> Function(TextEditingValue textEditingValue) - fetchSeeds; - final Widget Function(T option, ValueChanged 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( - 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, - ), - ], - ], - ), - ), - ), - }, - ], - ); - } -} diff --git a/lib/modules/library/playlist_generate/simple_track_tile.dart b/lib/modules/library/playlist_generate/simple_track_tile.dart deleted file mode 100644 index afa723f3..00000000 --- a/lib/modules/library/playlist_generate/simple_track_tile.dart +++ /dev/null @@ -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, - ); - } -} diff --git a/lib/modules/library/user_downloads/download_item.dart b/lib/modules/library/user_downloads/download_item.dart index 2c0a96a5..2dcfc28f 100644 --- a/lib/modules/library/user_downloads/download_item.dart +++ b/lib/modules/library/user_downloads/download_item.dart @@ -2,20 +2,19 @@ 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/image/universal_image.dart'; import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/components/ui/button_tile.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/services/download_manager/download_status.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; class DownloadItem extends HookConsumerWidget { - final Track track; + final SpotubeFullTrackObject track; const DownloadItem({ super.key, required this.track, @@ -29,7 +28,7 @@ class DownloadItem extends HookConsumerWidget { useEffect(() { if (track is! SourcedTrack) return null; - final notifier = downloadManager.getStatusNotifier(track as SourcedTrack); + final notifier = downloadManager.getStatusNotifier(track); taskStatus.value = notifier?.value; @@ -56,18 +55,18 @@ class DownloadItem extends HookConsumerWidget { child: UniversalImage( height: 40, width: 40, - path: (track.album?.images).asUrlString( + path: track.album.images.asUrlString( placeholder: ImagePlaceholder.albumArt, ), ), ), ), - title: Text(track.name ?? ''), + title: Text(track.name), subtitle: ArtistLink( - artists: track.artists ?? [], + artists: track.artists, mainAxisAlignment: WrapAlignment.start, onOverflowArtistClick: () { - context.navigateTo(TrackRoute(trackId: track.id!)); + context.navigateTo(TrackRoute(trackId: track.id)); }, ), trailing: isQueryingSourceInfo @@ -75,8 +74,7 @@ class DownloadItem extends HookConsumerWidget { : switch (taskStatus.value!) { DownloadStatus.downloading => HookBuilder(builder: (context) { final taskProgress = useListenable(useMemoized( - () => downloadManager - .getProgressNotifier(track as SourcedTrack), + () => downloadManager.getProgressNotifier(track), [track], )); return Row( @@ -88,13 +86,13 @@ class DownloadItem extends HookConsumerWidget { IconButton.ghost( icon: const Icon(SpotubeIcons.pause), onPressed: () { - downloadManager.pause(track as SourcedTrack); + downloadManager.pause(track); }), const SizedBox(width: 10), IconButton.ghost( icon: const Icon(SpotubeIcons.close), onPressed: () { - downloadManager.cancel(track as SourcedTrack); + downloadManager.cancel(track); }), ], ); @@ -105,13 +103,13 @@ class DownloadItem extends HookConsumerWidget { IconButton.ghost( icon: const Icon(SpotubeIcons.play), onPressed: () { - downloadManager.resume(track as SourcedTrack); + downloadManager.resume(track); }), const SizedBox(width: 10), IconButton.ghost( icon: const Icon(SpotubeIcons.close), onPressed: () { - downloadManager.cancel(track as SourcedTrack); + downloadManager.cancel(track); }) ], ), @@ -127,7 +125,7 @@ class DownloadItem extends HookConsumerWidget { IconButton.ghost( icon: const Icon(SpotubeIcons.refresh), onPressed: () { - downloadManager.retry(track as SourcedTrack); + downloadManager.retry(track); }, ), ], @@ -138,7 +136,7 @@ class DownloadItem extends HookConsumerWidget { DownloadStatus.queued => IconButton.ghost( icon: const Icon(SpotubeIcons.close), onPressed: () { - downloadManager.removeFromQueue(track as SourcedTrack); + downloadManager.removeFromQueue(track); }), }, ); diff --git a/lib/modules/player/player.dart b/lib/modules/player/player.dart index 28cd5835..8984455b 100644 --- a/lib/modules/player/player.dart +++ b/lib/modules/player/player.dart @@ -9,6 +9,7 @@ import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/spotube_icons.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_controls.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/titlebar/titlebar.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/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/provider/authentication/authentication.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; @@ -47,8 +45,8 @@ class PlayerView extends HookConsumerWidget { final sourcedCurrentTrack = ref.watch(activeTrackSourcesProvider); final currentActiveTrack = ref.watch(audioPlayerProvider.select((s) => s.activeTrack)); - final currentTrack = sourcedCurrentTrack ?? currentActiveTrack; - final isLocalTrack = currentTrack is LocalTrack; + final currentActiveTrackSource = sourcedCurrentTrack.asData?.value?.source; + final isLocalTrack = currentActiveTrack is SpotubeLocalTrackObject; final mediaQuery = MediaQuery.sizeOf(context); final shouldHide = useState(true); @@ -71,10 +69,10 @@ class PlayerView extends HookConsumerWidget { }, [mediaQuery.lgAndUp]); String albumArt = useMemoized( - () => (currentTrack?.album?.images).asUrlString( + () => (currentActiveTrack?.album.images).asUrlString( placeholder: ImagePlaceholder.albumArt, ), - [currentTrack?.album?.images], + [currentActiveTrack?.album.images], ); useEffect(() { @@ -115,7 +113,7 @@ class PlayerView extends HookConsumerWidget { ) ], trailing: [ - if (currentTrack is YoutubeSourcedTrack) + if (currentActiveTrackSource is YoutubeSourcedTrack) TextButton( leading: Assets.logos.songlinkTransparent.image( width: 20, @@ -123,31 +121,34 @@ class PlayerView extends HookConsumerWidget { color: theme.colorScheme.foreground, ), onPressed: () { - final url = "https://song.link/s/${currentTrack.id}"; + final url = + "https://song.link/s/${currentActiveTrack?.id}"; launchUrlString(url); }, child: Text(context.l10n.song_link), ), - Tooltip( - tooltip: TooltipContainer( - child: Text(context.l10n.details), - ).call, - child: IconButton.ghost( - icon: const Icon(SpotubeIcons.info, size: 18), - onPressed: currentTrack == null - ? null - : () { - showDialog( - context: context, - builder: (context) { - return TrackDetailsDialog( - track: currentTrack, - ); - }); - }, - ), - ) + if (!isLocalTrack) + Tooltip( + tooltip: TooltipContainer( + child: Text(context.l10n.details), + ).call, + child: IconButton.ghost( + icon: const Icon(SpotubeIcons.info, size: 18), + onPressed: currentActiveTrackSource == null + ? null + : () { + showDialog( + context: context, + builder: (context) { + return TrackDetailsDialog( + track: currentActiveTrack + as SpotubeFullTrackObject, + ); + }); + }, + ), + ) ], ), ), @@ -190,7 +191,7 @@ class PlayerView extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ AutoSizeText( - currentTrack?.name ?? context.l10n.not_playing, + currentActiveTrack?.name ?? context.l10n.not_playing, style: const TextStyle(fontSize: 22), maxFontSize: 22, maxLines: 1, @@ -198,13 +199,13 @@ class PlayerView extends HookConsumerWidget { ), if (isLocalTrack) Text( - currentTrack.artists?.asString() ?? "", + currentActiveTrack.artists.asString(), style: theme.typography.normal .copyWith(fontWeight: FontWeight.bold), ) else ArtistLink( - artists: currentTrack?.artists ?? [], + artists: currentActiveTrack?.artists ?? [], textStyle: theme.typography.normal .copyWith(fontWeight: FontWeight.bold), onRouteChange: (route) { @@ -212,7 +213,9 @@ class PlayerView extends HookConsumerWidget { context.router.navigateNamed(route); }, onOverflowArtistClick: () => context.navigateTo( - TrackRoute(trackId: currentTrack!.id!), + TrackRoute( + trackId: currentActiveTrack!.id, + ), ), ), ], diff --git a/lib/modules/player/player_actions.dart b/lib/modules/player/player_actions.dart index 53023a10..ee10e82a 100644 --- a/lib/modules/player/player_actions.dart +++ b/lib/modules/player/player_actions.dart @@ -8,14 +8,13 @@ import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/spotube_icons.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/sibling_tracks_sheet.dart'; import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.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/duration.dart'; -import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; @@ -38,12 +37,13 @@ class PlayerActions extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final playlist = ref.watch(audioPlayerProvider); - final isLocalTrack = playlist.activeTrack is LocalTrack; + final isLocalTrack = playlist.activeTrack is SpotubeLocalTrackObject; ref.watch(downloadManagerProvider); final downloader = ref.watch(downloadManagerProvider.notifier); final isInQueue = useMemoized(() { - if (playlist.activeTrack == null) return false; - return downloader.isActive(playlist.activeTrack!); + if (playlist.activeTrack is! SpotubeFullTrackObject) return false; + return downloader + .isActive(playlist.activeTrack! as SpotubeFullTrackObject); }, [ playlist.activeTrack, downloader, @@ -58,9 +58,9 @@ class PlayerActions extends HookConsumerWidget { return localTracks.any( (element) => element.name == playlist.activeTrack?.name && - element.album?.name == playlist.activeTrack?.album?.name && + element.album?.name == playlist.activeTrack?.album.name && element.artists?.asString() == - playlist.activeTrack?.artists?.asString(), + playlist.activeTrack?.artists.asString(), ) == true; }, [localTracks, playlist.activeTrack]); @@ -168,7 +168,8 @@ class PlayerActions extends HookConsumerWidget { isDownloaded ? SpotubeIcons.done : SpotubeIcons.download, ), onPressed: playlist.activeTrack != null - ? () => downloader.addToQueue(playlist.activeTrack!) + ? () => downloader.addToQueue( + playlist.activeTrack! as SpotubeFullTrackObject) : null, ), ), diff --git a/lib/modules/player/player_queue.dart b/lib/modules/player/player_queue.dart index 06d7e3c7..c9d5626f 100644 --- a/lib/modules/player/player_queue.dart +++ b/lib/modules/player/player_queue.dart @@ -7,16 +7,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/button/back_button.dart'; import 'package:spotube/components/fallbacks/not_found.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.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/context.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/state.dart'; @@ -24,7 +23,7 @@ class PlayerQueue extends HookConsumerWidget { final bool floating; final AudioPlayerState playlist; - final Future Function(Track track) onJump; + final Future Function(SpotubeTrackObject track) onJump; final Future Function(String trackId) onRemove; final Future Function(int oldIndex, int newIndex) onReorder; final Future Function() onStop; @@ -68,7 +67,7 @@ class PlayerQueue extends HookConsumerWidget { return tracks .map((e) => ( weightedRatio( - '${e.name!} - ${e.artists?.asString() ?? ""}', + '${e.name} - ${e.artists.asString()}', searchText.value, ), e @@ -161,7 +160,8 @@ class PlayerQueue extends HookConsumerWidget { const SizedBox(width: 10), Tooltip( tooltip: TooltipContainer( - child: Text(context.l10n.clear_all)).call, + child: Text(context.l10n.clear_all)) + .call, child: IconButton.outline( icon: const Icon(SpotubeIcons.playlistRemove), onPressed: () { @@ -244,7 +244,7 @@ class PlayerQueue extends HookConsumerWidget { icon: const Icon(SpotubeIcons.angleDown), onPressed: () { controller.scrollToIndex( - playlist.playlist.index, + playlist.currentIndex, preferPosition: AutoScrollPosition.middle, ); }, diff --git a/lib/modules/player/player_track_details.dart b/lib/modules/player/player_track_details.dart index 2e38bf37..995ed4ab 100644 --- a/lib/modules/player/player_track_details.dart +++ b/lib/modules/player/player_track_details.dart @@ -2,21 +2,19 @@ import 'package:auto_route/auto_route.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/links/artist_link.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/image.dart'; +import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; class PlayerTrackDetails extends HookConsumerWidget { final Color? color; - final Track? track; + final SpotubeTrackObject? track; const PlayerTrackDetails({super.key, this.color, this.track}); @override @@ -37,7 +35,7 @@ class PlayerTrackDetails extends HookConsumerWidget { child: ClipRRect( borderRadius: BorderRadius.circular(4), child: UniversalImage( - path: (track?.album?.images) + path: (track?.album.images) .asUrlString(placeholder: ImagePlaceholder.albumArt), placeholder: Assets.albumPlaceholder.path, ), @@ -59,7 +57,7 @@ class PlayerTrackDetails extends HookConsumerWidget { ), ), Text( - playback.activeTrack?.artists?.asString() ?? "", + playback.activeTrack?.artists.asString() ?? "", overflow: TextOverflow.ellipsis, style: theme.typography.small.copyWith(color: color), ) @@ -84,7 +82,7 @@ class PlayerTrackDetails extends HookConsumerWidget { context.router.navigateNamed(route); }, onOverflowArtistClick: () => - context.navigateTo(TrackRoute(trackId: track!.id!)), + context.navigateTo(TrackRoute(trackId: track!.id)), ) ], ), diff --git a/lib/modules/player/sibling_tracks_sheet.dart b/lib/modules/player/sibling_tracks_sheet.dart index 472acf1b..fee0c46a 100644 --- a/lib/modules/player/sibling_tracks_sheet.dart +++ b/lib/modules/player/sibling_tracks_sheet.dart @@ -10,30 +10,27 @@ import 'package:spotube/components/button/back_button.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.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/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart'; import 'package:spotube/hooks/utils/use_debounce.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/server/active_track_sources.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.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/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/piped.dart'; import 'package:spotube/services/sourced_track/sources/youtube.dart'; import 'package:spotube/utils/service_utils.dart'; final sourceInfoToIconMap = { - YoutubeSourceInfo: const Icon(SpotubeIcons.youtube, color: Color(0xFFFF0000)), - JioSaavnSourceInfo: Container( + AudioSource.youtube: + const Icon(SpotubeIcons.youtube, color: Color(0xFFFF0000)), + AudioSource.jiosaavn: Container( height: 30, width: 30, decoration: BoxDecoration( @@ -44,8 +41,8 @@ final sourceInfoToIconMap = { ), ), ), - PipedSourceInfo: const Icon(SpotubeIcons.piped), - InvidiousSourceInfo: Container( + AudioSource.piped: const Icon(SpotubeIcons.piped), + AudioSource.invidious: Container( height: 18, width: 18, decoration: BoxDecoration( @@ -68,25 +65,25 @@ class SiblingTracksSheet extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final theme = Theme.of(context); - final playlist = ref.watch(audioPlayerProvider); final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); final preferences = ref.watch(userPreferencesProvider); final youtubeEngine = ref.watch(youtubeEngineProvider); final isSearching = useState(false); final searchMode = useState(preferences.searchMode); - final activeTrackNotifier = ref.watch(activeTrackSourcesProvider.notifier); - final activeTrack = - ref.watch(activeTrackSourcesProvider) ?? playlist.activeTrack; + final activeTrackSources = ref.watch(activeTrackSourcesProvider); + final activeTrackNotifier = activeTrackSources.asData?.value?.notifier; + final activeTrack = activeTrackSources.asData?.value?.track; + final activeTrackSource = activeTrackSources.asData?.value?.source; final title = ServiceUtils.getTitle( activeTrack?.name ?? "", - artists: activeTrack?.artists?.map((e) => e.name!).toList() ?? [], + artists: activeTrack?.artists.map((e) => e.name).toList() ?? [], onlyCleanArtist: true, ).trim(); final defaultSearchTerm = - "$title - ${activeTrack?.artists?.asString() ?? ""}"; + "$title - ${activeTrack?.artists.asString() ?? ""}"; final searchController = useShadcnTextEditingController( text: defaultSearchTerm, ); @@ -99,7 +96,7 @@ class SiblingTracksSheet extends HookConsumerWidget { final searchRequest = useMemoized(() async { if (searchTerm.trim().isEmpty) { - return []; + return []; } if (preferences.audioSource == AudioSource.jiosaavn) { final resultsJioSaavn = @@ -110,7 +107,7 @@ class SiblingTracksSheet extends HookConsumerWidget { return siblingType.info; })); - final activeSourceInfo = (activeTrack! as SourcedTrack).sourceInfo; + final activeSourceInfo = activeTrackSource as TrackSourceInfo; return results ..removeWhere((element) => element.id == activeSourceInfo.id) @@ -130,7 +127,7 @@ class SiblingTracksSheet extends HookConsumerWidget { return siblingType.info; }), ); - final activeSourceInfo = (activeTrack! as SourcedTrack).sourceInfo; + final activeSourceInfo = activeTrackSource as TrackSourceInfo; return searchResults ..removeWhere((element) => element.id == activeSourceInfo.id) ..insert( @@ -142,6 +139,7 @@ class SiblingTracksSheet extends HookConsumerWidget { searchTerm, searchMode.value, activeTrack, + activeTrackSource, preferences.audioSource, youtubeEngine, ]); @@ -149,25 +147,25 @@ class SiblingTracksSheet extends HookConsumerWidget { final siblings = useMemoized( () => !isFetchingActiveTrack ? [ - (activeTrack as SourcedTrack).sourceInfo, - ...activeTrack.siblings, + if (activeTrackSource != null) activeTrackSource.info, + ...?activeTrackSource?.siblings, ] - : [], - [activeTrack, isFetchingActiveTrack], + : [], + [activeTrackSource, isFetchingActiveTrack], ); final previousActiveTrack = usePrevious(activeTrack); useEffect(() { /// Populate sibling when active track changes if (previousActiveTrack?.id == activeTrack?.id) return; - if (activeTrack is SourcedTrack && activeTrack.siblings.isEmpty) { - activeTrackNotifier.populateSibling(); + if (activeTrackSource != null && activeTrackSource.siblings.isEmpty) { + activeTrackNotifier?.copyWithSibling(); } return null; }, [activeTrack, previousActiveTrack]); final itemBuilder = useCallback( - (SourceInfo sourceInfo) { + (TrackSourceInfo sourceInfo) { final icon = sourceInfoToIconMap[sourceInfo.runtimeType]; return ButtonTile( style: ButtonVariance.ghost, @@ -182,13 +180,14 @@ class SiblingTracksSheet extends HookConsumerWidget { height: 60, width: 60, ), - trailing: Text(sourceInfo.duration.toHumanReadableString()), + trailing: Text(Duration(milliseconds: sourceInfo.durationMs) + .toHumanReadableString()), subtitle: Row( children: [ if (icon != null) icon, Flexible( child: Text( - " • ${sourceInfo.artist}", + " • ${sourceInfo.artists}", maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -197,11 +196,11 @@ class SiblingTracksSheet extends HookConsumerWidget { ), enabled: !isFetchingActiveTrack, selected: !isFetchingActiveTrack && - sourceInfo.id == (activeTrack as SourcedTrack).sourceInfo.id, + sourceInfo.id == activeTrackSource?.info.id, onPressed: () { if (!isFetchingActiveTrack && - sourceInfo.id != (activeTrack as SourcedTrack).sourceInfo.id) { - activeTrackNotifier.swapSibling(sourceInfo); + sourceInfo.id != activeTrackSource?.info.id) { + activeTrackNotifier?.swapWithSibling(sourceInfo); if (MediaQuery.sizeOf(context).mdAndUp) { closeOverlay(context); } else { @@ -211,7 +210,7 @@ class SiblingTracksSheet extends HookConsumerWidget { }, ); }, - [activeTrack, siblings], + [activeTrackSource, activeTrackNotifier, siblings], ); final scale = context.theme.scaling; diff --git a/lib/modules/playlist/playlist_card.dart b/lib/modules/playlist/playlist_card.dart index 3d180e7c..36127792 100644 --- a/lib/modules/playlist/playlist_card.dart +++ b/lib/modules/playlist/playlist_card.dart @@ -2,8 +2,6 @@ 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' 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/components/dialogs/select_device_dialog.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/history/history.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:stroke_text/stroke_text.dart'; class PlaylistCard extends HookConsumerWidget { final SpotubeSimplePlaylistObject playlist; @@ -48,26 +47,30 @@ class PlaylistCard extends HookConsumerWidget { ); final updating = useState(false); - final me = ref.watch(meProvider); + final me = ref.watch(metadataPluginUserProvider); - Future> fetchInitialTracks() async { + Future> fetchInitialTracks() async { 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; } - Future> fetchAllTracks() async { + Future> fetchAllTracks() async { final initialTracks = await fetchInitialTracks(); if (playlist.id == 'user-liked-tracks') { return initialTracks; } - return ref.read(playlistTracksProvider(playlist.id).notifier).fetchAll(); + return ref + .read(metadataPluginPlaylistTracksProvider(playlist.id).notifier) + .fetchAll(); } void onTap() { @@ -94,14 +97,14 @@ class PlaylistCard extends HookConsumerWidget { final allTracks = await fetchAllTracks(); await remotePlayback.load( WebSocketLoadEventData.playlist( - tracks: allTracks, - // collection: playlist, + tracks: allTracks as List, + collection: playlist, ), ); } else { await playlistNotifier.load(fetchedInitialTracks, autoPlay: true); playlistNotifier.addCollection(playlist.id); - // historyNotifier.addPlaylists([playlist]); + historyNotifier.addPlaylists([playlist]); final allTracks = await fetchAllTracks(); @@ -126,7 +129,7 @@ class PlaylistCard extends HookConsumerWidget { playlistNotifier.addTracks(fetchedInitialTracks); playlistNotifier.addCollection(playlist.id); - // historyNotifier.addPlaylists([playlist]); + historyNotifier.addPlaylists([playlist]); if (context.mounted) { showToast( context: context, @@ -141,7 +144,7 @@ class PlaylistCard extends HookConsumerWidget { child: Text(context.l10n.undo), onPressed: () { playlistNotifier - .removeTracks(fetchedInitialTracks.map((e) => e.id!)); + .removeTracks(fetchedInitialTracks.map((e) => e.id)); }, ), ), @@ -159,52 +162,15 @@ class PlaylistCard extends HookConsumerWidget { ); final isLoading = (isPlaylistPlaying && isFetchingActiveTrack) || updating.value; - final isOwner = - playlist.owner.id == me.asData?.value.id && 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; + final isOwner = playlist.owner.id == me.asData?.value?.id && + me.asData?.value?.id != null; if (_isTile) { return PlaybuttonTile( title: playlist.name, description: playlist.description, - image: image, - imageUrl: image == null ? imageUrl : null, + image: null, + imageUrl: imageUrl, isPlaying: isPlaylistPlaying, isLoading: isLoading, isOwner: isOwner, @@ -217,8 +183,8 @@ class PlaylistCard extends HookConsumerWidget { return PlaybuttonCard( title: playlist.name, description: playlist.description, - image: image, - imageUrl: image == null ? imageUrl : null, + image: null, + imageUrl: imageUrl, isPlaying: isPlaylistPlaying, isLoading: isLoading, isOwner: isOwner, diff --git a/lib/modules/playlist/playlist_create_dialog.dart b/lib/modules/playlist/playlist_create_dialog.dart index 3ee39583..dce17ed0 100644 --- a/lib/modules/playlist/playlist_create_dialog.dart +++ b/lib/modules/playlist/playlist_create_dialog.dart @@ -10,15 +10,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path/path.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/form/checkbox_form_field.dart'; import 'package:spotube/components/form/text_form_field.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/extensions/image.dart'; -import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/provider/metadata_plugin/library/playlists.dart'; +import 'package:spotube/provider/metadata_plugin/playlist/playlist.dart'; class PlaylistCreateDialog extends HookConsumerWidget { /// Track ids to add to the playlist @@ -32,10 +32,11 @@ class PlaylistCreateDialog extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final userPlaylists = ref.watch(favoritePlaylistsProvider); - final playlist = ref.watch(playlistProvider(playlistId ?? "")); + final userPlaylists = ref.watch(metadataPluginSavedPlaylistsProvider); + final playlist = + ref.watch(metadataPluginPlaylistProvider(playlistId ?? "")); final playlistNotifier = - ref.watch(playlistProvider(playlistId ?? "").notifier); + ref.watch(metadataPluginPlaylistProvider(playlistId ?? "").notifier); final isSubmitting = useState(false); @@ -55,25 +56,54 @@ class PlaylistCreateDialog extends HookConsumerWidget { final l10n = context.l10n; 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) { - if (error is SpotifyError || error is SpotifyException) { - showToast( - context: context, - location: ToastLocation.topRight, - builder: (context, overlay) { - return SurfaceCard( - child: Basic( - title: Text( - l10n.error(error.message ?? l10n.epic_failure), - style: theme.typography.normal.copyWith( - color: theme.colorScheme.destructive, - ), + // if (error is SpotifyError || error is SpotifyException) { + // showToast( + // context: context, + // location: ToastLocation.topRight, + // builder: (context, overlay) { + // return SurfaceCard( + // child: Basic( + // title: Text( + // l10n.error(error.message ?? l10n.epic_failure), + // style: theme.typography.normal.copyWith( + // 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]); Future onCreate() async { @@ -83,7 +113,7 @@ class PlaylistCreateDialog extends HookConsumerWidget { isSubmitting.value = true; final values = formKey.currentState!.value; - final PlaylistInput payload = ( + final payload = ( playlistName: values['playlistName'], collaborative: values['collaborative'], public: values['public'], @@ -96,9 +126,21 @@ class PlaylistCreateDialog extends HookConsumerWidget { ); 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 { - 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) { @@ -107,9 +149,12 @@ class PlaylistCreateDialog extends HookConsumerWidget { } finally { isSubmitting.value = false; if (context.mounted && - !ref.read(playlistProvider(playlistId ?? "")).hasError) { - context.router.maybePop( - await ref.read(playlistProvider(playlistId ?? "").future), + !ref + .read(metadataPluginPlaylistProvider(playlistId ?? "")) + .hasError) { + context.router.maybePop( + await ref + .read(metadataPluginPlaylistProvider(playlistId ?? "").future), ); } } @@ -144,8 +189,8 @@ class PlaylistCreateDialog extends HookConsumerWidget { initialValue: { 'playlistName': updatingPlaylist?.name, 'description': updatingPlaylist?.description, - 'public': updatingPlaylist?.public ?? false, - 'collaborative': updatingPlaylist?.collaborative ?? false, + 'public': playlist.asData?.value.public ?? false, + 'collaborative': playlist.asData?.value.collaborative ?? false, }, child: ListView( shrinkWrap: true, @@ -259,7 +304,7 @@ class PlaylistCreateDialog extends HookConsumerWidget { class PlaylistCreateDialogButton extends HookConsumerWidget { const PlaylistCreateDialogButton({super.key}); - showPlaylistDialog(BuildContext context, SpotifyApiWrapper spotify) { + showPlaylistDialog(BuildContext context) { showDialog( context: context, alignment: Alignment.center, @@ -271,12 +316,10 @@ class PlaylistCreateDialogButton extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final spotify = ref.watch(spotifyProvider); - return Button.secondary( leading: const Icon(SpotubeIcons.addFilled), child: Text(context.l10n.playlist), - onPressed: () => showPlaylistDialog(context, spotify), + onPressed: () => showPlaylistDialog(context), ); } } diff --git a/lib/modules/root/bottom_player.dart b/lib/modules/root/bottom_player.dart index c2e6369d..8af5d433 100644 --- a/lib/modules/root/bottom_player.dart +++ b/lib/modules/root/bottom_player.dart @@ -8,6 +8,7 @@ import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/spotube_icons.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_overlay.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/extensions/constrains.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/user_preferences/user_preferences_provider.dart'; @@ -35,13 +35,13 @@ class BottomPlayer extends HookConsumerWidget { final mediaQuery = MediaQuery.of(context); String albumArt = useMemoized( - () => playlist.activeTrack?.album?.images?.isNotEmpty == true - ? (playlist.activeTrack?.album?.images).asUrlString( - index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1, + () => playlist.activeTrack?.album.images.isNotEmpty == true + ? (playlist.activeTrack?.album.images).asUrlString( + index: (playlist.activeTrack?.album.images.length ?? 1) - 1, placeholder: ImagePlaceholder.albumArt, ) : Assets.albumPlaceholder.path, - [playlist.activeTrack?.album?.images], + [playlist.activeTrack?.album.images], ); // returning an empty non spacious Container as the overlay will take @@ -76,7 +76,8 @@ class BottomPlayer extends HookConsumerWidget { extraActions: [ Tooltip( tooltip: - TooltipContainer(child: Text(context.l10n.mini_player)).call, + TooltipContainer(child: Text(context.l10n.mini_player)) + .call, child: IconButton( variance: ButtonVariance.ghost, icon: const Icon(SpotubeIcons.miniPlayer), diff --git a/lib/modules/root/sidebar/sidebar_footer.dart b/lib/modules/root/sidebar/sidebar_footer.dart index fb3edddd..f7168086 100644 --- a/lib/modules/root/sidebar/sidebar_footer.dart +++ b/lib/modules/root/sidebar/sidebar_footer.dart @@ -6,13 +6,13 @@ import 'package:shadcn_flutter/shadcn_flutter.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/extensions/image.dart'; import 'package:spotube/extensions/constrains.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/provider/authentication/authentication.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 { const SidebarFooter({ @@ -25,11 +25,11 @@ class SidebarFooter extends HookConsumerWidget implements NavigationBarItem { final router = AutoRouter.of(context, watch: true); final mediaQuery = MediaQuery.of(context); final downloadCount = ref.watch(downloadManagerProvider).$downloadCount; - final userSnapshot = ref.watch(meProvider); + final userSnapshot = ref.watch(metadataPluginUserProvider); final data = userSnapshot.asData?.value; final avatarImg = (data?.images).asUrlString( - index: (data?.images?.length ?? 1) - 1, + index: (data?.images.length ?? 1) - 1, placeholder: ImagePlaceholder.artist, ); @@ -102,14 +102,13 @@ class SidebarFooter extends HookConsumerWidget implements NavigationBarItem { child: Row( children: [ Avatar( - initials: - Avatar.getInitials(data.displayName ?? "User"), + initials: Avatar.getInitials(data.name), provider: UniversalImage.imageProvider(avatarImg), ), const SizedBox(width: 10), Flexible( child: Text( - data.displayName ?? context.l10n.guest, + data.name, maxLines: 1, softWrap: false, overflow: TextOverflow.fade, diff --git a/lib/modules/stats/common/album_item.dart b/lib/modules/stats/common/album_item.dart index b69e1d15..2ac73b91 100644 --- a/lib/modules/stats/common/album_item.dart +++ b/lib/modules/stats/common/album_item.dart @@ -1,15 +1,14 @@ import 'package:auto_route/auto_route.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; -import 'package:spotify/spotify.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/models/metadata/metadata.dart'; import 'package:spotube/modules/album/album_card.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 { - final AlbumSimple album; + final SpotubeSimpleAlbumObject album; final Widget info; const StatsAlbumItem({super.key, required this.album, required this.info}); @@ -27,24 +26,24 @@ class StatsAlbumItem extends StatelessWidget { height: 40, ), ), - title: Text(album.name!), + title: Text(album.name), subtitle: Row( mainAxisSize: MainAxisSize.min, children: [ - Text("${album.albumType?.formatted} • "), - // Flexible( - // child: ArtistLink( - // artists: album.artists ?? [], - // mainAxisAlignment: WrapAlignment.start, - // onOverflowArtistClick: () => - // context.navigateTo(AlbumRoute(id: album.id!, album: album)), - // ), - // ), + Text("${album.albumType.formatted} • "), + Flexible( + child: ArtistLink( + artists: album.artists, + mainAxisAlignment: WrapAlignment.start, + onOverflowArtistClick: () => + context.navigateTo(AlbumRoute(id: album.id, album: album)), + ), + ), ], ), trailing: info, onPressed: () { - // context.navigateTo(AlbumRoute(id: album.id!, album: album)); + context.navigateTo(AlbumRoute(id: album.id, album: album)); }, ); } diff --git a/lib/modules/stats/common/artist_item.dart b/lib/modules/stats/common/artist_item.dart index 5eff9a9d..92d3b915 100644 --- a/lib/modules/stats/common/artist_item.dart +++ b/lib/modules/stats/common/artist_item.dart @@ -1,13 +1,12 @@ import 'package:auto_route/auto_route.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/ui/button_tile.dart'; -import 'package:spotube/extensions/image.dart'; +import 'package:spotube/models/metadata/metadata.dart'; class StatsArtistItem extends StatelessWidget { - final Artist artist; + final SpotubeSimpleArtistObject artist; final Widget info; const StatsArtistItem({ super.key, @@ -19,9 +18,9 @@ class StatsArtistItem extends StatelessWidget { Widget build(BuildContext context) { return ButtonTile( style: ButtonVariance.ghost, - title: Text(artist.name!), + title: Text(artist.name), leading: Avatar( - initials: artist.name!.substring(0, 1), + initials: artist.name.substring(0, 1), provider: UniversalImage.imageProvider( (artist.images).asUrlString( placeholder: ImagePlaceholder.artist, @@ -30,7 +29,7 @@ class StatsArtistItem extends StatelessWidget { ), trailing: info, onPressed: () { - context.navigateTo(ArtistRoute(artistId: artist.id!)); + context.navigateTo(ArtistRoute(artistId: artist.id)); }, ); } diff --git a/lib/modules/stats/common/playlist_item.dart b/lib/modules/stats/common/playlist_item.dart index b1fdc920..8121f946 100644 --- a/lib/modules/stats/common/playlist_item.dart +++ b/lib/modules/stats/common/playlist_item.dart @@ -1,14 +1,11 @@ -import 'package:auto_route/auto_route.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/ui/button_tile.dart'; -import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/string.dart'; +import 'package:spotube/models/metadata/metadata.dart'; class StatsPlaylistItem extends StatelessWidget { - final PlaylistSimple playlist; + final SpotubeSimplePlaylistObject playlist; final Widget info; const StatsPlaylistItem( {super.key, required this.playlist, required this.info}); @@ -27,9 +24,9 @@ class StatsPlaylistItem extends StatelessWidget { height: 40, ), ), - title: Text(playlist.name!), + title: Text(playlist.name), subtitle: Text( - playlist.description?.unescapeHtml() ?? '', + playlist.description.unescapeHtml(), maxLines: 1, overflow: TextOverflow.ellipsis, ), diff --git a/lib/modules/stats/common/track_item.dart b/lib/modules/stats/common/track_item.dart index ae2e22c6..eea3dd4b 100644 --- a/lib/modules/stats/common/track_item.dart +++ b/lib/modules/stats/common/track_item.dart @@ -1,14 +1,13 @@ import 'package:auto_route/auto_route.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/links/artist_link.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 { - final Track track; + final SpotubeTrackObject track; final Widget info; const StatsTrackItem({ super.key, @@ -23,24 +22,24 @@ class StatsTrackItem extends StatelessWidget { leading: ClipRRect( borderRadius: BorderRadius.circular(4), child: UniversalImage( - path: (track.album?.images).asUrlString( + path: (track.album.images).asUrlString( placeholder: ImagePlaceholder.albumArt, ), width: 40, height: 40, ), ), - title: Text(track.name!), + title: Text(track.name), subtitle: ArtistLink( - artists: track.artists!, + artists: track.artists, mainAxisAlignment: WrapAlignment.start, onOverflowArtistClick: () { - context.navigateTo(TrackRoute(trackId: track.id!)); + context.navigateTo(TrackRoute(trackId: track.id)); }, ), trailing: info, onPressed: () { - context.navigateTo(TrackRoute(trackId: track.id!)); + context.navigateTo(TrackRoute(trackId: track.id)); }, ); } diff --git a/lib/modules/stats/top/albums.dart b/lib/modules/stats/top/albums.dart index 09bf755c..e2a9042a 100644 --- a/lib/modules/stats/top/albums.dart +++ b/lib/modules/stats/top/albums.dart @@ -8,7 +8,7 @@ import 'package:spotube/modules/stats/common/album_item.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/history/top.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'; class TopAlbums extends HookConsumerWidget { diff --git a/lib/modules/stats/top/artists.dart b/lib/modules/stats/top/artists.dart index cb2a152f..5a8dc441 100644 --- a/lib/modules/stats/top/artists.dart +++ b/lib/modules/stats/top/artists.dart @@ -9,7 +9,7 @@ import 'package:spotube/modules/stats/common/artist_item.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/history/top.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'; class TopArtists extends HookConsumerWidget { @@ -24,14 +24,8 @@ class TopArtists extends HookConsumerWidget { final topTracksNotifier = ref.watch(historyTopTracksProvider(historyDuration).notifier); - final artistsData = useMemoized( - () => topTracks.asData?.value.artists ?? [], - [topTracks.asData?.value], - ); - - for (final artist in artistsData) { - print("${artist.artist.name} has ${artist.artist.images?.length} images"); - } + final artistsData = + useMemoized(() => topTracksNotifier.artists, [topTracks.asData?.value]); return Skeletonizer.sliver( enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, diff --git a/lib/modules/stats/top/tracks.dart b/lib/modules/stats/top/tracks.dart index c4015431..08c742c4 100644 --- a/lib/modules/stats/top/tracks.dart +++ b/lib/modules/stats/top/tracks.dart @@ -8,7 +8,7 @@ import 'package:spotube/modules/stats/common/track_item.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/history/top.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'; class TopTracks extends HookConsumerWidget { diff --git a/lib/pages/album/album.dart b/lib/pages/album/album.dart index dd57b2e3..11d06658 100644 --- a/lib/pages/album/album.dart +++ b/lib/pages/album/album.dart @@ -8,7 +8,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/metadata_plugin/library/albums.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() class AlbumPage extends HookConsumerWidget { diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index 1dce98e9..d6243f71 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -7,15 +7,17 @@ import 'package:spotube/components/button/back_button.dart'; import 'package:spotube/components/titlebar/titlebar.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/header.dart'; // import 'package:spotube/pages/artist/section/related_artists.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/spotify/spotify.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() class ArtistPage extends HookConsumerWidget { @@ -30,7 +32,6 @@ class ArtistPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final scrollController = useScrollController(); - final theme = Theme.of(context); final artistQuery = ref.watch(metadataPluginArtistProvider(artistId)); @@ -46,14 +47,15 @@ class ArtistPage extends HookConsumerWidget { floatingHeader: true, child: material.RefreshIndicator.adaptive( onRefresh: () async { - ref.invalidate(artistProvider(artistId)); - ref.invalidate(relatedArtistsProvider(artistId)); - ref.invalidate(artistAlbumsProvider(artistId)); - ref.invalidate(artistIsFollowingProvider(artistId)); - ref.invalidate(artistTopTracksProvider(artistId)); + ref.invalidate(metadataPluginArtistProvider(artistId)); + // ref.invalidate(relatedArtistsProvider(artistId)); + ref.invalidate(metadataPluginArtistAlbumsProvider(artistId)); + ref.invalidate(metadataPluginIsSavedArtistProvider(artistId)); + ref.invalidate(metadataPluginArtistTopTracksProvider(artistId)); if (artistQuery.hasValue) { ref.invalidate( - artistWikipediaSummaryProvider(artistQuery.asData!.value)); + artistWikipediaSummaryProvider(artistQuery.asData!.value), + ); } }, child: Builder(builder: (context) { diff --git a/lib/pages/artist/section/footer.dart b/lib/pages/artist/section/footer.dart index 247f8879..938fb6fc 100644 --- a/lib/pages/artist/section/footer.dart +++ b/lib/pages/artist/section/footer.dart @@ -5,8 +5,7 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/constrains.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'; class ArtistPageFooter extends ConsumerWidget { diff --git a/lib/pages/artist/section/header.dart b/lib/pages/artist/section/header.dart index 6ca03a01..3a850668 100644 --- a/lib/pages/artist/section/header.dart +++ b/lib/pages/artist/section/header.dart @@ -13,7 +13,7 @@ import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/blacklist_provider.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'; class ArtistPageHeader extends HookConsumerWidget { @@ -31,7 +31,7 @@ class ArtistPageHeader extends HookConsumerWidget { final auth = ref.watch(authenticationProvider); ref.watch(blacklistProvider); final blacklistNotifier = ref.watch(blacklistProvider.notifier); - final isBlackListed = /* blacklistNotifier.containsArtist(artist) */ false; + final isBlackListed = blacklistNotifier.containsArtist(artist.id); final image = artist.images.asUrlString( placeholder: ImagePlaceholder.artist, @@ -45,11 +45,10 @@ class ArtistPageHeader extends HookConsumerWidget { Consumer( builder: (context, ref, _) { final isFollowingQuery = ref.watch( - artistIsFollowingProvider(artist.id!), - ); - final followingArtistNotifier = ref.watch( - followedArtistsProvider.notifier, + metadataPluginIsSavedArtistProvider(artist.id), ); + final followingArtistNotifier = + ref.watch(metadataPluginSavedArtistsProvider.notifier); return switch (isFollowingQuery) { AsyncData(value: final following) => Builder( @@ -58,7 +57,7 @@ class ArtistPageHeader extends HookConsumerWidget { return Button.outline( onPressed: () async { await followingArtistNotifier - .removeArtists([artist.id!]); + .removeFavorite([artist]); }, child: Text(context.l10n.following), ); @@ -66,8 +65,7 @@ class ArtistPageHeader extends HookConsumerWidget { return Button.primary( onPressed: () async { - await followingArtistNotifier - .saveArtists([artist.id!]); + await followingArtistNotifier.addFavorite([artist]); }, child: Text(context.l10n.follow), ); @@ -96,12 +94,12 @@ class ArtistPageHeader extends HookConsumerWidget { : ButtonVariance.ghost, onPressed: () async { if (isBlackListed) { - await ref.read(blacklistProvider.notifier).remove(artist.id!); + await ref.read(blacklistProvider.notifier).remove(artist.id); } else { await ref.read(blacklistProvider.notifier).add( BlacklistTableCompanion.insert( - name: artist.name!, - elementId: artist.id!, + name: artist.name, + elementId: artist.id, elementType: BlacklistedType.artist, ), ); @@ -184,7 +182,7 @@ class ArtistPageHeader extends HookConsumerWidget { const Gap(10), Flexible( child: AutoSizeText( - artist.name!, + artist.name, style: constrains.smAndDown ? typography.h4 : typography.h3, diff --git a/lib/pages/artist/section/related_artists.dart b/lib/pages/artist/section/related_artists.dart index bc03fbe5..050c8a5c 100644 --- a/lib/pages/artist/section/related_artists.dart +++ b/lib/pages/artist/section/related_artists.dart @@ -1,7 +1,5 @@ import 'package:shadcn_flutter/shadcn_flutter.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") class ArtistPageRelatedArtists extends ConsumerWidget { @@ -13,38 +11,39 @@ class ArtistPageRelatedArtists extends ConsumerWidget { @override Widget build(BuildContext context, ref) { - final relatedArtists = ref.watch(relatedArtistsProvider(artistId)); + return const SizedBox.shrink(); + // final relatedArtists = ref.watch(relatedArtistsProvider(artistId)); - return switch (relatedArtists) { - AsyncData(value: final artists) => SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - sliver: SliverGrid.builder( - itemCount: artists.length, - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 200, - mainAxisExtent: 250, - mainAxisSpacing: 10, - crossAxisSpacing: 10, - childAspectRatio: 0.8, - ), - itemBuilder: (context, index) { - final artist = artists.elementAt(index); - return SizedBox( - width: 180, - // child: ArtistCard(artist), - ); - // return ArtistCard(artist); - }, - ), - ), - AsyncError(:final error) => SliverToBoxAdapter( - child: Center( - child: Text(error.toString()), - ), - ), - _ => const SliverToBoxAdapter( - child: Center(child: CircularProgressIndicator()), - ), - }; + // return switch (relatedArtists) { + // AsyncData(value: final artists) => SliverPadding( + // padding: const EdgeInsets.symmetric(horizontal: 8.0), + // sliver: SliverGrid.builder( + // itemCount: artists.length, + // gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + // maxCrossAxisExtent: 200, + // mainAxisExtent: 250, + // mainAxisSpacing: 10, + // crossAxisSpacing: 10, + // childAspectRatio: 0.8, + // ), + // itemBuilder: (context, index) { + // final artist = artists.elementAt(index); + // return SizedBox( + // width: 180, + // // child: ArtistCard(artist), + // ); + // // return ArtistCard(artist); + // }, + // ), + // ), + // AsyncError(:final error) => SliverToBoxAdapter( + // child: Center( + // child: Text(error.toString()), + // ), + // ), + // _ => const SliverToBoxAdapter( + // child: Center(child: CircularProgressIndicator()), + // ), + // }; } } diff --git a/lib/pages/artist/section/top_tracks.dart b/lib/pages/artist/section/top_tracks.dart index 72709751..9ec7314b 100644 --- a/lib/pages/artist/section/top_tracks.dart +++ b/lib/pages/artist/section/top_tracks.dart @@ -1,16 +1,16 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:skeletonizer/skeletonizer.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/dialogs/select_device_dialog.dart'; import 'package:spotube/components/track_tile/track_tile.dart'; import 'package:spotube/extensions/context.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/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 { final String artistId; @@ -22,10 +22,11 @@ class ArtistPageTopTracks extends HookConsumerWidget { final playlist = ref.watch(audioPlayerProvider); final playlistNotifier = ref.watch(audioPlayerProvider.notifier); - final topTracksQuery = ref.watch(artistTopTracksProvider(artistId)); + final topTracksQuery = + ref.watch(metadataPluginArtistTopTracksProvider(artistId)); final isPlaylistPlaying = playlist.containsTracks( - topTracksQuery.asData?.value ?? [], + topTracksQuery.asData?.value.items ?? [], ); 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); - void playPlaylist(List tracks, {Track? currentTrack}) async { + void playPlaylist(List tracks, + {SpotubeTrackObject? currentTrack}) async { currentTrack ??= tracks.first; final isRemoteDevice = await showSelectDeviceDialog(context, ref); @@ -61,7 +63,6 @@ class ArtistPageTopTracks extends HookConsumerWidget { ), ); } else if (isPlaylistPlaying && - currentTrack.id != null && currentTrack.id != remotePlaylist.activeTrack?.id) { final index = playlist.tracks .toList() @@ -76,7 +77,6 @@ class ArtistPageTopTracks extends HookConsumerWidget { autoPlay: true, ); } else if (isPlaylistPlaying && - currentTrack.id != null && currentTrack.id != playlist.activeTrack?.id) { await playlistNotifier.jumpToTrack(currentTrack); } diff --git a/lib/pages/connect/control/control.dart b/lib/pages/connect/control/control.dart index e28566fd..6abb11eb 100644 --- a/lib/pages/connect/control/control.dart +++ b/lib/pages/connect/control/control.dart @@ -8,6 +8,7 @@ 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/models/connect/connect.dart'; +import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/modules/player/player_queue.dart'; import 'package:spotube/modules/player/volume_slider.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/context.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/connect.dart'; import 'package:media_kit/media_kit.dart' hide Track; @@ -120,7 +120,7 @@ class ConnectControlPage extends HookConsumerWidget { child: ClipRRect( borderRadius: BorderRadius.circular(20), child: UniversalImage( - path: (playlist.activeTrack?.album?.images) + path: (playlist.activeTrack?.album.images) .asUrlString( placeholder: ImagePlaceholder.albumArt, ), @@ -140,8 +140,7 @@ class ConnectControlPage extends HookConsumerWidget { onTap: () { if (playlist.activeTrack == null) return; context.navigateTo( - TrackRoute( - trackId: playlist.activeTrack!.id!), + TrackRoute(trackId: playlist.activeTrack!.id), ); }, ), @@ -152,7 +151,7 @@ class ConnectControlPage extends HookConsumerWidget { textStyle: typography.normal, mainAxisAlignment: WrapAlignment.start, onOverflowArtistClick: () => context.navigateTo( - TrackRoute(trackId: playlist.activeTrack!.id!), + TrackRoute(trackId: playlist.activeTrack!.id), ), ), ), diff --git a/lib/pages/getting_started/sections/region.dart b/lib/pages/getting_started/sections/region.dart index f657f9d9..b0f3051c 100644 --- a/lib/pages/getting_started/sections/region.dart +++ b/lib/pages/getting_started/sections/region.dart @@ -1,6 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/collections/language_codes.dart'; import 'package:spotube/collections/spotify_markets.dart'; import 'package:spotube/collections/spotube_icons.dart'; @@ -14,7 +13,7 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget { const GettingStartedPageLanguageRegionSection( {super.key, required this.onNext}); - bool filterMarkets(Market item, String query) { + bool filterMarkets(dynamic item, String query) { final market = spotifyMarkets .firstWhere((element) => element.$1 == item) .$2 @@ -64,7 +63,7 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget { const Gap(8), SizedBox( width: double.infinity, - child: Select( + child: Select( value: preferences.market, onChanged: (value) { if (value == null) return; diff --git a/lib/pages/home/genres/genre_playlists.dart b/lib/pages/home/genres/genre_playlists.dart deleted file mode 100644 index bf36f834..00000000 --- a/lib/pages/home/genres/genre_playlists.dart +++ /dev/null @@ -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), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/pages/home/genres/genres.dart b/lib/pages/home/genres/genres.dart deleted file mode 100644 index 38d110db..00000000 --- a/lib/pages/home/genres/genres.dart +++ /dev/null @@ -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!, - ), - ), - ), - ], - ), - ); - }, - ), - ), - ); - } -} diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart index 53bb4fb2..d766fd2a 100644 --- a/lib/pages/home/home.dart +++ b/lib/pages/home/home.dart @@ -13,9 +13,6 @@ import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/connect/connect_device.dart'; import 'package:spotube/modules/home/sections/featured.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/recent.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; @@ -76,18 +73,18 @@ class HomePage extends HookConsumerWidget { else if (kIsMacOS) const SliverGap(10), const SliverGap(10), - // SliverList.builder( - // itemCount: 5, - // itemBuilder: (context, index) { - // return switch (index) { - // 0 => const HomeGenresSection(), - // 1 => const HomeRecentlyPlayedSection(), - // 2 => const HomeFeaturedSection(), - // 3 => const HomePageFriendsSection(), - // _ => const HomeNewReleasesSection() - // }; - // }, - // ), + SliverList.builder( + itemCount: 3, + itemBuilder: (context, index) { + return switch (index) { + // 0 => const HomeGenresSection(), + 0 => const HomeRecentlyPlayedSection(), + 1 => const HomeFeaturedSection(), + // 3 => const HomePageFriendsSection(), + _ => const HomeNewReleasesSection() + }; + }, + ), const HomePageBrowseSection(), ], ), diff --git a/lib/pages/library/playlist_generate/playlist_generate.dart b/lib/pages/library/playlist_generate/playlist_generate.dart deleted file mode 100644 index f1eca306..00000000 --- a/lib/pages/library/playlist_generate/playlist_generate.dart +++ /dev/null @@ -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(10); - final market = useValueNotifier(preferences.market); - - final genres = useState>([]); - final artists = useState>([]); - final tracks = useState>([]); - - 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()); - final max = useState(RecommendationSeeds()); - final target = useState(RecommendationSeeds()); - - final artistAutoComplete = SeedsMultiAutocomplete( - 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( - 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( - 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( - 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( - 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( - 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), - ), - ), - ], - ), - ); - }), - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/pages/library/playlist_generate/playlist_generate_result.dart b/lib/pages/library/playlist_generate/playlist_generate_result.dart deleted file mode 100644 index 87ae9fe4..00000000 --- a/lib/pages/library/playlist_generate/playlist_generate_result.dart +++ /dev/null @@ -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>( - 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( - 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( - 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), - ), - ), - ], - ) - ], - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/pages/library/user_albums.dart b/lib/pages/library/user_albums.dart index 4534e531..c2cec373 100644 --- a/lib/pages/library/user_albums.dart +++ b/lib/pages/library/user_albums.dart @@ -17,7 +17,6 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/metadata_plugin/auth.dart'; import 'package:spotube/provider/metadata_plugin/library/albums.dart'; -import 'package:spotube/provider/spotify/spotify.dart'; import 'package:auto_route/auto_route.dart'; @RoutePage() @@ -61,7 +60,7 @@ class UserAlbumsPage extends HookConsumerWidget { child: Scaffold( child: material.RefreshIndicator.adaptive( onRefresh: () async { - ref.invalidate(favoriteAlbumsProvider); + ref.invalidate(metadataPluginSavedAlbumsProvider); }, child: InterScrollbar( controller: controller, diff --git a/lib/pages/library/user_artists.dart b/lib/pages/library/user_artists.dart index 81db1451..6087f41c 100644 --- a/lib/pages/library/user_artists.dart +++ b/lib/pages/library/user_artists.dart @@ -19,7 +19,6 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/metadata_plugin/auth.dart'; import 'package:spotube/provider/metadata_plugin/library/artists.dart'; -import 'package:spotube/provider/spotify/spotify.dart'; import 'package:auto_route/auto_route.dart'; @RoutePage() @@ -65,7 +64,7 @@ class UserArtistsPage extends HookConsumerWidget { child: Scaffold( child: material.RefreshIndicator.adaptive( onRefresh: () async { - ref.invalidate(followedArtistsProvider); + ref.invalidate(metadataPluginSavedArtistsProvider); }, child: InterScrollbar( controller: controller, diff --git a/lib/pages/library/user_downloads.dart b/lib/pages/library/user_downloads.dart index 1d8f560a..6566bed6 100644 --- a/lib/pages/library/user_downloads.dart +++ b/lib/pages/library/user_downloads.dart @@ -16,10 +16,7 @@ class UserDownloadsPage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final downloadManager = ref.watch(downloadManagerProvider); - final history = [ - ...downloadManager.$history, - ...downloadManager.$backHistory, - ]; + final history = downloadManager.$backHistory; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -51,7 +48,7 @@ class UserDownloadsPage extends HookConsumerWidget { child: ListView.builder( itemCount: history.length, itemBuilder: (context, index) { - return DownloadItem(track: history[index]); + return DownloadItem(track: history.elementAt(index)); }, ), ), diff --git a/lib/pages/library/user_local_tracks/local_folder.dart b/lib/pages/library/user_local_tracks/local_folder.dart index a6f3ad51..c256af7f 100644 --- a/lib/pages/library/user_local_tracks/local_folder.dart +++ b/lib/pages/library/user_local_tracks/local_folder.dart @@ -17,6 +17,7 @@ import 'package:spotube/components/button/back_button.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/string.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/pages/library/user_local_tracks/user_local_tracks.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/track_presentation/sort_tracks_dropdown.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/models/local_track.dart'; import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -49,8 +48,8 @@ class LocalLibraryPage extends HookConsumerWidget { Future playLocalTracks( WidgetRef ref, - List tracks, { - LocalTrack? currentTrack, + List tracks, { + SpotubeLocalTrackObject? currentTrack, }) async { final playlist = ref.read(audioPlayerProvider); final playback = ref.read(audioPlayerProvider.notifier); @@ -64,7 +63,6 @@ class LocalLibraryPage extends HookConsumerWidget { autoPlay: true, ); } else if (isPlaylistPlaying && - currentTrack.id != null && currentTrack.id != playlist.activeTrack?.id) { await playback.jumpToTrack(currentTrack); } @@ -296,7 +294,8 @@ class LocalLibraryPage extends HookConsumerWidget { data: (tracks) { final sortedTracks = useMemoized(() { return ServiceUtils.sortTracks( - tracks[location] ?? [], + tracks[location] ?? + [], sortBy.value); }, [sortBy.value, tracks]); @@ -307,7 +306,7 @@ class LocalLibraryPage extends HookConsumerWidget { return sortedTracks .map((e) => ( weightedRatio( - "${e.name} - ${e.artists?.asString() ?? ""}", + "${e.name} - ${e.artists.asString()}", searchController.text, ), e, diff --git a/lib/pages/lyrics/lyrics.dart b/lib/pages/lyrics/lyrics.dart index 1bd58d61..b55dc02e 100644 --- a/lib/pages/lyrics/lyrics.dart +++ b/lib/pages/lyrics/lyrics.dart @@ -6,13 +6,13 @@ import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/image/universal_image.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/models/metadata/metadata.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart'; import 'package:spotube/pages/lyrics/synced_lyrics.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/provider/spotify/spotify.dart'; import 'package:auto_route/auto_route.dart'; @RoutePage() @@ -25,11 +25,11 @@ class LyricsPage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final playlist = ref.watch(audioPlayerProvider); String albumArt = useMemoized( - () => (playlist.activeTrack?.album?.images).asUrlString( - index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1, + () => (playlist.activeTrack?.album.images).asUrlString( + index: (playlist.activeTrack?.album.images.length ?? 1) - 1, placeholder: ImagePlaceholder.albumArt, ), - [playlist.activeTrack?.album?.images], + [playlist.activeTrack?.album.images], ); final palette = usePaletteColor(albumArt, ref); final selectedIndex = useState(0); diff --git a/lib/pages/lyrics/plain_lyrics.dart b/lib/pages/lyrics/plain_lyrics.dart index 0b5354a0..69f71cf4 100644 --- a/lib/pages/lyrics/plain_lyrics.dart +++ b/lib/pages/lyrics/plain_lyrics.dart @@ -5,14 +5,14 @@ import 'package:palette_generator/palette_generator.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.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/components/shimmers/shimmer_lyrics.dart'; -import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.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 { final PaletteColor palette; @@ -52,7 +52,7 @@ class PlainLyrics extends HookConsumerWidget { ), Center( child: Text( - playlist.activeTrack?.artists?.asString() ?? "", + playlist.activeTrack?.artists.asString() ?? "", style: (mediaQuery.mdAndUp ? typography.h4 : typography.large) .copyWith( color: palette.bodyTextColor, diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index b7423e14..5319d7ad 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -5,16 +5,16 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:shadcn_flutter/shadcn_flutter.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/components/shimmers/shimmer_lyrics.dart'; -import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart'; import 'package:spotube/modules/lyrics/use_synced_lyrics.dart'; import 'package:scroll_to_index/scroll_to_index.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/logger/logger.dart'; @@ -117,7 +117,7 @@ class SyncedLyrics extends HookConsumerWidget { bottom: PreferredSize( preferredSize: const Size.fromHeight(40), child: Text( - playlist.activeTrack?.artists?.asString() ?? "", + playlist.activeTrack?.artists.asString() ?? "", style: mediaQuery.mdAndUp ? typography.h4 : typography.x2Large, ), diff --git a/lib/pages/player/lyrics.dart b/lib/pages/player/lyrics.dart index 01a4e921..e1aad553 100644 --- a/lib/pages/player/lyrics.dart +++ b/lib/pages/player/lyrics.dart @@ -5,8 +5,8 @@ import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/button/back_button.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/models/metadata/metadata.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart'; import 'package:spotube/pages/lyrics/synced_lyrics.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; @@ -19,11 +19,11 @@ class PlayerLyricsPage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final playlist = ref.watch(audioPlayerProvider); String albumArt = useMemoized( - () => (playlist.activeTrack?.album?.images).asUrlString( - index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1, + () => (playlist.activeTrack?.album.images).asUrlString( + index: (playlist.activeTrack?.album.images.length ?? 1) - 1, placeholder: ImagePlaceholder.albumArt, ), - [playlist.activeTrack?.album?.images], + [playlist.activeTrack?.album.images], ); final selectedIndex = useState(0); final palette = usePaletteColor(albumArt, ref); diff --git a/lib/pages/playlist/liked_playlist.dart b/lib/pages/playlist/liked_playlist.dart index 5575b9d8..7c9a7fec 100644 --- a/lib/pages/playlist/liked_playlist.dart +++ b/lib/pages/playlist/liked_playlist.dart @@ -1,12 +1,11 @@ import 'package:flutter/material.dart' as material; import 'package:flutter/widgets.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/track_presentation.dart'; import 'package:spotube/models/metadata/metadata.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'; @RoutePage() @@ -21,12 +20,12 @@ class LikedPlaylistPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final likedTracks = ref.watch(likedTracksProvider); - final tracks = likedTracks.asData?.value ?? []; + final likedTracks = ref.watch(metadataPluginSavedTracksProvider); + final tracks = likedTracks.asData?.value.items ?? []; return material.RefreshIndicator.adaptive( onRefresh: () async { - ref.invalidate(likedTracksProvider); + ref.invalidate(metadataPluginSavedTracksProvider); }, child: TrackPresentation( options: TrackPresentationOptions( @@ -40,7 +39,7 @@ class LikedPlaylistPage extends HookConsumerWidget { return tracks.toList(); }, onRefresh: () async { - ref.invalidate(likedTracksProvider); + ref.invalidate(metadataPluginSavedTracksProvider); }, ), title: playlist.name, diff --git a/lib/pages/playlist/playlist.dart b/lib/pages/playlist/playlist.dart index 94c07ac1..1f6ca75d 100644 --- a/lib/pages/playlist/playlist.dart +++ b/lib/pages/playlist/playlist.dart @@ -9,8 +9,9 @@ import 'package:spotube/components/track_presentation/use_is_user_playlist.dart' import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/metadata/metadata.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:spotube/provider/metadata_plugin/tracks/playlist.dart'; +import 'package:spotube/provider/metadata_plugin/utils/common.dart'; @RoutePage() class PlaylistPage extends HookConsumerWidget { @@ -30,8 +31,8 @@ class PlaylistPage extends HookConsumerWidget { .watch( metadataPluginSavedPlaylistsProvider.select( (value) => value.whenData( - (value) => (value.items as List) - .firstWhereOrNull((s) => s.id == _playlist.id), + (value) => + value.items.firstWhereOrNull((s) => s.id == _playlist.id), ), ), ) @@ -39,11 +40,11 @@ class PlaylistPage extends HookConsumerWidget { ?.value ?? _playlist; - final tracks = ref.watch(playlistTracksProvider(playlist.id)); + final tracks = ref.watch(metadataPluginPlaylistTracksProvider(playlist.id)); final tracksNotifier = - ref.watch(playlistTracksProvider(playlist.id).notifier); + ref.watch(metadataPluginPlaylistTracksProvider(playlist.id).notifier); final isFavoritePlaylist = - ref.watch(isFavoritePlaylistProvider(playlist.id)); + ref.watch(metadataPluginIsSavedPlaylistProvider(playlist.id)); final favoritePlaylistsNotifier = ref.watch(metadataPluginSavedPlaylistsProvider.notifier); @@ -52,9 +53,9 @@ class PlaylistPage extends HookConsumerWidget { return material.RefreshIndicator.adaptive( onRefresh: () async { - ref.invalidate(playlistTracksProvider(playlist.id)); - ref.invalidate(isFavoritePlaylistProvider(playlist.id)); - ref.invalidate(favoritePlaylistsProvider); + ref.invalidate(metadataPluginPlaylistTracksProvider(playlist.id)); + ref.invalidate(metadataPluginSavedPlaylistsProvider); + ref.invalidate(metadataPluginIsSavedPlaylistProvider(playlist.id)); }, child: TrackPresentation( options: TrackPresentationOptions( @@ -67,7 +68,7 @@ class PlaylistPage extends HookConsumerWidget { isLoading: tracks.isLoading || tracks.isLoadingNextPage, onFetchMore: tracksNotifier.fetchMore, onRefresh: () async { - ref.invalidate(playlistTracksProvider(playlist.id)); + ref.invalidate(metadataPluginPlaylistTracksProvider(playlist.id)); }, onFetchAll: () async { return await tracksNotifier.fetchAll(); diff --git a/lib/pages/profile/profile.dart b/lib/pages/profile/profile.dart index b6c4a2cd..a03a8fa4 100644 --- a/lib/pages/profile/profile.dart +++ b/lib/pages/profile/profile.dart @@ -1,16 +1,14 @@ -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:sliver_tools/sliver_tools.dart'; import 'package:spotube/collections/fake.dart'; -import 'package:spotube/collections/spotify_markets.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/extensions/image.dart'; -import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/provider/metadata_plugin/user.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:auto_route/auto_route.dart'; @@ -22,22 +20,22 @@ class ProfilePage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final me = ref.watch(meProvider); + final me = ref.watch(metadataPluginUserProvider); final meData = me.asData?.value ?? FakeData.user; - final userProperties = useMemoized( - () => { - context.l10n.email: meData.email ?? "N/A", - context.l10n.profile_followers: - meData.followers?.total.toString() ?? "N/A", - context.l10n.birthday: meData.birthdate ?? context.l10n.not_born, - context.l10n.country: spotifyMarkets - .firstWhere((market) => market.$1 == meData.country) - .$2, - context.l10n.subscription: meData.product ?? context.l10n.hacker, - }, - [meData], - ); + // final userProperties = useMemoized( + // () => { + // context.l10n.email: meData.email ?? "N/A", + // context.l10n.profile_followers: + // meData.followers?.total.toString() ?? "N/A", + // context.l10n.birthday: meData.birthdate ?? context.l10n.not_born, + // context.l10n.country: spotifyMarkets + // .firstWhere((market) => market.$1 == meData.country) + // .$2, + // context.l10n.subscription: meData.product ?? context.l10n.hacker, + // }, + // [meData], + // ); return SafeArea( child: Scaffold( @@ -72,7 +70,7 @@ class ProfilePage extends HookConsumerWidget { const SliverGap(10), SliverToBoxAdapter( child: Text( - meData.displayName ?? context.l10n.no_name, + meData.name, textAlign: TextAlign.center, ).h4(), ), @@ -97,42 +95,42 @@ class ProfilePage extends HookConsumerWidget { ), ), ), - SliverCrossAxisConstrained( - maxCrossAxisExtent: 500, - child: SliverToBoxAdapter( - child: Card( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Table( - columnWidths: const { - 0: FixedTableSize(120), - }, - defaultRowHeight: const FixedTableSize(40), - rows: [ - for (final MapEntry(:key, :value) - in userProperties.entries) - TableRow( - cells: [ - TableCell( - child: Padding( - padding: const EdgeInsets.all(6), - child: Text(key).large(), - ), - ), - TableCell( - child: Padding( - padding: const EdgeInsets.all(6), - child: Text(value), - ), - ), - ], - ) - ], - ), - ), - ), - ), - ), + // SliverCrossAxisConstrained( + // maxCrossAxisExtent: 500, + // child: SliverToBoxAdapter( + // child: Card( + // child: Padding( + // padding: const EdgeInsets.all(8.0), + // child: Table( + // columnWidths: const { + // 0: FixedTableSize(120), + // }, + // defaultRowHeight: const FixedTableSize(40), + // rows: [ + // for (final MapEntry(:key, :value) + // in userProperties.entries) + // TableRow( + // cells: [ + // TableCell( + // child: Padding( + // padding: const EdgeInsets.all(6), + // child: Text(key).large(), + // ), + // ), + // TableCell( + // child: Padding( + // padding: const EdgeInsets.all(6), + // child: Text(value), + // ), + // ), + // ], + // ) + // ], + // ), + // ), + // ), + // ), + // ), const SliverGap(200), ], ), diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index 19043f42..b481300b 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -19,10 +19,13 @@ import 'package:spotube/pages/search/sections/artists.dart'; import 'package:spotube/pages/search/sections/playlists.dart'; import 'package:spotube/provider/metadata_plugin/auth.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:auto_route/auto_route.dart'; +final searchTermStateProvider = StateProvider((ref) { + return ""; +}); + @RoutePage() class SearchPage extends HookConsumerWidget { static const name = "search"; diff --git a/lib/pages/search/sections/albums.dart b/lib/pages/search/sections/albums.dart index 249e0e6d..e8bc71fc 100644 --- a/lib/pages/search/sections/albums.dart +++ b/lib/pages/search/sections/albums.dart @@ -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/extensions/context.dart'; +import 'package:spotube/pages/search/search.dart'; import 'package:spotube/provider/metadata_plugin/search/all.dart'; -import 'package:spotube/provider/spotify/spotify.dart'; class SearchAlbumsSection extends HookConsumerWidget { const SearchAlbumsSection({ diff --git a/lib/pages/search/sections/artists.dart b/lib/pages/search/sections/artists.dart index ac009cf4..9da3702c 100644 --- a/lib/pages/search/sections/artists.dart +++ b/lib/pages/search/sections/artists.dart @@ -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/extensions/context.dart'; +import 'package:spotube/pages/search/search.dart'; import 'package:spotube/provider/metadata_plugin/search/all.dart'; -import 'package:spotube/provider/spotify/spotify.dart'; class SearchArtistsSection extends HookConsumerWidget { const SearchArtistsSection({ diff --git a/lib/pages/search/sections/playlists.dart b/lib/pages/search/sections/playlists.dart index 2c387f28..7e03bdeb 100644 --- a/lib/pages/search/sections/playlists.dart +++ b/lib/pages/search/sections/playlists.dart @@ -2,8 +2,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.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/spotify/spotify.dart'; class SearchPlaylistsSection extends HookConsumerWidget { const SearchPlaylistsSection({ diff --git a/lib/pages/search/sections/tracks.dart b/lib/pages/search/sections/tracks.dart index bacbbb57..6bc60045 100644 --- a/lib/pages/search/sections/tracks.dart +++ b/lib/pages/search/sections/tracks.dart @@ -2,15 +2,15 @@ import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/components/dialogs/prompt_dialog.dart'; import 'package:spotube/components/dialogs/select_device_dialog.dart'; import 'package:spotube/components/track_tile/track_tile.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/connect/connect.dart'; +import 'package:spotube/pages/search/search.dart'; import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; -import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:spotube/provider/metadata_plugin/search/all.dart'; class SearchTracksSection extends HookConsumerWidget { const SearchTracksSection({ @@ -19,12 +19,9 @@ class SearchTracksSection extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final searchTrack = ref.watch(searchProvider(SearchType.track)); - - final searchTrackNotifier = - ref.watch(searchProvider(SearchType.track).notifier); - - final tracks = searchTrack.asData?.value.items.cast() ?? []; + final searchTerm = ref.watch(searchTermStateProvider); + final search = ref.watch(metadataPluginSearchAllProvider(searchTerm)); + final tracks = search.asData?.value.tracks ?? []; final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final playlist = ref.watch(audioPlayerProvider); final theme = Theme.of(context); @@ -41,10 +38,8 @@ class SearchTracksSection extends HookConsumerWidget { style: theme.typography.h4, ), ), - if (searchTrack.isLoading) + if (search.isLoading) const CircularProgressIndicator() - else if (searchTrack.hasError) - Text(searchTrack.error.toString()) else ...tracks.mapIndexed((i, track) { return TrackTile( @@ -69,7 +64,7 @@ class SearchTracksSection extends HookConsumerWidget { ? await showPromptDialog( context: context, title: context.l10n.playing_track( - track.name!, + track.name, ), message: context.l10n.queue_clear_alert( playlist.tracks.length, @@ -92,7 +87,7 @@ class SearchTracksSection extends HookConsumerWidget { ? await showPromptDialog( context: context, title: context.l10n.playing_track( - track.name!, + track.name, ), message: context.l10n.queue_clear_alert( playlist.tracks.length, @@ -111,17 +106,6 @@ class SearchTracksSection extends HookConsumerWidget { }, ); }), - if (searchTrack.asData?.value.hasMore == true && tracks.isNotEmpty) - Center( - child: TextButton( - onPressed: searchTrack.isLoadingNextPage - ? null - : searchTrackNotifier.fetchMore, - child: searchTrack.isLoadingNextPage - ? const CircularProgressIndicator() - : Text(context.l10n.load_more), - ), - ) ], ); } diff --git a/lib/pages/settings/sections/language_region.dart b/lib/pages/settings/sections/language_region.dart index 44b364af..06d9ff01 100644 --- a/lib/pages/settings/sections/language_region.dart +++ b/lib/pages/settings/sections/language_region.dart @@ -1,10 +1,10 @@ import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/collections/language_codes.dart'; import 'package:spotube/collections/spotify_markets.dart'; import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/models/metadata/market.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/components/adaptive/adaptive_select_tile.dart'; import 'package:spotube/extensions/constrains.dart'; diff --git a/lib/pages/stats/albums/albums.dart b/lib/pages/stats/albums/albums.dart index 834837af..363e7962 100644 --- a/lib/pages/stats/albums/albums.dart +++ b/lib/pages/stats/albums/albums.dart @@ -8,7 +8,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/history/top.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:auto_route/auto_route.dart'; diff --git a/lib/pages/stats/artists/artists.dart b/lib/pages/stats/artists/artists.dart index f3d2f0dd..340f7b4b 100644 --- a/lib/pages/stats/artists/artists.dart +++ b/lib/pages/stats/artists/artists.dart @@ -9,7 +9,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/history/top.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:auto_route/auto_route.dart'; @@ -27,7 +27,9 @@ class StatsArtistsPage extends HookConsumerWidget { ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier); final artistsData = useMemoized( - () => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]); + () => topTracksNotifier.artists, + [topTracks.asData?.value], + ); return SafeArea( bottom: false, diff --git a/lib/pages/stats/fees/fees.dart b/lib/pages/stats/fees/fees.dart index 2f1e4107..7419b381 100644 --- a/lib/pages/stats/fees/fees.dart +++ b/lib/pages/stats/fees/fees.dart @@ -10,7 +10,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/history/top.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:auto_route/auto_route.dart'; @@ -31,7 +31,9 @@ class StatsStreamFeesPage extends HookConsumerWidget { ref.watch(historyTopTracksProvider(duration.value).notifier); final artistsData = useMemoized( - () => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]); + () => topTracksNotifier.artists, + [topTracks.asData?.value], + ); final total = useMemoized( () => artistsData.fold( diff --git a/lib/pages/stats/minutes/minutes.dart b/lib/pages/stats/minutes/minutes.dart index 2ee4c8d7..a6c95992 100644 --- a/lib/pages/stats/minutes/minutes.dart +++ b/lib/pages/stats/minutes/minutes.dart @@ -8,7 +8,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/history/top.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:auto_route/auto_route.dart'; @@ -52,8 +52,13 @@ class StatsMinutesPage extends HookConsumerWidget { return StatsTrackItem( track: track.track, info: Text( - context.l10n.count_mins(compactNumberFormatter - .format(track.count * track.track.duration!.inMinutes)), + context.l10n.count_mins( + compactNumberFormatter.format( + track.count * + Duration(milliseconds: track.track.durationMs) + .inMinutes, + ), + ), ), ); }, diff --git a/lib/pages/stats/playlists/playlists.dart b/lib/pages/stats/playlists/playlists.dart index 03ea5126..369066f7 100644 --- a/lib/pages/stats/playlists/playlists.dart +++ b/lib/pages/stats/playlists/playlists.dart @@ -8,7 +8,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top/playlists.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:auto_route/auto_route.dart'; diff --git a/lib/pages/stats/streams/streams.dart b/lib/pages/stats/streams/streams.dart index 0d919a44..b2cc671d 100644 --- a/lib/pages/stats/streams/streams.dart +++ b/lib/pages/stats/streams/streams.dart @@ -8,7 +8,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/history/top.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:auto_route/auto_route.dart'; diff --git a/lib/pages/track/track.dart b/lib/pages/track/track.dart index 9f94650b..128c5103 100644 --- a/lib/pages/track/track.dart +++ b/lib/pages/track/track.dart @@ -4,19 +4,17 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.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/components/heart_button/heart_button.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/links/artist_link.dart'; -import 'package:spotube/components/links/link_text.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/track_tile/track_options.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/list.dart'; +import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; -import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:spotube/provider/metadata_plugin/tracks/track.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/extensions/constrains.dart'; @@ -42,7 +40,7 @@ class TrackPage extends HookConsumerWidget { final isActive = playlist.activeTrack?.id == trackId; - final trackQuery = ref.watch(trackProvider(trackId)); + final trackQuery = ref.watch(metadataPluginTrackProvider(trackId)); final track = trackQuery.asData?.value ?? FakeData.track; @@ -71,7 +69,7 @@ class TrackPage extends HookConsumerWidget { decoration: BoxDecoration( image: DecorationImage( image: UniversalImage.imageProvider( - track.album!.images.asUrlString( + track.album.images.asUrlString( placeholder: ImagePlaceholder.albumArt, ), ), @@ -116,7 +114,7 @@ class TrackPage extends HookConsumerWidget { child: ClipRRect( borderRadius: BorderRadius.circular(10), child: UniversalImage( - path: track.album!.images.asUrlString( + path: track.album.images.asUrlString( placeholder: ImagePlaceholder.albumArt, ), height: 200, @@ -134,7 +132,7 @@ class TrackPage extends HookConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - track.name!, + track.name, ).large().semiBold(), const Gap(10), Row( @@ -162,7 +160,7 @@ class TrackPage extends HookConsumerWidget { const Gap(5), Flexible( child: ArtistLink( - artists: track.artists!, + artists: track.artists, hideOverflowArtist: false, ), ), diff --git a/lib/provider/audio_player/audio_player_streams.dart b/lib/provider/audio_player/audio_player_streams.dart index baf7b624..5b9731c5 100644 --- a/lib/provider/audio_player/audio_player_streams.dart +++ b/lib/provider/audio_player/audio_player_streams.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:math'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; @@ -87,8 +86,8 @@ class AudioPlayerStreamListeners { String? lastScrobbled; return audioPlayer.positionStream.listen((position) async { try { - final uid = audioPlayerState.activeTrack is LocalTrack - ? (audioPlayerState.activeTrack as LocalTrack).path + final uid = audioPlayerState.activeTrack is SpotubeLocalTrackObject + ? (audioPlayerState.activeTrack as SpotubeLocalTrackObject).path : audioPlayerState.activeTrack?.id; if (audioPlayerState.activeTrack == null || diff --git a/lib/provider/blacklist_provider.dart b/lib/provider/blacklist_provider.dart index ff7ec8fb..f916c491 100644 --- a/lib/provider/blacklist_provider.dart +++ b/lib/provider/blacklist_provider.dart @@ -1,6 +1,5 @@ import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/database/database.dart'; @@ -47,9 +46,9 @@ class BlackListNotifier extends AsyncNotifier> { return containsTrack || containsTrackArtists; } - bool containsArtist(ArtistSimple artist) { + bool containsArtist(String artistId) { return state.asData?.value - .any((element) => element.elementId == artist.id) ?? + .any((element) => element.elementId == artistId) ?? false; } diff --git a/lib/provider/custom_spotify_endpoint_provider.dart b/lib/provider/custom_spotify_endpoint_provider.dart deleted file mode 100644 index 1f36282a..00000000 --- a/lib/provider/custom_spotify_endpoint_provider.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/provider/authentication/authentication.dart'; -import 'package:spotube/provider/spotify/spotify.dart'; -import 'package:spotube/services/custom_spotify_endpoints/spotify_endpoints.dart'; - -final customSpotifyEndpointProvider = Provider((ref) { - ref.watch(spotifyProvider); - final auth = ref.watch(authenticationProvider); - return CustomSpotifyEndpoints(auth.asData?.value?.accessToken.value ?? ""); -}); diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index a7e2b768..2a79f60d 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -112,10 +112,14 @@ class DownloadManagerProvider extends ChangeNotifier { return join(downloadDirectory, PrimitiveUtils.toSafeFileName(name)); } - Future isActive(SpotubeFullTrackObject track) async { + bool isActive(SpotubeFullTrackObject track) { if ($backHistory.contains(track)) return true; - final sourcedTrack = await mapToSourcedTrack(track); + final sourcedTrack = $history.firstWhereOrNull( + (element) => element.query.id == track.id, + ); + + if (sourcedTrack == null) return false; return dl .getAllDownloads() @@ -196,9 +200,10 @@ class DownloadManagerProvider extends ChangeNotifier { } } - Future removeFromQueue(SourcedTrack track) async { - await dl.removeDownload(track.getUrlOfCodec(downloadCodec)); - $history.remove(track); + Future removeFromQueue(SpotubeFullTrackObject track) async { + final sourcedTrack = await mapToSourcedTrack(track); + await dl.removeDownload(sourcedTrack.getUrlOfCodec(downloadCodec)); + $history.remove(sourcedTrack); } Future pause(SpotubeFullTrackObject track) async { @@ -242,12 +247,26 @@ class DownloadManagerProvider extends ChangeNotifier { return sourcedTrack; } - ValueNotifier? getStatusNotifier(SourcedTrack track) { - return dl.getDownload(track.getUrlOfCodec(downloadCodec))?.status; + ValueNotifier? getStatusNotifier( + SpotubeFullTrackObject track, + ) { + final sourcedTrack = $history.firstWhereOrNull( + (element) => element.query.id == track.id, + ); + if (sourcedTrack == null) { + return null; + } + return dl.getDownload(sourcedTrack.getUrlOfCodec(downloadCodec))?.status; } - ValueNotifier? getProgressNotifier(SourcedTrack track) { - return dl.getDownload(track.getUrlOfCodec(downloadCodec))?.progress; + ValueNotifier? getProgressNotifier(SpotubeFullTrackObject track) { + final sourcedTrack = $history.firstWhereOrNull( + (element) => element.query.id == track.id, + ); + if (sourcedTrack == null) { + return null; + } + return dl.getDownload(sourcedTrack.getUrlOfCodec(downloadCodec))?.progress; } } diff --git a/lib/provider/history/top/albums.dart b/lib/provider/history/top/albums.dart index b11e62d2..1caad5cd 100644 --- a/lib/provider/history/top/albums.dart +++ b/lib/provider/history/top/albums.dart @@ -3,42 +3,19 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotify/spotify.dart'; +import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/history/top.dart'; -import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart'; -typedef PlaybackHistoryAlbum = ({int count, AlbumSimple album}); - -class HistoryTopAlbumsState extends PaginatedState { - HistoryTopAlbumsState({ - required super.items, - required super.offset, - required super.limit, - required super.hasMore, - }); - - @override - HistoryTopAlbumsState copyWith({ - List? items, - int? offset, - int? limit, - bool? hasMore, - }) { - return HistoryTopAlbumsState( - items: items ?? this.items, - offset: offset ?? this.offset, - limit: limit ?? this.limit, - hasMore: hasMore ?? this.hasMore, - ); - } -} +typedef PlaybackHistoryAlbum = ({int count, SpotubeSimpleAlbumObject album}); class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier< - PlaybackHistoryAlbum, HistoryTopAlbumsState, HistoryDuration> { + PlaybackHistoryAlbum, HistoryDuration> { HistoryTopAlbumsNotifier() : super(); - Selectable createAlbumsQuery({int? limit, int? offset}) { + Selectable createAlbumsQuery( + {int? limit, int? offset}) { final database = ref.read(databaseProvider); final duration = switch (arg) { @@ -81,28 +58,28 @@ class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier< readsFrom: {database.historyTable}, ).map((row) { final data = row.read('data'); - final album = AlbumSimple.fromJson(jsonDecode(data)); + final album = SpotubeSimpleAlbumObject.fromJson(jsonDecode(data)); return album; }); } @override - fetch(arg, offset, limit) async { + fetch(offset, limit) async { final albumsQuery = createAlbumsQuery(limit: limit, offset: offset); final items = getAlbumsWithCount(await albumsQuery.get()); - return ( + return SpotubePaginationResponseObject( items: items, + limit: limit, hasMore: items.length == limit, - nextOffset: offset + limit, + nextOffset: (offset + limit).toInt(), + total: items.length, ); } @override build(arg) async { - final (items: albums, :hasMore, :nextOffset) = await fetch(arg, 0, 20); - final subscription = createAlbumsQuery().watch().listen((event) { if (state.asData == null) return; state = AsyncData(state.asData!.value.copyWith( @@ -115,18 +92,13 @@ class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier< subscription.cancel(); }); - return HistoryTopAlbumsState( - items: albums, - offset: nextOffset, - limit: 20, - hasMore: hasMore, - ); + return await fetch(0, 20); } List getAlbumsWithCount( - List albumsWithTrackAlbums, + List albumsWithTrackAlbums, ) { - return groupBy(albumsWithTrackAlbums, (album) => album.id!) + return groupBy(albumsWithTrackAlbums, (album) => album.id) .entries .map((entry) { return (count: entry.value.length, album: entry.value.first); @@ -137,6 +109,8 @@ class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier< } final historyTopAlbumsProvider = AsyncNotifierProviderFamily< - HistoryTopAlbumsNotifier, HistoryTopAlbumsState, HistoryDuration>( + HistoryTopAlbumsNotifier, + SpotubePaginationResponseObject, + HistoryDuration>( () => HistoryTopAlbumsNotifier(), ); diff --git a/lib/provider/history/top/playlists.dart b/lib/provider/history/top/playlists.dart index 19eb3622..1beabb80 100644 --- a/lib/provider/history/top/playlists.dart +++ b/lib/provider/history/top/playlists.dart @@ -1,40 +1,19 @@ import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/models/database/database.dart'; +import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/history/top.dart'; -import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart'; -typedef PlaybackHistoryPlaylist = ({int count, PlaylistSimple playlist}); - -class HistoryTopPlaylistsState extends PaginatedState { - HistoryTopPlaylistsState({ - required super.items, - required super.offset, - required super.limit, - required super.hasMore, - }); - - @override - HistoryTopPlaylistsState copyWith({ - List? items, - int? offset, - int? limit, - bool? hasMore, - }) { - return HistoryTopPlaylistsState( - items: items ?? this.items, - offset: offset ?? this.offset, - limit: limit ?? this.limit, - hasMore: hasMore ?? this.hasMore, - ); - } -} +typedef PlaybackHistoryPlaylist = ({ + int count, + SpotubeSimplePlaylistObject playlist +}); class HistoryTopPlaylistsNotifier extends FamilyPaginatedAsyncNotifier< - PlaybackHistoryPlaylist, HistoryTopPlaylistsState, HistoryDuration> { + PlaybackHistoryPlaylist, HistoryDuration> { HistoryTopPlaylistsNotifier() : super(); SimpleSelectStatement<$HistoryTableTable, HistoryTableData> @@ -52,22 +31,22 @@ class HistoryTopPlaylistsNotifier extends FamilyPaginatedAsyncNotifier< } @override - fetch(arg, offset, limit) async { + fetch(offset, limit) async { final playlistsQuery = createPlaylistsQuery()..limit(limit, offset: offset); final items = getPlaylistsWithCount(await playlistsQuery.get()); - return ( + return SpotubePaginationResponseObject( items: items, - hasMore: items.length == limit, nextOffset: offset + limit, + total: items.length, + limit: limit, + hasMore: items.length == limit, ); } @override build(arg) async { - final (items: playlists, :hasMore, :nextOffset) = await fetch(arg, 0, 20); - final subscription = createPlaylistsQuery().watch().listen((event) { if (state.asData == null) return; state = AsyncData(state.asData!.value.copyWith( @@ -80,18 +59,13 @@ class HistoryTopPlaylistsNotifier extends FamilyPaginatedAsyncNotifier< subscription.cancel(); }); - return HistoryTopPlaylistsState( - items: playlists, - offset: nextOffset, - limit: 20, - hasMore: hasMore, - ); + return await fetch(0, 20); } List getPlaylistsWithCount( List playlists, ) { - return groupBy(playlists, (playlist) => playlist.playlist!.id!) + return groupBy(playlists, (playlist) => playlist.playlist!.id) .entries .map((entry) { return ( @@ -105,6 +79,8 @@ class HistoryTopPlaylistsNotifier extends FamilyPaginatedAsyncNotifier< } final historyTopPlaylistsProvider = AsyncNotifierProviderFamily< - HistoryTopPlaylistsNotifier, HistoryTopPlaylistsState, HistoryDuration>( + HistoryTopPlaylistsNotifier, + SpotubePaginationResponseObject, + HistoryDuration>( () => HistoryTopPlaylistsNotifier(), ); diff --git a/lib/provider/history/top/tracks.dart b/lib/provider/history/top/tracks.dart index 3c057e56..5c1dbdbf 100644 --- a/lib/provider/history/top/tracks.dart +++ b/lib/provider/history/top/tracks.dart @@ -1,65 +1,18 @@ import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/models/database/database.dart'; +import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/history/top.dart'; -import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:spotube/provider/metadata_plugin/artist/artist.dart'; +import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart'; -typedef PlaybackHistoryTrack = ({int count, Track track}); -typedef PlaybackHistoryArtist = ({int count, Artist artist}); - -class HistoryTopTracksState extends PaginatedState { - HistoryTopTracksState({ - required super.items, - required super.offset, - required super.limit, - required super.hasMore, - }); - - List get artists { - return getArtistsWithCount( - items.expand((e) => e.track.artists ?? []), - ); - } - - List getArtistsWithCount(Iterable artists) { - return groupBy(artists, (artist) => artist.id!) - .entries - .map((entry) { - return ( - count: entry.value.length, - - /// Previously, due to a bug, artist images were not being saved. - /// Now it's fixed, but we need to handle the case where images are null. - /// So we take the first artist with images if available, otherwise the first one. - artist: entry.value.firstWhereOrNull((a) => a.images != null) ?? - entry.value.first, - ); - }) - .sorted((a, b) => b.count.compareTo(a.count)) - .toList(); - } - - @override - HistoryTopTracksState copyWith({ - List? items, - int? offset, - int? limit, - bool? hasMore, - }) { - return HistoryTopTracksState( - items: items ?? this.items, - offset: offset ?? this.offset, - limit: limit ?? this.limit, - hasMore: hasMore ?? this.hasMore, - ); - } -} +typedef PlaybackHistoryTrack = ({int count, SpotubeTrackObject track}); +typedef PlaybackHistoryArtist = ({int count, SpotubeSimpleArtistObject artist}); class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier< - PlaybackHistoryTrack, HistoryTopTracksState, HistoryDuration> { + PlaybackHistoryTrack, HistoryDuration> { HistoryTopTracksNotifier() : super(); SimpleSelectStatement<$HistoryTableTable, HistoryTableData> @@ -97,35 +50,45 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier< List entries, ) async { final nonImageArtistTracks = - entries.where((e) => e.track!.artists!.any((a) => a.images == null)); + entries.where((e) => e.track!.artists.any((a) => a.images == null)); if (nonImageArtistTracks.isEmpty) return; final artistIds = nonImageArtistTracks - .map((e) => e.track!.artists!.map((a) => a.id!)) + .map((e) => e.track!.artists.map((a) => a.id)) .expand((e) => e) .toSet() .toList(); if (artistIds.isEmpty) return; - final artists = await ref.read(spotifyProvider).api.artists.list(artistIds); + final artists = await Future.wait([ + for (final id in artistIds) + ref.read(metadataPluginArtistProvider(id).future), + ]); final imagedArtistTracks = nonImageArtistTracks.map((e) { - final track = e.track!; - final includedArtists = track.artists! - .map((a) => artists.firstWhereOrNull((artist) => artist.id == a.id)) + var track = e.track!; + final includedArtists = track.artists + .map((a) { + final fullArtist = + artists.firstWhereOrNull((artist) => artist.id == a.id); + + return fullArtist != null + ? a.copyWith(images: fullArtist.images) + : a; + }) .nonNulls .toList(); - track.artists = includedArtists; + track = track.copyWith(artists: includedArtists); return e.copyWith(data: track.toJson()); }); assert( imagedArtistTracks - .every((e) => e.track!.artists!.every((a) => a.images != null)), + .every((e) => e.track!.artists.every((a) => a.images != null)), 'Tracks artists should have images', ); @@ -139,24 +102,24 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier< } @override - fetch(arg, offset, limit) async { + fetch(offset, limit) async { final tracksQuery = createTracksQuery()..limit(limit, offset: offset); final entries = await tracksQuery.get(); final items = getTracksWithCount(entries); - return ( + return SpotubePaginationResponseObject( items: items, - hasMore: items.length == limit, nextOffset: offset + limit, + total: items.length, + limit: limit, + hasMore: items.length == limit, ); } @override build(arg) async { - final (items: tracks, :hasMore, :nextOffset) = await fetch(arg, 0, 20); - final subscription = createTracksQuery().watch().listen((event) { if (state.asData == null) return; state = AsyncData(state.asData!.value.copyWith( @@ -169,20 +132,41 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier< subscription.cancel(); }); - return HistoryTopTracksState( - items: tracks, - offset: nextOffset, - limit: 20, - hasMore: hasMore, + return await fetch(0, 20); + } + + List get artists { + return getArtistsWithCount( + state.asData?.value.items.expand((e) => e.track.artists) ?? [], ); } + List getArtistsWithCount( + Iterable artists, + ) { + return groupBy(artists, (artist) => artist.id) + .entries + .map((entry) { + return ( + count: entry.value.length, + + /// Previously, due to a bug, artist images were not being saved. + /// Now it's fixed, but we need to handle the case where images are null. + /// So we take the first artist with images if available, otherwise the first one. + artist: entry.value.firstWhereOrNull((a) => a.images != null) ?? + entry.value.first, + ); + }) + .sorted((a, b) => b.count.compareTo(a.count)) + .toList(); + } + List getTracksWithCount(List tracks) { fixImageNotLoadingForArtistIssue(tracks); return groupBy( tracks, - (track) => track.track!.id!, + (track) => track.track!.id, ) .entries .map((entry) { @@ -194,7 +178,7 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier< /// So we take the first artist with images if available, otherwise the first one. track: entry.value .firstWhereOrNull( - (t) => t.track!.artists!.every((a) => a.images != null)) + (t) => t.track!.artists.every((a) => a.images != null)) ?.track! ?? entry.value.first.track!, ); @@ -205,6 +189,8 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier< } final historyTopTracksProvider = AsyncNotifierProviderFamily< - HistoryTopTracksNotifier, HistoryTopTracksState, HistoryDuration>( + HistoryTopTracksNotifier, + SpotubePaginationResponseObject, + HistoryDuration>( () => HistoryTopTracksNotifier(), ); diff --git a/lib/provider/local_tracks/local_tracks_provider.dart b/lib/provider/local_tracks/local_tracks_provider.dart index c69d3169..3a94815b 100644 --- a/lib/provider/local_tracks/local_tracks_provider.dart +++ b/lib/provider/local_tracks/local_tracks_provider.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -9,9 +10,6 @@ import 'package:mime/mime.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/extensions/track.dart'; -import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; // ignore: depend_on_referenced_packages import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FrbException; @@ -38,10 +36,10 @@ const imgMimeToExt = { }; final localTracksProvider = - FutureProvider>>((ref) async { + FutureProvider>>((ref) async { try { if (kIsWeb) return {}; - final Map> libraryToTracks = {}; + final Map> libraryToTracks = {}; final downloadLocation = ref.watch( userPreferencesProvider.select((s) => s.downloadLocation), @@ -121,14 +119,11 @@ final localTracksProvider = final tracksFromMetadata = filesWithMetadata .map( - (fileWithMetadata) => LocalTrack.fromTrack( - track: Track().fromFile( - fileWithMetadata["file"], - metadata: fileWithMetadata["metadata"], - art: fileWithMetadata["art"], - ), - path: fileWithMetadata["file"].path, - ), + (fileWithMetadata) => SpotubeTrackObject.localTrackFromFile( + fileWithMetadata["file"] as File, + metadata: fileWithMetadata["metadata"] as Metadata?, + art: fileWithMetadata["art"] as String?, + ) as SpotubeLocalTrackObject, ) .toList(); diff --git a/lib/provider/spotify/lyrics/synced.dart b/lib/provider/lyrics/synced.dart similarity index 54% rename from lib/provider/spotify/lyrics/synced.dart rename to lib/provider/lyrics/synced.dart index ff2a73f1..0c43d53e 100644 --- a/lib/provider/spotify/lyrics/synced.dart +++ b/lib/provider/lyrics/synced.dart @@ -1,54 +1,67 @@ -part of '../spotify.dart'; +import 'dart:async'; -class SyncedLyricsNotifier extends FamilyAsyncNotifier { - Track get _track => arg!; +import 'package:dio/dio.dart'; +import 'package:drift/drift.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lrc/lrc.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/models/lyrics.dart'; +import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/services/dio/dio.dart'; +import 'package:spotube/services/logger/logger.dart'; - Future getSpotifyLyrics(String? token) async { - final res = await globalDio.getUri( - Uri.parse( - "https://spclient.wg.spotify.com/color-lyrics/v2/track/${_track.id}?format=json&market=from_token", - ), - options: Options( - headers: { - "User-Agent": - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36", - "App-platform": "WebPlayer", - "authorization": "Bearer $token" - }, - responseType: ResponseType.json, - validateStatus: (status) => true, - ), - ); +class SyncedLyricsNotifier + extends FamilyAsyncNotifier { + SpotubeTrackObject get _track => arg!; - if (res.statusCode != 200) { - return SubtitleSimple( - lyrics: [], - name: _track.name!, - uri: res.realUri, - rating: 0, - provider: "Spotify", - ); - } - final linesRaw = - Map.castFrom(res.data)["lyrics"] - ?["lines"] as List?; + // Future getSpotifyLyrics(String? token) async { + // final res = await globalDio.getUri( + // Uri.parse( + // "https://spclient.wg.spotify.com/color-lyrics/v2/track/${_track.id}?format=json&market=from_token", + // ), + // options: Options( + // headers: { + // "User-Agent": + // "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36", + // "App-platform": "WebPlayer", + // "authorization": "Bearer $token" + // }, + // responseType: ResponseType.json, + // validateStatus: (status) => true, + // ), + // ); - final lines = linesRaw?.map((line) { - return LyricSlice( - time: Duration(milliseconds: int.parse(line["startTimeMs"])), - text: line["words"] as String, - ); - }).toList() ?? - []; + // if (res.statusCode != 200) { + // return SubtitleSimple( + // lyrics: [], + // name: _track.name!, + // uri: res.realUri, + // rating: 0, + // provider: "Spotify", + // ); + // } + // final linesRaw = + // Map.castFrom(res.data)["lyrics"] + // ?["lines"] as List?; - return SubtitleSimple( - lyrics: lines, - name: _track.name!, - uri: res.realUri, - rating: 100, - provider: "Spotify", - ); - } + // final lines = linesRaw?.map((line) { + // return LyricSlice( + // time: Duration(milliseconds: int.parse(line["startTimeMs"])), + // text: line["words"] as String, + // ); + // }).toList() ?? + // []; + + // return SubtitleSimple( + // lyrics: lines, + // name: _track.name!, + // uri: res.realUri, + // rating: 100, + // provider: "Spotify", + // ); + // } /// Lyrics credits: [lrclib.net](https://lrclib.net) and their contributors /// Thanks for their generous public API @@ -61,10 +74,10 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier { host: "lrclib.net", path: "/api/get", queryParameters: { - "artist_name": _track.artists?.first.name, + "artist_name": _track.artists.first.name, "track_name": _track.name, - "album_name": _track.album?.name, - "duration": _track.duration?.inSeconds.toString(), + "album_name": _track.album.name, + "duration": (_track.durationMs / 1000).toInt().toString(), }, ), options: Options( @@ -79,7 +92,7 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier { if (res.statusCode != 200) { return SubtitleSimple( lyrics: [], - name: _track.name!, + name: _track.name, uri: res.realUri, rating: 0, provider: "LRCLib", @@ -99,7 +112,7 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier { if (syncedLyrics?.isNotEmpty == true) { return SubtitleSimple( lyrics: syncedLyrics!, - name: _track.name!, + name: _track.name, uri: res.realUri, rating: 100, provider: "LRCLib", @@ -113,7 +126,7 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier { return SubtitleSimple( lyrics: plainLyrics, - name: _track.name!, + name: _track.name, uri: res.realUri, rating: 0, provider: "LRCLib", @@ -124,26 +137,18 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier { FutureOr build(track) async { try { final database = ref.watch(databaseProvider); - final spotify = ref.watch(spotifyProvider); - final auth = await ref.watch(authenticationProvider.future); if (track == null) { throw "No track currently"; } final cachedLyrics = await (database.select(database.lyricsTable) - ..where((tbl) => tbl.trackId.equals(track.id!))) + ..where((tbl) => tbl.trackId.equals(track.id))) .map((row) => row.data) .getSingleOrNull(); SubtitleSimple? lyrics = cachedLyrics; - final token = await spotify.invoke((api) => api.getCredentials()); - - if ((lyrics == null || lyrics.lyrics.isEmpty) && auth != null) { - lyrics = await getSpotifyLyrics(token.accessToken); - } - if (lyrics == null || lyrics.lyrics.isEmpty || lyrics.lyrics.length <= 5) { @@ -157,7 +162,7 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier { if (cachedLyrics == null || cachedLyrics.lyrics.isEmpty) { await database.into(database.lyricsTable).insert( LyricsTableCompanion.insert( - trackId: track.id!, + trackId: track.id, data: lyrics, ), mode: InsertMode.replace, @@ -174,13 +179,13 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier { final syncedLyricsDelayProvider = StateProvider((ref) => 0); -final syncedLyricsProvider = - AsyncNotifierProviderFamily( +final syncedLyricsProvider = AsyncNotifierProviderFamily( () => SyncedLyricsNotifier(), ); final syncedLyricsMapProvider = - FutureProvider.family((ref, Track? track) async { + FutureProvider.family((ref, SpotubeTrackObject? track) async { final syncedLyrics = await ref.watch(syncedLyricsProvider(track).future); final isStaticLyrics = diff --git a/lib/provider/metadata_plugin/album/releases.dart b/lib/provider/metadata_plugin/album/releases.dart new file mode 100644 index 00000000..0d557d0a --- /dev/null +++ b/lib/provider/metadata_plugin/album/releases.dart @@ -0,0 +1,29 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; +import 'package:spotube/provider/metadata_plugin/utils/paginated.dart'; + +class MetadataPluginAlbumReleasesNotifier + extends PaginatedAsyncNotifier { + @override + Future> fetch( + int offset, + int limit, + ) async { + return await (await metadataPlugin) + .album + .releases(limit: limit, offset: offset); + } + + @override + build() async { + ref.watch(metadataPluginProvider); + return await fetch(0, 20); + } +} + +final metadataPluginAlbumReleasesProvider = AsyncNotifierProvider< + MetadataPluginAlbumReleasesNotifier, + SpotubePaginationResponseObject>( + () => MetadataPluginAlbumReleasesNotifier(), +); diff --git a/lib/provider/metadata_plugin/artist/wikipedia.dart b/lib/provider/metadata_plugin/artist/wikipedia.dart new file mode 100644 index 00000000..81fcc77c --- /dev/null +++ b/lib/provider/metadata_plugin/artist/wikipedia.dart @@ -0,0 +1,18 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/services/wikipedia/wikipedia.dart'; +import 'package:wikipedia_api/wikipedia_api.dart'; + +final artistWikipediaSummaryProvider = + FutureProvider.autoDispose.family( + (ref, artist) async { + final query = artist.name.replaceAll(" ", "_"); + final res = await wikipedia.pageContent.pageSummaryTitleGet(query); + + if (res?.type != "standard") { + return await wikipedia.pageContent + .pageSummaryTitleGet("${query}_(singer)"); + } + return res; + }, +); diff --git a/lib/provider/metadata_plugin/library/playlists.dart b/lib/provider/metadata_plugin/library/playlists.dart index 1ebe2a63..40db7951 100644 --- a/lib/provider/metadata_plugin/library/playlists.dart +++ b/lib/provider/metadata_plugin/library/playlists.dart @@ -63,16 +63,16 @@ class MetadataPluginSavedPlaylistsNotifier ref.invalidate(metadataPluginIsSavedPlaylistProvider(playlist.id)); } - Future delete(SpotubeSimplePlaylistObject playlist) async { + Future delete(String playlistId) async { await update((state) async { - (await metadataPlugin).playlist.deletePlaylist(playlist.id); + (await metadataPlugin).playlist.deletePlaylist(playlistId); return state.copyWith( - items: state.items.where((e) => (e).id != playlist.id).toList(), + items: state.items.where((e) => (e).id != playlistId).toList(), ); }); - ref.invalidate(metadataPluginIsSavedPlaylistProvider(playlist.id)); - ref.invalidate(metadataPluginPlaylistTracksProvider(playlist.id)); + ref.invalidate(metadataPluginIsSavedPlaylistProvider(playlistId)); + ref.invalidate(metadataPluginPlaylistTracksProvider(playlistId)); } Future addTracks(String playlistId, List trackIds) async { diff --git a/lib/provider/metadata_plugin/playlist/playlist.dart b/lib/provider/metadata_plugin/playlist/playlist.dart index 9752cb21..8d5b71be 100644 --- a/lib/provider/metadata_plugin/playlist/playlist.dart +++ b/lib/provider/metadata_plugin/playlist/playlist.dart @@ -1,22 +1,133 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotube/models/metadata/metadata.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/user.dart'; import 'package:spotube/provider/metadata_plugin/utils/common.dart'; import 'package:spotube/services/metadata/endpoints/error.dart'; +import 'package:spotube/services/metadata/metadata.dart'; -final metadataPluginPlaylistProvider = - FutureProvider.autoDispose.family( - (ref, id) async { - ref.cacheFor(); - - final metadataPlugin = await ref.watch(metadataPluginProvider.future); +class MetadataPluginPlaylistNotifier + extends AutoDisposeFamilyAsyncNotifier { + Future get metadataPlugin async { + final metadataPlugin = await ref.read(metadataPluginProvider.future); if (metadataPlugin == null) { throw MetadataPluginException.noDefaultPlugin( - "No metadata plugin is not set", + "Metadata plugin is not set", ); } - return metadataPlugin.playlist.getPlaylist(id); - }, + return metadataPlugin; + } + + @override + build(playlistId) async { + ref.cacheFor(); + + return (await metadataPlugin).playlist.getPlaylist(playlistId); + } + + Future create({ + required String name, + String? description, + bool? public, + bool? collaborative, + void Function(dynamic error)? onError, + }) async { + final userId = await ref + .read(metadataPluginUserProvider.selectAsync((data) => data?.id)); + if (userId == null) { + throw Exception('User ID is not available. Please log in first.'); + } + await update( + (prev) async { + try { + final playlist = await (await metadataPlugin).playlist.create( + userId, + name: name, + description: description, + public: public, + collaborative: collaborative, + ); + return playlist!; + } catch (e) { + onError?.call(e); + rethrow; + } + }, + ); + } + + Future modify({ + String? name, + String? description, + bool? public, + bool? collaborative, + void Function(dynamic error)? onError, + }) async { + try { + if (name == null && + description == null && + public == null && + collaborative == null) { + throw Exception('No modifications provided.'); + } + await (await metadataPlugin).playlist.update( + arg, + name: name, + description: description, + public: public, + collaborative: collaborative, + ); + ref.invalidateSelf(); + } on Exception catch (e) { + onError?.call(e); + rethrow; + } + } + + Future addTracks(List trackIds, + [void Function(dynamic error)? onError]) async { + if (state.value == null) return; + + try { + await ref + .read(metadataPluginSavedPlaylistsProvider.notifier) + .addTracks(arg, trackIds); + } catch (e) { + onError?.call(e); + rethrow; + } + } + + Future removeTracks(List trackIds, + [void Function(dynamic error)? onError]) async { + try { + if (state.value == null) return; + + await ref + .read(metadataPluginSavedPlaylistsProvider.notifier) + .removeTracks(arg, trackIds); + } catch (e) { + onError?.call(e); + rethrow; + } + } + + Future delete() async { + if (state.value == null) return; + final userId = await ref + .read(metadataPluginUserProvider.selectAsync((data) => data?.id)); + if (userId == null || userId != state.value!.owner.id) { + throw Exception('You can only delete your own playlists.'); + } + + await ref.read(metadataPluginSavedPlaylistsProvider.notifier).delete(arg); + } +} + +final metadataPluginPlaylistProvider = AutoDisposeAsyncNotifierProviderFamily< + MetadataPluginPlaylistNotifier, SpotubeFullPlaylistObject, String>( + () => MetadataPluginPlaylistNotifier(), ); diff --git a/lib/provider/metadata_plugin/tracks/track.dart b/lib/provider/metadata_plugin/tracks/track.dart new file mode 100644 index 00000000..261e967d --- /dev/null +++ b/lib/provider/metadata_plugin/tracks/track.dart @@ -0,0 +1,16 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; +import 'package:spotube/services/metadata/endpoints/error.dart'; + +final metadataPluginTrackProvider = + FutureProvider.family((ref, trackId) async { + final metadataPlugin = await ref.watch(metadataPluginProvider.future); + + if (metadataPlugin == null) { + throw MetadataPluginException.noDefaultPlugin( + "No metadata plugin is set as default."); + } + + return metadataPlugin.track.getTrack(trackId); +}); diff --git a/lib/provider/metadata_plugin/utils/family_paginated.dart b/lib/provider/metadata_plugin/utils/family_paginated.dart index a994bdab..97717d04 100644 --- a/lib/provider/metadata_plugin/utils/family_paginated.dart +++ b/lib/provider/metadata_plugin/utils/family_paginated.dart @@ -25,8 +25,7 @@ abstract class FamilyPaginatedAsyncNotifier state.value!.items.isEmpty ? [] : state.value!.items.cast(); final items = newState.items.isEmpty ? [] : newState.items.cast(); - return newState.copyWith(items: [...oldItems, ...items]) - as SpotubePaginationResponseObject; + return newState.copyWith(items: [...oldItems, ...items]); }, ); } @@ -46,8 +45,7 @@ abstract class FamilyPaginatedAsyncNotifier hasMore = newState.hasMore; final oldItems = state.items.isEmpty ? [] : state.items.cast(); final items = newState.items.isEmpty ? [] : newState.items.cast(); - return newState.copyWith(items: [...oldItems, ...items]) - as SpotubePaginationResponseObject; + return newState.copyWith(items: [...oldItems, ...items]); }); } @@ -74,7 +72,7 @@ abstract class AutoDisposeFamilyPaginatedAsyncNotifier return newState.copyWith(items: [ ...state.value!.items.cast(), ...newState.items.cast(), - ]) as SpotubePaginationResponseObject; + ]); }, ); } @@ -95,7 +93,7 @@ abstract class AutoDisposeFamilyPaginatedAsyncNotifier return newState.copyWith(items: [ ...state.items.cast(), ...newState.items.cast(), - ]) as SpotubePaginationResponseObject; + ]); }); } diff --git a/lib/provider/server/track_sources.dart b/lib/provider/server/track_sources.dart index 2112c2af..4a0f29ca 100644 --- a/lib/provider/server/track_sources.dart +++ b/lib/provider/server/track_sources.dart @@ -23,10 +23,7 @@ class TrackSourcesNotifier }); } - Future copyWithSibling( - TrackSourceInfo info, - TrackSourceQuery query, - ) async { + Future copyWithSibling() async { return await update((prev) async { return prev.copyWithSibling(); }); diff --git a/lib/provider/spotify/album/favorite.dart b/lib/provider/spotify/album/favorite.dart deleted file mode 100644 index 157ab225..00000000 --- a/lib/provider/spotify/album/favorite.dart +++ /dev/null @@ -1,91 +0,0 @@ -part of '../spotify.dart'; - -class FavoriteAlbumState extends PaginatedState { - FavoriteAlbumState({ - required super.items, - required super.offset, - required super.limit, - required super.hasMore, - }); - - @override - FavoriteAlbumState copyWith({items, offset, limit, hasMore}) { - return FavoriteAlbumState( - items: items ?? this.items, - offset: offset ?? this.offset, - limit: limit ?? this.limit, - hasMore: hasMore ?? this.hasMore, - ); - } -} - -class FavoriteAlbumNotifier - extends PaginatedAsyncNotifier { - @override - Future> fetch(int offset, int limit) async { - return await spotify - .invoke( - (api) => api.me.savedAlbums().getPage(limit, offset), - ) - .then( - (value) => value.items?.toList() ?? [], - ); - } - - @override - build() async { - ref.watch(spotifyProvider); - final items = await fetch(0, 20); - return FavoriteAlbumState( - items: items, - offset: 0, - limit: 20, - hasMore: items.length == 20, - ); - } - - Future addFavorites(List ids) async { - if (state.value == null) return; - - state = await AsyncValue.guard(() async { - await spotify.invoke((api) => api.me.saveAlbums(ids)); - final albums = await spotify.invoke( - (api) => api.albums.list(ids), - ); - - return state.value!.copyWith( - items: [ - ...state.value!.items, - ...albums, - ], - ); - }); - - for (final id in ids) { - ref.invalidate(albumsIsSavedProvider(id)); - } - } - - Future removeFavorites(List ids) async { - if (state.value == null) return; - - state = await AsyncValue.guard(() async { - await spotify.invoke((api) => api.me.removeAlbums(ids)); - - return state.value!.copyWith( - items: state.value!.items - .where((element) => !ids.contains(element.id)) - .toList(), - ); - }); - - for (final id in ids) { - ref.invalidate(albumsIsSavedProvider(id)); - } - } -} - -final favoriteAlbumsProvider = - AsyncNotifierProvider( - () => FavoriteAlbumNotifier(), -); diff --git a/lib/provider/spotify/album/is_saved.dart b/lib/provider/spotify/album/is_saved.dart deleted file mode 100644 index aa48dfa0..00000000 --- a/lib/provider/spotify/album/is_saved.dart +++ /dev/null @@ -1,12 +0,0 @@ -part of '../spotify.dart'; - -final albumsIsSavedProvider = FutureProvider.autoDispose.family( - (ref, albumId) async { - final spotify = ref.watch(spotifyProvider); - return spotify.invoke( - (api) => api.me.containsSavedAlbums([albumId]).then( - (value) => value[albumId] ?? false, - ), - ); - }, -); diff --git a/lib/provider/spotify/album/releases.dart b/lib/provider/spotify/album/releases.dart deleted file mode 100644 index 25bb46b4..00000000 --- a/lib/provider/spotify/album/releases.dart +++ /dev/null @@ -1,87 +0,0 @@ -part of '../spotify.dart'; - -class AlbumReleasesState extends PaginatedState { - AlbumReleasesState({ - required super.items, - required super.offset, - required super.limit, - required super.hasMore, - }); - - @override - AlbumReleasesState copyWith({ - List? items, - int? offset, - int? limit, - bool? hasMore, - }) { - return AlbumReleasesState( - items: items ?? this.items, - offset: offset ?? this.offset, - limit: limit ?? this.limit, - hasMore: hasMore ?? this.hasMore, - ); - } -} - -class AlbumReleasesNotifier - extends PaginatedAsyncNotifier { - AlbumReleasesNotifier() : super(); - - @override - fetch(int offset, int limit) async { - final market = ref.read(userPreferencesProvider).market; - - final albums = await spotify.invoke( - (api) => api.browse.newReleases(country: market).getPage(limit, offset), - ); - - return albums.items?.map((album) => album.toAlbum()).toList() ?? []; - } - - @override - build() async { - ref.watch(spotifyProvider); - ref.watch( - userPreferencesProvider.select((s) => s.market), - ); - ref.watch(allFollowedArtistsProvider); - - final albums = await fetch(0, 20); - - return AlbumReleasesState( - items: albums, - offset: 0, - limit: 20, - hasMore: albums.length == 20, - ); - } -} - -final albumReleasesProvider = - AsyncNotifierProvider( - () => AlbumReleasesNotifier(), -); - -final userArtistAlbumReleasesProvider = Provider>((ref) { - final newReleases = ref.watch(albumReleasesProvider); - final userArtistsQuery = ref.watch(allFollowedArtistsProvider); - - if (newReleases.isLoading || userArtistsQuery.isLoading) { - return const []; - } - - final userArtists = - userArtistsQuery.asData?.value.map((s) => s.id!).toList() ?? const []; - - final allReleases = newReleases.asData?.value.items; - final userArtistReleases = allReleases?.where((album) { - return album.artists?.any((artist) => userArtists.contains(artist.id!)) == - true; - }).toList(); - - if (userArtistReleases?.isEmpty == true) { - return allReleases?.toList() ?? []; - } - return userArtistReleases ?? []; -}); diff --git a/lib/provider/spotify/album/tracks.dart b/lib/provider/spotify/album/tracks.dart deleted file mode 100644 index 13c48886..00000000 --- a/lib/provider/spotify/album/tracks.dart +++ /dev/null @@ -1,63 +0,0 @@ -part of '../spotify.dart'; - -class AlbumTracksState extends PaginatedState { - AlbumTracksState({ - required super.items, - required super.offset, - required super.limit, - required super.hasMore, - }); - - @override - AlbumTracksState copyWith({ - List? items, - int? offset, - int? limit, - bool? hasMore, - }) { - return AlbumTracksState( - items: items ?? this.items, - offset: offset ?? this.offset, - limit: limit ?? this.limit, - hasMore: hasMore ?? this.hasMore, - ); - } -} - -class AlbumTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier { - AlbumTracksNotifier() : super(); - - @override - fetch(arg, offset, limit) async { - final tracks = await spotify.invoke( - (api) => api.albums.tracks(arg.id!).getPage(limit, offset), - ); - final items = await tracks.items!.asTracks(arg, ref); - - return ( - items: items, - hasMore: !tracks.isLast, - nextOffset: tracks.nextOffset, - ); - } - - @override - build(arg) async { - ref.cacheFor(); - - ref.watch(spotifyProvider); - final (:items, :nextOffset, :hasMore) = await fetch(arg, 0, 20); - return AlbumTracksState( - items: items, - offset: nextOffset, - limit: 20, - hasMore: hasMore, - ); - } -} - -final albumTracksProvider = AutoDisposeAsyncNotifierProviderFamily< - AlbumTracksNotifier, AlbumTracksState, AlbumSimple>( - () => AlbumTracksNotifier(), -); diff --git a/lib/provider/spotify/artist/albums.dart b/lib/provider/spotify/artist/albums.dart deleted file mode 100644 index 7852738a..00000000 --- a/lib/provider/spotify/artist/albums.dart +++ /dev/null @@ -1,68 +0,0 @@ -part of '../spotify.dart'; - -class ArtistAlbumsState extends PaginatedState { - ArtistAlbumsState({ - required super.items, - required super.offset, - required super.limit, - required super.hasMore, - }); - - @override - ArtistAlbumsState copyWith({ - List? items, - int? offset, - int? limit, - bool? hasMore, - }) { - return ArtistAlbumsState( - items: items ?? this.items, - offset: offset ?? this.offset, - limit: limit ?? this.limit, - hasMore: hasMore ?? this.hasMore, - ); - } -} - -class ArtistAlbumsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< - Album, ArtistAlbumsState, String> { - ArtistAlbumsNotifier() : super(); - - @override - fetch(arg, offset, limit) async { - final market = ref.read(userPreferencesProvider).market; - final albums = await spotify.invoke( - (api) => api.artists.albums(arg, country: market).getPage(limit, offset), - ); - - final items = albums.items?.toList() ?? []; - - return ( - items: items, - hasMore: !albums.isLast, - nextOffset: albums.nextOffset, - ); - } - - @override - build(arg) async { - ref.cacheFor(); - - ref.watch(spotifyProvider); - ref.watch( - userPreferencesProvider.select((s) => s.market), - ); - final (:items, :hasMore, :nextOffset) = await fetch(arg, 0, 20); - return ArtistAlbumsState( - items: items, - offset: nextOffset, - limit: 20, - hasMore: hasMore, - ); - } -} - -final artistAlbumsProvider = AutoDisposeAsyncNotifierProviderFamily< - ArtistAlbumsNotifier, ArtistAlbumsState, String>( - () => ArtistAlbumsNotifier(), -); diff --git a/lib/provider/spotify/artist/artist.dart b/lib/provider/spotify/artist/artist.dart deleted file mode 100644 index dfee03e9..00000000 --- a/lib/provider/spotify/artist/artist.dart +++ /dev/null @@ -1,10 +0,0 @@ -part of '../spotify.dart'; - -final artistProvider = - FutureProvider.autoDispose.family((ref, String artistId) { - ref.cacheFor(); - - final spotify = ref.watch(spotifyProvider); - - return spotify.invoke((api) => api.artists.get(artistId)); -}); diff --git a/lib/provider/spotify/artist/following.dart b/lib/provider/spotify/artist/following.dart deleted file mode 100644 index 3a3795b7..00000000 --- a/lib/provider/spotify/artist/following.dart +++ /dev/null @@ -1,152 +0,0 @@ -part of '../spotify.dart'; - -class FollowedArtistsState extends CursorPaginatedState { - FollowedArtistsState({ - required super.items, - required super.offset, - required super.limit, - required super.hasMore, - }); - - @override - FollowedArtistsState copyWith({ - List? items, - String? offset, - int? limit, - bool? hasMore, - }) { - return FollowedArtistsState( - items: items ?? this.items, - offset: offset ?? this.offset, - limit: limit ?? this.limit, - hasMore: hasMore ?? this.hasMore, - ); - } -} - -class FollowedArtistsNotifier - extends CursorPaginatedAsyncNotifier { - final Dio dio; - FollowedArtistsNotifier() - : dio = Dio(), - super(); - - @override - fetch(offset, limit) async { - final artists = await spotify.invoke( - (api) => api.me.following(FollowingType.artist).getPage( - limit, - offset ?? '', - ), - ); - - return (artists.items?.toList() ?? [], artists.after); - } - - @override - build() async { - ref.watch(spotifyProvider); - final (artists, nextCursor) = await fetch(null, 50); - return FollowedArtistsState( - items: artists, - offset: nextCursor, - limit: 50, - hasMore: artists.length == 50, - ); - } - - Future _followArtists(List artistIds) async { - try { - final creds = await spotify.invoke( - (api) => api.getCredentials(), - ); - - await dio.post( - "https://api-partner.spotify.com/pathfinder/v1/query", - data: { - "variables": { - "uris": artistIds.map((id) => "spotify:artist:$id").toList() - }, - "operationName": "addToLibrary", - "extensions": { - "persistedQuery": { - "version": 1, - "sha256Hash": - "a3c1ff58e6a36fec5fe1e3a193dc95d9071d96b9ba53c5ba9c1494fb1ee73915" - } - } - }, - options: Options( - headers: { - "accept": "application/json", - 'app-platform': 'WebPlayer', - 'authorization': 'Bearer ${creds.accessToken}', - 'content-type': 'application/json;charset=UTF-8', - }, - responseType: ResponseType.json, - ), - ); - } on DioException catch (e, stack) { - AppLogger.reportError(e, stack); - } - } - - Future saveArtists(List artistIds) async { - if (state.value == null) return; - // await spotify.me.follow(FollowingType.artist, artistIds); - await _followArtists(artistIds); - - state = await AsyncValue.guard(() async { - final artists = await spotify.invoke( - (api) => api.artists.list(artistIds), - ); - - return state.value!.copyWith( - items: [ - ...state.value!.items, - ...artists, - ], - ); - }); - - for (final id in artistIds) { - ref.invalidate(artistIsFollowingProvider(id)); - } - } - - Future removeArtists(List artistIds) async { - if (state.value == null) return; - await spotify.invoke( - (api) => api.me.unfollow(FollowingType.artist, artistIds), - ); - - state = await AsyncValue.guard(() async { - final artists = state.value!.items.where((artist) { - return !artistIds.contains(artist.id); - }).toList(); - - return state.value!.copyWith( - items: artists, - ); - }); - - for (final id in artistIds) { - ref.invalidate(artistIsFollowingProvider(id)); - } - } -} - -final followedArtistsProvider = - AsyncNotifierProvider( - () => FollowedArtistsNotifier(), -); - -final allFollowedArtistsProvider = FutureProvider>( - (ref) async { - final spotify = ref.watch(spotifyProvider); - final artists = await spotify.invoke( - (api) => api.me.following(FollowingType.artist).all(), - ); - return artists.toList(); - }, -); diff --git a/lib/provider/spotify/artist/is_following.dart b/lib/provider/spotify/artist/is_following.dart deleted file mode 100644 index fb519518..00000000 --- a/lib/provider/spotify/artist/is_following.dart +++ /dev/null @@ -1,12 +0,0 @@ -part of '../spotify.dart'; - -final artistIsFollowingProvider = FutureProvider.family( - (ref, String artistId) async { - final spotify = ref.watch(spotifyProvider); - return spotify.invoke( - (api) => api.me.checkFollowing(FollowingType.artist, [artistId]).then( - (value) => value[artistId] ?? false, - ), - ); - }, -); diff --git a/lib/provider/spotify/artist/related.dart b/lib/provider/spotify/artist/related.dart deleted file mode 100644 index 7246fa11..00000000 --- a/lib/provider/spotify/artist/related.dart +++ /dev/null @@ -1,13 +0,0 @@ -part of '../spotify.dart'; - -final relatedArtistsProvider = FutureProvider.autoDispose - .family, String>((ref, artistId) async { - ref.cacheFor(); - - final spotify = ref.watch(spotifyProvider); - final artists = await spotify.invoke( - (api) => api.artists.relatedArtists(artistId), - ); - - return artists.toList(); -}); diff --git a/lib/provider/spotify/artist/top_tracks.dart b/lib/provider/spotify/artist/top_tracks.dart deleted file mode 100644 index 51321b21..00000000 --- a/lib/provider/spotify/artist/top_tracks.dart +++ /dev/null @@ -1,16 +0,0 @@ -part of '../spotify.dart'; - -final artistTopTracksProvider = - FutureProvider.autoDispose.family, String>( - (ref, artistId) async { - ref.cacheFor(); - - final spotify = ref.watch(spotifyProvider); - final market = ref.watch(userPreferencesProvider.select((s) => s.market)); - final tracks = await spotify.invoke( - (api) => api.artists.topTracks(artistId, market), - ); - - return tracks.toList(); - }, -); diff --git a/lib/provider/spotify/artist/wikipedia.dart b/lib/provider/spotify/artist/wikipedia.dart deleted file mode 100644 index 7f22d5f6..00000000 --- a/lib/provider/spotify/artist/wikipedia.dart +++ /dev/null @@ -1,12 +0,0 @@ -part of '../spotify.dart'; - -final artistWikipediaSummaryProvider = FutureProvider.autoDispose - .family((ref, artist) async { - final query = artist.name.replaceAll(" ", "_"); - final res = await wikipedia.pageContent.pageSummaryTitleGet(query); - - if (res?.type != "standard") { - return await wikipedia.pageContent.pageSummaryTitleGet("${query}_(singer)"); - } - return res; -}); diff --git a/lib/provider/spotify/category/categories.dart b/lib/provider/spotify/category/categories.dart deleted file mode 100644 index 67476f34..00000000 --- a/lib/provider/spotify/category/categories.dart +++ /dev/null @@ -1,21 +0,0 @@ -part of '../spotify.dart'; - -final categoriesProvider = FutureProvider( - (ref) async { - final spotify = ref.watch(spotifyProvider); - final market = ref.watch(userPreferencesProvider.select((s) => s.market)); - final locale = ref.watch(userPreferencesProvider.select((s) => s.locale)); - final categories = await spotify.invoke( - (api) => api.categories - .list( - country: market, - locale: Intl.canonicalizedLocale( - locale.toString(), - ), - ) - .all(), - ); - - return categories.toList()..shuffle(); - }, -); diff --git a/lib/provider/spotify/category/genres.dart b/lib/provider/spotify/category/genres.dart deleted file mode 100644 index b4b75b7b..00000000 --- a/lib/provider/spotify/category/genres.dart +++ /dev/null @@ -1,6 +0,0 @@ -part of '../spotify.dart'; - -final categoryGenresProvider = FutureProvider>((ref) async { - final customSpotify = ref.watch(customSpotifyEndpointProvider); - return await customSpotify.listGenreSeeds(); -}); diff --git a/lib/provider/spotify/category/playlists.dart b/lib/provider/spotify/category/playlists.dart deleted file mode 100644 index 2afd8d97..00000000 --- a/lib/provider/spotify/category/playlists.dart +++ /dev/null @@ -1,73 +0,0 @@ -part of '../spotify.dart'; - -class CategoryPlaylistsState extends PaginatedState { - CategoryPlaylistsState({ - required super.items, - required super.offset, - required super.limit, - required super.hasMore, - }); - - @override - CategoryPlaylistsState copyWith({ - List? items, - int? offset, - int? limit, - bool? hasMore, - }) { - return CategoryPlaylistsState( - items: items ?? this.items, - offset: offset ?? this.offset, - limit: limit ?? this.limit, - hasMore: hasMore ?? this.hasMore, - ); - } -} - -class CategoryPlaylistsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< - PlaylistSimple, CategoryPlaylistsState, String> { - CategoryPlaylistsNotifier() : super(); - - @override - fetch(arg, offset, limit) async { - final preferences = ref.read(userPreferencesProvider); - final playlists = await Pages( - spotify.api, - "v1/browse/categories/$arg/playlists?country=${preferences.market.name}&locale=${preferences.locale}", - (json) => json == null ? null : PlaylistSimple.fromJson(json), - 'playlists', - (json) => PlaylistsFeatured.fromJson(json), - ).getPage(limit, offset); - - final items = playlists.items?.nonNulls.toList() ?? []; - - return ( - items: items, - hasMore: !playlists.isLast, - nextOffset: playlists.nextOffset, - ); - } - - @override - build(arg) async { - ref.cacheFor(); - - ref.watch(spotifyProvider); - ref.watch(userPreferencesProvider.select((s) => s.locale)); - ref.watch(userPreferencesProvider.select((s) => s.market)); - - final (:items, :hasMore, :nextOffset) = await fetch(arg, 0, 8); - - return CategoryPlaylistsState( - items: items, - offset: nextOffset, - limit: 8, - hasMore: hasMore, - ); - } -} - -final categoryPlaylistsProvider = AutoDisposeAsyncNotifierProviderFamily< - CategoryPlaylistsNotifier, CategoryPlaylistsState, String>( - () => CategoryPlaylistsNotifier(), -); diff --git a/lib/provider/spotify/playlist/favorite.dart b/lib/provider/spotify/playlist/favorite.dart deleted file mode 100644 index 4df888ce..00000000 --- a/lib/provider/spotify/playlist/favorite.dart +++ /dev/null @@ -1,146 +0,0 @@ -part of '../spotify.dart'; - -class FavoritePlaylistsState extends PaginatedState { - FavoritePlaylistsState({ - required super.items, - required super.offset, - required super.limit, - required super.hasMore, - }); - - @override - FavoritePlaylistsState copyWith({ - List? items, - int? offset, - int? limit, - bool? hasMore, - }) { - return FavoritePlaylistsState( - items: items ?? this.items, - offset: offset ?? this.offset, - limit: limit ?? this.limit, - hasMore: hasMore ?? this.hasMore, - ); - } -} - -class FavoritePlaylistsNotifier - extends PaginatedAsyncNotifier { - FavoritePlaylistsNotifier() : super(); - - @override - fetch(int offset, int limit) async { - final playlists = await spotify.invoke( - (api) => api.playlists.me.getPage( - limit, - offset, - ), - ); - - return playlists.items?.toList() ?? []; - } - - @override - build() async { - ref.watch(spotifyProvider); - final playlists = await fetch(0, 20); - - return FavoritePlaylistsState( - items: playlists, - offset: 0, - limit: 20, - hasMore: playlists.length == 20, - ); - } - - void updatePlaylist(PlaylistSimple playlist) { - if (state.value == null) return; - - if (state.value!.items.none((e) => e.id == playlist.id)) return; - - state = AsyncData( - state.value!.copyWith( - items: state.value!.items - .map((element) => element.id == playlist.id ? playlist : element) - .toList(), - ), - ); - } - - Future addFavorite(PlaylistSimple playlist) async { - await update((state) async { - await spotify.invoke( - (api) => api.playlists.followPlaylist(playlist.id!), - ); - return state.copyWith( - items: [...state.items, playlist], - ); - }); - - ref.invalidate(isFavoritePlaylistProvider(playlist.id!)); - } - - Future removeFavorite(PlaylistSimple playlist) async { - await update((state) async { - await spotify.invoke( - (api) => api.playlists.unfollowPlaylist(playlist.id!), - ); - return state.copyWith( - items: state.items.where((e) => e.id != playlist.id).toList(), - ); - }); - - ref.invalidate(isFavoritePlaylistProvider(playlist.id!)); - } - - Future addTracks(String playlistId, List trackIds) async { - if (state.value == null) return; - - final spotify = ref.read(spotifyProvider); - - await spotify.invoke( - (api) => api.playlists.addTracks( - trackIds.map((id) => 'spotify:track:$id').toList(), - playlistId, - ), - ); - - ref.invalidate(playlistTracksProvider(playlistId)); - } - - Future removeTracks(String playlistId, List trackIds) async { - if (state.value == null) return; - - final spotify = ref.read(spotifyProvider); - - await spotify.invoke( - (api) => api.playlists.removeTracks( - trackIds.map((id) => 'spotify:track:$id').toList(), - playlistId, - ), - ); - - ref.invalidate(playlistTracksProvider(playlistId)); - } -} - -final favoritePlaylistsProvider = - AsyncNotifierProvider( - () => FavoritePlaylistsNotifier(), -); - -final isFavoritePlaylistProvider = FutureProvider.family( - (ref, id) async { - final spotify = ref.watch(spotifyProvider); - final me = ref.watch(meProvider); - - if (me.value == null) { - return false; - } - - final follows = await spotify - .invoke((api) => api.playlists.followedByUsers(id, [me.value!.id!])); - - return follows[me.value!.id!] ?? false; - }, -); diff --git a/lib/provider/spotify/playlist/featured.dart b/lib/provider/spotify/playlist/featured.dart deleted file mode 100644 index 9f751909..00000000 --- a/lib/provider/spotify/playlist/featured.dart +++ /dev/null @@ -1,57 +0,0 @@ -part of '../spotify.dart'; - -class FeaturedPlaylistsState extends PaginatedState { - FeaturedPlaylistsState({ - required super.items, - required super.offset, - required super.limit, - required super.hasMore, - }); - - @override - FeaturedPlaylistsState copyWith({ - List? items, - int? offset, - int? limit, - bool? hasMore, - }) { - return FeaturedPlaylistsState( - items: items ?? this.items, - offset: offset ?? this.offset, - limit: limit ?? this.limit, - hasMore: hasMore ?? this.hasMore, - ); - } -} - -class FeaturedPlaylistsNotifier - extends PaginatedAsyncNotifier { - FeaturedPlaylistsNotifier() : super(); - - @override - fetch(int offset, int limit) async { - final playlists = await spotify.invoke( - (api) => api.playlists.featured.getPage(limit, offset), - ); - - return playlists.items?.toList() ?? []; - } - - @override - build() async { - ref.watch(spotifyProvider); - final playlists = await fetch(0, 20); - - return FeaturedPlaylistsState( - items: playlists, - offset: 0, - limit: 20, - hasMore: playlists.length == 20, - ); - } -} - -final featuredPlaylistsProvider = - AsyncNotifierProvider( - () => FeaturedPlaylistsNotifier(), -); diff --git a/lib/provider/spotify/playlist/generate.dart b/lib/provider/spotify/playlist/generate.dart deleted file mode 100644 index b2250df6..00000000 --- a/lib/provider/spotify/playlist/generate.dart +++ /dev/null @@ -1,44 +0,0 @@ -part of '../spotify.dart'; - -final generatePlaylistProvider = FutureProvider.autoDispose - .family, GeneratePlaylistProviderInput>( - (ref, input) async { - final spotify = ref.watch(spotifyProvider); - final market = ref.watch( - userPreferencesProvider.select((s) => s.market), - ); - - final recommendation = await spotify.invoke( - (api) => api.recommendations - .get( - limit: input.limit, - seedArtists: input.seedArtists?.toList(), - seedGenres: input.seedGenres?.toList(), - seedTracks: input.seedTracks?.toList(), - market: market, - max: (input.max?.toJson()?..removeWhere((key, value) => value == null)) - ?.cast(), - min: (input.min?.toJson()?..removeWhere((key, value) => value == null)) - ?.cast(), - target: (input.target?.toJson() - ?..removeWhere((key, value) => value == null)) - ?.cast(), - ) - .catchError((e, stackTrace) { - AppLogger.reportError(e, stackTrace); - return Recommendations(); - }), - ); - - if (recommendation.tracks?.isEmpty ?? true) { - return []; - } - - final tracks = await spotify.invoke( - (api) => - api.tracks.list(recommendation.tracks!.map((e) => e.id!).toList()), - ); - - return tracks.toList(); - }, -); diff --git a/lib/provider/spotify/playlist/liked.dart b/lib/provider/spotify/playlist/liked.dart deleted file mode 100644 index 99c75719..00000000 --- a/lib/provider/spotify/playlist/liked.dart +++ /dev/null @@ -1,39 +0,0 @@ -part of '../spotify.dart'; - -class LikedTracksNotifier extends AsyncNotifier> { - @override - FutureOr> build() async { - final spotify = ref.watch(spotifyProvider); - final savedTracked = await spotify.invoke( - (api) => api.tracks.me.saved.all(), - ); - - return savedTracked.map((e) => e.track!).toList(); - } - - Future toggleFavorite(Track track) async { - if (state.value == null) return; - final spotify = ref.read(spotifyProvider); - - await update((tracks) async { - final isLiked = tracks.map((e) => e.id).contains(track.id); - - if (isLiked) { - await spotify.invoke( - (api) => api.tracks.me.removeOne(track.id!), - ); - return tracks.where((e) => e.id != track.id).toList(); - } else { - await spotify.invoke( - (api) => api.tracks.me.saveOne(track.id!), - ); - return [track, ...tracks]; - } - }); - } -} - -final likedTracksProvider = - AsyncNotifierProvider>( - () => LikedTracksNotifier(), -); diff --git a/lib/provider/spotify/playlist/playlist.dart b/lib/provider/spotify/playlist/playlist.dart deleted file mode 100644 index 34d1fe8e..00000000 --- a/lib/provider/spotify/playlist/playlist.dart +++ /dev/null @@ -1,173 +0,0 @@ -part of '../spotify.dart'; - -typedef PlaylistInput = ({ - String playlistName, - bool? public, - bool? collaborative, - String? description, - String? base64Image, -}); - -class PlaylistNotifier extends FamilyAsyncNotifier { - @override - FutureOr build(String arg) { - final spotify = ref.watch(spotifyProvider); - return spotify.invoke( - (api) => api.playlists.get(arg), - ); - } - - Future create(PlaylistInput input, [ValueChanged? onError]) async { - if (state is AsyncLoading) return; - state = const AsyncLoading(); - - final spotify = ref.read(spotifyProvider); - final me = ref.read(meProvider); - - if (me.value == null) return; - - state = await AsyncValue.guard(() async { - try { - final playlist = await spotify.invoke( - (api) => api.playlists.createPlaylist( - me.value!.id!, - input.playlistName, - collaborative: input.collaborative, - description: input.description, - public: input.public, - ), - ); - - if (input.base64Image != null) { - await spotify.invoke( - (api) => api.playlists.updatePlaylistImage( - playlist.id!, - input.base64Image!, - ), - ); - } - - return playlist; - } catch (e) { - onError?.call(e); - rethrow; - } - }); - - ref.invalidate(favoritePlaylistsProvider); - } - - Future modify(PlaylistInput input, [ValueChanged? onError]) async { - if (state.value == null) return; - - final spotify = ref.read(spotifyProvider); - - await update((state) async { - try { - await spotify.invoke( - (api) => api.playlists.updatePlaylist( - state.id!, - input.playlistName, - collaborative: input.collaborative, - description: input.description, - public: input.public, - ), - ); - - if (input.base64Image != null) { - await spotify.invoke( - (api) => api.playlists.updatePlaylistImage( - state.id!, - input.base64Image!, - ), - ); - - final playlist = await spotify.invoke( - (api) => api.playlists.get(state.id!), - ); - - ref.read(favoritePlaylistsProvider.notifier).updatePlaylist(playlist); - return playlist; - } - - final playlist = Playlist.fromJson( - { - ...state.toJson(), - 'name': input.playlistName, - 'collaborative': input.collaborative, - 'description': input.description, - 'public': input.public, - }, - ); - - ref.read(favoritePlaylistsProvider.notifier).updatePlaylist(playlist); - - return playlist; - } catch (e, stack) { - onError?.call(e); - AppLogger.reportError(e, stack); - rethrow; - } - }); - } - - Future addTracks(List trackIds, [ValueChanged? onError]) async { - try { - if (state.value == null) return; - - final spotify = ref.read(spotifyProvider); - - await spotify.invoke( - (api) => api.playlists.addTracks( - trackIds.map((id) => "spotify:track:$id").toList(), - state.value!.id!, - ), - ); - } catch (e, stack) { - onError?.call(e); - AppLogger.reportError(e, stack); - rethrow; - } - } -} - -final playlistProvider = - AsyncNotifierProvider.family( - () => PlaylistNotifier(), -); - -final _blendModes = BlendMode.values - .where((e) => switch (e) { - BlendMode.clear || - BlendMode.src || - BlendMode.srcATop || - BlendMode.srcIn || - BlendMode.srcOut || - BlendMode.srcOver || - BlendMode.dstOut || - BlendMode.xor => - false, - _ => true - }) - .toList(); - -typedef PlaylistImageInfo = ({ - Color color, - BlendMode colorBlendMode, - String src, - Alignment placement, -}); - -final playlistImageProvider = Provider.family( - (ref, playlistId) { - final random = Random(); - - return ( - color: Colors.primaries[random.nextInt(Colors.primaries.length)], - colorBlendMode: _blendModes[random.nextInt(_blendModes.length)], - src: Assets - .patterns.values[random.nextInt(Assets.patterns.values.length)].path, - placement: random.nextBool() ? Alignment.topLeft : Alignment.bottomLeft, - ); - }, -); diff --git a/lib/provider/spotify/playlist/tracks.dart b/lib/provider/spotify/playlist/tracks.dart deleted file mode 100644 index 1dbb83be..00000000 --- a/lib/provider/spotify/playlist/tracks.dart +++ /dev/null @@ -1,70 +0,0 @@ -part of '../spotify.dart'; - -class PlaylistTracksState extends PaginatedState { - PlaylistTracksState({ - required super.items, - required super.offset, - required super.limit, - required super.hasMore, - }); - - @override - PlaylistTracksState copyWith({ - List? items, - int? offset, - int? limit, - bool? hasMore, - }) { - return PlaylistTracksState( - items: items ?? this.items, - offset: offset ?? this.offset, - limit: limit ?? this.limit, - hasMore: hasMore ?? this.hasMore, - ); - } -} - -class PlaylistTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< - Track, PlaylistTracksState, String> { - PlaylistTracksNotifier() : super(); - - @override - fetch(arg, offset, limit) async { - final tracks = await spotify.invoke( - (api) => api.playlists.getTracksByPlaylistId(arg).getPage(limit, offset), - ); - - /// Filter out tracks with null id because some personal playlists - /// may contain local tracks that are not available in the Spotify catalog - final items = tracks.items - ?.where((track) => track.id != null && track.type == "track") - .toList() ?? - []; - - return ( - items: items, - hasMore: !tracks.isLast, - nextOffset: tracks.nextOffset, - ); - } - - @override - build(arg) async { - ref.cacheFor(); - - ref.watch(spotifyProvider); - final (items: tracks, :hasMore, :nextOffset) = await fetch(arg, 0, 20); - - return PlaylistTracksState( - items: tracks, - offset: nextOffset, - limit: 20, - hasMore: hasMore, - ); - } -} - -final playlistTracksProvider = AutoDisposeAsyncNotifierProviderFamily< - PlaylistTracksNotifier, PlaylistTracksState, String>( - () => PlaylistTracksNotifier(), -); diff --git a/lib/provider/spotify/search/search.dart b/lib/provider/spotify/search/search.dart deleted file mode 100644 index 828cc382..00000000 --- a/lib/provider/spotify/search/search.dart +++ /dev/null @@ -1,90 +0,0 @@ -part of '../spotify.dart'; - -final searchTermStateProvider = StateProvider.autoDispose( - (ref) { - ref.cacheFor(const Duration(minutes: 2)); - return ""; - }, -); - -class SearchState extends PaginatedState { - SearchState({ - required super.items, - required super.offset, - required super.limit, - required super.hasMore, - }); - - @override - SearchState copyWith({ - List? items, - int? offset, - int? limit, - bool? hasMore, - }) { - return SearchState( - items: items ?? this.items, - offset: offset ?? this.offset, - limit: limit ?? this.limit, - hasMore: hasMore ?? this.hasMore, - ); - } -} - -class SearchNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier, SearchType> { - SearchNotifier() : super(); - - @override - fetch(arg, offset, limit) async { - if (state.value == null) { - return ( - items: [], - hasMore: false, - nextOffset: 0, - ); - } - final results = await spotify.invoke( - (api) => api.search - .get( - ref.read(searchTermStateProvider), - types: [arg], - market: ref.read(userPreferencesProvider).market, - ) - .getPage(limit, offset), - ); - - final items = results.expand((e) => e.items ?? []).toList().cast(); - - return ( - items: items, - hasMore: items.length == limit, - nextOffset: offset + limit, - ); - } - - @override - build(arg) async { - ref.cacheFor(const Duration(minutes: 2)); - - ref.watch(searchTermStateProvider); - ref.watch(spotifyProvider); - ref.watch( - userPreferencesProvider.select((value) => value.market), - ); - - final (:items, :hasMore, :nextOffset) = await fetch(arg, 0, 10); - - return SearchState( - items: items, - offset: nextOffset, - limit: 10, - hasMore: hasMore, - ); - } -} - -final searchProvider = AsyncNotifierProvider.autoDispose - .family( - () => SearchNotifier(), -); diff --git a/lib/provider/spotify/spotify.dart b/lib/provider/spotify/spotify.dart deleted file mode 100644 index d954bb8d..00000000 --- a/lib/provider/spotify/spotify.dart +++ /dev/null @@ -1,134 +0,0 @@ -library spotify; - -import 'dart:async'; -import 'dart:math'; - -import 'package:drift/drift.dart'; -import 'package:spotube/collections/assets.gen.dart'; -import 'package:spotube/collections/env.dart'; -import 'package:spotube/models/database/database.dart'; -import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/provider/authentication/authentication.dart'; -import 'package:spotube/provider/database/database.dart'; -import 'package:spotube/services/logger/logger.dart'; -import 'package:collection/collection.dart'; -import 'package:dio/dio.dart'; -import 'package:shadcn_flutter/shadcn_flutter.dart'; -import 'package:intl/intl.dart'; -import 'package:lrc/lrc.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:spotify/spotify.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -// ignore: depend_on_referenced_packages, implementation_imports -import 'package:riverpod/src/async_notifier.dart'; -import 'package:spotube/extensions/album_simple.dart'; -import 'package:spotube/extensions/track.dart'; -import 'package:spotube/models/lyrics.dart'; -import 'package:spotube/models/spotify/recommendation_seeds.dart'; -import 'package:spotube/models/spotify_friends.dart'; -import 'package:spotube/provider/custom_spotify_endpoint_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/services/dio/dio.dart'; -import 'package:spotube/services/wikipedia/wikipedia.dart'; -import 'package:spotube/utils/primitive_utils.dart'; - -import 'package:wikipedia_api/wikipedia_api.dart'; - -part 'album/favorite.dart'; -part 'album/tracks.dart'; -part 'album/releases.dart'; -part 'album/is_saved.dart'; - -part 'artist/artist.dart'; -part 'artist/is_following.dart'; -part 'artist/following.dart'; -part 'artist/top_tracks.dart'; -part 'artist/albums.dart'; -part 'artist/wikipedia.dart'; -part 'artist/related.dart'; - -part 'category/genres.dart'; -part 'category/categories.dart'; -part 'category/playlists.dart'; - -part 'lyrics/synced.dart'; - -part 'playlist/favorite.dart'; -part 'playlist/playlist.dart'; -part 'playlist/liked.dart'; -part 'playlist/tracks.dart'; -part 'playlist/featured.dart'; -part 'playlist/generate.dart'; - -part 'search/search.dart'; - -part 'user/me.dart'; -part 'user/friends.dart'; - -part 'tracks/track.dart'; - -part 'views/view.dart'; - -part 'utils/mixin.dart'; -part 'utils/state.dart'; -part 'utils/provider.dart'; -part 'utils/async.dart'; - -part 'utils/provider/paginated.dart'; -part 'utils/provider/cursor.dart'; -part 'utils/provider/paginated_family.dart'; -part 'utils/provider/cursor_family.dart'; - -class SpotifyApiWrapper { - final SpotifyApi api; - - final Ref ref; - SpotifyApiWrapper( - this.ref, - this.api, - ); - - bool _isRefreshing = false; - - FutureOr invoke( - FutureOr Function(SpotifyApi api) fn, - ) async { - try { - return await fn(api); - } catch (e) { - if (((e is AuthorizationException && e.error == 'invalid_token') || - e is ExpirationException) && - !_isRefreshing) { - _isRefreshing = true; - await ref.read(authenticationProvider.notifier).refreshCredentials(); - - _isRefreshing = false; - return await fn(api); - } - rethrow; - } - } -} - -final spotifyProvider = Provider( - (ref) { - final authState = ref.watch(authenticationProvider); - final anonCred = PrimitiveUtils.getRandomElement(Env.spotifySecrets); - - final wrapper = SpotifyApiWrapper( - ref, - authState.asData?.value == null - ? SpotifyApi( - SpotifyApiCredentials( - anonCred["clientId"], - anonCred["clientSecret"], - ), - ) - : SpotifyApi.withAccessToken( - authState.asData!.value!.accessToken.value, - ), - ); - - return wrapper; - }, -); diff --git a/lib/provider/spotify/tracks/track.dart b/lib/provider/spotify/tracks/track.dart deleted file mode 100644 index 9863aa25..00000000 --- a/lib/provider/spotify/tracks/track.dart +++ /dev/null @@ -1,10 +0,0 @@ -part of '../spotify.dart'; - -final trackProvider = - FutureProvider.autoDispose.family((ref, id) async { - ref.cacheFor(); - - final spotify = ref.watch(spotifyProvider); - - return spotify.invoke((api) => api.tracks.get(id)); -}); diff --git a/lib/provider/spotify/user/friends.dart b/lib/provider/spotify/user/friends.dart deleted file mode 100644 index b9cc0f46..00000000 --- a/lib/provider/spotify/user/friends.dart +++ /dev/null @@ -1,7 +0,0 @@ -part of '../spotify.dart'; - -final friendsProvider = FutureProvider((ref) async { - final customSpotify = ref.watch(customSpotifyEndpointProvider); - - return customSpotify.getFriendActivity(); -}); diff --git a/lib/provider/spotify/user/me.dart b/lib/provider/spotify/user/me.dart deleted file mode 100644 index 09f5fc2d..00000000 --- a/lib/provider/spotify/user/me.dart +++ /dev/null @@ -1,6 +0,0 @@ -part of '../spotify.dart'; - -final meProvider = FutureProvider((ref) async { - final spotify = ref.watch(spotifyProvider); - return spotify.invoke((api) => api.me.get()); -}); diff --git a/lib/provider/spotify/utils/async.dart b/lib/provider/spotify/utils/async.dart deleted file mode 100644 index 1040d682..00000000 --- a/lib/provider/spotify/utils/async.dart +++ /dev/null @@ -1,5 +0,0 @@ -part of '../spotify.dart'; - -extension PaginationExtension on AsyncValue { - bool get isLoadingNextPage => this is AsyncData && this is AsyncLoadingNext; -} diff --git a/lib/provider/spotify/utils/json_cast.dart b/lib/provider/spotify/utils/json_cast.dart deleted file mode 100644 index 30700971..00000000 --- a/lib/provider/spotify/utils/json_cast.dart +++ /dev/null @@ -1,21 +0,0 @@ -Map castNestedJson(Map map) { - return Map.castFrom( - map.map((key, value) { - if (value is Map) { - return MapEntry( - key, - castNestedJson(value), - ); - } else if (value is Iterable) { - return MapEntry( - key, - value.map((e) { - if (e is Map) return castNestedJson(e); - return e; - }).toList(), - ); - } - return MapEntry(key, value); - }), - ); -} diff --git a/lib/provider/spotify/utils/mixin.dart b/lib/provider/spotify/utils/mixin.dart deleted file mode 100644 index 60788814..00000000 --- a/lib/provider/spotify/utils/mixin.dart +++ /dev/null @@ -1,24 +0,0 @@ -part of '../spotify.dart'; - -// ignore: invalid_use_of_internal_member -mixin SpotifyMixin on AsyncNotifierBase { - SpotifyApiWrapper get spotify => ref.read(spotifyProvider); -} - -extension on AutoDisposeAsyncNotifierProviderRef { - // When invoked keeps your provider alive for [duration] - void cacheFor([Duration duration = const Duration(minutes: 5)]) { - final link = keepAlive(); - final timer = Timer(duration, () => link.close()); - onDispose(() => timer.cancel()); - } -} - -extension on AutoDisposeRef { - // When invoked keeps your provider alive for [duration] - void cacheFor([Duration duration = const Duration(minutes: 5)]) { - final link = keepAlive(); - final timer = Timer(duration, () => link.close()); - onDispose(() => timer.cancel()); - } -} diff --git a/lib/provider/spotify/utils/provider.dart b/lib/provider/spotify/utils/provider.dart deleted file mode 100644 index 50458c3a..00000000 --- a/lib/provider/spotify/utils/provider.dart +++ /dev/null @@ -1,6 +0,0 @@ -part of '../spotify.dart'; - -// ignore: subtype_of_sealed_class -class AsyncLoadingNext extends AsyncData { - const AsyncLoadingNext(super.value); -} diff --git a/lib/provider/spotify/utils/provider/cursor.dart b/lib/provider/spotify/utils/provider/cursor.dart deleted file mode 100644 index c241827e..00000000 --- a/lib/provider/spotify/utils/provider/cursor.dart +++ /dev/null @@ -1,56 +0,0 @@ -part of '../../spotify.dart'; - -mixin CursorPaginatedAsyncNotifierMixin> - // ignore: invalid_use_of_internal_member - on AsyncNotifierBase { - Future<(List items, String nextCursor)> fetch(String? offset, int limit); - - Future fetchMore() async { - if (state.value == null || !state.value!.hasMore) return; - - state = AsyncLoadingNext(state.asData!.value); - - state = await AsyncValue.guard( - () async { - final items = await fetch(state.value!.offset, state.value!.limit); - return state.value!.copyWith( - hasMore: items.$1.length == state.value!.limit, - items: [ - ...state.value!.items, - ...items.$1, - ], - offset: items.$2, - ) as T; - }, - ); - } - - Future> fetchAll() async { - if (state.value == null) return []; - if (!state.value!.hasMore) return state.value!.items; - - bool hasMore = true; - while (hasMore) { - await update((state) async { - final items = await fetch(state.offset, state.limit); - - hasMore = items.$1.length == state.limit; - return state.copyWith( - items: [...state.items, ...items.$1], - offset: items.$2, - hasMore: hasMore, - ) as T; - }); - } - - return state.value!.items; - } -} - -abstract class CursorPaginatedAsyncNotifier> extends AsyncNotifier - with CursorPaginatedAsyncNotifierMixin, SpotifyMixin {} - -abstract class AutoDisposeCursorPaginatedAsyncNotifier> extends AutoDisposeAsyncNotifier - with CursorPaginatedAsyncNotifierMixin, SpotifyMixin {} diff --git a/lib/provider/spotify/utils/provider/cursor_family.dart b/lib/provider/spotify/utils/provider/cursor_family.dart deleted file mode 100644 index ea8577de..00000000 --- a/lib/provider/spotify/utils/provider/cursor_family.dart +++ /dev/null @@ -1,113 +0,0 @@ -part of '../../spotify.dart'; - -abstract class FamilyCursorPaginatedAsyncNotifier< - K, - T extends CursorPaginatedState, - A> extends FamilyAsyncNotifier with SpotifyMixin { - Future<(List items, String nextCursor)> fetch( - A arg, - String? offset, - int limit, - ); - - Future fetchMore() async { - if (state.value == null || !state.value!.hasMore) return; - - state = AsyncLoadingNext(state.asData!.value); - - state = await AsyncValue.guard( - () async { - final items = await fetch(arg, state.value!.offset, state.value!.limit); - return state.value!.copyWith( - hasMore: items.$1.length == state.value!.limit, - items: [ - ...state.value!.items, - ...items.$1, - ], - offset: items.$2, - ) as T; - }, - ); - } - - Future> fetchAll() async { - if (state.value == null) return []; - if (!state.value!.hasMore) return state.value!.items; - - bool hasMore = true; - while (hasMore) { - await update((state) async { - final items = await fetch( - arg, - state.offset, - state.limit, - ); - - hasMore = items.$1.length == state.limit; - return state.copyWith( - items: [...state.items, ...items.$1], - offset: items.$2, - hasMore: hasMore, - ) as T; - }); - } - - return state.value!.items; - } -} - -abstract class AutoDisposeFamilyCursorPaginatedAsyncNotifier< - K, - T extends CursorPaginatedState, - A> extends AutoDisposeFamilyAsyncNotifier with SpotifyMixin { - Future<(List items, String nextCursor)> fetch( - A arg, - String? offset, - int limit, - ); - - Future fetchMore() async { - if (state.value == null || !state.value!.hasMore) return; - - state = AsyncLoadingNext(state.asData!.value); - - state = await AsyncValue.guard( - () async { - final items = await fetch(arg, state.value!.offset, state.value!.limit); - return state.value!.copyWith( - hasMore: items.$1.length == state.value!.limit, - items: [ - ...state.value!.items, - ...items.$1, - ], - offset: items.$2, - ) as T; - }, - ); - } - - Future> fetchAll() async { - if (state.value == null) return []; - if (!state.value!.hasMore) return state.value!.items; - - bool hasMore = true; - while (hasMore) { - await update((state) async { - final items = await fetch( - arg, - state.offset, - state.limit, - ); - - hasMore = items.$1.length == state.limit; - return state.copyWith( - items: [...state.items, ...items.$1], - offset: items.$2, - hasMore: hasMore, - ) as T; - }); - } - - return state.value!.items; - } -} diff --git a/lib/provider/spotify/utils/provider/paginated.dart b/lib/provider/spotify/utils/provider/paginated.dart deleted file mode 100644 index 30b66e67..00000000 --- a/lib/provider/spotify/utils/provider/paginated.dart +++ /dev/null @@ -1,63 +0,0 @@ -part of '../../spotify.dart'; - -mixin PaginatedAsyncNotifierMixin> - // ignore: invalid_use_of_internal_member - on AsyncNotifierBase { - Future> fetch(int offset, int limit); - - Future fetchMore() async { - if (state.value == null || !state.value!.hasMore) return; - - state = AsyncLoadingNext(state.asData!.value); - - state = await AsyncValue.guard( - () async { - final items = await fetch( - state.value!.offset + state.value!.limit, - state.value!.limit, - ); - return state.value!.copyWith( - hasMore: items.length == state.value!.limit, - items: [ - ...state.value!.items, - ...items, - ], - offset: state.value!.offset + state.value!.limit, - ) as T; - }, - ); - } - - Future> fetchAll() async { - if (state.value == null) return []; - if (!state.value!.hasMore) return state.value!.items; - - bool hasMore = true; - while (hasMore) { - await update((state) async { - final items = await fetch( - state.offset + state.limit, - state.limit, - ); - - hasMore = items.length == state.limit; - return state.copyWith( - items: [...state.items, ...items], - offset: state.offset + state.limit, - hasMore: hasMore, - ) as T; - }); - } - - return state.value!.items; - } -} - -abstract class PaginatedAsyncNotifier> - extends AsyncNotifier - with PaginatedAsyncNotifierMixin, SpotifyMixin {} - -abstract class AutoDisposePaginatedAsyncNotifier> - extends AutoDisposeAsyncNotifier - with PaginatedAsyncNotifierMixin, SpotifyMixin {} diff --git a/lib/provider/spotify/utils/provider/paginated_family.dart b/lib/provider/spotify/utils/provider/paginated_family.dart deleted file mode 100644 index c08c8673..00000000 --- a/lib/provider/spotify/utils/provider/paginated_family.dart +++ /dev/null @@ -1,120 +0,0 @@ -part of '../../spotify.dart'; - -typedef PseudoPaginatedProps = ({ - List items, - int nextOffset, - bool hasMore, -}); - -abstract class FamilyPaginatedAsyncNotifier< - K, - T extends BasePaginatedState, - A> extends FamilyAsyncNotifier with SpotifyMixin { - Future> fetch(A arg, int offset, int limit); - - Future fetchMore() async { - if (state.value == null || !state.value!.hasMore) return; - - state = AsyncLoadingNext(state.asData!.value); - - state = await AsyncValue.guard( - () async { - final (:items, :hasMore, :nextOffset) = await fetch( - arg, - state.value!.offset, - state.value!.limit, - ); - return state.value!.copyWith( - hasMore: hasMore, - items: [ - ...state.value!.items, - ...items, - ], - offset: nextOffset, - ) as T; - }, - ); - } - - Future> fetchAll() async { - if (state.value == null) return []; - if (!state.value!.hasMore) return state.value!.items; - - bool hasMore = true; - while (hasMore) { - await update((state) async { - final res = await fetch( - arg, - state.offset, - state.limit, - ); - - hasMore = res.hasMore; - return state.copyWith( - items: [...state.items, ...res.items], - offset: res.nextOffset, - hasMore: hasMore, - ) as T; - }); - } - - return state.value!.items; - } -} - -abstract class AutoDisposeFamilyPaginatedAsyncNotifier< - K, - T extends BasePaginatedState, - A> extends AutoDisposeFamilyAsyncNotifier with SpotifyMixin { - Future> fetch(A arg, int offset, int limit); - - Future fetchMore() async { - if (state.value == null || !state.value!.hasMore) return; - - state = AsyncLoadingNext(state.asData!.value); - - state = await AsyncValue.guard( - () async { - final (:items, :hasMore, :nextOffset) = await fetch( - arg, - state.value!.offset, - state.value!.limit, - ); - - return state.value!.copyWith( - hasMore: hasMore, - items: [ - ...state.value!.items, - ...items, - ], - offset: nextOffset, - ) as T; - }, - ); - } - - Future> fetchAll() async { - if (state.value == null) return []; - if (!state.value!.hasMore) return state.value!.items; - - bool hasMore = true; - while (hasMore) { - await update((state) async { - final res = await fetch( - arg, - state.offset, - state.limit, - ); - - hasMore = res.hasMore; - return state.copyWith( - items: [...state.items, ...res.items], - offset: res.nextOffset, - hasMore: hasMore, - ) as T; - }); - } - - return state.value!.items; - } -} diff --git a/lib/provider/spotify/utils/state.dart b/lib/provider/spotify/utils/state.dart deleted file mode 100644 index 4b79ac7d..00000000 --- a/lib/provider/spotify/utils/state.dart +++ /dev/null @@ -1,56 +0,0 @@ -part of '../spotify.dart'; - -abstract class BasePaginatedState { - final List items; - final Cursor offset; - final int limit; - final bool hasMore; - - BasePaginatedState({ - required this.items, - required this.offset, - required this.limit, - required this.hasMore, - }); - - BasePaginatedState copyWith({ - List? items, - Cursor? offset, - int? limit, - bool? hasMore, - }); -} - -abstract class PaginatedState extends BasePaginatedState { - PaginatedState({ - required super.items, - required super.offset, - required super.limit, - required super.hasMore, - }); - - @override - PaginatedState copyWith({ - List? items, - int? offset, - int? limit, - bool? hasMore, - }); -} - -abstract class CursorPaginatedState extends BasePaginatedState { - CursorPaginatedState({ - required super.items, - required super.offset, - required super.limit, - required super.hasMore, - }); - - @override - CursorPaginatedState copyWith({ - List? items, - String? offset, - int? limit, - bool? hasMore, - }); -} diff --git a/lib/provider/spotify/views/home.dart b/lib/provider/spotify/views/home.dart deleted file mode 100644 index 87c049f9..00000000 --- a/lib/provider/spotify/views/home.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/provider/authentication/authentication.dart'; -import 'package:spotube/provider/custom_spotify_endpoint_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; - -final homeViewProvider = FutureProvider((ref) async { - final country = ref.watch( - userPreferencesProvider.select((s) => s.market), - ); - final spTCookie = ref.watch( - authenticationProvider.select((s) => s.asData?.value?.getCookie("sp_t")), - ); - - final spotify = ref.watch(customSpotifyEndpointProvider); - - return spotify.getHomeFeed( - country: country, - spTCookie: spTCookie, - ); -}); diff --git a/lib/provider/spotify/views/home_section.dart b/lib/provider/spotify/views/home_section.dart deleted file mode 100644 index 13f547e1..00000000 --- a/lib/provider/spotify/views/home_section.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/models/spotify/home_feed.dart'; -import 'package:spotube/provider/authentication/authentication.dart'; -import 'package:spotube/provider/custom_spotify_endpoint_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; - -final homeSectionViewProvider = - FutureProvider.family( - (ref, sectionUri) async { - final country = ref.watch( - userPreferencesProvider.select((s) => s.market), - ); - final spTCookie = ref.watch( - authenticationProvider.select((s) => s.asData?.value?.getCookie("sp_t")), - ); - - final spotify = ref.watch(customSpotifyEndpointProvider); - - return spotify.getHomeFeedSection( - sectionUri, - country: country, - spTCookie: spTCookie, - ); -}); diff --git a/lib/provider/spotify/views/view.dart b/lib/provider/spotify/views/view.dart deleted file mode 100644 index ff565feb..00000000 --- a/lib/provider/spotify/views/view.dart +++ /dev/null @@ -1,19 +0,0 @@ -part of '../spotify.dart'; - -final viewProvider = FutureProvider.family, String>( - (ref, viewName) async { - final customSpotify = ref.watch(customSpotifyEndpointProvider); - final market = ref.watch( - userPreferencesProvider.select((s) => s.market), - ); - final locale = ref.watch( - userPreferencesProvider.select((s) => s.locale), - ); - - return customSpotify.getView( - viewName, - market: market, - locale: Intl.canonicalizedLocale(locale.toString()), - ); - }, -); diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index 30ee8b3f..a5be97e2 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -4,8 +4,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart' as paths; import 'package:shadcn_flutter/shadcn_flutter.dart' hide join; -import 'package:spotify/spotify.dart'; import 'package:spotube/models/database/database.dart'; +import 'package:spotube/models/metadata/market.dart'; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index ead81967..4deaf720 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -5,7 +5,6 @@ import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:flutter/foundation.dart'; -import 'package:spotube/models/local_track.dart'; import 'package:spotube/services/audio_player/custom_player.dart'; import 'dart:async'; @@ -54,7 +53,7 @@ class SpotubeMedia extends mk.Media { return switch (track) { /// [super.uri] must be used instead of [track.path] to prevent wrong /// path format exceptions in Windows causing [extras] to be null - LocalTrack() => super.uri, + SpotubeLocalTrackObject() => super.uri, _ => "http://$_host:" "$serverPort/stream/${track.id}", }; diff --git a/lib/services/custom_spotify_endpoints/spotify_endpoints.dart b/lib/services/custom_spotify_endpoints/spotify_endpoints.dart deleted file mode 100644 index c05095b3..00000000 --- a/lib/services/custom_spotify_endpoints/spotify_endpoints.dart +++ /dev/null @@ -1,222 +0,0 @@ -import 'dart:convert'; - -import 'package:dio/dio.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/models/spotify/home_feed.dart'; -import 'package:spotube/models/spotify_friends.dart'; -import 'package:timezone/timezone.dart' as tz; - -class CustomSpotifyEndpoints { - static const _baseUrl = 'https://api.spotify.com/v1'; - final String accessToken; - final Dio _client; - - CustomSpotifyEndpoints(this.accessToken) - : _client = Dio( - BaseOptions( - baseUrl: _baseUrl, - responseType: ResponseType.json, - headers: { - "content-type": "application/json", - if (accessToken.isNotEmpty) - "authorization": "Bearer $accessToken", - "accept": "application/json", - }, - ), - ); - - // views API - - /// Get a single view of given genre - /// - /// Currently known genres are: - /// - new-releases-page - /// - made-for-x-hub (it requires authentication) - /// - my-mix-genres (it requires authentication) - /// - artist-seed-mixes (it requires authentication) - /// - my-mix-decades (it requires authentication) - /// - my-mix-moods (it requires authentication) - /// - podcasts-and-more (it requires authentication) - /// - uniquely-yours-in-hub (it requires authentication) - /// - made-for-x-dailymix (it requires authentication) - /// - made-for-x-discovery (it requires authentication) - Future> getView( - String view, { - int limit = 20, - int contentLimit = 10, - List types = const [ - "album", - "playlist", - "artist", - "show", - "station", - "episode", - "merch", - "artist_concerts", - "uri_link" - ], - String imageStyle = "gradient_overlay", - String includeExternal = "audio", - String? locale, - Market? market, - Market? country, - }) async { - if (accessToken.isEmpty) { - throw Exception('[CustomSpotifyEndpoints.getView]: accessToken is empty'); - } - - final queryParams = { - 'limit': limit.toString(), - 'content_limit': contentLimit.toString(), - 'types': types.join(','), - 'image_style': imageStyle, - 'include_external': includeExternal, - 'timestamp': DateTime.now().toUtc().toIso8601String(), - if (locale != null) 'locale': locale, - if (market != null) 'market': market.name, - if (country != null) 'country': country.name, - }.entries.map((e) => '${e.key}=${e.value}').join('&'); - - final res = await _client.getUri( - Uri.parse('$_baseUrl/views/$view?$queryParams'), - ); - - if (res.statusCode == 200) { - return res.data; - } else { - throw Exception( - '[CustomSpotifyEndpoints.getView]: Failed to get view' - '\nStatus code: ${res.statusCode}' - '\nBody: ${res.data}', - ); - } - } - - Future> listGenreSeeds() async { - final res = await _client.getUri( - Uri.parse("$_baseUrl/recommendations/available-genre-seeds"), - ); - - if (res.statusCode == 200) { - final body = res.data; - return List.from(body["genres"] ?? []); - } else { - throw Exception( - '[CustomSpotifyEndpoints.listGenreSeeds]: Failed to get genre seeds' - '\nStatus code: ${res.statusCode}' - '\nBody: ${res.data}', - ); - } - } - - Future getFriendActivity() async { - final res = await _client.getUri( - Uri.parse("https://guc-spclient.spotify.com/presence-view/v1/buddylist"), - ); - return SpotifyFriends.fromJson(res.data); - } - - Future getHomeFeed({ - required Market country, - String? spTCookie, - }) async { - final headers = { - 'app-platform': 'WebPlayer', - 'authorization': 'Bearer $accessToken', - 'content-type': 'application/json;charset=UTF-8', - 'dnt': '1', - 'origin': 'https://open.spotify.com', - 'referer': 'https://open.spotify.com/' - }; - final response = await _client.getUri( - Uri( - scheme: "https", - host: "api-partner.spotify.com", - path: "/pathfinder/v1/query", - queryParameters: { - "operationName": "home", - "variables": jsonEncode({ - "timeZone": tz.local.name, - "sp_t": spTCookie ?? "", - "country": country.name, - "facet": null, - "sectionItemsLimit": 10 - }), - "extensions": jsonEncode( - { - "persistedQuery": { - "version": 1, - - /// GraphQL persisted Query hash - /// This can change overtime. We've to lookout for it - /// Docs: https://www.apollographql.com/docs/graphos/operations/persisted-queries/ - "sha256Hash": - "eb3fba2d388cf4fc4d696b1757a58584e9538a3b515ea742e9cc9465807340be", - } - }, - ), - }, - ), - options: Options(headers: headers), - ); - - final data = SpotifyHomeFeed.fromJson( - transformHomeFeedJsonMap(response.data), - ); - - return data; - } - - Future getHomeFeedSection( - String sectionUri, { - String? spTCookie, - required Market country, - }) async { - final headers = { - 'app-platform': 'WebPlayer', - 'authorization': 'Bearer $accessToken', - 'content-type': 'application/json;charset=UTF-8', - 'dnt': '1', - 'origin': 'https://open.spotify.com', - 'referer': 'https://open.spotify.com/' - }; - final response = await _client.getUri( - Uri( - scheme: "https", - host: "api-partner.spotify.com", - path: "/pathfinder/v1/query", - queryParameters: { - "operationName": "homeSection", - "variables": jsonEncode({ - "timeZone": tz.local.name, - "sp_t": spTCookie ?? "", - "country": country.name, - "uri": sectionUri - }), - "extensions": jsonEncode( - { - "persistedQuery": { - "version": 1, - - /// GraphQL persisted Query hash - /// This can change overtime. We've to lookout for it - /// Docs: https://www.apollographql.com/docs/graphos/operations/persisted-queries/ - "sha256Hash": - "eb3fba2d388cf4fc4d696b1757a58584e9538a3b515ea742e9cc9465807340be", - } - }, - ), - }, - ), - options: Options(headers: headers), - ); - - final data = SpotifyHomeFeedSection.fromJson( - transformSectionItemJsonMap( - response.data["data"]["homeSections"]["sections"][0], - ), - ); - - return data; - } -} diff --git a/lib/services/metadata/endpoints/search.dart b/lib/services/metadata/endpoints/search.dart index 070628c2..be4e8e30 100644 --- a/lib/services/metadata/endpoints/search.dart +++ b/lib/services/metadata/endpoints/search.dart @@ -124,13 +124,13 @@ class MetadataPluginSearchEndpoint { ); } - Future> tracks( + Future> tracks( String query, { int? limit, int? offset, }) async { if (query.isEmpty) { - return SpotubePaginationResponseObject( + return SpotubePaginationResponseObject( items: [], total: 0, limit: limit ?? 20, @@ -148,9 +148,9 @@ class MetadataPluginSearchEndpoint { }..removeWhere((key, value) => value == null), ) as Map; - return SpotubePaginationResponseObject.fromJson( + return SpotubePaginationResponseObject.fromJson( raw.cast(), - (json) => SpotubeSimpleTrackObject.fromJson(json.cast()), + (json) => SpotubeFullTrackObject.fromJson(json.cast()), ); } } diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index ccff62b0..b862a83e 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -6,7 +6,7 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:html/dom.dart' hide Text; import 'package:shadcn_flutter/shadcn_flutter.dart' hide Element; -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/modules/root/update_dialog.dart'; @@ -279,46 +279,39 @@ abstract class ServiceUtils { return subtitle; } - static DateTime parseSpotifyAlbumDate(AlbumSimple? album) { - if (album == null || album.releaseDate == null) { + static DateTime parseSpotifyAlbumDate(SpotubeFullAlbumObject? album) { + if (album == null) { return DateTime.parse("1975-01-01"); } - switch (album.releaseDatePrecision ?? DatePrecision.year) { - case DatePrecision.day: - return DateTime.parse(album.releaseDate!); - case DatePrecision.month: - return DateTime.parse("${album.releaseDate}-01"); - case DatePrecision.year: - return DateTime.parse("${album.releaseDate}-01-01"); - } + return DateTime.parse(album.releaseDate); } - static List sortTracks(List tracks, SortBy sortBy) { + static List sortTracks( + List tracks, SortBy sortBy) { if (sortBy == SortBy.none) return tracks; return List.from(tracks) ..sort((a, b) { switch (sortBy) { case SortBy.ascending: - return a.name?.compareTo(b.name ?? "") ?? 0; + return a.name.compareTo(b.name); case SortBy.descending: - return b.name?.compareTo(a.name ?? "") ?? 0; - case SortBy.newest: - final aDate = parseSpotifyAlbumDate(a.album); - final bDate = parseSpotifyAlbumDate(b.album); - return bDate.compareTo(aDate); - case SortBy.oldest: - final aDate = parseSpotifyAlbumDate(a.album); - final bDate = parseSpotifyAlbumDate(b.album); - return aDate.compareTo(bDate); + return b.name.compareTo(a.name); + // TODO: We'll figure this one out later :') + // case SortBy.newest: + // final aDate = parseSpotifyAlbumDate(a.album); + // final bDate = parseSpotifyAlbumDate(b.album); + // return bDate.compareTo(aDate); + // case SortBy.oldest: + // final aDate = parseSpotifyAlbumDate(a.album); + // final bDate = parseSpotifyAlbumDate(b.album); + // return aDate.compareTo(bDate); case SortBy.duration: - return a.durationMs?.compareTo(b.durationMs ?? 0) ?? 0; + return a.durationMs.compareTo(b.durationMs); case SortBy.artist: - return a.artists?.first.name - ?.compareTo(b.artists?.first.name ?? "") ?? - 0; + return a.artists.first.name.compareTo(b.artists.first.name); case SortBy.album: - return a.album?.name?.compareTo(b.album?.name ?? "") ?? 0; + return a.album.name.compareTo(b.album.name); default: return 0; } diff --git a/pubspec.lock b/pubspec.lock index 8c5211f7..909b8217 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2288,14 +2288,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" - spotify: - dependency: "direct main" - description: - name: spotify - sha256: "705f09a457a893973451c15f4072670ac4783d67e42c35c080c55a48dee3a01f" - url: "https://pub.dev" - source: hosted - version: "0.13.7" sprintf: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d221c3d7..e8cc7ef5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -156,7 +156,6 @@ dependencies: url: https://github.com/KRTirtho/hetu_spotube_plugin.git ref: main get_it: ^8.0.3 - spotify: ^0.13.7 dev_dependencies: build_runner: ^2.4.13