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

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

View File

@ -1,19 +1,13 @@
import 'package:spotify/spotify.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/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<Track> 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,

View File

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

View File

@ -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<Track> tracks;
final List<SpotubeTrackObject> 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(<String, bool>{});
@ -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),
),
);
},

View File

@ -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<bool?>((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) {

View File

@ -1,32 +1,34 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package: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 ?? <Artist>[],
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,

View File

@ -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) {

View File

@ -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<ArtistSimple> artists;
final List<SpotubeSimpleArtistObject> 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,

View File

@ -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<void> actionDownloadTracks({
required BuildContext context,
required List<Track> tracks,
required List<SpotubeTrackObject> tracks,
required String action,
}) async {
final fullTrackObjects =
tracks.whereType<SpotubeFullTrackObject>().toList();
final confirmed = audioSource == AudioSource.piped ||
(await showDialog<bool>(
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;

View File

@ -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<void> Function() onRefresh;
final Future<List<Track>> Function() onFetchAll;
final Future<List<SpotubeFullTrackObject>> Function() onFetchAll;
const PaginationProps({
required this.hasNextPage,
@ -46,7 +46,7 @@ class TrackPresentationOptions {
final String? ownerImage;
final String image;
final String routePath;
final List<Track> tracks;
final List<SpotubeFullTrackObject> 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<TrackPresentationOptions>(context);

View File

@ -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<Track> selectedTracks;
final List<Track> presentationTracks;
final List<SpotubeTrackObject> selectedTracks;
final List<SpotubeTrackObject> presentationTracks;
final SortBy sortBy;
const PresentationState({
@ -18,8 +20,8 @@ class PresentationState {
});
PresentationState copyWith({
List<Track>? selectedTracks,
List<Track>? presentationTracks,
List<SpotubeTrackObject>? selectedTracks,
List<SpotubeTrackObject>? presentationTracks,
SortBy? sortBy,
}) {
return PresentationState(
@ -34,15 +36,15 @@ class PresentationStateNotifier
extends AutoDisposeFamilyNotifier<PresentationState, Object> {
@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<Track> get tracks {
List<SpotubeTrackObject> 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,
} ??
[];
<SpotubeFullTrackObject>[];
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)

View File

@ -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,22 +23,7 @@ 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(
final decorationImage = DecorationImage(
image: UniversalImage.imageProvider(options.image),
fit: BoxFit.cover,
);
@ -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(),
);
},
);

View File

@ -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();

View File

@ -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],
);

View File

@ -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<void> Function(Track track, int index) useTrackTilePlayCallback(
Future<void> Function(SpotubeTrackObject track, int index)
useTrackTilePlayCallback(
WidgetRef ref,
) {
final context = useContext();
@ -26,7 +27,8 @@ Future<void> 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<void> 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<void> 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]);
}
}
}

View File

@ -8,7 +8,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_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<ValueChanged<RelativeRect>?>? 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);
if (metadataPlugin == null) {
throw MetadataPluginException.noDefaultPlugin(
"No default metadata plugin set",
);
}
final radios = pages
.expand((e) => e.items?.cast<PlaylistSimple>().toList() ?? [])
.toList();
final pages = await metadataPlugin.search.playlists(query);
final artists = track.artists!.map((e) => e.name);
final artists = track.artists.map((e) => e.name);
final radio = radios.firstWhere(
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<String?>(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<TrackOptionValue>(
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),

View File

@ -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<bool?>? onChanged;
final Future<void> 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,

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package: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<SharedFile> 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<SharedFile> 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;
}
}
}
StreamSubscription? mediaStream;
if (kIsMobile) {
FlutterSharingIntent.instance.getInitialSharing().then(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;
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]);
// 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;
// if (kIsMobile) {
// FlutterSharingIntent.instance.getInitialSharing().then(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;
// 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]);
}

View File

@ -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';

View File

@ -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';

View File

@ -3,8 +3,8 @@ import 'package:drift/internal/versioned_schema.dart' as i0;
import 'package:drift/drift.dart' as i1;
import 'package:drift/drift.dart'; // 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.

View File

@ -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;
}

View File

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

View File

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

View File

@ -1,11 +1,12 @@
library metadata_objects;
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';

View File

@ -2571,8 +2571,7 @@ mixin _$SpotubeSearchResponseObject {
throw _privateConstructorUsedError;
List<SpotubeSimplePlaylistObject> get playlists =>
throw _privateConstructorUsedError;
List<SpotubeSimpleTrackObject> get tracks =>
throw _privateConstructorUsedError;
List<SpotubeFullTrackObject> get tracks => throw _privateConstructorUsedError;
/// Serializes this SpotubeSearchResponseObject to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@ -2596,7 +2595,7 @@ abstract class $SpotubeSearchResponseObjectCopyWith<$Res> {
{List<SpotubeSimpleAlbumObject> albums,
List<SpotubeFullArtistObject> artists,
List<SpotubeSimplePlaylistObject> playlists,
List<SpotubeSimpleTrackObject> tracks});
List<SpotubeFullTrackObject> tracks});
}
/// @nodoc
@ -2636,7 +2635,7 @@ class _$SpotubeSearchResponseObjectCopyWithImpl<$Res,
tracks: null == tracks
? _value.tracks
: tracks // ignore: cast_nullable_to_non_nullable
as List<SpotubeSimpleTrackObject>,
as List<SpotubeFullTrackObject>,
) as $Val);
}
}
@ -2654,7 +2653,7 @@ abstract class _$$SpotubeSearchResponseObjectImplCopyWith<$Res>
{List<SpotubeSimpleAlbumObject> albums,
List<SpotubeFullArtistObject> artists,
List<SpotubeSimplePlaylistObject> playlists,
List<SpotubeSimpleTrackObject> tracks});
List<SpotubeFullTrackObject> tracks});
}
/// @nodoc
@ -2693,7 +2692,7 @@ class __$$SpotubeSearchResponseObjectImplCopyWithImpl<$Res>
tracks: null == tracks
? _value._tracks
: tracks // ignore: cast_nullable_to_non_nullable
as List<SpotubeSimpleTrackObject>,
as List<SpotubeFullTrackObject>,
));
}
}
@ -2706,7 +2705,7 @@ class _$SpotubeSearchResponseObjectImpl
{required final List<SpotubeSimpleAlbumObject> albums,
required final List<SpotubeFullArtistObject> artists,
required final List<SpotubeSimplePlaylistObject> playlists,
required final List<SpotubeSimpleTrackObject> tracks})
required final List<SpotubeFullTrackObject> tracks})
: _albums = albums,
_artists = artists,
_playlists = playlists,
@ -2740,9 +2739,9 @@ class _$SpotubeSearchResponseObjectImpl
return EqualUnmodifiableListView(_playlists);
}
final List<SpotubeSimpleTrackObject> _tracks;
final List<SpotubeFullTrackObject> _tracks;
@override
List<SpotubeSimpleTrackObject> get tracks {
List<SpotubeFullTrackObject> 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<SpotubeSimpleAlbumObject> albums,
required final List<SpotubeFullArtistObject> artists,
required final List<SpotubeSimplePlaylistObject> playlists,
required final List<SpotubeSimpleTrackObject> tracks}) =
required final List<SpotubeFullTrackObject> tracks}) =
_$SpotubeSearchResponseObjectImpl;
factory _SpotubeSearchResponseObject.fromJson(Map<String, dynamic> json) =
@ -2810,7 +2809,7 @@ abstract class _SpotubeSearchResponseObject
@override
List<SpotubeSimplePlaylistObject> get playlists;
@override
List<SpotubeSimpleTrackObject> get tracks;
List<SpotubeFullTrackObject> 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<String, dynamic> 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<SpotubeSimpleArtistObject> get artists =>
throw _privateConstructorUsedError;
SpotubeSimpleAlbumObject? get album => throw _privateConstructorUsedError;
SpotubeSimpleAlbumObject get album => throw _privateConstructorUsedError;
int get durationMs => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult when<TResult extends Object?>({
@ -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<SpotubeSimpleArtistObject> 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<SpotubeSimpleArtistObject> 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<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject? album)?
simple,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
@ -2945,21 +2915,18 @@ mixin _$SpotubeTrackObject {
TResult map<TResult extends Object?>({
required TResult Function(SpotubeLocalTrackObject value) local,
required TResult Function(SpotubeFullTrackObject value) full,
required TResult Function(SpotubeSimpleTrackObject value) simple,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(SpotubeLocalTrackObject value)? local,
TResult? Function(SpotubeFullTrackObject value)? full,
TResult? Function(SpotubeSimpleTrackObject value)? simple,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
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<SpotubeSimpleArtistObject>,
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<SpotubeSimpleArtistObject> 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<SpotubeSimpleArtistObject> 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<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject? album)?
simple,
required TResult orElse(),
}) {
if (local != null) {
@ -3336,7 +3262,6 @@ class _$SpotubeLocalTrackObjectImpl implements SpotubeLocalTrackObject {
TResult map<TResult extends Object?>({
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 extends Object?>({
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 extends Object?>({
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<SpotubeSimpleArtistObject> 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<SpotubeSimpleArtistObject> 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<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject? album)?
simple,
required TResult orElse(),
}) {
if (full != null) {
@ -3709,7 +3595,6 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject {
TResult map<TResult extends Object?>({
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 extends Object?>({
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 extends Object?>({
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<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject? album});
@override
$SpotubeSimpleAlbumObjectCopyWith<$Res>? get album;
}
/// @nodoc
class __$$SpotubeSimpleTrackObjectImplCopyWithImpl<$Res>
extends _$SpotubeTrackObjectCopyWithImpl<$Res,
_$SpotubeSimpleTrackObjectImpl>
implements _$$SpotubeSimpleTrackObjectImplCopyWith<$Res> {
__$$SpotubeSimpleTrackObjectImplCopyWithImpl(
_$SpotubeSimpleTrackObjectImpl _value,
$Res Function(_$SpotubeSimpleTrackObjectImpl) _then)
: super(_value, _then);
/// Create a copy of SpotubeTrackObject
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? externalUri = null,
Object? durationMs = null,
Object? explicit = null,
Object? artists = null,
Object? album = freezed,
}) {
return _then(_$SpotubeSimpleTrackObjectImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
externalUri: null == externalUri
? _value.externalUri
: externalUri // ignore: cast_nullable_to_non_nullable
as String,
durationMs: null == durationMs
? _value.durationMs
: durationMs // ignore: cast_nullable_to_non_nullable
as int,
explicit: null == explicit
? _value.explicit
: explicit // ignore: cast_nullable_to_non_nullable
as bool,
artists: null == artists
? _value._artists
: artists // ignore: cast_nullable_to_non_nullable
as List<SpotubeSimpleArtistObject>,
album: freezed == album
? _value.album
: album // ignore: cast_nullable_to_non_nullable
as SpotubeSimpleAlbumObject?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$SpotubeSimpleTrackObjectImpl implements SpotubeSimpleTrackObject {
_$SpotubeSimpleTrackObjectImpl(
{required this.id,
required this.name,
required this.externalUri,
required this.durationMs,
required this.explicit,
final List<SpotubeSimpleArtistObject> artists = const [],
this.album,
final String? $type})
: _artists = artists,
$type = $type ?? 'simple';
factory _$SpotubeSimpleTrackObjectImpl.fromJson(Map<String, dynamic> json) =>
_$$SpotubeSimpleTrackObjectImplFromJson(json);
@override
final String id;
@override
final String name;
@override
final String externalUri;
@override
final int durationMs;
@override
final bool explicit;
final List<SpotubeSimpleArtistObject> _artists;
@override
@JsonKey()
List<SpotubeSimpleArtistObject> get artists {
if (_artists is EqualUnmodifiableListView) return _artists;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_artists);
}
@override
final SpotubeSimpleAlbumObject? album;
@JsonKey(name: 'runtimeType')
final String $type;
@override
String toString() {
return 'SpotubeTrackObject.simple(id: $id, name: $name, externalUri: $externalUri, durationMs: $durationMs, explicit: $explicit, artists: $artists, album: $album)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SpotubeSimpleTrackObjectImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.externalUri, externalUri) ||
other.externalUri == externalUri) &&
(identical(other.durationMs, durationMs) ||
other.durationMs == durationMs) &&
(identical(other.explicit, explicit) ||
other.explicit == explicit) &&
const DeepCollectionEquality().equals(other._artists, _artists) &&
(identical(other.album, album) || other.album == album));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
name,
externalUri,
durationMs,
explicit,
const DeepCollectionEquality().hash(_artists),
album);
/// Create a copy of SpotubeTrackObject
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$SpotubeSimpleTrackObjectImplCopyWith<_$SpotubeSimpleTrackObjectImpl>
get copyWith => __$$SpotubeSimpleTrackObjectImplCopyWithImpl<
_$SpotubeSimpleTrackObjectImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(
String id,
String name,
String externalUri,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject album,
int durationMs,
String path)
local,
required TResult Function(
String id,
String name,
String externalUri,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject album,
int durationMs,
String isrc,
bool explicit)
full,
required TResult Function(
String id,
String name,
String externalUri,
int durationMs,
bool explicit,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject? album)
simple,
}) {
return simple(id, name, externalUri, durationMs, explicit, artists, album);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(
String id,
String name,
String externalUri,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject album,
int durationMs,
String path)?
local,
TResult? Function(
String id,
String name,
String externalUri,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject album,
int durationMs,
String isrc,
bool explicit)?
full,
TResult? Function(
String id,
String name,
String externalUri,
int durationMs,
bool explicit,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject? album)?
simple,
}) {
return simple?.call(
id, name, externalUri, durationMs, explicit, artists, album);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(
String id,
String name,
String externalUri,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject album,
int durationMs,
String path)?
local,
TResult Function(
String id,
String name,
String externalUri,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject album,
int durationMs,
String isrc,
bool explicit)?
full,
TResult Function(
String id,
String name,
String externalUri,
int durationMs,
bool explicit,
List<SpotubeSimpleArtistObject> artists,
SpotubeSimpleAlbumObject? album)?
simple,
required TResult orElse(),
}) {
if (simple != null) {
return simple(
id, name, externalUri, durationMs, explicit, artists, album);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(SpotubeLocalTrackObject value) local,
required TResult Function(SpotubeFullTrackObject value) full,
required TResult Function(SpotubeSimpleTrackObject value) simple,
}) {
return simple(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(SpotubeLocalTrackObject value)? local,
TResult? Function(SpotubeFullTrackObject value)? full,
TResult? Function(SpotubeSimpleTrackObject value)? simple,
}) {
return simple?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(SpotubeLocalTrackObject value)? local,
TResult Function(SpotubeFullTrackObject value)? full,
TResult Function(SpotubeSimpleTrackObject value)? simple,
required TResult orElse(),
}) {
if (simple != null) {
return simple(this);
}
return orElse();
}
@override
Map<String, dynamic> toJson() {
return _$$SpotubeSimpleTrackObjectImplToJson(
this,
);
}
}
abstract class SpotubeSimpleTrackObject implements SpotubeTrackObject {
factory SpotubeSimpleTrackObject(
{required final String id,
required final String name,
required final String externalUri,
required final int durationMs,
required final bool explicit,
final List<SpotubeSimpleArtistObject> artists,
final SpotubeSimpleAlbumObject? album}) = _$SpotubeSimpleTrackObjectImpl;
factory SpotubeSimpleTrackObject.fromJson(Map<String, dynamic> json) =
_$SpotubeSimpleTrackObjectImpl.fromJson;
@override
String get id;
@override
String get name;
@override
String get externalUri;
@override
int get durationMs;
bool get explicit;
@override
List<SpotubeSimpleArtistObject> get artists;
@override
SpotubeSimpleAlbumObject? get album;
/// Create a copy of SpotubeTrackObject
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SpotubeSimpleTrackObjectImplCopyWith<_$SpotubeSimpleTrackObjectImpl>
get copyWith => throw _privateConstructorUsedError;
}
SpotubeUserObject _$SpotubeUserObjectFromJson(Map<String, dynamic> json) {
return _SpotubeUserObject.fromJson(json);
}

View File

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

View File

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

View File

@ -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<SpotubeSimpleArtistObject> 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<String, dynamic> json) =>
_$SpotubeTrackObjectFromJson(
json.containsKey("isrc")
? {...json, "runtimeType": "full"}
: json.containsKey("path")
json.containsKey("path")
? {...json, "runtimeType": "local"}
: {...json, "runtimeType": "simple"},
: {...json, "runtimeType": "full"},
);
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,14 +2,11 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package: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<List<Track>> fetchAllTrack() async {
// return ref.read(metadataPluginAlbumTracksProvider(album).notifier).fetchAll();
return [];
Future<List<SpotubeFullTrackObject>> 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));
},
),
),

View File

@ -4,7 +4,7 @@ import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_pl
import 'package:spotube/extensions/context.dart';
import 'package:spotube/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;

View File

@ -3,44 +3,45 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_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<PlaylistSimple>(
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<PlaylistSimple>(
// items: featuredPlaylists.asData?.value.items ?? [],
// title: Text(context.l10n.featured),
// isLoadingNextPage: featuredPlaylists.isLoadingNextPage,
// hasNextPage: featuredPlaylists.asData?.value.hasMore ?? false,
// onFetchMore: featuredPlaylistsNotifier.fetchMore,
// ),
// );
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,11 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package: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<Album>(
items: albums,
return HorizontalPlaybuttonCardView<SpotubeSimpleAlbumObject>(
items: newReleases.asData?.value.items ?? [],
title: Text(context.l10n.new_releases),
isLoadingNextPage: newReleases.isLoadingNextPage,
hasNextPage: newReleases.asData?.value.hasMore ?? false,

View File

@ -11,8 +11,8 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,20 +2,19 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package: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 ?? <Artist>[],
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);
}),
},
);

View File

@ -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,26 +121,29 @@ 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),
),
if (!isLocalTrack)
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.details),
).call,
child: IconButton.ghost(
icon: const Icon(SpotubeIcons.info, size: 18),
onPressed: currentTrack == null
onPressed: currentActiveTrackSource == null
? null
: () {
showDialog(
context: context,
builder: (context) {
return TrackDetailsDialog(
track: currentTrack,
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,
),
),
),
],

View File

@ -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,
),
),

View File

@ -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<void> Function(Track track) onJump;
final Future<void> Function(SpotubeTrackObject track) onJump;
final Future<void> Function(String trackId) onRemove;
final Future<void> Function(int oldIndex, int newIndex) onReorder;
final Future<void> 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,
);
},

View File

@ -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)),
)
],
),

View File

@ -10,30 +10,27 @@ import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/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 <SourceInfo>[];
return <TrackSourceInfo>[];
}
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,
]
: <SourceInfo>[],
[activeTrack, isFetchingActiveTrack],
: <TrackSourceInfo>[],
[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;

View File

@ -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<List<Track>> fetchInitialTracks() async {
Future<List<SpotubeTrackObject>> 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<List<Track>> fetchAllTracks() async {
Future<List<SpotubeTrackObject>> 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<SpotubeFullTrackObject>,
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,

View File

@ -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,8 +56,38 @@ 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) {
// 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,
@ -64,7 +95,7 @@ class PlaylistCreateDialog extends HookConsumerWidget {
return SurfaceCard(
child: Basic(
title: Text(
l10n.error(error.message ?? l10n.epic_failure),
l10n.error(l10n.epic_failure),
style: theme.typography.normal.copyWith(
color: theme.colorScheme.destructive,
),
@ -73,7 +104,6 @@ class PlaylistCreateDialog extends HookConsumerWidget {
);
},
);
}
}, [l10n, theme]);
Future<void> 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<Playlist>(
await ref.read(playlistProvider(playlistId ?? "").future),
!ref
.read(metadataPluginPlaylistProvider(playlistId ?? ""))
.hasError) {
context.router.maybePop<SpotubeFullPlaylistObject>(
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),
);
}
}

View File

@ -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),

View File

@ -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,

View File

@ -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));
},
);
}

View File

@ -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));
},
);
}

View File

@ -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,
),

View File

@ -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));
},
);
}

View File

@ -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 {

View File

@ -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,

View File

@ -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 {

View File

@ -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 {

View File

@ -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) {

View File

@ -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 {

View File

@ -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,

View File

@ -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()),
// ),
// };
}
}

View File

@ -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 ?? <Track>[],
topTracksQuery.asData?.value.items ?? <SpotubeTrackObject>[],
);
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<Track> tracks, {Track? currentTrack}) async {
void playPlaylist(List<SpotubeFullTrackObject> 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);
}

View File

@ -8,6 +8,7 @@ import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/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),
),
),
),

View File

@ -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<Market>(
child: Select(
value: preferences.market,
onChanged: (value) {
if (value == null) return;

View File

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

View File

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

View File

@ -13,9 +13,6 @@ import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/connect/connect_device.dart';
import 'package:spotube/modules/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) {
SliverList.builder(
itemCount: 3,
itemBuilder: (context, index) {
return switch (index) {
// 0 => const HomeGenresSection(),
// 1 => const HomeRecentlyPlayedSection(),
// 2 => const HomeFeaturedSection(),
0 => const HomeRecentlyPlayedSection(),
1 => const HomeFeaturedSection(),
// 3 => const HomePageFriendsSection(),
// _ => const HomeNewReleasesSection()
// };
// },
// ),
_ => const HomeNewReleasesSection()
};
},
),
const HomePageBrowseSection(),
],
),

View File

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

View File

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

View File

@ -17,7 +17,6 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/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,

View File

@ -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,

View File

@ -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));
},
),
),

View File

@ -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<void> playLocalTracks(
WidgetRef ref,
List<LocalTrack> tracks, {
LocalTrack? currentTrack,
List<SpotubeLocalTrackObject> 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] ?? <LocalTrack>[],
tracks[location] ??
<SpotubeLocalTrackObject>[],
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,

View File

@ -6,13 +6,13 @@ import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/components/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);

View File

@ -5,14 +5,14 @@ import 'package:palette_generator/palette_generator.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_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,

View File

@ -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,
),

View File

@ -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);

View File

@ -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 ?? <Track>[];
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,

View File

@ -9,8 +9,9 @@ import 'package:spotube/components/track_presentation/use_is_user_playlist.dart'
import 'package:spotube/extensions/context.dart';
import 'package:spotube/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<SpotubeSimplePlaylistObject>)
.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();

View File

@ -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),
],
),

View File

@ -19,10 +19,13 @@ import 'package:spotube/pages/search/sections/artists.dart';
import 'package:spotube/pages/search/sections/playlists.dart';
import 'package:spotube/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<String>((ref) {
return "";
});
@RoutePage()
class SearchPage extends HookConsumerWidget {
static const name = "search";

View File

@ -3,8 +3,8 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/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({

View File

@ -3,8 +3,8 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/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({

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