mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
refactor: remove old spotify.dart types and custom spotube metadata types
This commit is contained in:
parent
4e6db8b9e1
commit
5f47dc3d6d
@ -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,
|
||||
|
@ -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)"),
|
||||
|
@ -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),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -3,8 +3,6 @@ import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/env.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/heart_button/heart_button.dart';
|
||||
import 'package:spotube/components/image/universal_image.dart';
|
||||
@ -14,7 +12,6 @@ import 'package:spotube/components/track_presentation/use_is_user_playlist.dart'
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
|
||||
class TrackPresentationTopSection extends HookConsumerWidget {
|
||||
const TrackPresentationTopSection({super.key});
|
||||
@ -26,25 +23,10 @@ class TrackPresentationTopSection extends HookConsumerWidget {
|
||||
final scale = context.theme.scaling;
|
||||
final isUserPlaylist = useIsUserPlaylist(ref, options.collectionId);
|
||||
|
||||
final playlistImage = (options.collection is PlaylistSimple &&
|
||||
(options.collection as PlaylistSimple).owner?.displayName ==
|
||||
"Spotify" &&
|
||||
Env.disableSpotifyImages)
|
||||
? ref.watch(playlistImageProvider(options.collectionId))
|
||||
: null;
|
||||
final decorationImage = playlistImage != null
|
||||
? DecorationImage(
|
||||
image: AssetImage(playlistImage.src),
|
||||
fit: BoxFit.cover,
|
||||
colorFilter: ColorFilter.mode(
|
||||
playlistImage.color,
|
||||
playlistImage.colorBlendMode,
|
||||
),
|
||||
)
|
||||
: DecorationImage(
|
||||
image: UniversalImage.imageProvider(options.image),
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
final decorationImage = DecorationImage(
|
||||
image: UniversalImage.imageProvider(options.image),
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
|
||||
final imageDimension = mediaQuery.mdAndUp ? 200 : 120;
|
||||
|
||||
@ -116,7 +98,7 @@ class TrackPresentationTopSection extends HookConsumerWidget {
|
||||
builder: (context) {
|
||||
return PlaylistCreateDialog(
|
||||
playlistId: options.collectionId,
|
||||
trackIds: options.tracks.map((e) => e.id!).toList(),
|
||||
trackIds: options.tracks.map((e) => e.id).toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -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();
|
||||
|
@ -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],
|
||||
);
|
||||
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
final radios = pages
|
||||
.expand((e) => e.items?.cast<PlaylistSimple>().toList() ?? [])
|
||||
.toList();
|
||||
if (metadataPlugin == null) {
|
||||
throw MetadataPluginException.noDefaultPlugin(
|
||||
"No default metadata plugin set",
|
||||
);
|
||||
}
|
||||
|
||||
final artists = track.artists!.map((e) => e.name);
|
||||
final pages = await metadataPlugin.search.playlists(query);
|
||||
|
||||
final radio = radios.firstWhere(
|
||||
final artists = track.artists.map((e) => e.name);
|
||||
|
||||
final radio = pages.items.firstWhere(
|
||||
(e) {
|
||||
final validPlaylists =
|
||||
artists.where((a) => e.description!.contains(a!));
|
||||
final validPlaylists = artists.where((a) => e.description.contains(a));
|
||||
return e.name == "${track.name} Radio" &&
|
||||
(validPlaylists.length >= 2 ||
|
||||
validPlaylists.length == artists.length) &&
|
||||
e.owner?.displayName == "Spotify";
|
||||
e.owner.name == "Spotify";
|
||||
},
|
||||
orElse: () => radios.first,
|
||||
orElse: () => pages.items.first,
|
||||
);
|
||||
|
||||
bool replaceQueue = false;
|
||||
@ -154,10 +160,10 @@ class TrackOptions extends HookConsumerWidget {
|
||||
} else {
|
||||
await playback.addTrack(track);
|
||||
}
|
||||
|
||||
final tracks = await spotify.invoke(
|
||||
(api) => api.playlists.getTracksByPlaylistId(radio.id!).all(),
|
||||
);
|
||||
await ref.read(metadataPluginPlaylistTracksProvider(radio.id).future);
|
||||
final tracks = await ref
|
||||
.read(metadataPluginPlaylistTracksProvider(radio.id).notifier)
|
||||
.fetchAll();
|
||||
|
||||
await playback.addTracks(
|
||||
tracks.toList()
|
||||
@ -179,7 +185,7 @@ class TrackOptions extends HookConsumerWidget {
|
||||
ref.watch(downloadManagerProvider);
|
||||
final downloadManager = ref.watch(downloadManagerProvider.notifier);
|
||||
final blacklist = ref.watch(blacklistProvider);
|
||||
final me = ref.watch(meProvider);
|
||||
final me = ref.watch(metadataPluginUserProvider);
|
||||
|
||||
final favorites = useTrackToggleLike(track, ref);
|
||||
|
||||
@ -192,23 +198,32 @@ class TrackOptions extends HookConsumerWidget {
|
||||
|
||||
final removingTrack = useState<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),
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import 'package:spotify/spotify.dart';
|
||||
|
||||
extension ArtistExtension on List<ArtistSimple> {
|
||||
String asString() {
|
||||
return map((e) => e.name?.replaceAll(",", " ")).join(", ");
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
// switch (url.pathSegments.first) {
|
||||
// case "album":
|
||||
// final album = await spotify.invoke((api) {
|
||||
// return api.albums.get(url.pathSegments.last);
|
||||
// });
|
||||
// // router.navigate(
|
||||
// // AlbumRoute(id: album.id!, album: album),
|
||||
// // );
|
||||
// break;
|
||||
// case "artist":
|
||||
// router.navigate(ArtistRoute(artistId: url.pathSegments.last));
|
||||
// break;
|
||||
// case "playlist":
|
||||
// final playlist = await spotify.invoke((api) {
|
||||
// return api.playlists.get(url.pathSegments.last);
|
||||
// });
|
||||
// // router
|
||||
// // .navigate(PlaylistRoute(id: playlist.id!, playlist: playlist));
|
||||
// break;
|
||||
// case "track":
|
||||
// router.navigate(TrackRoute(trackId: url.pathSegments.last));
|
||||
// break;
|
||||
// default:
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
StreamSubscription? mediaStream;
|
||||
// StreamSubscription? mediaStream;
|
||||
|
||||
if (kIsMobile) {
|
||||
FlutterSharingIntent.instance.getInitialSharing().then(uriListener);
|
||||
// if (kIsMobile) {
|
||||
// FlutterSharingIntent.instance.getInitialSharing().then(uriListener);
|
||||
|
||||
mediaStream =
|
||||
FlutterSharingIntent.instance.getMediaStream().listen(uriListener);
|
||||
}
|
||||
// mediaStream =
|
||||
// FlutterSharingIntent.instance.getMediaStream().listen(uriListener);
|
||||
// }
|
||||
|
||||
final subscription = linkStream.listen((uri) async {
|
||||
try {
|
||||
final startSegment = uri.split(":").take(2).join(":");
|
||||
final endSegment = uri.split(":").last;
|
||||
// final subscription = linkStream.listen((uri) async {
|
||||
// try {
|
||||
// final startSegment = uri.split(":").take(2).join(":");
|
||||
// final endSegment = uri.split(":").last;
|
||||
|
||||
switch (startSegment) {
|
||||
case "spotify:album":
|
||||
final album = await spotify.invoke((api) {
|
||||
return api.albums.get(endSegment);
|
||||
});
|
||||
// await router.navigate(
|
||||
// AlbumRoute(id: album.id!, album: album),
|
||||
// );
|
||||
break;
|
||||
case "spotify:artist":
|
||||
await router.navigate(ArtistRoute(artistId: endSegment));
|
||||
break;
|
||||
case "spotify:track":
|
||||
await router.navigate(TrackRoute(trackId: endSegment));
|
||||
break;
|
||||
case "spotify:playlist":
|
||||
final playlist = await spotify.invoke((api) {
|
||||
return api.playlists.get(endSegment);
|
||||
});
|
||||
// await router.navigate(
|
||||
// PlaylistRoute(id: playlist.id!, playlist: playlist),
|
||||
// );
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
});
|
||||
// switch (startSegment) {
|
||||
// case "spotify:album":
|
||||
// final album = await spotify.invoke((api) {
|
||||
// return api.albums.get(endSegment);
|
||||
// });
|
||||
// // await router.navigate(
|
||||
// // AlbumRoute(id: album.id!, album: album),
|
||||
// // );
|
||||
// break;
|
||||
// case "spotify:artist":
|
||||
// await router.navigate(ArtistRoute(artistId: endSegment));
|
||||
// break;
|
||||
// case "spotify:track":
|
||||
// await router.navigate(TrackRoute(trackId: endSegment));
|
||||
// break;
|
||||
// case "spotify:playlist":
|
||||
// final playlist = await spotify.invoke((api) {
|
||||
// return api.playlists.get(endSegment);
|
||||
// });
|
||||
// // await router.navigate(
|
||||
// // PlaylistRoute(id: playlist.id!, playlist: playlist),
|
||||
// // );
|
||||
// break;
|
||||
// default:
|
||||
// break;
|
||||
// }
|
||||
// } catch (e, stack) {
|
||||
// AppLogger.reportError(e, stack);
|
||||
// }
|
||||
// });
|
||||
|
||||
return () {
|
||||
mediaStream?.cancel();
|
||||
subscription.cancel();
|
||||
};
|
||||
}, [spotify]);
|
||||
// return () {
|
||||
// mediaStream?.cancel();
|
||||
// subscription.cancel();
|
||||
// };
|
||||
// }, [spotify]);
|
||||
}
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
252
lib/models/metadata/market.dart
Normal file
252
lib/models/metadata/market.dart
Normal 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,
|
||||
}
|
@ -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';
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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) =>
|
||||
|
@ -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, "runtimeType": "local"}
|
||||
: {...json, "runtimeType": "simple"},
|
||||
json.containsKey("path")
|
||||
? {...json, "runtimeType": "local"}
|
||||
: {...json, "runtimeType": "full"},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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
@ -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(),
|
||||
};
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
@ -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,
|
||||
};
|
@ -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);
|
||||
}
|
@ -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(),
|
||||
);
|
@ -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));
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
// ),
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
@ -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]);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
// );
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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: () {},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
@ -9,6 +9,7 @@ import 'package:spotube/collections/assets.gen.dart';
|
||||
import 'package:spotube/collections/routes.gr.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/framework/app_pop_scope.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/modules/player/player_actions.dart';
|
||||
import 'package:spotube/modules/player/player_controls.dart';
|
||||
import 'package:spotube/modules/player/volume_slider.dart';
|
||||
@ -16,11 +17,8 @@ import 'package:spotube/components/dialogs/track_details_dialog.dart';
|
||||
import 'package:spotube/components/links/artist_link.dart';
|
||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||
import 'package:spotube/components/image/universal_image.dart';
|
||||
import 'package:spotube/extensions/artist_simple.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/extensions/image.dart';
|
||||
import 'package:spotube/models/local_track.dart';
|
||||
import 'package:spotube/modules/root/spotube_navigation_bar.dart';
|
||||
import 'package:spotube/provider/authentication/authentication.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
@ -47,8 +45,8 @@ class PlayerView extends HookConsumerWidget {
|
||||
final sourcedCurrentTrack = ref.watch(activeTrackSourcesProvider);
|
||||
final currentActiveTrack =
|
||||
ref.watch(audioPlayerProvider.select((s) => s.activeTrack));
|
||||
final currentTrack = sourcedCurrentTrack ?? currentActiveTrack;
|
||||
final isLocalTrack = currentTrack is LocalTrack;
|
||||
final currentActiveTrackSource = sourcedCurrentTrack.asData?.value?.source;
|
||||
final isLocalTrack = currentActiveTrack is SpotubeLocalTrackObject;
|
||||
final mediaQuery = MediaQuery.sizeOf(context);
|
||||
|
||||
final shouldHide = useState(true);
|
||||
@ -71,10 +69,10 @@ class PlayerView extends HookConsumerWidget {
|
||||
}, [mediaQuery.lgAndUp]);
|
||||
|
||||
String albumArt = useMemoized(
|
||||
() => (currentTrack?.album?.images).asUrlString(
|
||||
() => (currentActiveTrack?.album.images).asUrlString(
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
),
|
||||
[currentTrack?.album?.images],
|
||||
[currentActiveTrack?.album.images],
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
@ -115,7 +113,7 @@ class PlayerView extends HookConsumerWidget {
|
||||
)
|
||||
],
|
||||
trailing: [
|
||||
if (currentTrack is YoutubeSourcedTrack)
|
||||
if (currentActiveTrackSource is YoutubeSourcedTrack)
|
||||
TextButton(
|
||||
leading: Assets.logos.songlinkTransparent.image(
|
||||
width: 20,
|
||||
@ -123,31 +121,34 @@ class PlayerView extends HookConsumerWidget {
|
||||
color: theme.colorScheme.foreground,
|
||||
),
|
||||
onPressed: () {
|
||||
final url = "https://song.link/s/${currentTrack.id}";
|
||||
final url =
|
||||
"https://song.link/s/${currentActiveTrack?.id}";
|
||||
|
||||
launchUrlString(url);
|
||||
},
|
||||
child: Text(context.l10n.song_link),
|
||||
),
|
||||
Tooltip(
|
||||
tooltip: TooltipContainer(
|
||||
child: Text(context.l10n.details),
|
||||
).call,
|
||||
child: IconButton.ghost(
|
||||
icon: const Icon(SpotubeIcons.info, size: 18),
|
||||
onPressed: currentTrack == null
|
||||
? null
|
||||
: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return TrackDetailsDialog(
|
||||
track: currentTrack,
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
if (!isLocalTrack)
|
||||
Tooltip(
|
||||
tooltip: TooltipContainer(
|
||||
child: Text(context.l10n.details),
|
||||
).call,
|
||||
child: IconButton.ghost(
|
||||
icon: const Icon(SpotubeIcons.info, size: 18),
|
||||
onPressed: currentActiveTrackSource == null
|
||||
? null
|
||||
: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return TrackDetailsDialog(
|
||||
track: currentActiveTrack
|
||||
as SpotubeFullTrackObject,
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -190,7 +191,7 @@ class PlayerView extends HookConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AutoSizeText(
|
||||
currentTrack?.name ?? context.l10n.not_playing,
|
||||
currentActiveTrack?.name ?? context.l10n.not_playing,
|
||||
style: const TextStyle(fontSize: 22),
|
||||
maxFontSize: 22,
|
||||
maxLines: 1,
|
||||
@ -198,13 +199,13 @@ class PlayerView extends HookConsumerWidget {
|
||||
),
|
||||
if (isLocalTrack)
|
||||
Text(
|
||||
currentTrack.artists?.asString() ?? "",
|
||||
currentActiveTrack.artists.asString(),
|
||||
style: theme.typography.normal
|
||||
.copyWith(fontWeight: FontWeight.bold),
|
||||
)
|
||||
else
|
||||
ArtistLink(
|
||||
artists: currentTrack?.artists ?? [],
|
||||
artists: currentActiveTrack?.artists ?? [],
|
||||
textStyle: theme.typography.normal
|
||||
.copyWith(fontWeight: FontWeight.bold),
|
||||
onRouteChange: (route) {
|
||||
@ -212,7 +213,9 @@ class PlayerView extends HookConsumerWidget {
|
||||
context.router.navigateNamed(route);
|
||||
},
|
||||
onOverflowArtistClick: () => context.navigateTo(
|
||||
TrackRoute(trackId: currentTrack!.id!),
|
||||
TrackRoute(
|
||||
trackId: currentActiveTrack!.id,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
@ -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,
|
||||
);
|
||||
},
|
||||
|
@ -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)),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -10,15 +10,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/form/checkbox_form_field.dart';
|
||||
import 'package:spotube/components/form/text_form_field.dart';
|
||||
import 'package:spotube/components/image/universal_image.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/extensions/image.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/playlist/playlist.dart';
|
||||
|
||||
class PlaylistCreateDialog extends HookConsumerWidget {
|
||||
/// Track ids to add to the playlist
|
||||
@ -32,10 +32,11 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final userPlaylists = ref.watch(favoritePlaylistsProvider);
|
||||
final playlist = ref.watch(playlistProvider(playlistId ?? ""));
|
||||
final userPlaylists = ref.watch(metadataPluginSavedPlaylistsProvider);
|
||||
final playlist =
|
||||
ref.watch(metadataPluginPlaylistProvider(playlistId ?? ""));
|
||||
final playlistNotifier =
|
||||
ref.watch(playlistProvider(playlistId ?? "").notifier);
|
||||
ref.watch(metadataPluginPlaylistProvider(playlistId ?? "").notifier);
|
||||
|
||||
final isSubmitting = useState(false);
|
||||
|
||||
@ -55,25 +56,54 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
||||
final l10n = context.l10n;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
useEffect(() {
|
||||
if (playlist.asData?.value != null) {
|
||||
formKey.currentState?.patchValue({
|
||||
'playlistName': playlist.asData!.value.name,
|
||||
'description': playlist.asData!.value.description,
|
||||
'public': playlist.asData!.value.public,
|
||||
'collaborative': playlist.asData!.value.collaborative,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}, [playlist]);
|
||||
|
||||
final onError = useCallback((error) {
|
||||
if (error is SpotifyError || error is SpotifyException) {
|
||||
showToast(
|
||||
context: context,
|
||||
location: ToastLocation.topRight,
|
||||
builder: (context, overlay) {
|
||||
return SurfaceCard(
|
||||
child: Basic(
|
||||
title: Text(
|
||||
l10n.error(error.message ?? l10n.epic_failure),
|
||||
style: theme.typography.normal.copyWith(
|
||||
color: theme.colorScheme.destructive,
|
||||
),
|
||||
// if (error is SpotifyError || error is SpotifyException) {
|
||||
// showToast(
|
||||
// context: context,
|
||||
// location: ToastLocation.topRight,
|
||||
// builder: (context, overlay) {
|
||||
// return SurfaceCard(
|
||||
// child: Basic(
|
||||
// title: Text(
|
||||
// l10n.error(error.message ?? l10n.epic_failure),
|
||||
// style: theme.typography.normal.copyWith(
|
||||
// color: theme.colorScheme.destructive,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
showToast(
|
||||
context: context,
|
||||
location: ToastLocation.topRight,
|
||||
builder: (context, overlay) {
|
||||
return SurfaceCard(
|
||||
child: Basic(
|
||||
title: Text(
|
||||
l10n.error(l10n.epic_failure),
|
||||
style: theme.typography.normal.copyWith(
|
||||
color: theme.colorScheme.destructive,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}, [l10n, theme]);
|
||||
|
||||
Future<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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -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));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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()),
|
||||
// ),
|
||||
// };
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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;
|
||||
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -13,9 +13,6 @@ import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/modules/connect/connect_device.dart';
|
||||
import 'package:spotube/modules/home/sections/featured.dart';
|
||||
import 'package:spotube/modules/home/sections/sections.dart';
|
||||
import 'package:spotube/modules/home/sections/friends.dart';
|
||||
import 'package:spotube/modules/home/sections/genres/genres.dart';
|
||||
import 'package:spotube/modules/home/sections/made_for_user.dart';
|
||||
import 'package:spotube/modules/home/sections/new_releases.dart';
|
||||
import 'package:spotube/modules/home/sections/recent.dart';
|
||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||
@ -76,18 +73,18 @@ class HomePage extends HookConsumerWidget {
|
||||
else if (kIsMacOS)
|
||||
const SliverGap(10),
|
||||
const SliverGap(10),
|
||||
// SliverList.builder(
|
||||
// itemCount: 5,
|
||||
// itemBuilder: (context, index) {
|
||||
// return switch (index) {
|
||||
// 0 => const HomeGenresSection(),
|
||||
// 1 => const HomeRecentlyPlayedSection(),
|
||||
// 2 => const HomeFeaturedSection(),
|
||||
// 3 => const HomePageFriendsSection(),
|
||||
// _ => const HomeNewReleasesSection()
|
||||
// };
|
||||
// },
|
||||
// ),
|
||||
SliverList.builder(
|
||||
itemCount: 3,
|
||||
itemBuilder: (context, index) {
|
||||
return switch (index) {
|
||||
// 0 => const HomeGenresSection(),
|
||||
0 => const HomeRecentlyPlayedSection(),
|
||||
1 => const HomeFeaturedSection(),
|
||||
// 3 => const HomePageFriendsSection(),
|
||||
_ => const HomeNewReleasesSection()
|
||||
};
|
||||
},
|
||||
),
|
||||
const HomePageBrowseSection(),
|
||||
],
|
||||
),
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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));
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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),
|
||||
],
|
||||
),
|
||||
|
@ -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";
|
||||
|
@ -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({
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user