mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-06 07:29:42 +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/database/database.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/models/spotify/home_feed.dart';
|
|
||||||
import 'package:spotube/models/spotify_friends.dart';
|
|
||||||
import 'package:spotube/provider/history/summary.dart';
|
import 'package:spotube/provider/history/summary.dart';
|
||||||
|
|
||||||
abstract class FakeData {
|
abstract class FakeData {
|
||||||
static final Image image = Image()
|
static final SpotubeImageObject image = SpotubeImageObject(
|
||||||
..height = 1
|
height: 100,
|
||||||
..width = 1
|
width: 100,
|
||||||
..url = "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg";
|
url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
|
||||||
|
);
|
||||||
static final Followers followers = Followers()
|
|
||||||
..href = "text"
|
|
||||||
..total = 1;
|
|
||||||
|
|
||||||
static final SpotubeFullArtistObject artist = SpotubeFullArtistObject(
|
static final SpotubeFullArtistObject artist = SpotubeFullArtistObject(
|
||||||
id: "1",
|
id: "1",
|
||||||
@ -30,43 +24,26 @@ abstract class FakeData {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
static final externalIds = ExternalIds()
|
static final SpotubeFullAlbumObject album = SpotubeFullAlbumObject(
|
||||||
..isrc = "text"
|
id: "1",
|
||||||
..ean = "text"
|
name: "A good album",
|
||||||
..upc = "text";
|
externalUri: "https://example.com",
|
||||||
|
artists: [artistSimple],
|
||||||
|
releaseDate: "2021-01-01",
|
||||||
|
albumType: SpotubeAlbumType.album,
|
||||||
|
images: [image],
|
||||||
|
totalTracks: 10,
|
||||||
|
genres: ["genre"],
|
||||||
|
recordLabel: "Record Label",
|
||||||
|
);
|
||||||
|
|
||||||
static final externalUrls = ExternalUrls()..spotify = "text";
|
static final SpotubeSimpleArtistObject artistSimple =
|
||||||
|
SpotubeSimpleArtistObject(
|
||||||
static final Album album = Album()
|
id: "1",
|
||||||
..id = "1"
|
name: "What an artist",
|
||||||
..genres = ["genre"]
|
externalUri: "https://example.com",
|
||||||
..label = "label"
|
images: null,
|
||||||
..popularity = 1
|
);
|
||||||
..albumType = AlbumType.album
|
|
||||||
// ..artists = [artist]
|
|
||||||
..availableMarkets = [Market.BD]
|
|
||||||
..externalUrls = externalUrls
|
|
||||||
..href = "text"
|
|
||||||
..images = [image]
|
|
||||||
..name = "Another good album"
|
|
||||||
..releaseDate = "2021-01-01"
|
|
||||||
..releaseDatePrecision = DatePrecision.day
|
|
||||||
..tracks = [track]
|
|
||||||
..type = "type"
|
|
||||||
..uri = "uri"
|
|
||||||
..externalIds = externalIds
|
|
||||||
..copyrights = [
|
|
||||||
Copyright()
|
|
||||||
..type = CopyrightType.C
|
|
||||||
..text = "text",
|
|
||||||
];
|
|
||||||
|
|
||||||
static final ArtistSimple artistSimple = ArtistSimple()
|
|
||||||
..id = "1"
|
|
||||||
..name = "What an artist"
|
|
||||||
..type = "type"
|
|
||||||
..uri = "uri"
|
|
||||||
..externalUrls = externalUrls;
|
|
||||||
|
|
||||||
static final SpotubeSimpleAlbumObject albumSimple = SpotubeSimpleAlbumObject(
|
static final SpotubeSimpleAlbumObject albumSimple = SpotubeSimpleAlbumObject(
|
||||||
albumType: SpotubeAlbumType.album,
|
albumType: SpotubeAlbumType.album,
|
||||||
@ -84,163 +61,51 @@ abstract class FakeData {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
static final Track track = Track()
|
static final SpotubeFullTrackObject track = SpotubeTrackObject.full(
|
||||||
..id = "1"
|
|
||||||
// ..artists = [artist, artist, artist]
|
|
||||||
// ..album = albumSimple
|
|
||||||
..availableMarkets = [Market.BD]
|
|
||||||
..discNumber = 1
|
|
||||||
..durationMs = 50000
|
|
||||||
..explicit = false
|
|
||||||
..externalUrls = externalUrls
|
|
||||||
..href = "text"
|
|
||||||
..name = "A Track Name"
|
|
||||||
..popularity = 1
|
|
||||||
..previewUrl = "url"
|
|
||||||
..trackNumber = 1
|
|
||||||
..type = "type"
|
|
||||||
..uri = "uri"
|
|
||||||
..externalIds = externalIds
|
|
||||||
..isPlayable = true
|
|
||||||
..explicit = false
|
|
||||||
..linkedFrom = trackLink;
|
|
||||||
|
|
||||||
static final simpleTrack = SpotubeSimpleTrackObject(
|
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "A Track Name",
|
name: "A good track",
|
||||||
artists: [],
|
|
||||||
album: albumSimple,
|
|
||||||
externalUri: "https://example.com",
|
externalUri: "https://example.com",
|
||||||
durationMs: 50000,
|
album: albumSimple,
|
||||||
|
durationMs: 3 * 60 * 1000, // 3 minutes
|
||||||
|
isrc: "USUM72112345",
|
||||||
explicit: false,
|
explicit: false,
|
||||||
|
) as SpotubeFullTrackObject;
|
||||||
|
|
||||||
|
static final SpotubeUserObject user = SpotubeUserObject(
|
||||||
|
id: "1",
|
||||||
|
name: "User Name",
|
||||||
|
externalUri: "https://example.com",
|
||||||
|
images: [image],
|
||||||
);
|
);
|
||||||
|
|
||||||
static final TrackLink trackLink = TrackLink()
|
static final SpotubeFullPlaylistObject playlist = SpotubeFullPlaylistObject(
|
||||||
..id = "1"
|
id: "1",
|
||||||
..type = "type"
|
name: "A good playlist",
|
||||||
..uri = "uri"
|
description: "A very good playlist description",
|
||||||
..externalUrls = {"spotify": "text"}
|
externalUri: "https://example.com",
|
||||||
..href = "text";
|
collaborative: false,
|
||||||
|
public: true,
|
||||||
|
owner: user,
|
||||||
|
images: [image],
|
||||||
|
collaborators: [user]);
|
||||||
|
|
||||||
static final Paging<Track> paging = Paging()
|
static final SpotubeSimplePlaylistObject playlistSimple =
|
||||||
..href = "text"
|
SpotubeSimplePlaylistObject(
|
||||||
..itemsNative = [track.toJson()]
|
id: "1",
|
||||||
..limit = 1
|
name: "A good playlist",
|
||||||
..next = "text"
|
description: "A very good playlist description",
|
||||||
..offset = 1
|
externalUri: "https://example.com",
|
||||||
..previous = "text"
|
owner: user,
|
||||||
..total = 1;
|
images: [image],
|
||||||
|
|
||||||
static final User user = User()
|
|
||||||
..id = "1"
|
|
||||||
..displayName = "Your Name"
|
|
||||||
..birthdate = "2021-01-01"
|
|
||||||
..country = Market.BD
|
|
||||||
..email = "test@email.com"
|
|
||||||
..followers = followers
|
|
||||||
..href = "text"
|
|
||||||
..images = [image]
|
|
||||||
..type = "type"
|
|
||||||
..uri = "uri";
|
|
||||||
|
|
||||||
static final TracksLink tracksLink = TracksLink()
|
|
||||||
..href = "text"
|
|
||||||
..total = 1;
|
|
||||||
|
|
||||||
static final Playlist playlist = Playlist()
|
|
||||||
..id = "1"
|
|
||||||
..collaborative = false
|
|
||||||
..description = "A very good playlist description"
|
|
||||||
..externalUrls = externalUrls
|
|
||||||
..followers = followers
|
|
||||||
..href = "text"
|
|
||||||
..images = [image]
|
|
||||||
..name = "A good playlist"
|
|
||||||
..owner = user
|
|
||||||
..public = true
|
|
||||||
..snapshotId = "text"
|
|
||||||
..tracks = paging
|
|
||||||
..tracksLink = tracksLink
|
|
||||||
..type = "type"
|
|
||||||
..uri = "uri";
|
|
||||||
|
|
||||||
static final PlaylistSimple playlistSimple = PlaylistSimple()
|
|
||||||
..id = "1"
|
|
||||||
..collaborative = false
|
|
||||||
..externalUrls = externalUrls
|
|
||||||
..href = "text"
|
|
||||||
..images = [image]
|
|
||||||
..name = "A good playlist"
|
|
||||||
..owner = user
|
|
||||||
..public = true
|
|
||||||
..snapshotId = "text"
|
|
||||||
..tracksLink = tracksLink
|
|
||||||
..type = "type"
|
|
||||||
..description = "A very good playlist description"
|
|
||||||
..uri = "uri";
|
|
||||||
|
|
||||||
static final Category category = Category()
|
|
||||||
..href = "text"
|
|
||||||
..icons = [image]
|
|
||||||
..id = "1"
|
|
||||||
..name = "category";
|
|
||||||
|
|
||||||
static final friends = SpotifyFriends(
|
|
||||||
friends: [
|
|
||||||
for (var i = 0; i < 3; i++)
|
|
||||||
SpotifyFriendActivity(
|
|
||||||
user: const SpotifyFriend(
|
|
||||||
name: "name",
|
|
||||||
imageUrl: "imageUrl",
|
|
||||||
uri: "uri",
|
|
||||||
),
|
|
||||||
track: SpotifyActivityTrack(
|
|
||||||
name: "name",
|
|
||||||
artist: const SpotifyActivityArtist(
|
|
||||||
name: "name",
|
|
||||||
uri: "uri",
|
|
||||||
),
|
|
||||||
album: const SpotifyActivityAlbum(
|
|
||||||
name: "name",
|
|
||||||
uri: "uri",
|
|
||||||
),
|
|
||||||
context: SpotifyActivityContext(
|
|
||||||
name: "name",
|
|
||||||
index: i,
|
|
||||||
uri: "uri",
|
|
||||||
),
|
|
||||||
imageUrl: "imageUrl",
|
|
||||||
uri: "uri",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
static final feedSection = SpotifyHomeFeedSection(
|
static final SpotubeBrowseSectionObject browseSection =
|
||||||
typename: "HomeGenericSectionData",
|
SpotubeBrowseSectionObject(
|
||||||
uri: "spotify:section:lol",
|
id: "section-id",
|
||||||
title: "Dummy",
|
title: "Browse Section",
|
||||||
items: [
|
browseMore: true,
|
||||||
for (int i = 0; i < 10; i++)
|
externalUri: "https://example.com/browse/section",
|
||||||
SpotifyHomeFeedSectionItem(
|
items: [playlistSimple, playlistSimple, playlistSimple]);
|
||||||
typename: "PlaylistResponseWrapper",
|
|
||||||
playlist: SpotifySectionPlaylist(
|
|
||||||
name: "Playlist $i",
|
|
||||||
description: "Really super important description $i",
|
|
||||||
format: "daily-mix",
|
|
||||||
images: [
|
|
||||||
const SpotifySectionItemImage(
|
|
||||||
height: 1,
|
|
||||||
width: 1,
|
|
||||||
url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
owner: "Spotify",
|
|
||||||
uri: "spotify:playlist:id",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
static const historySummary = PlaybackHistorySummary(
|
static const historySummary = PlaybackHistorySummary(
|
||||||
albums: 1,
|
albums: 1,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Country Codes contributed by momobobe <https://github.com/momobobe>
|
// Country Codes contributed by momobobe <https://github.com/momobobe>
|
||||||
|
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotube/models/metadata/market.dart';
|
||||||
|
|
||||||
final spotifyMarkets = [
|
final spotifyMarkets = [
|
||||||
(Market.AL, "Albania (AL)"),
|
(Market.AL, "Albania (AL)"),
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
|
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
|
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
|
||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/metadata_plugin/user.dart';
|
||||||
|
|
||||||
class PlaylistAddTrackDialog extends HookConsumerWidget {
|
class PlaylistAddTrackDialog extends HookConsumerWidget {
|
||||||
/// The id of the playlist this dialog was opened from
|
/// The id of the playlist this dialog was opened from
|
||||||
final String? openFromPlaylist;
|
final String? openFromPlaylist;
|
||||||
final List<Track> tracks;
|
final List<SpotubeTrackObject> tracks;
|
||||||
const PlaylistAddTrackDialog({
|
const PlaylistAddTrackDialog({
|
||||||
required this.tracks,
|
required this.tracks,
|
||||||
required this.openFromPlaylist,
|
required this.openFromPlaylist,
|
||||||
@ -22,24 +22,23 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final typography = Theme.of(context).typography;
|
final typography = Theme.of(context).typography;
|
||||||
final userPlaylists = ref.watch(favoritePlaylistsProvider);
|
final userPlaylists = ref.watch(metadataPluginSavedPlaylistsProvider);
|
||||||
final favoritePlaylistsNotifier =
|
final favoritePlaylistsNotifier =
|
||||||
ref.watch(favoritePlaylistsProvider.notifier);
|
ref.watch(metadataPluginSavedPlaylistsProvider.notifier);
|
||||||
|
|
||||||
final me = ref.watch(meProvider);
|
final me = ref.watch(metadataPluginUserProvider);
|
||||||
|
|
||||||
final filteredPlaylists = useMemoized(
|
final filteredPlaylists = useMemoized(
|
||||||
() =>
|
() =>
|
||||||
userPlaylists.asData?.value.items
|
userPlaylists.asData?.value.items
|
||||||
.where(
|
.where(
|
||||||
(playlist) =>
|
(playlist) =>
|
||||||
playlist.owner?.id != null &&
|
playlist.owner.id == me.asData?.value?.id &&
|
||||||
playlist.owner!.id == me.asData?.value.id &&
|
|
||||||
playlist.id != openFromPlaylist,
|
playlist.id != openFromPlaylist,
|
||||||
)
|
)
|
||||||
.toList() ??
|
.toList() ??
|
||||||
[],
|
[],
|
||||||
[userPlaylists.asData?.value, me.asData?.value.id, openFromPlaylist],
|
[userPlaylists.asData?.value, me.asData?.value?.id, openFromPlaylist],
|
||||||
);
|
);
|
||||||
|
|
||||||
final playlistsCheck = useState(<String, bool>{});
|
final playlistsCheck = useState(<String, bool>{});
|
||||||
@ -60,7 +59,7 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
|
|||||||
selectedPlaylists.map(
|
selectedPlaylists.map(
|
||||||
(playlistId) => favoritePlaylistsNotifier.addTracks(
|
(playlistId) => favoritePlaylistsNotifier.addTracks(
|
||||||
playlistId,
|
playlistId,
|
||||||
tracks.map((e) => e.id!).toList(),
|
tracks.map((e) => e.id).toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).then((_) => context.mounted ? Navigator.pop(context, true) : null);
|
).then((_) => context.mounted ? Navigator.pop(context, true) : null);
|
||||||
@ -109,8 +108,7 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
leading: Avatar(
|
leading: Avatar(
|
||||||
initials:
|
initials: Avatar.getInitials(playlist.name),
|
||||||
Avatar.getInitials(playlist.name ?? "Playlist"),
|
|
||||||
provider: UniversalImage.imageProvider(
|
provider: UniversalImage.imageProvider(
|
||||||
playlist.images.asUrlString(
|
playlist.images.asUrlString(
|
||||||
placeholder: ImagePlaceholder.collection,
|
placeholder: ImagePlaceholder.collection,
|
||||||
@ -124,20 +122,20 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
|
|||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
playlistsCheck.value = {
|
playlistsCheck.value = {
|
||||||
...playlistsCheck.value,
|
...playlistsCheck.value,
|
||||||
playlist.id!: val == CheckboxState.checked,
|
playlist.id: val == CheckboxState.checked,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
playlistsCheck.value = {
|
playlistsCheck.value = {
|
||||||
...playlistsCheck.value,
|
...playlistsCheck.value,
|
||||||
playlist.id!:
|
playlist.id:
|
||||||
!(playlistsCheck.value[playlist.id] ?? false),
|
!(playlistsCheck.value[playlist.id] ?? false),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
child: Text(playlist.name!),
|
child: Text(playlist.name),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
|
|
||||||
final replaceDownloadedFileState = StateProvider<bool?>((ref) => null);
|
final replaceDownloadedFileState = StateProvider<bool?>((ref) => null);
|
||||||
|
|
||||||
class ReplaceDownloadedDialog extends ConsumerWidget {
|
class ReplaceDownloadedDialog extends ConsumerWidget {
|
||||||
final Track track;
|
final SpotubeTrackObject track;
|
||||||
const ReplaceDownloadedDialog({required this.track, super.key});
|
const ReplaceDownloadedDialog({required this.track, super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -16,7 +16,7 @@ class ReplaceDownloadedDialog extends ConsumerWidget {
|
|||||||
final replaceAll = ref.watch(replaceDownloadedFileState);
|
final replaceAll = ref.watch(replaceDownloadedFileState);
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(context.l10n.track_exists(track.name ?? "")),
|
title: Text(context.l10n.track_exists(track.name)),
|
||||||
content: RadioGroup(
|
content: RadioGroup(
|
||||||
value: groupValue,
|
value: groupValue,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
|
|||||||
@ -1,32 +1,34 @@
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/links/artist_link.dart';
|
import 'package:spotube/components/links/artist_link.dart';
|
||||||
import 'package:spotube/components/links/hyper_link.dart';
|
import 'package:spotube/components/links/hyper_link.dart';
|
||||||
import 'package:spotube/components/links/link_text.dart';
|
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
|
||||||
import 'package:spotube/extensions/duration.dart';
|
import 'package:spotube/extensions/duration.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
|
import 'package:spotube/models/playback/track_sources.dart';
|
||||||
|
import 'package:spotube/provider/server/track_sources.dart';
|
||||||
|
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||||
|
|
||||||
class TrackDetailsDialog extends HookWidget {
|
class TrackDetailsDialog extends HookConsumerWidget {
|
||||||
final Track track;
|
final SpotubeFullTrackObject track;
|
||||||
const TrackDetailsDialog({
|
const TrackDetailsDialog({
|
||||||
super.key,
|
super.key,
|
||||||
required this.track,
|
required this.track,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
final sourcedTrack =
|
||||||
|
ref.read(trackSourcesProvider(TrackSourceQuery.fromTrack(track)));
|
||||||
|
|
||||||
final detailsMap = {
|
final detailsMap = {
|
||||||
context.l10n.title: track.name!,
|
context.l10n.title: track.name,
|
||||||
context.l10n.artist: ArtistLink(
|
context.l10n.artist: ArtistLink(
|
||||||
artists: track.artists ?? <Artist>[],
|
artists: track.artists,
|
||||||
mainAxisAlignment: WrapAlignment.start,
|
mainAxisAlignment: WrapAlignment.start,
|
||||||
textStyle: const TextStyle(color: Colors.blue),
|
textStyle: const TextStyle(color: Colors.blue),
|
||||||
hideOverflowArtist: false,
|
hideOverflowArtist: false,
|
||||||
@ -37,17 +39,15 @@ class TrackDetailsDialog extends HookWidget {
|
|||||||
// overflow: TextOverflow.ellipsis,
|
// overflow: TextOverflow.ellipsis,
|
||||||
// style: const TextStyle(color: Colors.blue),
|
// style: const TextStyle(color: Colors.blue),
|
||||||
// ),
|
// ),
|
||||||
context.l10n.duration: (track is SourcedTrack
|
context.l10n.duration: sourcedTrack.asData != null
|
||||||
? (track as SourcedTrack).sourceInfo.duration
|
? Duration(milliseconds: sourcedTrack.asData!.value.info.durationMs)
|
||||||
: track.duration!)
|
.toHumanReadableString()
|
||||||
.toHumanReadableString(),
|
: Duration(milliseconds: track.durationMs).toHumanReadableString(),
|
||||||
if (track.album!.releaseDate != null)
|
if (track.album.releaseDate != null)
|
||||||
context.l10n.released: track.album!.releaseDate,
|
context.l10n.released: track.album.releaseDate,
|
||||||
context.l10n.popularity: track.popularity?.toString() ?? "0",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
final sourceInfo =
|
final sourceInfo = sourcedTrack.asData?.value.info;
|
||||||
track is SourcedTrack ? (track as SourcedTrack).sourceInfo : null;
|
|
||||||
|
|
||||||
final ytTracksDetailsMap = sourceInfo == null
|
final ytTracksDetailsMap = sourceInfo == null
|
||||||
? {}
|
? {}
|
||||||
@ -58,12 +58,7 @@ class TrackDetailsDialog extends HookWidget {
|
|||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
context.l10n.channel: Hyperlink(
|
context.l10n.channel: Text(sourceInfo.artists),
|
||||||
sourceInfo.artist,
|
|
||||||
sourceInfo.artistUrl,
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
context.l10n.streamUrl: Hyperlink(
|
context.l10n.streamUrl: Hyperlink(
|
||||||
(track as SourcedTrack).url,
|
(track as SourcedTrack).url,
|
||||||
(track as SourcedTrack).url,
|
(track as SourcedTrack).url,
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/components/heart_button/use_track_toggle_like.dart';
|
import 'package:spotube/components/heart_button/use_track_toggle_like.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/authentication/authentication.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/user.dart';
|
||||||
|
|
||||||
class HeartButton extends HookConsumerWidget {
|
class HeartButton extends HookConsumerWidget {
|
||||||
final bool isLiked;
|
final bool isLiked;
|
||||||
@ -63,7 +64,7 @@ class HeartButton extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class TrackHeartButton extends HookConsumerWidget {
|
class TrackHeartButton extends HookConsumerWidget {
|
||||||
final Track track;
|
final SpotubeTrackObject track;
|
||||||
const TrackHeartButton({
|
const TrackHeartButton({
|
||||||
super.key,
|
super.key,
|
||||||
required this.track,
|
required this.track,
|
||||||
@ -71,8 +72,8 @@ class TrackHeartButton extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final savedTracks = ref.watch(likedTracksProvider);
|
final savedTracks = ref.watch(metadataPluginSavedTracksProvider);
|
||||||
final me = ref.watch(meProvider);
|
final me = ref.watch(metadataPluginUserProvider);
|
||||||
final (:isLiked, :toggleTrackLike) = useTrackToggleLike(track, ref);
|
final (:isLiked, :toggleTrackLike) = useTrackToggleLike(track, ref);
|
||||||
|
|
||||||
if (me.isLoading) {
|
if (me.isLoading) {
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/components/links/anchor_button.dart';
|
import 'package:spotube/components/links/anchor_button.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
|
|
||||||
class ArtistLink extends StatelessWidget {
|
class ArtistLink extends StatelessWidget {
|
||||||
final List<ArtistSimple> artists;
|
final List<SpotubeSimpleArtistObject> artists;
|
||||||
final WrapCrossAlignment crossAxisAlignment;
|
final WrapCrossAlignment crossAxisAlignment;
|
||||||
final WrapAlignment mainAxisAlignment;
|
final WrapAlignment mainAxisAlignment;
|
||||||
final TextStyle textStyle;
|
final TextStyle textStyle;
|
||||||
@ -38,19 +38,16 @@ class ArtistLink extends StatelessWidget {
|
|||||||
.entries
|
.entries
|
||||||
.map(
|
.map(
|
||||||
(artist) => Builder(builder: (context) {
|
(artist) => Builder(builder: (context) {
|
||||||
if (artist.value.name == null) {
|
|
||||||
return Text("Spotify", style: textStyle);
|
|
||||||
}
|
|
||||||
return AnchorButton(
|
return AnchorButton(
|
||||||
(artist.key != artists.length - 1)
|
(artist.key != artists.length - 1)
|
||||||
? "${artist.value.name}, "
|
? "${artist.value.name}, "
|
||||||
: artist.value.name!,
|
: artist.value.name,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (onRouteChange != null) {
|
if (onRouteChange != null) {
|
||||||
onRouteChange?.call("/artist/${artist.value.id}");
|
onRouteChange?.call("/artist/${artist.value.id}");
|
||||||
} else {
|
} else {
|
||||||
context
|
context
|
||||||
.navigateTo(ArtistRoute(artistId: artist.value.id!));
|
.navigateTo(ArtistRoute(artistId: artist.value.id));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart';
|
import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart';
|
||||||
import 'package:spotube/components/dialogs/confirm_download_dialog.dart';
|
import 'package:spotube/components/dialogs/confirm_download_dialog.dart';
|
||||||
@ -10,6 +9,7 @@ import 'package:spotube/components/track_presentation/presentation_props.dart';
|
|||||||
import 'package:spotube/components/track_presentation/presentation_state.dart';
|
import 'package:spotube/components/track_presentation/presentation_state.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/history/history.dart';
|
import 'package:spotube/provider/history/history.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
@ -76,9 +76,11 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
|
|||||||
|
|
||||||
Future<void> actionDownloadTracks({
|
Future<void> actionDownloadTracks({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required List<Track> tracks,
|
required List<SpotubeTrackObject> tracks,
|
||||||
required String action,
|
required String action,
|
||||||
}) async {
|
}) async {
|
||||||
|
final fullTrackObjects =
|
||||||
|
tracks.whereType<SpotubeFullTrackObject>().toList();
|
||||||
final confirmed = audioSource == AudioSource.piped ||
|
final confirmed = audioSource == AudioSource.piped ||
|
||||||
(await showDialog<bool>(
|
(await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
@ -88,10 +90,10 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
|
|||||||
) ??
|
) ??
|
||||||
false);
|
false);
|
||||||
if (confirmed != true) return;
|
if (confirmed != true) return;
|
||||||
downloader.batchAddToQueue(tracks);
|
downloader.batchAddToQueue(fullTrackObjects);
|
||||||
notifier.deselectAllTracks();
|
notifier.deselectAllTracks();
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
showToastForAction(context, action, tracks.length);
|
showToastForAction(context, action, fullTrackObjects.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
return AdaptivePopSheetList(
|
return AdaptivePopSheetList(
|
||||||
@ -143,11 +145,12 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
|
|||||||
{
|
{
|
||||||
playlistNotifier.addTracksAtFirst(tracks);
|
playlistNotifier.addTracksAtFirst(tracks);
|
||||||
playlistNotifier.addCollection(options.collectionId);
|
playlistNotifier.addCollection(options.collectionId);
|
||||||
if (options.collection is AlbumSimple) {
|
if (options.collection is SpotubeSimpleAlbumObject) {
|
||||||
historyNotifier.addAlbums([options.collection as AlbumSimple]);
|
historyNotifier.addAlbums(
|
||||||
|
[options.collection as SpotubeSimpleAlbumObject]);
|
||||||
} else {
|
} else {
|
||||||
historyNotifier
|
historyNotifier.addPlaylists(
|
||||||
.addPlaylists([options.collection as PlaylistSimple]);
|
[options.collection as SpotubeSimplePlaylistObject]);
|
||||||
}
|
}
|
||||||
notifier.deselectAllTracks();
|
notifier.deselectAllTracks();
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
@ -158,11 +161,12 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
|
|||||||
{
|
{
|
||||||
playlistNotifier.addTracks(tracks);
|
playlistNotifier.addTracks(tracks);
|
||||||
playlistNotifier.addCollection(options.collectionId);
|
playlistNotifier.addCollection(options.collectionId);
|
||||||
if (options.collection is AlbumSimple) {
|
if (options.collection is SpotubeSimpleAlbumObject) {
|
||||||
historyNotifier.addAlbums([options.collection as AlbumSimple]);
|
historyNotifier.addAlbums(
|
||||||
|
[options.collection as SpotubeSimpleAlbumObject]);
|
||||||
} else {
|
} else {
|
||||||
historyNotifier
|
historyNotifier.addPlaylists(
|
||||||
.addPlaylists([options.collection as PlaylistSimple]);
|
[options.collection as SpotubeSimplePlaylistObject]);
|
||||||
}
|
}
|
||||||
notifier.deselectAllTracks();
|
notifier.deselectAllTracks();
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
|
|
||||||
class PaginationProps {
|
class PaginationProps {
|
||||||
final bool hasNextPage;
|
final bool hasNextPage;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final VoidCallback onFetchMore;
|
final VoidCallback onFetchMore;
|
||||||
final Future<void> Function() onRefresh;
|
final Future<void> Function() onRefresh;
|
||||||
final Future<List<Track>> Function() onFetchAll;
|
final Future<List<SpotubeFullTrackObject>> Function() onFetchAll;
|
||||||
|
|
||||||
const PaginationProps({
|
const PaginationProps({
|
||||||
required this.hasNextPage,
|
required this.hasNextPage,
|
||||||
@ -46,7 +46,7 @@ class TrackPresentationOptions {
|
|||||||
final String? ownerImage;
|
final String? ownerImage;
|
||||||
final String image;
|
final String image;
|
||||||
final String routePath;
|
final String routePath;
|
||||||
final List<Track> tracks;
|
final List<SpotubeFullTrackObject> tracks;
|
||||||
final PaginationProps pagination;
|
final PaginationProps pagination;
|
||||||
final bool isLiked;
|
final bool isLiked;
|
||||||
final String? shareUrl;
|
final String? shareUrl;
|
||||||
@ -67,11 +67,12 @@ class TrackPresentationOptions {
|
|||||||
this.shareUrl,
|
this.shareUrl,
|
||||||
this.isLiked = false,
|
this.isLiked = false,
|
||||||
this.onHeart,
|
this.onHeart,
|
||||||
}) : assert(collection is AlbumSimple || collection is PlaylistSimple);
|
}) : assert(collection is SpotubeSimpleAlbumObject ||
|
||||||
|
collection is SpotubeSimplePlaylistObject);
|
||||||
|
|
||||||
String get collectionId => collection is AlbumSimple
|
String get collectionId => collection is SpotubeSimpleAlbumObject
|
||||||
? (collection as AlbumSimple).id!
|
? (collection as SpotubeSimpleAlbumObject).id
|
||||||
: (collection as PlaylistSimple).id!;
|
: (collection as SpotubeSimplePlaylistObject).id;
|
||||||
|
|
||||||
static TrackPresentationOptions of(BuildContext context) {
|
static TrackPresentationOptions of(BuildContext context) {
|
||||||
return Data.of<TrackPresentationOptions>(context);
|
return Data.of<TrackPresentationOptions>(context);
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/pages/library/user_local_tracks/user_local_tracks.dart';
|
import 'package:spotube/pages/library/user_local_tracks/user_local_tracks.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/tracks/album.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class PresentationState {
|
class PresentationState {
|
||||||
final List<Track> selectedTracks;
|
final List<SpotubeTrackObject> selectedTracks;
|
||||||
final List<Track> presentationTracks;
|
final List<SpotubeTrackObject> presentationTracks;
|
||||||
final SortBy sortBy;
|
final SortBy sortBy;
|
||||||
|
|
||||||
const PresentationState({
|
const PresentationState({
|
||||||
@ -18,8 +20,8 @@ class PresentationState {
|
|||||||
});
|
});
|
||||||
|
|
||||||
PresentationState copyWith({
|
PresentationState copyWith({
|
||||||
List<Track>? selectedTracks,
|
List<SpotubeTrackObject>? selectedTracks,
|
||||||
List<Track>? presentationTracks,
|
List<SpotubeTrackObject>? presentationTracks,
|
||||||
SortBy? sortBy,
|
SortBy? sortBy,
|
||||||
}) {
|
}) {
|
||||||
return PresentationState(
|
return PresentationState(
|
||||||
@ -34,15 +36,15 @@ class PresentationStateNotifier
|
|||||||
extends AutoDisposeFamilyNotifier<PresentationState, Object> {
|
extends AutoDisposeFamilyNotifier<PresentationState, Object> {
|
||||||
@override
|
@override
|
||||||
PresentationState build(collection) {
|
PresentationState build(collection) {
|
||||||
if (arg case PlaylistSimple() || AlbumSimple()) {
|
if (arg case SpotubeSimplePlaylistObject() || SpotubeSimpleAlbumObject()) {
|
||||||
if (isSavedTrackPlaylist) {
|
if (isSavedTrackPlaylist) {
|
||||||
ref.listen(
|
ref.listen(
|
||||||
likedTracksProvider,
|
metadataPluginSavedTracksProvider,
|
||||||
(previous, next) {
|
(previous, next) {
|
||||||
next.whenData((value) {
|
next.whenData((value) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
presentationTracks: ServiceUtils.sortTracks(
|
presentationTracks: ServiceUtils.sortTracks(
|
||||||
value,
|
value.items,
|
||||||
state.sortBy,
|
state.sortBy,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -51,9 +53,11 @@ class PresentationStateNotifier
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ref.listen(
|
ref.listen(
|
||||||
arg is PlaylistSimple
|
arg is SpotubeSimplePlaylistObject
|
||||||
? playlistTracksProvider((arg as PlaylistSimple).id!)
|
? metadataPluginPlaylistTracksProvider(
|
||||||
: albumTracksProvider((arg as AlbumSimple)),
|
(arg as SpotubeSimplePlaylistObject).id)
|
||||||
|
: metadataPluginAlbumTracksProvider(
|
||||||
|
(arg as SpotubeSimpleAlbumObject).id),
|
||||||
(previous, next) {
|
(previous, next) {
|
||||||
next.whenData((value) {
|
next.whenData((value) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
@ -76,36 +80,39 @@ class PresentationStateNotifier
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool get isSavedTrackPlaylist =>
|
bool get isSavedTrackPlaylist =>
|
||||||
arg is PlaylistSimple &&
|
arg is SpotubeSimplePlaylistObject &&
|
||||||
(arg as PlaylistSimple).id == "user-liked-tracks";
|
(arg as SpotubeSimplePlaylistObject).id == "user-liked-tracks";
|
||||||
|
|
||||||
List<Track> get tracks {
|
List<SpotubeTrackObject> get tracks {
|
||||||
assert(
|
assert(
|
||||||
arg is PlaylistSimple || arg is AlbumSimple,
|
arg is SpotubeSimplePlaylistObject || arg is SpotubeSimpleAlbumObject,
|
||||||
"arg must be PlaylistSimple or AlbumSimple",
|
"arg must be SpotubeSimplePlaylistObject or SpotubeSimpleAlbumObject",
|
||||||
);
|
);
|
||||||
|
|
||||||
final isPlaylist = arg is PlaylistSimple;
|
final isPlaylist = arg is SpotubeSimplePlaylistObject;
|
||||||
|
|
||||||
final tracks = switch ((isPlaylist, isSavedTrackPlaylist)) {
|
final tracks = switch ((isPlaylist, isSavedTrackPlaylist)) {
|
||||||
(true, true) => ref.read(likedTracksProvider).asData?.value,
|
(true, true) =>
|
||||||
|
ref.read(metadataPluginSavedTracksProvider).asData?.value.items,
|
||||||
(true, false) => ref
|
(true, false) => ref
|
||||||
.read(playlistTracksProvider((arg as PlaylistSimple).id!))
|
.read(metadataPluginPlaylistTracksProvider(
|
||||||
|
(arg as SpotubeSimplePlaylistObject).id))
|
||||||
.asData
|
.asData
|
||||||
?.value
|
?.value
|
||||||
.items,
|
.items,
|
||||||
_ => ref
|
_ => ref
|
||||||
.read(albumTracksProvider((arg as AlbumSimple)))
|
.read(metadataPluginAlbumTracksProvider(
|
||||||
|
(arg as SpotubeSimpleAlbumObject).id))
|
||||||
.asData
|
.asData
|
||||||
?.value
|
?.value
|
||||||
.items,
|
.items,
|
||||||
} ??
|
} ??
|
||||||
[];
|
<SpotubeFullTrackObject>[];
|
||||||
|
|
||||||
return tracks;
|
return tracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
void selectTrack(Track track) {
|
void selectTrack(SpotubeTrackObject track) {
|
||||||
if (state.selectedTracks.any((e) => e.id == track.id)) {
|
if (state.selectedTracks.any((e) => e.id == track.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -121,7 +128,7 @@ class PresentationStateNotifier
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void deselectTrack(Track track) {
|
void deselectTrack(SpotubeTrackObject track) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedTracks: state.selectedTracks.where((e) => e != track).toList(),
|
selectedTracks: state.selectedTracks.where((e) => e != track).toList(),
|
||||||
);
|
);
|
||||||
@ -141,7 +148,7 @@ class PresentationStateNotifier
|
|||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
presentationTracks: ServiceUtils.sortTracks(
|
presentationTracks: ServiceUtils.sortTracks(
|
||||||
tracks
|
tracks
|
||||||
.map((e) => (weightedRatio(e.name!, query), e))
|
.map((e) => (weightedRatio(e.name, query), e))
|
||||||
.sorted((a, b) => b.$1.compareTo(a.$1))
|
.sorted((a, b) => b.$1.compareTo(a.$1))
|
||||||
.where((e) => e.$1 > 50)
|
.where((e) => e.$1 > 50)
|
||||||
.map((e) => e.$2)
|
.map((e) => e.$2)
|
||||||
|
|||||||
@ -3,8 +3,6 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/env.dart';
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/heart_button/heart_button.dart';
|
import 'package:spotube/components/heart_button/heart_button.dart';
|
||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
@ -14,7 +12,6 @@ import 'package:spotube/components/track_presentation/use_is_user_playlist.dart'
|
|||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
|
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
|
||||||
|
|
||||||
class TrackPresentationTopSection extends HookConsumerWidget {
|
class TrackPresentationTopSection extends HookConsumerWidget {
|
||||||
const TrackPresentationTopSection({super.key});
|
const TrackPresentationTopSection({super.key});
|
||||||
@ -26,25 +23,10 @@ class TrackPresentationTopSection extends HookConsumerWidget {
|
|||||||
final scale = context.theme.scaling;
|
final scale = context.theme.scaling;
|
||||||
final isUserPlaylist = useIsUserPlaylist(ref, options.collectionId);
|
final isUserPlaylist = useIsUserPlaylist(ref, options.collectionId);
|
||||||
|
|
||||||
final playlistImage = (options.collection is PlaylistSimple &&
|
final decorationImage = DecorationImage(
|
||||||
(options.collection as PlaylistSimple).owner?.displayName ==
|
image: UniversalImage.imageProvider(options.image),
|
||||||
"Spotify" &&
|
fit: BoxFit.cover,
|
||||||
Env.disableSpotifyImages)
|
);
|
||||||
? ref.watch(playlistImageProvider(options.collectionId))
|
|
||||||
: null;
|
|
||||||
final decorationImage = playlistImage != null
|
|
||||||
? DecorationImage(
|
|
||||||
image: AssetImage(playlistImage.src),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
colorFilter: ColorFilter.mode(
|
|
||||||
playlistImage.color,
|
|
||||||
playlistImage.colorBlendMode,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: DecorationImage(
|
|
||||||
image: UniversalImage.imageProvider(options.image),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
);
|
|
||||||
|
|
||||||
final imageDimension = mediaQuery.mdAndUp ? 200 : 120;
|
final imageDimension = mediaQuery.mdAndUp ? 200 : 120;
|
||||||
|
|
||||||
@ -116,7 +98,7 @@ class TrackPresentationTopSection extends HookConsumerWidget {
|
|||||||
builder: (context) {
|
builder: (context) {
|
||||||
return PlaylistCreateDialog(
|
return PlaylistCreateDialog(
|
||||||
playlistId: options.collectionId,
|
playlistId: options.collectionId,
|
||||||
trackIds: options.tracks.map((e) => e.id!).toList(),
|
trackIds: options.tracks.map((e) => e.id).toList(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,11 +2,11 @@ import 'dart:math';
|
|||||||
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/components/dialogs/select_device_dialog.dart';
|
import 'package:spotube/components/dialogs/select_device_dialog.dart';
|
||||||
import 'package:spotube/components/track_presentation/presentation_props.dart';
|
import 'package:spotube/components/track_presentation/presentation_props.dart';
|
||||||
|
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/connect/connect.dart';
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
import 'package:spotube/provider/history/history.dart';
|
import 'package:spotube/provider/history/history.dart';
|
||||||
@ -45,14 +45,14 @@ UseActionCallbacks useActionCallbacks(WidgetRef ref) {
|
|||||||
final allTracks = await options.pagination.onFetchAll();
|
final allTracks = await options.pagination.onFetchAll();
|
||||||
final remotePlayback = ref.read(connectProvider.notifier);
|
final remotePlayback = ref.read(connectProvider.notifier);
|
||||||
await remotePlayback.load(
|
await remotePlayback.load(
|
||||||
options.collection is AlbumSimple
|
options.collection is SpotubeSimpleAlbumObject
|
||||||
? WebSocketLoadEventData.album(
|
? WebSocketLoadEventData.album(
|
||||||
tracks: allTracks,
|
tracks: allTracks,
|
||||||
collection: options.collection as AlbumSimple,
|
collection: options.collection as SpotubeSimpleAlbumObject,
|
||||||
initialIndex: Random().nextInt(allTracks.length))
|
initialIndex: Random().nextInt(allTracks.length))
|
||||||
: WebSocketLoadEventData.playlist(
|
: WebSocketLoadEventData.playlist(
|
||||||
tracks: allTracks,
|
tracks: allTracks,
|
||||||
collection: options.collection as PlaylistSimple,
|
collection: options.collection as SpotubeSimplePlaylistObject,
|
||||||
initialIndex: Random().nextInt(allTracks.length),
|
initialIndex: Random().nextInt(allTracks.length),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -65,10 +65,12 @@ UseActionCallbacks useActionCallbacks(WidgetRef ref) {
|
|||||||
);
|
);
|
||||||
await audioPlayer.setShuffle(true);
|
await audioPlayer.setShuffle(true);
|
||||||
playlistNotifier.addCollection(options.collectionId);
|
playlistNotifier.addCollection(options.collectionId);
|
||||||
if (options.collection is AlbumSimple) {
|
if (options.collection is SpotubeSimpleAlbumObject) {
|
||||||
historyNotifier.addAlbums([options.collection as AlbumSimple]);
|
historyNotifier
|
||||||
|
.addAlbums([options.collection as SpotubeSimpleAlbumObject]);
|
||||||
} else {
|
} else {
|
||||||
historyNotifier.addPlaylists([options.collection as PlaylistSimple]);
|
historyNotifier.addPlaylists(
|
||||||
|
[options.collection as SpotubeSimplePlaylistObject]);
|
||||||
}
|
}
|
||||||
|
|
||||||
final allTracks = await options.pagination.onFetchAll();
|
final allTracks = await options.pagination.onFetchAll();
|
||||||
@ -96,23 +98,25 @@ UseActionCallbacks useActionCallbacks(WidgetRef ref) {
|
|||||||
final allTracks = await options.pagination.onFetchAll();
|
final allTracks = await options.pagination.onFetchAll();
|
||||||
final remotePlayback = ref.read(connectProvider.notifier);
|
final remotePlayback = ref.read(connectProvider.notifier);
|
||||||
await remotePlayback.load(
|
await remotePlayback.load(
|
||||||
options.collection is AlbumSimple
|
options.collection is SpotubeSimpleAlbumObject
|
||||||
? WebSocketLoadEventData.album(
|
? WebSocketLoadEventData.album(
|
||||||
tracks: allTracks,
|
tracks: allTracks,
|
||||||
collection: options.collection as AlbumSimple,
|
collection: options.collection as SpotubeSimpleAlbumObject,
|
||||||
)
|
)
|
||||||
: WebSocketLoadEventData.playlist(
|
: WebSocketLoadEventData.playlist(
|
||||||
tracks: allTracks,
|
tracks: allTracks,
|
||||||
collection: options.collection as PlaylistSimple,
|
collection: options.collection as SpotubeSimplePlaylistObject,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await playlistNotifier.load(initialTracks, autoPlay: true);
|
await playlistNotifier.load(initialTracks, autoPlay: true);
|
||||||
playlistNotifier.addCollection(options.collectionId);
|
playlistNotifier.addCollection(options.collectionId);
|
||||||
if (options.collection is AlbumSimple) {
|
if (options.collection is SpotubeSimpleAlbumObject) {
|
||||||
historyNotifier.addAlbums([options.collection as AlbumSimple]);
|
historyNotifier
|
||||||
|
.addAlbums([options.collection as SpotubeSimpleAlbumObject]);
|
||||||
} else {
|
} else {
|
||||||
historyNotifier.addPlaylists([options.collection as PlaylistSimple]);
|
historyNotifier.addPlaylists(
|
||||||
|
[options.collection as SpotubeSimplePlaylistObject]);
|
||||||
}
|
}
|
||||||
|
|
||||||
final allTracks = await options.pagination.onFetchAll();
|
final allTracks = await options.pagination.onFetchAll();
|
||||||
|
|||||||
@ -1,17 +1,18 @@
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/user.dart';
|
||||||
|
|
||||||
bool useIsUserPlaylist(WidgetRef ref, String playlistId) {
|
bool useIsUserPlaylist(WidgetRef ref, String playlistId) {
|
||||||
final userPlaylistsQuery = ref.watch(favoritePlaylistsProvider);
|
final userPlaylistsQuery = ref.watch(metadataPluginSavedPlaylistsProvider);
|
||||||
final me = ref.watch(meProvider);
|
final me = ref.watch(metadataPluginUserProvider);
|
||||||
|
|
||||||
return useMemoized(
|
return useMemoized(
|
||||||
() =>
|
() =>
|
||||||
userPlaylistsQuery.asData?.value.items.any((e) =>
|
userPlaylistsQuery.asData?.value.items.any((e) =>
|
||||||
e.id == playlistId &&
|
e.id == playlistId &&
|
||||||
me.asData?.value != null &&
|
me.asData?.value != null &&
|
||||||
e.owner?.id == me.asData?.value.id) ??
|
e.owner.id == me.asData?.value?.id) ??
|
||||||
false,
|
false,
|
||||||
[userPlaylistsQuery.asData?.value, playlistId, me.asData?.value],
|
[userPlaylistsQuery.asData?.value, playlistId, me.asData?.value],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,18 +1,19 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/components/dialogs/select_device_dialog.dart';
|
import 'package:spotube/components/dialogs/select_device_dialog.dart';
|
||||||
import 'package:spotube/components/track_presentation/presentation_props.dart';
|
import 'package:spotube/components/track_presentation/presentation_props.dart';
|
||||||
import 'package:spotube/components/track_presentation/presentation_state.dart';
|
import 'package:spotube/components/track_presentation/presentation_state.dart';
|
||||||
import 'package:spotube/extensions/list.dart';
|
import 'package:spotube/extensions/list.dart';
|
||||||
|
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/connect/connect.dart';
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
import 'package:spotube/provider/history/history.dart';
|
import 'package:spotube/provider/history/history.dart';
|
||||||
|
|
||||||
Future<void> Function(Track track, int index) useTrackTilePlayCallback(
|
Future<void> Function(SpotubeTrackObject track, int index)
|
||||||
|
useTrackTilePlayCallback(
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
) {
|
) {
|
||||||
final context = useContext();
|
final context = useContext();
|
||||||
@ -26,7 +27,8 @@ Future<void> Function(Track track, int index) useTrackTilePlayCallback(
|
|||||||
[playlist.collections, options.collectionId],
|
[playlist.collections, options.collectionId],
|
||||||
);
|
);
|
||||||
|
|
||||||
final onTapTrackTile = useCallback((Track track, int index) async {
|
final onTapTrackTile =
|
||||||
|
useCallback((SpotubeTrackObject track, int index) async {
|
||||||
final state = ref.read(presentationStateProvider(options.collection));
|
final state = ref.read(presentationStateProvider(options.collection));
|
||||||
final notifier =
|
final notifier =
|
||||||
ref.read(presentationStateProvider(options.collection).notifier);
|
ref.read(presentationStateProvider(options.collection).notifier);
|
||||||
@ -52,15 +54,15 @@ Future<void> Function(Track track, int index) useTrackTilePlayCallback(
|
|||||||
} else {
|
} else {
|
||||||
final tracks = await options.pagination.onFetchAll();
|
final tracks = await options.pagination.onFetchAll();
|
||||||
await remotePlayback.load(
|
await remotePlayback.load(
|
||||||
options.collection is AlbumSimple
|
options.collection is SpotubeSimpleAlbumObject
|
||||||
? WebSocketLoadEventData.album(
|
? WebSocketLoadEventData.album(
|
||||||
tracks: tracks,
|
tracks: tracks,
|
||||||
collection: options.collection as AlbumSimple,
|
collection: options.collection as SpotubeSimpleAlbumObject,
|
||||||
initialIndex: index,
|
initialIndex: index,
|
||||||
)
|
)
|
||||||
: WebSocketLoadEventData.playlist(
|
: WebSocketLoadEventData.playlist(
|
||||||
tracks: tracks,
|
tracks: tracks,
|
||||||
collection: options.collection as PlaylistSimple,
|
collection: options.collection as SpotubeSimplePlaylistObject,
|
||||||
initialIndex: index,
|
initialIndex: index,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -76,10 +78,12 @@ Future<void> Function(Track track, int index) useTrackTilePlayCallback(
|
|||||||
autoPlay: true,
|
autoPlay: true,
|
||||||
);
|
);
|
||||||
playlistNotifier.addCollection(options.collectionId);
|
playlistNotifier.addCollection(options.collectionId);
|
||||||
if (options.collection is AlbumSimple) {
|
if (options.collection is SpotubeSimpleAlbumObject) {
|
||||||
historyNotifier.addAlbums([options.collection as AlbumSimple]);
|
historyNotifier
|
||||||
|
.addAlbums([options.collection as SpotubeSimpleAlbumObject]);
|
||||||
} else {
|
} else {
|
||||||
historyNotifier.addPlaylists([options.collection as PlaylistSimple]);
|
historyNotifier.addPlaylists(
|
||||||
|
[options.collection as SpotubeSimplePlaylistObject]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
|
||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
import 'package:spotify/spotify.dart' hide Offset;
|
|
||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
@ -21,15 +20,18 @@ import 'package:spotube/components/image/universal_image.dart';
|
|||||||
import 'package:spotube/components/links/artist_link.dart';
|
import 'package:spotube/components/links/artist_link.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
|
||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/authentication/authentication.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/user.dart';
|
||||||
|
import 'package:spotube/services/metadata/endpoints/error.dart';
|
||||||
|
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
@ -50,8 +52,9 @@ enum TrackOptionValue {
|
|||||||
startRadio,
|
startRadio,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [track] must be a [SpotubeFullTrackObject] or [SpotubeLocalTrackObject]
|
||||||
class TrackOptions extends HookConsumerWidget {
|
class TrackOptions extends HookConsumerWidget {
|
||||||
final Track track;
|
final SpotubeTrackObject track;
|
||||||
final bool userPlaylist;
|
final bool userPlaylist;
|
||||||
final String? playlistId;
|
final String? playlistId;
|
||||||
final ObjectRef<ValueChanged<RelativeRect>?>? showMenuCbRef;
|
final ObjectRef<ValueChanged<RelativeRect>?>? showMenuCbRef;
|
||||||
@ -63,9 +66,12 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
this.userPlaylist = false,
|
this.userPlaylist = false,
|
||||||
this.playlistId,
|
this.playlistId,
|
||||||
this.icon,
|
this.icon,
|
||||||
});
|
}) : assert(
|
||||||
|
track is SpotubeFullTrackObject || track is SpotubeLocalTrackObject,
|
||||||
|
"Track must be a SpotubeFullTrackObject, SpotubeLocalTrackObject",
|
||||||
|
);
|
||||||
|
|
||||||
void actionShare(BuildContext context, Track track) {
|
void actionShare(BuildContext context, SpotubeTrackObject track) {
|
||||||
final data = "https://open.spotify.com/track/${track.id}";
|
final data = "https://open.spotify.com/track/${track.id}";
|
||||||
Clipboard.setData(ClipboardData(text: data)).then((_) {
|
Clipboard.setData(ClipboardData(text: data)).then((_) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
@ -87,7 +93,7 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
|
|
||||||
void actionAddToPlaylist(
|
void actionAddToPlaylist(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Track track,
|
SpotubeTrackObject track,
|
||||||
) {
|
) {
|
||||||
/// showDialog doesn't work for some reason. So we have to
|
/// showDialog doesn't work for some reason. So we have to
|
||||||
/// manually push a Dialog Route in the Navigator to get it working
|
/// manually push a Dialog Route in the Navigator to get it working
|
||||||
@ -105,32 +111,32 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
void actionStartRadio(
|
void actionStartRadio(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
Track track,
|
SpotubeTrackObject track,
|
||||||
) async {
|
) async {
|
||||||
final playback = ref.read(audioPlayerProvider.notifier);
|
final playback = ref.read(audioPlayerProvider.notifier);
|
||||||
final playlist = ref.read(audioPlayerProvider);
|
final playlist = ref.read(audioPlayerProvider);
|
||||||
final spotify = ref.read(spotifyProvider);
|
|
||||||
final query = "${track.name} Radio";
|
final query = "${track.name} Radio";
|
||||||
final pages = await spotify.invoke(
|
final metadataPlugin = await ref.read(metadataPluginProvider.future);
|
||||||
(api) => api.search.get(query, types: [SearchType.playlist]).first(),
|
|
||||||
);
|
|
||||||
|
|
||||||
final radios = pages
|
if (metadataPlugin == null) {
|
||||||
.expand((e) => e.items?.cast<PlaylistSimple>().toList() ?? [])
|
throw MetadataPluginException.noDefaultPlugin(
|
||||||
.toList();
|
"No default metadata plugin set",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final artists = track.artists!.map((e) => e.name);
|
final pages = await metadataPlugin.search.playlists(query);
|
||||||
|
|
||||||
final radio = radios.firstWhere(
|
final artists = track.artists.map((e) => e.name);
|
||||||
|
|
||||||
|
final radio = pages.items.firstWhere(
|
||||||
(e) {
|
(e) {
|
||||||
final validPlaylists =
|
final validPlaylists = artists.where((a) => e.description.contains(a));
|
||||||
artists.where((a) => e.description!.contains(a!));
|
|
||||||
return e.name == "${track.name} Radio" &&
|
return e.name == "${track.name} Radio" &&
|
||||||
(validPlaylists.length >= 2 ||
|
(validPlaylists.length >= 2 ||
|
||||||
validPlaylists.length == artists.length) &&
|
validPlaylists.length == artists.length) &&
|
||||||
e.owner?.displayName == "Spotify";
|
e.owner.name == "Spotify";
|
||||||
},
|
},
|
||||||
orElse: () => radios.first,
|
orElse: () => pages.items.first,
|
||||||
);
|
);
|
||||||
|
|
||||||
bool replaceQueue = false;
|
bool replaceQueue = false;
|
||||||
@ -154,10 +160,10 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
} else {
|
} else {
|
||||||
await playback.addTrack(track);
|
await playback.addTrack(track);
|
||||||
}
|
}
|
||||||
|
await ref.read(metadataPluginPlaylistTracksProvider(radio.id).future);
|
||||||
final tracks = await spotify.invoke(
|
final tracks = await ref
|
||||||
(api) => api.playlists.getTracksByPlaylistId(radio.id!).all(),
|
.read(metadataPluginPlaylistTracksProvider(radio.id).notifier)
|
||||||
);
|
.fetchAll();
|
||||||
|
|
||||||
await playback.addTracks(
|
await playback.addTracks(
|
||||||
tracks.toList()
|
tracks.toList()
|
||||||
@ -179,7 +185,7 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
ref.watch(downloadManagerProvider);
|
ref.watch(downloadManagerProvider);
|
||||||
final downloadManager = ref.watch(downloadManagerProvider.notifier);
|
final downloadManager = ref.watch(downloadManagerProvider.notifier);
|
||||||
final blacklist = ref.watch(blacklistProvider);
|
final blacklist = ref.watch(blacklistProvider);
|
||||||
final me = ref.watch(meProvider);
|
final me = ref.watch(metadataPluginUserProvider);
|
||||||
|
|
||||||
final favorites = useTrackToggleLike(track, ref);
|
final favorites = useTrackToggleLike(track, ref);
|
||||||
|
|
||||||
@ -192,23 +198,32 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
|
|
||||||
final removingTrack = useState<String?>(null);
|
final removingTrack = useState<String?>(null);
|
||||||
final favoritePlaylistsNotifier =
|
final favoritePlaylistsNotifier =
|
||||||
ref.watch(favoritePlaylistsProvider.notifier);
|
ref.watch(metadataPluginSavedPlaylistsProvider.notifier);
|
||||||
|
|
||||||
final isInQueue = useMemoized(() {
|
final isInDownloadQueue = useMemoized(() {
|
||||||
if (playlist.activeTrack == null) return false;
|
if (playlist.activeTrack == null ||
|
||||||
return downloadManager.isActive(playlist.activeTrack!);
|
playlist.activeTrack! is SpotubeLocalTrackObject) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return downloadManager.isActive(
|
||||||
|
playlist.activeTrack! as SpotubeFullTrackObject,
|
||||||
|
);
|
||||||
}, [
|
}, [
|
||||||
playlist.activeTrack,
|
playlist.activeTrack,
|
||||||
downloadManager,
|
downloadManager,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
final progressNotifier = useMemoized(() {
|
final progressNotifier = useMemoized(() {
|
||||||
final spotubeTrack = downloadManager.mapToSourcedTrack(track);
|
if (track is! SpotubeFullTrackObject) {
|
||||||
if (spotubeTrack == null) return null;
|
return throw Exception(
|
||||||
return downloadManager.getProgressNotifier(spotubeTrack);
|
"Invalid usage of `progressNotifierFuture`. Track must be a SpotubeFullTrackObject to get download progress",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return downloadManager
|
||||||
|
.getProgressNotifier(track as SpotubeFullTrackObject);
|
||||||
});
|
});
|
||||||
|
|
||||||
final isLocalTrack = track is LocalTrack;
|
final isLocalTrack = track is SpotubeLocalTrackObject;
|
||||||
|
|
||||||
final adaptivePopSheetList = AdaptivePopSheetList<TrackOptionValue>(
|
final adaptivePopSheetList = AdaptivePopSheetList<TrackOptionValue>(
|
||||||
tooltip: context.l10n.more_actions,
|
tooltip: context.l10n.more_actions,
|
||||||
@ -220,7 +235,7 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
// );
|
// );
|
||||||
break;
|
break;
|
||||||
case TrackOptionValue.delete:
|
case TrackOptionValue.delete:
|
||||||
await File((track as LocalTrack).path).delete();
|
await File((track as SpotubeLocalTrackObject).path).delete();
|
||||||
ref.invalidate(localTracksProvider);
|
ref.invalidate(localTracksProvider);
|
||||||
break;
|
break;
|
||||||
case TrackOptionValue.addToQueue:
|
case TrackOptionValue.addToQueue:
|
||||||
@ -232,7 +247,7 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
builder: (context, overlay) {
|
builder: (context, overlay) {
|
||||||
return SurfaceCard(
|
return SurfaceCard(
|
||||||
child: Text(
|
child: Text(
|
||||||
context.l10n.added_track_to_queue(track.name!),
|
context.l10n.added_track_to_queue(track.name),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -250,7 +265,7 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
builder: (context, overlay) {
|
builder: (context, overlay) {
|
||||||
return SurfaceCard(
|
return SurfaceCard(
|
||||||
child: Text(
|
child: Text(
|
||||||
context.l10n.track_will_play_next(track.name!),
|
context.l10n.track_will_play_next(track.name),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -259,7 +274,7 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TrackOptionValue.removeFromQueue:
|
case TrackOptionValue.removeFromQueue:
|
||||||
playback.removeTrack(track.id!);
|
playback.removeTrack(track.id);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showToast(
|
showToast(
|
||||||
@ -269,7 +284,7 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
return SurfaceCard(
|
return SurfaceCard(
|
||||||
child: Text(
|
child: Text(
|
||||||
context.l10n.removed_track_from_queue(
|
context.l10n.removed_track_from_queue(
|
||||||
track.name!,
|
track.name,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
@ -285,19 +300,19 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
actionAddToPlaylist(context, track);
|
actionAddToPlaylist(context, track);
|
||||||
break;
|
break;
|
||||||
case TrackOptionValue.removeFromPlaylist:
|
case TrackOptionValue.removeFromPlaylist:
|
||||||
removingTrack.value = track.uri;
|
removingTrack.value = track.externalUri;
|
||||||
favoritePlaylistsNotifier
|
favoritePlaylistsNotifier
|
||||||
.removeTracks(playlistId ?? "", [track.id!]);
|
.removeTracks(playlistId ?? "", [track.id]);
|
||||||
break;
|
break;
|
||||||
case TrackOptionValue.blacklist:
|
case TrackOptionValue.blacklist:
|
||||||
if (isBlackListed == null) break;
|
if (isBlackListed == null) break;
|
||||||
if (isBlackListed == true) {
|
if (isBlackListed == true) {
|
||||||
await ref.read(blacklistProvider.notifier).remove(track.id!);
|
await ref.read(blacklistProvider.notifier).remove(track.id);
|
||||||
} else {
|
} else {
|
||||||
await ref.read(blacklistProvider.notifier).add(
|
await ref.read(blacklistProvider.notifier).add(
|
||||||
BlacklistTableCompanion.insert(
|
BlacklistTableCompanion.insert(
|
||||||
name: track.name!,
|
name: track.name,
|
||||||
elementId: track.id!,
|
elementId: track.id,
|
||||||
elementType: BlacklistedType.track,
|
elementType: BlacklistedType.track,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -311,16 +326,19 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
await launchUrlString(url);
|
await launchUrlString(url);
|
||||||
break;
|
break;
|
||||||
case TrackOptionValue.details:
|
case TrackOptionValue.details:
|
||||||
|
if (track is! SpotubeFullTrackObject) break;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => ConstrainedBox(
|
builder: (context) => ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 400),
|
constraints: const BoxConstraints(maxWidth: 400),
|
||||||
child: TrackDetailsDialog(track: track),
|
child:
|
||||||
|
TrackDetailsDialog(track: track as SpotubeFullTrackObject),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case TrackOptionValue.download:
|
case TrackOptionValue.download:
|
||||||
await downloadManager.addToQueue(track);
|
if (track is! SpotubeFullTrackObject) break;
|
||||||
|
await downloadManager.addToQueue(track as SpotubeFullTrackObject);
|
||||||
break;
|
break;
|
||||||
case TrackOptionValue.startRadio:
|
case TrackOptionValue.startRadio:
|
||||||
actionStartRadio(context, ref, track);
|
actionStartRadio(context, ref, track);
|
||||||
@ -336,23 +354,23 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
child: UniversalImage(
|
child: UniversalImage(
|
||||||
path: track.album!.images
|
path: track.album.images
|
||||||
.asUrlString(placeholder: ImagePlaceholder.albumArt),
|
.asUrlString(placeholder: ImagePlaceholder.albumArt),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
track.name!,
|
track.name,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
).semiBold(),
|
).semiBold(),
|
||||||
subtitle: Align(
|
subtitle: Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: ArtistLink(
|
child: ArtistLink(
|
||||||
artists: track.artists!,
|
artists: track.artists,
|
||||||
onOverflowArtistClick: () => context.navigateTo(
|
onOverflowArtistClick: () => context.navigateTo(
|
||||||
TrackRoute(trackId: track.id!),
|
TrackRoute(trackId: track.id),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -375,7 +393,7 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(context.l10n.go_to_album),
|
Text(context.l10n.go_to_album),
|
||||||
Text(
|
Text(
|
||||||
track.album!.name!,
|
track.album.name,
|
||||||
style: context.theme.typography.xSmall,
|
style: context.theme.typography.xSmall,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -435,12 +453,12 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
if (!isLocalTrack)
|
if (!isLocalTrack)
|
||||||
AdaptiveMenuButton(
|
AdaptiveMenuButton(
|
||||||
value: TrackOptionValue.download,
|
value: TrackOptionValue.download,
|
||||||
enabled: !isInQueue,
|
enabled: !isInDownloadQueue,
|
||||||
leading: isInQueue
|
leading: isInDownloadQueue
|
||||||
? HookBuilder(builder: (context) {
|
? HookBuilder(builder: (context) {
|
||||||
final progress = useListenable(progressNotifier!);
|
final progress = useListenable(progressNotifier);
|
||||||
return CircularProgressIndicator(
|
return CircularProgressIndicator(
|
||||||
value: progress.value,
|
value: progress?.value,
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
: const Icon(SpotubeIcons.download),
|
: const Icon(SpotubeIcons.download),
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/hover_builder.dart';
|
import 'package:spotube/components/hover_builder.dart';
|
||||||
@ -16,11 +15,9 @@ import 'package:spotube/components/links/artist_link.dart';
|
|||||||
import 'package:spotube/components/links/link_text.dart';
|
import 'package:spotube/components/links/link_text.dart';
|
||||||
import 'package:spotube/components/track_tile/track_options.dart';
|
import 'package:spotube/components/track_tile/track_options.dart';
|
||||||
import 'package:spotube/components/ui/button_tile.dart';
|
import 'package:spotube/components/ui/button_tile.dart';
|
||||||
import 'package:spotube/extensions/artist_simple.dart';
|
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/duration.dart';
|
import 'package:spotube/extensions/duration.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
|
||||||
import 'package:spotube/provider/audio_player/querying_track_info.dart';
|
import 'package:spotube/provider/audio_player/querying_track_info.dart';
|
||||||
import 'package:spotube/provider/audio_player/state.dart';
|
import 'package:spotube/provider/audio_player/state.dart';
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
@ -29,7 +26,7 @@ import 'package:spotube/utils/platform.dart';
|
|||||||
class TrackTile extends HookConsumerWidget {
|
class TrackTile extends HookConsumerWidget {
|
||||||
/// [index] will not be shown if null
|
/// [index] will not be shown if null
|
||||||
final int? index;
|
final int? index;
|
||||||
final Track track;
|
final SpotubeTrackObject track;
|
||||||
final bool selected;
|
final bool selected;
|
||||||
final ValueChanged<bool?>? onChanged;
|
final ValueChanged<bool?>? onChanged;
|
||||||
final Future<void> Function()? onTap;
|
final Future<void> Function()? onTap;
|
||||||
@ -151,7 +148,7 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
image: UniversalImage.imageProvider(
|
image: UniversalImage.imageProvider(
|
||||||
(track.album?.images).asUrlString(
|
(track.album.images).asUrlString(
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -217,8 +214,8 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
flex: 6,
|
flex: 6,
|
||||||
child: switch (track) {
|
child: switch (track) {
|
||||||
LocalTrack() => Text(
|
SpotubeLocalTrackObject() => Text(
|
||||||
track.name!,
|
track.name,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@ -233,10 +230,10 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context
|
context
|
||||||
.navigateTo(TrackRoute(trackId: track.id!));
|
.navigateTo(TrackRoute(trackId: track.id));
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
track.name!,
|
track.name,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@ -251,22 +248,22 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
flex: 4,
|
flex: 4,
|
||||||
child: switch (track) {
|
child: switch (track) {
|
||||||
LocalTrack() => Text(
|
SpotubeLocalTrackObject() => Text(
|
||||||
track.album!.name!,
|
track.album.name,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
_ => Align(
|
_ => Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
/* child: LinkText(
|
child: LinkText(
|
||||||
track.album!.name!,
|
track.album.name,
|
||||||
AlbumRoute(
|
AlbumRoute(
|
||||||
album: track.album!,
|
album: track.album,
|
||||||
id: track.album!.id!,
|
id: track.album.id,
|
||||||
),
|
),
|
||||||
push: true,
|
push: true,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
), */
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -275,18 +272,18 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
subtitle: Align(
|
subtitle: Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: track is LocalTrack
|
child: track is SpotubeLocalTrackObject
|
||||||
? Text(
|
? Text(
|
||||||
track.artists?.asString() ?? '',
|
track.artists.asString(),
|
||||||
)
|
)
|
||||||
: ClipRect(
|
: ClipRect(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxHeight: 40),
|
constraints: const BoxConstraints(maxHeight: 40),
|
||||||
child: ArtistLink(
|
child: ArtistLink(
|
||||||
artists: track.artists ?? [],
|
artists: track.artists,
|
||||||
onOverflowArtistClick: () {
|
onOverflowArtistClick: () {
|
||||||
context.navigateTo(
|
context.navigateTo(
|
||||||
TrackRoute(trackId: track.id!),
|
TrackRoute(trackId: track.id),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -298,7 +295,7 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
Duration(milliseconds: track.durationMs ?? 0)
|
Duration(milliseconds: track.durationMs)
|
||||||
.toHumanReadableString(padZero: false),
|
.toHumanReadableString(padZero: false),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
|||||||
@ -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:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/collections/routes.dart';
|
import 'package:spotube/collections/routes.dart';
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
|
||||||
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
|
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
|
||||||
import 'package:flutter_sharing_intent/model/sharing_file.dart';
|
import 'package:flutter_sharing_intent/model/sharing_file.dart';
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
@ -14,93 +13,95 @@ import 'package:spotube/utils/platform.dart';
|
|||||||
final appLinks = AppLinks();
|
final appLinks = AppLinks();
|
||||||
final linkStream = appLinks.stringLinkStream.asBroadcastStream();
|
final linkStream = appLinks.stringLinkStream.asBroadcastStream();
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
"Deeplinking is deprecated. Later a custom API for metadata provider will be created.")
|
||||||
void useDeepLinking(WidgetRef ref, AppRouter router) {
|
void useDeepLinking(WidgetRef ref, AppRouter router) {
|
||||||
// single instance no worries
|
// // single instance no worries
|
||||||
final spotify = ref.watch(spotifyProvider);
|
// final spotify = ref.watch(spotifyProvider);
|
||||||
|
|
||||||
useEffect(() {
|
// useEffect(() {
|
||||||
void uriListener(List<SharedFile> files) async {
|
// void uriListener(List<SharedFile> files) async {
|
||||||
for (final file in files) {
|
// for (final file in files) {
|
||||||
if (file.type != SharedMediaType.URL) continue;
|
// if (file.type != SharedMediaType.URL) continue;
|
||||||
final url = Uri.parse(file.value!);
|
// final url = Uri.parse(file.value!);
|
||||||
if (url.pathSegments.length != 2) continue;
|
// if (url.pathSegments.length != 2) continue;
|
||||||
|
|
||||||
switch (url.pathSegments.first) {
|
// switch (url.pathSegments.first) {
|
||||||
case "album":
|
// case "album":
|
||||||
final album = await spotify.invoke((api) {
|
// final album = await spotify.invoke((api) {
|
||||||
return api.albums.get(url.pathSegments.last);
|
// return api.albums.get(url.pathSegments.last);
|
||||||
});
|
// });
|
||||||
// router.navigate(
|
// // router.navigate(
|
||||||
// AlbumRoute(id: album.id!, album: album),
|
// // AlbumRoute(id: album.id!, album: album),
|
||||||
// );
|
// // );
|
||||||
break;
|
// break;
|
||||||
case "artist":
|
// case "artist":
|
||||||
router.navigate(ArtistRoute(artistId: url.pathSegments.last));
|
// router.navigate(ArtistRoute(artistId: url.pathSegments.last));
|
||||||
break;
|
// break;
|
||||||
case "playlist":
|
// case "playlist":
|
||||||
final playlist = await spotify.invoke((api) {
|
// final playlist = await spotify.invoke((api) {
|
||||||
return api.playlists.get(url.pathSegments.last);
|
// return api.playlists.get(url.pathSegments.last);
|
||||||
});
|
// });
|
||||||
// router
|
// // router
|
||||||
// .navigate(PlaylistRoute(id: playlist.id!, playlist: playlist));
|
// // .navigate(PlaylistRoute(id: playlist.id!, playlist: playlist));
|
||||||
break;
|
// break;
|
||||||
case "track":
|
// case "track":
|
||||||
router.navigate(TrackRoute(trackId: url.pathSegments.last));
|
// router.navigate(TrackRoute(trackId: url.pathSegments.last));
|
||||||
break;
|
// break;
|
||||||
default:
|
// default:
|
||||||
break;
|
// break;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
StreamSubscription? mediaStream;
|
// StreamSubscription? mediaStream;
|
||||||
|
|
||||||
if (kIsMobile) {
|
// if (kIsMobile) {
|
||||||
FlutterSharingIntent.instance.getInitialSharing().then(uriListener);
|
// FlutterSharingIntent.instance.getInitialSharing().then(uriListener);
|
||||||
|
|
||||||
mediaStream =
|
// mediaStream =
|
||||||
FlutterSharingIntent.instance.getMediaStream().listen(uriListener);
|
// FlutterSharingIntent.instance.getMediaStream().listen(uriListener);
|
||||||
}
|
// }
|
||||||
|
|
||||||
final subscription = linkStream.listen((uri) async {
|
// final subscription = linkStream.listen((uri) async {
|
||||||
try {
|
// try {
|
||||||
final startSegment = uri.split(":").take(2).join(":");
|
// final startSegment = uri.split(":").take(2).join(":");
|
||||||
final endSegment = uri.split(":").last;
|
// final endSegment = uri.split(":").last;
|
||||||
|
|
||||||
switch (startSegment) {
|
// switch (startSegment) {
|
||||||
case "spotify:album":
|
// case "spotify:album":
|
||||||
final album = await spotify.invoke((api) {
|
// final album = await spotify.invoke((api) {
|
||||||
return api.albums.get(endSegment);
|
// return api.albums.get(endSegment);
|
||||||
});
|
// });
|
||||||
// await router.navigate(
|
// // await router.navigate(
|
||||||
// AlbumRoute(id: album.id!, album: album),
|
// // AlbumRoute(id: album.id!, album: album),
|
||||||
// );
|
// // );
|
||||||
break;
|
// break;
|
||||||
case "spotify:artist":
|
// case "spotify:artist":
|
||||||
await router.navigate(ArtistRoute(artistId: endSegment));
|
// await router.navigate(ArtistRoute(artistId: endSegment));
|
||||||
break;
|
// break;
|
||||||
case "spotify:track":
|
// case "spotify:track":
|
||||||
await router.navigate(TrackRoute(trackId: endSegment));
|
// await router.navigate(TrackRoute(trackId: endSegment));
|
||||||
break;
|
// break;
|
||||||
case "spotify:playlist":
|
// case "spotify:playlist":
|
||||||
final playlist = await spotify.invoke((api) {
|
// final playlist = await spotify.invoke((api) {
|
||||||
return api.playlists.get(endSegment);
|
// return api.playlists.get(endSegment);
|
||||||
});
|
// });
|
||||||
// await router.navigate(
|
// // await router.navigate(
|
||||||
// PlaylistRoute(id: playlist.id!, playlist: playlist),
|
// // PlaylistRoute(id: playlist.id!, playlist: playlist),
|
||||||
// );
|
// // );
|
||||||
break;
|
// break;
|
||||||
default:
|
// default:
|
||||||
break;
|
// break;
|
||||||
}
|
// }
|
||||||
} catch (e, stack) {
|
// } catch (e, stack) {
|
||||||
AppLogger.reportError(e, stack);
|
// AppLogger.reportError(e, stack);
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
return () {
|
// return () {
|
||||||
mediaStream?.cancel();
|
// mediaStream?.cancel();
|
||||||
subscription.cancel();
|
// subscription.cancel();
|
||||||
};
|
// };
|
||||||
}, [spotify]);
|
// }, [spotify]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,8 @@ import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart';
|
|||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/provider/authentication/authentication.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
|
||||||
|
|||||||
@ -10,9 +10,9 @@ import 'package:media_kit/media_kit.dart' hide Track;
|
|||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart' show ThemeMode, Colors;
|
import 'package:shadcn_flutter/shadcn_flutter.dart' show ThemeMode, Colors;
|
||||||
import 'package:spotify/spotify.dart' hide Playlist;
|
|
||||||
import 'package:spotube/models/database/database.steps.dart';
|
import 'package:spotube/models/database/database.steps.dart';
|
||||||
import 'package:spotube/models/lyrics.dart';
|
import 'package:spotube/models/lyrics.dart';
|
||||||
|
import 'package:spotube/models/metadata/market.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/services/kv_store/encrypted_kv_store.dart';
|
import 'package:spotube/services/kv_store/encrypted_kv_store.dart';
|
||||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import 'package:drift/internal/versioned_schema.dart' as i0;
|
|||||||
import 'package:drift/drift.dart' as i1;
|
import 'package:drift/drift.dart' as i1;
|
||||||
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
|
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
|
import 'package:spotube/models/metadata/market.dart';
|
||||||
import 'package:spotube/services/sourced_track/enums.dart';
|
import 'package:spotube/services/sourced_track/enums.dart';
|
||||||
|
|
||||||
// GENERATED BY drift_dev, DO NOT MODIFY.
|
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||||
|
|||||||
@ -16,10 +16,12 @@ class HistoryTable extends Table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension HistoryItemParseExtension on HistoryTableData {
|
extension HistoryItemParseExtension on HistoryTableData {
|
||||||
PlaylistSimple? get playlist =>
|
SpotubeSimplePlaylistObject? get playlist => type == HistoryEntryType.playlist
|
||||||
type == HistoryEntryType.playlist ? PlaylistSimple.fromJson(data) : null;
|
? SpotubeSimplePlaylistObject.fromJson(data)
|
||||||
AlbumSimple? get album =>
|
: null;
|
||||||
type == HistoryEntryType.album ? AlbumSimple.fromJson(data) : null;
|
SpotubeSimpleAlbumObject? get album => type == HistoryEntryType.album
|
||||||
Track? get track =>
|
? SpotubeSimpleAlbumObject.fromJson(data)
|
||||||
type == HistoryEntryType.track ? Track.fromJson(data) : null;
|
: null;
|
||||||
|
SpotubeTrackObject? get track =>
|
||||||
|
type == HistoryEntryType.track ? SpotubeTrackObject.fromJson(data) : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
library metadata_objects;
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
|
||||||
import 'package:metadata_god/metadata_god.dart';
|
import 'package:metadata_god/metadata_god.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/utils/primitive_utils.dart';
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
|
|||||||
@ -2571,8 +2571,7 @@ mixin _$SpotubeSearchResponseObject {
|
|||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
List<SpotubeSimplePlaylistObject> get playlists =>
|
List<SpotubeSimplePlaylistObject> get playlists =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
List<SpotubeSimpleTrackObject> get tracks =>
|
List<SpotubeFullTrackObject> get tracks => throw _privateConstructorUsedError;
|
||||||
throw _privateConstructorUsedError;
|
|
||||||
|
|
||||||
/// Serializes this SpotubeSearchResponseObject to a JSON map.
|
/// Serializes this SpotubeSearchResponseObject to a JSON map.
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
@ -2596,7 +2595,7 @@ abstract class $SpotubeSearchResponseObjectCopyWith<$Res> {
|
|||||||
{List<SpotubeSimpleAlbumObject> albums,
|
{List<SpotubeSimpleAlbumObject> albums,
|
||||||
List<SpotubeFullArtistObject> artists,
|
List<SpotubeFullArtistObject> artists,
|
||||||
List<SpotubeSimplePlaylistObject> playlists,
|
List<SpotubeSimplePlaylistObject> playlists,
|
||||||
List<SpotubeSimpleTrackObject> tracks});
|
List<SpotubeFullTrackObject> tracks});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -2636,7 +2635,7 @@ class _$SpotubeSearchResponseObjectCopyWithImpl<$Res,
|
|||||||
tracks: null == tracks
|
tracks: null == tracks
|
||||||
? _value.tracks
|
? _value.tracks
|
||||||
: tracks // ignore: cast_nullable_to_non_nullable
|
: tracks // ignore: cast_nullable_to_non_nullable
|
||||||
as List<SpotubeSimpleTrackObject>,
|
as List<SpotubeFullTrackObject>,
|
||||||
) as $Val);
|
) as $Val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2654,7 +2653,7 @@ abstract class _$$SpotubeSearchResponseObjectImplCopyWith<$Res>
|
|||||||
{List<SpotubeSimpleAlbumObject> albums,
|
{List<SpotubeSimpleAlbumObject> albums,
|
||||||
List<SpotubeFullArtistObject> artists,
|
List<SpotubeFullArtistObject> artists,
|
||||||
List<SpotubeSimplePlaylistObject> playlists,
|
List<SpotubeSimplePlaylistObject> playlists,
|
||||||
List<SpotubeSimpleTrackObject> tracks});
|
List<SpotubeFullTrackObject> tracks});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -2693,7 +2692,7 @@ class __$$SpotubeSearchResponseObjectImplCopyWithImpl<$Res>
|
|||||||
tracks: null == tracks
|
tracks: null == tracks
|
||||||
? _value._tracks
|
? _value._tracks
|
||||||
: tracks // ignore: cast_nullable_to_non_nullable
|
: tracks // ignore: cast_nullable_to_non_nullable
|
||||||
as List<SpotubeSimpleTrackObject>,
|
as List<SpotubeFullTrackObject>,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2706,7 +2705,7 @@ class _$SpotubeSearchResponseObjectImpl
|
|||||||
{required final List<SpotubeSimpleAlbumObject> albums,
|
{required final List<SpotubeSimpleAlbumObject> albums,
|
||||||
required final List<SpotubeFullArtistObject> artists,
|
required final List<SpotubeFullArtistObject> artists,
|
||||||
required final List<SpotubeSimplePlaylistObject> playlists,
|
required final List<SpotubeSimplePlaylistObject> playlists,
|
||||||
required final List<SpotubeSimpleTrackObject> tracks})
|
required final List<SpotubeFullTrackObject> tracks})
|
||||||
: _albums = albums,
|
: _albums = albums,
|
||||||
_artists = artists,
|
_artists = artists,
|
||||||
_playlists = playlists,
|
_playlists = playlists,
|
||||||
@ -2740,9 +2739,9 @@ class _$SpotubeSearchResponseObjectImpl
|
|||||||
return EqualUnmodifiableListView(_playlists);
|
return EqualUnmodifiableListView(_playlists);
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<SpotubeSimpleTrackObject> _tracks;
|
final List<SpotubeFullTrackObject> _tracks;
|
||||||
@override
|
@override
|
||||||
List<SpotubeSimpleTrackObject> get tracks {
|
List<SpotubeFullTrackObject> get tracks {
|
||||||
if (_tracks is EqualUnmodifiableListView) return _tracks;
|
if (_tracks is EqualUnmodifiableListView) return _tracks;
|
||||||
// ignore: implicit_dynamic_type
|
// ignore: implicit_dynamic_type
|
||||||
return EqualUnmodifiableListView(_tracks);
|
return EqualUnmodifiableListView(_tracks);
|
||||||
@ -2797,7 +2796,7 @@ abstract class _SpotubeSearchResponseObject
|
|||||||
{required final List<SpotubeSimpleAlbumObject> albums,
|
{required final List<SpotubeSimpleAlbumObject> albums,
|
||||||
required final List<SpotubeFullArtistObject> artists,
|
required final List<SpotubeFullArtistObject> artists,
|
||||||
required final List<SpotubeSimplePlaylistObject> playlists,
|
required final List<SpotubeSimplePlaylistObject> playlists,
|
||||||
required final List<SpotubeSimpleTrackObject> tracks}) =
|
required final List<SpotubeFullTrackObject> tracks}) =
|
||||||
_$SpotubeSearchResponseObjectImpl;
|
_$SpotubeSearchResponseObjectImpl;
|
||||||
|
|
||||||
factory _SpotubeSearchResponseObject.fromJson(Map<String, dynamic> json) =
|
factory _SpotubeSearchResponseObject.fromJson(Map<String, dynamic> json) =
|
||||||
@ -2810,7 +2809,7 @@ abstract class _SpotubeSearchResponseObject
|
|||||||
@override
|
@override
|
||||||
List<SpotubeSimplePlaylistObject> get playlists;
|
List<SpotubeSimplePlaylistObject> get playlists;
|
||||||
@override
|
@override
|
||||||
List<SpotubeSimpleTrackObject> get tracks;
|
List<SpotubeFullTrackObject> get tracks;
|
||||||
|
|
||||||
/// Create a copy of SpotubeSearchResponseObject
|
/// Create a copy of SpotubeSearchResponseObject
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@ -2826,8 +2825,6 @@ SpotubeTrackObject _$SpotubeTrackObjectFromJson(Map<String, dynamic> json) {
|
|||||||
return SpotubeLocalTrackObject.fromJson(json);
|
return SpotubeLocalTrackObject.fromJson(json);
|
||||||
case 'full':
|
case 'full':
|
||||||
return SpotubeFullTrackObject.fromJson(json);
|
return SpotubeFullTrackObject.fromJson(json);
|
||||||
case 'simple':
|
|
||||||
return SpotubeSimpleTrackObject.fromJson(json);
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw CheckedFromJsonException(json, 'runtimeType', 'SpotubeTrackObject',
|
throw CheckedFromJsonException(json, 'runtimeType', 'SpotubeTrackObject',
|
||||||
@ -2842,7 +2839,7 @@ mixin _$SpotubeTrackObject {
|
|||||||
String get externalUri => throw _privateConstructorUsedError;
|
String get externalUri => throw _privateConstructorUsedError;
|
||||||
List<SpotubeSimpleArtistObject> get artists =>
|
List<SpotubeSimpleArtistObject> get artists =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
SpotubeSimpleAlbumObject? get album => throw _privateConstructorUsedError;
|
SpotubeSimpleAlbumObject get album => throw _privateConstructorUsedError;
|
||||||
int get durationMs => throw _privateConstructorUsedError;
|
int get durationMs => throw _privateConstructorUsedError;
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult when<TResult extends Object?>({
|
TResult when<TResult extends Object?>({
|
||||||
@ -2865,15 +2862,6 @@ mixin _$SpotubeTrackObject {
|
|||||||
String isrc,
|
String isrc,
|
||||||
bool explicit)
|
bool explicit)
|
||||||
full,
|
full,
|
||||||
required TResult Function(
|
|
||||||
String id,
|
|
||||||
String name,
|
|
||||||
String externalUri,
|
|
||||||
int durationMs,
|
|
||||||
bool explicit,
|
|
||||||
List<SpotubeSimpleArtistObject> artists,
|
|
||||||
SpotubeSimpleAlbumObject? album)
|
|
||||||
simple,
|
|
||||||
}) =>
|
}) =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
@ -2897,15 +2885,6 @@ mixin _$SpotubeTrackObject {
|
|||||||
String isrc,
|
String isrc,
|
||||||
bool explicit)?
|
bool explicit)?
|
||||||
full,
|
full,
|
||||||
TResult? Function(
|
|
||||||
String id,
|
|
||||||
String name,
|
|
||||||
String externalUri,
|
|
||||||
int durationMs,
|
|
||||||
bool explicit,
|
|
||||||
List<SpotubeSimpleArtistObject> artists,
|
|
||||||
SpotubeSimpleAlbumObject? album)?
|
|
||||||
simple,
|
|
||||||
}) =>
|
}) =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
@ -2929,15 +2908,6 @@ mixin _$SpotubeTrackObject {
|
|||||||
String isrc,
|
String isrc,
|
||||||
bool explicit)?
|
bool explicit)?
|
||||||
full,
|
full,
|
||||||
TResult Function(
|
|
||||||
String id,
|
|
||||||
String name,
|
|
||||||
String externalUri,
|
|
||||||
int durationMs,
|
|
||||||
bool explicit,
|
|
||||||
List<SpotubeSimpleArtistObject> artists,
|
|
||||||
SpotubeSimpleAlbumObject? album)?
|
|
||||||
simple,
|
|
||||||
required TResult orElse(),
|
required TResult orElse(),
|
||||||
}) =>
|
}) =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@ -2945,21 +2915,18 @@ mixin _$SpotubeTrackObject {
|
|||||||
TResult map<TResult extends Object?>({
|
TResult map<TResult extends Object?>({
|
||||||
required TResult Function(SpotubeLocalTrackObject value) local,
|
required TResult Function(SpotubeLocalTrackObject value) local,
|
||||||
required TResult Function(SpotubeFullTrackObject value) full,
|
required TResult Function(SpotubeFullTrackObject value) full,
|
||||||
required TResult Function(SpotubeSimpleTrackObject value) simple,
|
|
||||||
}) =>
|
}) =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult? mapOrNull<TResult extends Object?>({
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
TResult? Function(SpotubeLocalTrackObject value)? local,
|
TResult? Function(SpotubeLocalTrackObject value)? local,
|
||||||
TResult? Function(SpotubeFullTrackObject value)? full,
|
TResult? Function(SpotubeFullTrackObject value)? full,
|
||||||
TResult? Function(SpotubeSimpleTrackObject value)? simple,
|
|
||||||
}) =>
|
}) =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult maybeMap<TResult extends Object?>({
|
TResult maybeMap<TResult extends Object?>({
|
||||||
TResult Function(SpotubeLocalTrackObject value)? local,
|
TResult Function(SpotubeLocalTrackObject value)? local,
|
||||||
TResult Function(SpotubeFullTrackObject value)? full,
|
TResult Function(SpotubeFullTrackObject value)? full,
|
||||||
TResult Function(SpotubeSimpleTrackObject value)? simple,
|
|
||||||
required TResult orElse(),
|
required TResult orElse(),
|
||||||
}) =>
|
}) =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@ -2988,7 +2955,7 @@ abstract class $SpotubeTrackObjectCopyWith<$Res> {
|
|||||||
SpotubeSimpleAlbumObject album,
|
SpotubeSimpleAlbumObject album,
|
||||||
int durationMs});
|
int durationMs});
|
||||||
|
|
||||||
$SpotubeSimpleAlbumObjectCopyWith<$Res>? get album;
|
$SpotubeSimpleAlbumObjectCopyWith<$Res> get album;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -3031,7 +2998,7 @@ class _$SpotubeTrackObjectCopyWithImpl<$Res, $Val extends SpotubeTrackObject>
|
|||||||
: artists // ignore: cast_nullable_to_non_nullable
|
: artists // ignore: cast_nullable_to_non_nullable
|
||||||
as List<SpotubeSimpleArtistObject>,
|
as List<SpotubeSimpleArtistObject>,
|
||||||
album: null == album
|
album: null == album
|
||||||
? _value.album!
|
? _value.album
|
||||||
: album // ignore: cast_nullable_to_non_nullable
|
: album // ignore: cast_nullable_to_non_nullable
|
||||||
as SpotubeSimpleAlbumObject,
|
as SpotubeSimpleAlbumObject,
|
||||||
durationMs: null == durationMs
|
durationMs: null == durationMs
|
||||||
@ -3045,12 +3012,8 @@ class _$SpotubeTrackObjectCopyWithImpl<$Res, $Val extends SpotubeTrackObject>
|
|||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override
|
@override
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
$SpotubeSimpleAlbumObjectCopyWith<$Res>? get album {
|
$SpotubeSimpleAlbumObjectCopyWith<$Res> get album {
|
||||||
if (_value.album == null) {
|
return $SpotubeSimpleAlbumObjectCopyWith<$Res>(_value.album, (value) {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $SpotubeSimpleAlbumObjectCopyWith<$Res>(_value.album!, (value) {
|
|
||||||
return _then(_value.copyWith(album: value) as $Val);
|
return _then(_value.copyWith(album: value) as $Val);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -3132,16 +3095,6 @@ class __$$SpotubeLocalTrackObjectImplCopyWithImpl<$Res>
|
|||||||
as String,
|
as String,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a copy of SpotubeTrackObject
|
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
|
||||||
@override
|
|
||||||
@pragma('vm:prefer-inline')
|
|
||||||
$SpotubeSimpleAlbumObjectCopyWith<$Res> get album {
|
|
||||||
return $SpotubeSimpleAlbumObjectCopyWith<$Res>(_value.album, (value) {
|
|
||||||
return _then(_value.copyWith(album: value));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -3244,15 +3197,6 @@ class _$SpotubeLocalTrackObjectImpl implements SpotubeLocalTrackObject {
|
|||||||
String isrc,
|
String isrc,
|
||||||
bool explicit)
|
bool explicit)
|
||||||
full,
|
full,
|
||||||
required TResult Function(
|
|
||||||
String id,
|
|
||||||
String name,
|
|
||||||
String externalUri,
|
|
||||||
int durationMs,
|
|
||||||
bool explicit,
|
|
||||||
List<SpotubeSimpleArtistObject> artists,
|
|
||||||
SpotubeSimpleAlbumObject? album)
|
|
||||||
simple,
|
|
||||||
}) {
|
}) {
|
||||||
return local(id, name, externalUri, artists, album, durationMs, path);
|
return local(id, name, externalUri, artists, album, durationMs, path);
|
||||||
}
|
}
|
||||||
@ -3279,15 +3223,6 @@ class _$SpotubeLocalTrackObjectImpl implements SpotubeLocalTrackObject {
|
|||||||
String isrc,
|
String isrc,
|
||||||
bool explicit)?
|
bool explicit)?
|
||||||
full,
|
full,
|
||||||
TResult? Function(
|
|
||||||
String id,
|
|
||||||
String name,
|
|
||||||
String externalUri,
|
|
||||||
int durationMs,
|
|
||||||
bool explicit,
|
|
||||||
List<SpotubeSimpleArtistObject> artists,
|
|
||||||
SpotubeSimpleAlbumObject? album)?
|
|
||||||
simple,
|
|
||||||
}) {
|
}) {
|
||||||
return local?.call(id, name, externalUri, artists, album, durationMs, path);
|
return local?.call(id, name, externalUri, artists, album, durationMs, path);
|
||||||
}
|
}
|
||||||
@ -3314,15 +3249,6 @@ class _$SpotubeLocalTrackObjectImpl implements SpotubeLocalTrackObject {
|
|||||||
String isrc,
|
String isrc,
|
||||||
bool explicit)?
|
bool explicit)?
|
||||||
full,
|
full,
|
||||||
TResult Function(
|
|
||||||
String id,
|
|
||||||
String name,
|
|
||||||
String externalUri,
|
|
||||||
int durationMs,
|
|
||||||
bool explicit,
|
|
||||||
List<SpotubeSimpleArtistObject> artists,
|
|
||||||
SpotubeSimpleAlbumObject? album)?
|
|
||||||
simple,
|
|
||||||
required TResult orElse(),
|
required TResult orElse(),
|
||||||
}) {
|
}) {
|
||||||
if (local != null) {
|
if (local != null) {
|
||||||
@ -3336,7 +3262,6 @@ class _$SpotubeLocalTrackObjectImpl implements SpotubeLocalTrackObject {
|
|||||||
TResult map<TResult extends Object?>({
|
TResult map<TResult extends Object?>({
|
||||||
required TResult Function(SpotubeLocalTrackObject value) local,
|
required TResult Function(SpotubeLocalTrackObject value) local,
|
||||||
required TResult Function(SpotubeFullTrackObject value) full,
|
required TResult Function(SpotubeFullTrackObject value) full,
|
||||||
required TResult Function(SpotubeSimpleTrackObject value) simple,
|
|
||||||
}) {
|
}) {
|
||||||
return local(this);
|
return local(this);
|
||||||
}
|
}
|
||||||
@ -3346,7 +3271,6 @@ class _$SpotubeLocalTrackObjectImpl implements SpotubeLocalTrackObject {
|
|||||||
TResult? mapOrNull<TResult extends Object?>({
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
TResult? Function(SpotubeLocalTrackObject value)? local,
|
TResult? Function(SpotubeLocalTrackObject value)? local,
|
||||||
TResult? Function(SpotubeFullTrackObject value)? full,
|
TResult? Function(SpotubeFullTrackObject value)? full,
|
||||||
TResult? Function(SpotubeSimpleTrackObject value)? simple,
|
|
||||||
}) {
|
}) {
|
||||||
return local?.call(this);
|
return local?.call(this);
|
||||||
}
|
}
|
||||||
@ -3356,7 +3280,6 @@ class _$SpotubeLocalTrackObjectImpl implements SpotubeLocalTrackObject {
|
|||||||
TResult maybeMap<TResult extends Object?>({
|
TResult maybeMap<TResult extends Object?>({
|
||||||
TResult Function(SpotubeLocalTrackObject value)? local,
|
TResult Function(SpotubeLocalTrackObject value)? local,
|
||||||
TResult Function(SpotubeFullTrackObject value)? full,
|
TResult Function(SpotubeFullTrackObject value)? full,
|
||||||
TResult Function(SpotubeSimpleTrackObject value)? simple,
|
|
||||||
required TResult orElse(),
|
required TResult orElse(),
|
||||||
}) {
|
}) {
|
||||||
if (local != null) {
|
if (local != null) {
|
||||||
@ -3489,16 +3412,6 @@ class __$$SpotubeFullTrackObjectImplCopyWithImpl<$Res>
|
|||||||
as bool,
|
as bool,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a copy of SpotubeTrackObject
|
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
|
||||||
@override
|
|
||||||
@pragma('vm:prefer-inline')
|
|
||||||
$SpotubeSimpleAlbumObjectCopyWith<$Res> get album {
|
|
||||||
return $SpotubeSimpleAlbumObjectCopyWith<$Res>(_value.album, (value) {
|
|
||||||
return _then(_value.copyWith(album: value));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -3614,15 +3527,6 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject {
|
|||||||
String isrc,
|
String isrc,
|
||||||
bool explicit)
|
bool explicit)
|
||||||
full,
|
full,
|
||||||
required TResult Function(
|
|
||||||
String id,
|
|
||||||
String name,
|
|
||||||
String externalUri,
|
|
||||||
int durationMs,
|
|
||||||
bool explicit,
|
|
||||||
List<SpotubeSimpleArtistObject> artists,
|
|
||||||
SpotubeSimpleAlbumObject? album)
|
|
||||||
simple,
|
|
||||||
}) {
|
}) {
|
||||||
return full(
|
return full(
|
||||||
id, name, externalUri, artists, album, durationMs, isrc, explicit);
|
id, name, externalUri, artists, album, durationMs, isrc, explicit);
|
||||||
@ -3650,15 +3554,6 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject {
|
|||||||
String isrc,
|
String isrc,
|
||||||
bool explicit)?
|
bool explicit)?
|
||||||
full,
|
full,
|
||||||
TResult? Function(
|
|
||||||
String id,
|
|
||||||
String name,
|
|
||||||
String externalUri,
|
|
||||||
int durationMs,
|
|
||||||
bool explicit,
|
|
||||||
List<SpotubeSimpleArtistObject> artists,
|
|
||||||
SpotubeSimpleAlbumObject? album)?
|
|
||||||
simple,
|
|
||||||
}) {
|
}) {
|
||||||
return full?.call(
|
return full?.call(
|
||||||
id, name, externalUri, artists, album, durationMs, isrc, explicit);
|
id, name, externalUri, artists, album, durationMs, isrc, explicit);
|
||||||
@ -3686,15 +3581,6 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject {
|
|||||||
String isrc,
|
String isrc,
|
||||||
bool explicit)?
|
bool explicit)?
|
||||||
full,
|
full,
|
||||||
TResult Function(
|
|
||||||
String id,
|
|
||||||
String name,
|
|
||||||
String externalUri,
|
|
||||||
int durationMs,
|
|
||||||
bool explicit,
|
|
||||||
List<SpotubeSimpleArtistObject> artists,
|
|
||||||
SpotubeSimpleAlbumObject? album)?
|
|
||||||
simple,
|
|
||||||
required TResult orElse(),
|
required TResult orElse(),
|
||||||
}) {
|
}) {
|
||||||
if (full != null) {
|
if (full != null) {
|
||||||
@ -3709,7 +3595,6 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject {
|
|||||||
TResult map<TResult extends Object?>({
|
TResult map<TResult extends Object?>({
|
||||||
required TResult Function(SpotubeLocalTrackObject value) local,
|
required TResult Function(SpotubeLocalTrackObject value) local,
|
||||||
required TResult Function(SpotubeFullTrackObject value) full,
|
required TResult Function(SpotubeFullTrackObject value) full,
|
||||||
required TResult Function(SpotubeSimpleTrackObject value) simple,
|
|
||||||
}) {
|
}) {
|
||||||
return full(this);
|
return full(this);
|
||||||
}
|
}
|
||||||
@ -3719,7 +3604,6 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject {
|
|||||||
TResult? mapOrNull<TResult extends Object?>({
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
TResult? Function(SpotubeLocalTrackObject value)? local,
|
TResult? Function(SpotubeLocalTrackObject value)? local,
|
||||||
TResult? Function(SpotubeFullTrackObject value)? full,
|
TResult? Function(SpotubeFullTrackObject value)? full,
|
||||||
TResult? Function(SpotubeSimpleTrackObject value)? simple,
|
|
||||||
}) {
|
}) {
|
||||||
return full?.call(this);
|
return full?.call(this);
|
||||||
}
|
}
|
||||||
@ -3729,7 +3613,6 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject {
|
|||||||
TResult maybeMap<TResult extends Object?>({
|
TResult maybeMap<TResult extends Object?>({
|
||||||
TResult Function(SpotubeLocalTrackObject value)? local,
|
TResult Function(SpotubeLocalTrackObject value)? local,
|
||||||
TResult Function(SpotubeFullTrackObject value)? full,
|
TResult Function(SpotubeFullTrackObject value)? full,
|
||||||
TResult Function(SpotubeSimpleTrackObject value)? simple,
|
|
||||||
required TResult orElse(),
|
required TResult orElse(),
|
||||||
}) {
|
}) {
|
||||||
if (full != null) {
|
if (full != null) {
|
||||||
@ -3783,358 +3666,6 @@ abstract class SpotubeFullTrackObject implements SpotubeTrackObject {
|
|||||||
get copyWith => throw _privateConstructorUsedError;
|
get copyWith => throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
|
||||||
abstract class _$$SpotubeSimpleTrackObjectImplCopyWith<$Res>
|
|
||||||
implements $SpotubeTrackObjectCopyWith<$Res> {
|
|
||||||
factory _$$SpotubeSimpleTrackObjectImplCopyWith(
|
|
||||||
_$SpotubeSimpleTrackObjectImpl value,
|
|
||||||
$Res Function(_$SpotubeSimpleTrackObjectImpl) then) =
|
|
||||||
__$$SpotubeSimpleTrackObjectImplCopyWithImpl<$Res>;
|
|
||||||
@override
|
|
||||||
@useResult
|
|
||||||
$Res call(
|
|
||||||
{String id,
|
|
||||||
String name,
|
|
||||||
String externalUri,
|
|
||||||
int durationMs,
|
|
||||||
bool explicit,
|
|
||||||
List<SpotubeSimpleArtistObject> artists,
|
|
||||||
SpotubeSimpleAlbumObject? album});
|
|
||||||
|
|
||||||
@override
|
|
||||||
$SpotubeSimpleAlbumObjectCopyWith<$Res>? get album;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @nodoc
|
|
||||||
class __$$SpotubeSimpleTrackObjectImplCopyWithImpl<$Res>
|
|
||||||
extends _$SpotubeTrackObjectCopyWithImpl<$Res,
|
|
||||||
_$SpotubeSimpleTrackObjectImpl>
|
|
||||||
implements _$$SpotubeSimpleTrackObjectImplCopyWith<$Res> {
|
|
||||||
__$$SpotubeSimpleTrackObjectImplCopyWithImpl(
|
|
||||||
_$SpotubeSimpleTrackObjectImpl _value,
|
|
||||||
$Res Function(_$SpotubeSimpleTrackObjectImpl) _then)
|
|
||||||
: super(_value, _then);
|
|
||||||
|
|
||||||
/// Create a copy of SpotubeTrackObject
|
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
|
||||||
@pragma('vm:prefer-inline')
|
|
||||||
@override
|
|
||||||
$Res call({
|
|
||||||
Object? id = null,
|
|
||||||
Object? name = null,
|
|
||||||
Object? externalUri = null,
|
|
||||||
Object? durationMs = null,
|
|
||||||
Object? explicit = null,
|
|
||||||
Object? artists = null,
|
|
||||||
Object? album = freezed,
|
|
||||||
}) {
|
|
||||||
return _then(_$SpotubeSimpleTrackObjectImpl(
|
|
||||||
id: null == id
|
|
||||||
? _value.id
|
|
||||||
: id // ignore: cast_nullable_to_non_nullable
|
|
||||||
as String,
|
|
||||||
name: null == name
|
|
||||||
? _value.name
|
|
||||||
: name // ignore: cast_nullable_to_non_nullable
|
|
||||||
as String,
|
|
||||||
externalUri: null == externalUri
|
|
||||||
? _value.externalUri
|
|
||||||
: externalUri // ignore: cast_nullable_to_non_nullable
|
|
||||||
as String,
|
|
||||||
durationMs: null == durationMs
|
|
||||||
? _value.durationMs
|
|
||||||
: durationMs // ignore: cast_nullable_to_non_nullable
|
|
||||||
as int,
|
|
||||||
explicit: null == explicit
|
|
||||||
? _value.explicit
|
|
||||||
: explicit // ignore: cast_nullable_to_non_nullable
|
|
||||||
as bool,
|
|
||||||
artists: null == artists
|
|
||||||
? _value._artists
|
|
||||||
: artists // ignore: cast_nullable_to_non_nullable
|
|
||||||
as List<SpotubeSimpleArtistObject>,
|
|
||||||
album: freezed == album
|
|
||||||
? _value.album
|
|
||||||
: album // ignore: cast_nullable_to_non_nullable
|
|
||||||
as SpotubeSimpleAlbumObject?,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @nodoc
|
|
||||||
@JsonSerializable()
|
|
||||||
class _$SpotubeSimpleTrackObjectImpl implements SpotubeSimpleTrackObject {
|
|
||||||
_$SpotubeSimpleTrackObjectImpl(
|
|
||||||
{required this.id,
|
|
||||||
required this.name,
|
|
||||||
required this.externalUri,
|
|
||||||
required this.durationMs,
|
|
||||||
required this.explicit,
|
|
||||||
final List<SpotubeSimpleArtistObject> artists = const [],
|
|
||||||
this.album,
|
|
||||||
final String? $type})
|
|
||||||
: _artists = artists,
|
|
||||||
$type = $type ?? 'simple';
|
|
||||||
|
|
||||||
factory _$SpotubeSimpleTrackObjectImpl.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$$SpotubeSimpleTrackObjectImplFromJson(json);
|
|
||||||
|
|
||||||
@override
|
|
||||||
final String id;
|
|
||||||
@override
|
|
||||||
final String name;
|
|
||||||
@override
|
|
||||||
final String externalUri;
|
|
||||||
@override
|
|
||||||
final int durationMs;
|
|
||||||
@override
|
|
||||||
final bool explicit;
|
|
||||||
final List<SpotubeSimpleArtistObject> _artists;
|
|
||||||
@override
|
|
||||||
@JsonKey()
|
|
||||||
List<SpotubeSimpleArtistObject> get artists {
|
|
||||||
if (_artists is EqualUnmodifiableListView) return _artists;
|
|
||||||
// ignore: implicit_dynamic_type
|
|
||||||
return EqualUnmodifiableListView(_artists);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
final SpotubeSimpleAlbumObject? album;
|
|
||||||
|
|
||||||
@JsonKey(name: 'runtimeType')
|
|
||||||
final String $type;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'SpotubeTrackObject.simple(id: $id, name: $name, externalUri: $externalUri, durationMs: $durationMs, explicit: $explicit, artists: $artists, album: $album)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return identical(this, other) ||
|
|
||||||
(other.runtimeType == runtimeType &&
|
|
||||||
other is _$SpotubeSimpleTrackObjectImpl &&
|
|
||||||
(identical(other.id, id) || other.id == id) &&
|
|
||||||
(identical(other.name, name) || other.name == name) &&
|
|
||||||
(identical(other.externalUri, externalUri) ||
|
|
||||||
other.externalUri == externalUri) &&
|
|
||||||
(identical(other.durationMs, durationMs) ||
|
|
||||||
other.durationMs == durationMs) &&
|
|
||||||
(identical(other.explicit, explicit) ||
|
|
||||||
other.explicit == explicit) &&
|
|
||||||
const DeepCollectionEquality().equals(other._artists, _artists) &&
|
|
||||||
(identical(other.album, album) || other.album == album));
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
|
||||||
@override
|
|
||||||
int get hashCode => Object.hash(
|
|
||||||
runtimeType,
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
externalUri,
|
|
||||||
durationMs,
|
|
||||||
explicit,
|
|
||||||
const DeepCollectionEquality().hash(_artists),
|
|
||||||
album);
|
|
||||||
|
|
||||||
/// Create a copy of SpotubeTrackObject
|
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
|
||||||
@override
|
|
||||||
@pragma('vm:prefer-inline')
|
|
||||||
_$$SpotubeSimpleTrackObjectImplCopyWith<_$SpotubeSimpleTrackObjectImpl>
|
|
||||||
get copyWith => __$$SpotubeSimpleTrackObjectImplCopyWithImpl<
|
|
||||||
_$SpotubeSimpleTrackObjectImpl>(this, _$identity);
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult when<TResult extends Object?>({
|
|
||||||
required TResult Function(
|
|
||||||
String id,
|
|
||||||
String name,
|
|
||||||
String externalUri,
|
|
||||||
List<SpotubeSimpleArtistObject> artists,
|
|
||||||
SpotubeSimpleAlbumObject album,
|
|
||||||
int durationMs,
|
|
||||||
String path)
|
|
||||||
local,
|
|
||||||
required TResult Function(
|
|
||||||
String id,
|
|
||||||
String name,
|
|
||||||
String externalUri,
|
|
||||||
List<SpotubeSimpleArtistObject> artists,
|
|
||||||
SpotubeSimpleAlbumObject album,
|
|
||||||
int durationMs,
|
|
||||||
String isrc,
|
|
||||||
bool explicit)
|
|
||||||
full,
|
|
||||||
required TResult Function(
|
|
||||||
String id,
|
|
||||||
String name,
|
|
||||||
String externalUri,
|
|
||||||
int durationMs,
|
|
||||||
bool explicit,
|
|
||||||
List<SpotubeSimpleArtistObject> artists,
|
|
||||||
SpotubeSimpleAlbumObject? album)
|
|
||||||
simple,
|
|
||||||
}) {
|
|
||||||
return simple(id, name, externalUri, durationMs, explicit, artists, album);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult? whenOrNull<TResult extends Object?>({
|
|
||||||
TResult? Function(
|
|
||||||
String id,
|
|
||||||
String name,
|
|
||||||
String externalUri,
|
|
||||||
List<SpotubeSimpleArtistObject> artists,
|
|
||||||
SpotubeSimpleAlbumObject album,
|
|
||||||
int durationMs,
|
|
||||||
String path)?
|
|
||||||
local,
|
|
||||||
TResult? Function(
|
|
||||||
String id,
|
|
||||||
String name,
|
|
||||||
String externalUri,
|
|
||||||
List<SpotubeSimpleArtistObject> artists,
|
|
||||||
SpotubeSimpleAlbumObject album,
|
|
||||||
int durationMs,
|
|
||||||
String isrc,
|
|
||||||
bool explicit)?
|
|
||||||
full,
|
|
||||||
TResult? Function(
|
|
||||||
String id,
|
|
||||||
String name,
|
|
||||||
String externalUri,
|
|
||||||
int durationMs,
|
|
||||||
bool explicit,
|
|
||||||
List<SpotubeSimpleArtistObject> artists,
|
|
||||||
SpotubeSimpleAlbumObject? album)?
|
|
||||||
simple,
|
|
||||||
}) {
|
|
||||||
return simple?.call(
|
|
||||||
id, name, externalUri, durationMs, explicit, artists, album);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult maybeWhen<TResult extends Object?>({
|
|
||||||
TResult Function(
|
|
||||||
String id,
|
|
||||||
String name,
|
|
||||||
String externalUri,
|
|
||||||
List<SpotubeSimpleArtistObject> artists,
|
|
||||||
SpotubeSimpleAlbumObject album,
|
|
||||||
int durationMs,
|
|
||||||
String path)?
|
|
||||||
local,
|
|
||||||
TResult Function(
|
|
||||||
String id,
|
|
||||||
String name,
|
|
||||||
String externalUri,
|
|
||||||
List<SpotubeSimpleArtistObject> artists,
|
|
||||||
SpotubeSimpleAlbumObject album,
|
|
||||||
int durationMs,
|
|
||||||
String isrc,
|
|
||||||
bool explicit)?
|
|
||||||
full,
|
|
||||||
TResult Function(
|
|
||||||
String id,
|
|
||||||
String name,
|
|
||||||
String externalUri,
|
|
||||||
int durationMs,
|
|
||||||
bool explicit,
|
|
||||||
List<SpotubeSimpleArtistObject> artists,
|
|
||||||
SpotubeSimpleAlbumObject? album)?
|
|
||||||
simple,
|
|
||||||
required TResult orElse(),
|
|
||||||
}) {
|
|
||||||
if (simple != null) {
|
|
||||||
return simple(
|
|
||||||
id, name, externalUri, durationMs, explicit, artists, album);
|
|
||||||
}
|
|
||||||
return orElse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult map<TResult extends Object?>({
|
|
||||||
required TResult Function(SpotubeLocalTrackObject value) local,
|
|
||||||
required TResult Function(SpotubeFullTrackObject value) full,
|
|
||||||
required TResult Function(SpotubeSimpleTrackObject value) simple,
|
|
||||||
}) {
|
|
||||||
return simple(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult? mapOrNull<TResult extends Object?>({
|
|
||||||
TResult? Function(SpotubeLocalTrackObject value)? local,
|
|
||||||
TResult? Function(SpotubeFullTrackObject value)? full,
|
|
||||||
TResult? Function(SpotubeSimpleTrackObject value)? simple,
|
|
||||||
}) {
|
|
||||||
return simple?.call(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult maybeMap<TResult extends Object?>({
|
|
||||||
TResult Function(SpotubeLocalTrackObject value)? local,
|
|
||||||
TResult Function(SpotubeFullTrackObject value)? full,
|
|
||||||
TResult Function(SpotubeSimpleTrackObject value)? simple,
|
|
||||||
required TResult orElse(),
|
|
||||||
}) {
|
|
||||||
if (simple != null) {
|
|
||||||
return simple(this);
|
|
||||||
}
|
|
||||||
return orElse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return _$$SpotubeSimpleTrackObjectImplToJson(
|
|
||||||
this,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class SpotubeSimpleTrackObject implements SpotubeTrackObject {
|
|
||||||
factory SpotubeSimpleTrackObject(
|
|
||||||
{required final String id,
|
|
||||||
required final String name,
|
|
||||||
required final String externalUri,
|
|
||||||
required final int durationMs,
|
|
||||||
required final bool explicit,
|
|
||||||
final List<SpotubeSimpleArtistObject> artists,
|
|
||||||
final SpotubeSimpleAlbumObject? album}) = _$SpotubeSimpleTrackObjectImpl;
|
|
||||||
|
|
||||||
factory SpotubeSimpleTrackObject.fromJson(Map<String, dynamic> json) =
|
|
||||||
_$SpotubeSimpleTrackObjectImpl.fromJson;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get id;
|
|
||||||
@override
|
|
||||||
String get name;
|
|
||||||
@override
|
|
||||||
String get externalUri;
|
|
||||||
@override
|
|
||||||
int get durationMs;
|
|
||||||
bool get explicit;
|
|
||||||
@override
|
|
||||||
List<SpotubeSimpleArtistObject> get artists;
|
|
||||||
@override
|
|
||||||
SpotubeSimpleAlbumObject? get album;
|
|
||||||
|
|
||||||
/// Create a copy of SpotubeTrackObject
|
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
|
||||||
@override
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
|
||||||
_$$SpotubeSimpleTrackObjectImplCopyWith<_$SpotubeSimpleTrackObjectImpl>
|
|
||||||
get copyWith => throw _privateConstructorUsedError;
|
|
||||||
}
|
|
||||||
|
|
||||||
SpotubeUserObject _$SpotubeUserObjectFromJson(Map<String, dynamic> json) {
|
SpotubeUserObject _$SpotubeUserObjectFromJson(Map<String, dynamic> json) {
|
||||||
return _SpotubeUserObject.fromJson(json);
|
return _SpotubeUserObject.fromJson(json);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -273,7 +273,7 @@ _$SpotubeSearchResponseObjectImpl _$$SpotubeSearchResponseObjectImplFromJson(
|
|||||||
Map<String, dynamic>.from(e as Map)))
|
Map<String, dynamic>.from(e as Map)))
|
||||||
.toList(),
|
.toList(),
|
||||||
tracks: (json['tracks'] as List<dynamic>)
|
tracks: (json['tracks'] as List<dynamic>)
|
||||||
.map((e) => SpotubeSimpleTrackObject.fromJson(
|
.map((e) => SpotubeFullTrackObject.fromJson(
|
||||||
Map<String, dynamic>.from(e as Map)))
|
Map<String, dynamic>.from(e as Map)))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
@ -350,39 +350,6 @@ Map<String, dynamic> _$$SpotubeFullTrackObjectImplToJson(
|
|||||||
'runtimeType': instance.$type,
|
'runtimeType': instance.$type,
|
||||||
};
|
};
|
||||||
|
|
||||||
_$SpotubeSimpleTrackObjectImpl _$$SpotubeSimpleTrackObjectImplFromJson(
|
|
||||||
Map json) =>
|
|
||||||
_$SpotubeSimpleTrackObjectImpl(
|
|
||||||
id: json['id'] as String,
|
|
||||||
name: json['name'] as String,
|
|
||||||
externalUri: json['externalUri'] as String,
|
|
||||||
durationMs: (json['durationMs'] as num).toInt(),
|
|
||||||
explicit: json['explicit'] as bool,
|
|
||||||
artists: (json['artists'] as List<dynamic>?)
|
|
||||||
?.map((e) => SpotubeSimpleArtistObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(e as Map)))
|
|
||||||
.toList() ??
|
|
||||||
const [],
|
|
||||||
album: json['album'] == null
|
|
||||||
? null
|
|
||||||
: SpotubeSimpleAlbumObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(json['album'] as Map)),
|
|
||||||
$type: json['runtimeType'] as String?,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubeSimpleTrackObjectImplToJson(
|
|
||||||
_$SpotubeSimpleTrackObjectImpl instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'id': instance.id,
|
|
||||||
'name': instance.name,
|
|
||||||
'externalUri': instance.externalUri,
|
|
||||||
'durationMs': instance.durationMs,
|
|
||||||
'explicit': instance.explicit,
|
|
||||||
'artists': instance.artists.map((e) => e.toJson()).toList(),
|
|
||||||
'album': instance.album?.toJson(),
|
|
||||||
'runtimeType': instance.$type,
|
|
||||||
};
|
|
||||||
|
|
||||||
_$SpotubeUserObjectImpl _$$SpotubeUserObjectImplFromJson(Map json) =>
|
_$SpotubeUserObjectImpl _$$SpotubeUserObjectImplFromJson(Map json) =>
|
||||||
_$SpotubeUserObjectImpl(
|
_$SpotubeUserObjectImpl(
|
||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
|
|||||||
@ -6,7 +6,7 @@ class SpotubeSearchResponseObject with _$SpotubeSearchResponseObject {
|
|||||||
required List<SpotubeSimpleAlbumObject> albums,
|
required List<SpotubeSimpleAlbumObject> albums,
|
||||||
required List<SpotubeFullArtistObject> artists,
|
required List<SpotubeFullArtistObject> artists,
|
||||||
required List<SpotubeSimplePlaylistObject> playlists,
|
required List<SpotubeSimplePlaylistObject> playlists,
|
||||||
required List<SpotubeSimpleTrackObject> tracks,
|
required List<SpotubeFullTrackObject> tracks,
|
||||||
}) = _SpotubeSearchResponseObject;
|
}) = _SpotubeSearchResponseObject;
|
||||||
|
|
||||||
factory SpotubeSearchResponseObject.fromJson(Map<String, dynamic> json) =>
|
factory SpotubeSearchResponseObject.fromJson(Map<String, dynamic> json) =>
|
||||||
|
|||||||
@ -23,23 +23,54 @@ class SpotubeTrackObject with _$SpotubeTrackObject {
|
|||||||
required bool explicit,
|
required bool explicit,
|
||||||
}) = SpotubeFullTrackObject;
|
}) = SpotubeFullTrackObject;
|
||||||
|
|
||||||
factory SpotubeTrackObject.simple({
|
factory SpotubeTrackObject.localTrackFromFile(
|
||||||
required String id,
|
File file, {
|
||||||
required String name,
|
Metadata? metadata,
|
||||||
required String externalUri,
|
String? art,
|
||||||
required int durationMs,
|
}) {
|
||||||
required bool explicit,
|
return SpotubeLocalTrackObject(
|
||||||
@Default([]) List<SpotubeSimpleArtistObject> artists,
|
id: file.absolute.path,
|
||||||
SpotubeSimpleAlbumObject? album,
|
name: metadata?.title ?? basenameWithoutExtension(file.path),
|
||||||
}) = SpotubeSimpleTrackObject;
|
externalUri: "file://${file.absolute.path}",
|
||||||
|
artists: metadata?.artist?.split(",").map((a) {
|
||||||
|
return SpotubeSimpleArtistObject(
|
||||||
|
id: a.trim(),
|
||||||
|
name: a.trim(),
|
||||||
|
externalUri: "file://${file.absolute.path}",
|
||||||
|
);
|
||||||
|
}).toList() ??
|
||||||
|
[
|
||||||
|
SpotubeSimpleArtistObject(
|
||||||
|
id: "unknown",
|
||||||
|
name: "Unknown Artist",
|
||||||
|
externalUri: "file://${file.absolute.path}",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
album: SpotubeSimpleAlbumObject(
|
||||||
|
albumType: SpotubeAlbumType.album,
|
||||||
|
id: metadata?.album ?? "unknown",
|
||||||
|
name: metadata?.album ?? "Unknown Album",
|
||||||
|
externalUri: "file://${file.absolute.path}",
|
||||||
|
artists: [
|
||||||
|
SpotubeSimpleArtistObject(
|
||||||
|
id: metadata?.albumArtist ?? "unknown",
|
||||||
|
name: metadata?.albumArtist ?? "Unknown Artist",
|
||||||
|
externalUri: "file://${file.absolute.path}",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
releaseDate:
|
||||||
|
metadata?.year != null ? "${metadata!.year}-01-01" : "1970-01-01",
|
||||||
|
),
|
||||||
|
durationMs: metadata?.durationMs?.toInt() ?? 0,
|
||||||
|
path: file.path,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
factory SpotubeTrackObject.fromJson(Map<String, dynamic> json) =>
|
factory SpotubeTrackObject.fromJson(Map<String, dynamic> json) =>
|
||||||
_$SpotubeTrackObjectFromJson(
|
_$SpotubeTrackObjectFromJson(
|
||||||
json.containsKey("isrc")
|
json.containsKey("path")
|
||||||
? {...json, "runtimeType": "full"}
|
? {...json, "runtimeType": "local"}
|
||||||
: json.containsKey("path")
|
: {...json, "runtimeType": "full"},
|
||||||
? {...json, "runtimeType": "local"}
|
|
||||||
: {...json, "runtimeType": "simple"},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/components/dialogs/select_device_dialog.dart';
|
import 'package:spotube/components/dialogs/select_device_dialog.dart';
|
||||||
import 'package:spotube/components/playbutton_view/playbutton_card.dart';
|
import 'package:spotube/components/playbutton_view/playbutton_card.dart';
|
||||||
import 'package:spotube/components/playbutton_view/playbutton_tile.dart';
|
import 'package:spotube/components/playbutton_view/playbutton_tile.dart';
|
||||||
import 'package:spotube/extensions/artist_simple.dart';
|
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/track.dart';
|
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/audio_player/querying_track_info.dart';
|
import 'package:spotube/provider/audio_player/querying_track_info.dart';
|
||||||
@ -17,10 +14,9 @@ import 'package:spotube/provider/connect/connect.dart';
|
|||||||
import 'package:spotube/provider/history/history.dart';
|
import 'package:spotube/provider/history/history.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/tracks/album.dart';
|
import 'package:spotube/provider/metadata_plugin/tracks/album.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
|
||||||
extension FormattedAlbumType on AlbumType {
|
extension FormattedAlbumType on SpotubeAlbumType {
|
||||||
String get formatted => name.replaceFirst(name[0], name[0].toUpperCase());
|
String get formatted => name.replaceFirst(name[0], name[0].toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,9 +49,11 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
|
|
||||||
final updating = useState(false);
|
final updating = useState(false);
|
||||||
|
|
||||||
Future<List<Track>> fetchAllTrack() async {
|
Future<List<SpotubeFullTrackObject>> fetchAllTrack() async {
|
||||||
// return ref.read(metadataPluginAlbumTracksProvider(album).notifier).fetchAll();
|
await ref.read(metadataPluginAlbumTracksProvider(album.id).future);
|
||||||
return [];
|
return ref
|
||||||
|
.read(metadataPluginAlbumTracksProvider(album.id).notifier)
|
||||||
|
.fetchAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
var imageUrl = album.images.asUrlString(
|
var imageUrl = album.images.asUrlString(
|
||||||
@ -87,13 +85,13 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
await remotePlayback.load(
|
await remotePlayback.load(
|
||||||
WebSocketLoadEventData.album(
|
WebSocketLoadEventData.album(
|
||||||
tracks: fetchedTracks,
|
tracks: fetchedTracks,
|
||||||
// collection: album,
|
collection: album,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await playlistNotifier.load(fetchedTracks, autoPlay: true);
|
await playlistNotifier.load(fetchedTracks, autoPlay: true);
|
||||||
playlistNotifier.addCollection(album.id);
|
playlistNotifier.addCollection(album.id);
|
||||||
// historyNotifier.addAlbums([album]);
|
historyNotifier.addAlbums([album]);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
updating.value = false;
|
updating.value = false;
|
||||||
@ -112,7 +110,7 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
if (fetchedTracks.isEmpty) return;
|
if (fetchedTracks.isEmpty) return;
|
||||||
playlistNotifier.addTracks(fetchedTracks);
|
playlistNotifier.addTracks(fetchedTracks);
|
||||||
playlistNotifier.addCollection(album.id);
|
playlistNotifier.addCollection(album.id);
|
||||||
// historyNotifier.addAlbums([album]);
|
historyNotifier.addAlbums([album]);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showToast(
|
showToast(
|
||||||
context: context,
|
context: context,
|
||||||
@ -126,7 +124,7 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
child: Text(context.l10n.undo),
|
child: Text(context.l10n.undo),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
playlistNotifier
|
playlistNotifier
|
||||||
.removeTracks(fetchedTracks.map((e) => e.id!));
|
.removeTracks(fetchedTracks.map((e) => e.id));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_pl
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/artist/albums.dart';
|
import 'package:spotube/provider/metadata_plugin/artist/albums.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||||
|
|
||||||
class ArtistAlbumList extends HookConsumerWidget {
|
class ArtistAlbumList extends HookConsumerWidget {
|
||||||
final String artistId;
|
final String artistId;
|
||||||
|
|||||||
@ -3,44 +3,45 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
"Later a featured playlists API will be added for metadata plugins.")
|
||||||
class HomeFeaturedSection extends HookConsumerWidget {
|
class HomeFeaturedSection extends HookConsumerWidget {
|
||||||
const HomeFeaturedSection({super.key});
|
const HomeFeaturedSection({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final featuredPlaylists = ref.watch(featuredPlaylistsProvider);
|
return const SizedBox.shrink();
|
||||||
final featuredPlaylistsNotifier =
|
// final featuredPlaylists = ref.watch(featuredPlaylistsProvider);
|
||||||
ref.watch(featuredPlaylistsProvider.notifier);
|
// final featuredPlaylistsNotifier =
|
||||||
|
// ref.watch(featuredPlaylistsProvider.notifier);
|
||||||
|
|
||||||
if (featuredPlaylists.hasError) {
|
// if (featuredPlaylists.hasError) {
|
||||||
return Column(
|
// return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
// mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
// children: [
|
||||||
Undraw(
|
// Undraw(
|
||||||
illustration: UndrawIllustration.fixingBugs,
|
// illustration: UndrawIllustration.fixingBugs,
|
||||||
height: 200 * context.theme.scaling,
|
// height: 200 * context.theme.scaling,
|
||||||
color: context.theme.colorScheme.primary,
|
// color: context.theme.colorScheme.primary,
|
||||||
),
|
// ),
|
||||||
Text(context.l10n.something_went_wrong).small().muted(),
|
// Text(context.l10n.something_went_wrong).small().muted(),
|
||||||
const Gap(8),
|
// const Gap(8),
|
||||||
],
|
// ],
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
return Skeletonizer(
|
// return Skeletonizer(
|
||||||
enabled: featuredPlaylists.isLoading,
|
// enabled: featuredPlaylists.isLoading,
|
||||||
child: HorizontalPlaybuttonCardView<PlaylistSimple>(
|
// child: HorizontalPlaybuttonCardView<PlaylistSimple>(
|
||||||
items: featuredPlaylists.asData?.value.items ?? [],
|
// items: featuredPlaylists.asData?.value.items ?? [],
|
||||||
title: Text(context.l10n.featured),
|
// title: Text(context.l10n.featured),
|
||||||
isLoadingNextPage: featuredPlaylists.isLoadingNextPage,
|
// isLoadingNextPage: featuredPlaylists.isLoadingNextPage,
|
||||||
hasNextPage: featuredPlaylists.asData?.value.hasMore ?? false,
|
// hasNextPage: featuredPlaylists.asData?.value.hasMore ?? false,
|
||||||
onFetchMore: featuredPlaylistsNotifier.fetchMore,
|
// onFetchMore: featuredPlaylistsNotifier.fetchMore,
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/authentication/authentication.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/metadata_plugin/album/releases.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||||
|
|
||||||
class HomeNewReleasesSection extends HookConsumerWidget {
|
class HomeNewReleasesSection extends HookConsumerWidget {
|
||||||
const HomeNewReleasesSection({super.key});
|
const HomeNewReleasesSection({super.key});
|
||||||
@ -13,10 +14,9 @@ class HomeNewReleasesSection extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final auth = ref.watch(authenticationProvider);
|
final auth = ref.watch(authenticationProvider);
|
||||||
|
|
||||||
final newReleases = ref.watch(albumReleasesProvider);
|
final newReleases = ref.watch(metadataPluginAlbumReleasesProvider);
|
||||||
final newReleasesNotifier = ref.read(albumReleasesProvider.notifier);
|
final newReleasesNotifier =
|
||||||
|
ref.read(metadataPluginAlbumReleasesProvider.notifier);
|
||||||
final albums = ref.watch(userArtistAlbumReleasesProvider);
|
|
||||||
|
|
||||||
if (auth.asData?.value == null ||
|
if (auth.asData?.value == null ||
|
||||||
newReleases.isLoading ||
|
newReleases.isLoading ||
|
||||||
@ -24,8 +24,8 @@ class HomeNewReleasesSection extends HookConsumerWidget {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
return HorizontalPlaybuttonCardView<Album>(
|
return HorizontalPlaybuttonCardView<SpotubeSimpleAlbumObject>(
|
||||||
items: albums,
|
items: newReleases.asData?.value.items ?? [],
|
||||||
title: Text(context.l10n.new_releases),
|
title: Text(context.l10n.new_releases),
|
||||||
isLoadingNextPage: newReleases.isLoadingNextPage,
|
isLoadingNextPage: newReleases.isLoadingNextPage,
|
||||||
hasNextPage: newReleases.asData?.value.hasMore ?? false,
|
hasNextPage: newReleases.asData?.value.hasMore ?? false,
|
||||||
|
|||||||
@ -11,8 +11,8 @@ import 'package:spotube/collections/spotube_icons.dart';
|
|||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
|
||||||
import 'package:spotube/extensions/string.dart';
|
import 'package:spotube/extensions/string.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ class LocalFolderItem extends HookConsumerWidget {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final track = tracks[index];
|
final track = tracks[index];
|
||||||
return UniversalImage(
|
return UniversalImage(
|
||||||
path: (track.album?.images).asUrlString(
|
path: track.album.images.asUrlString(
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
),
|
),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
|||||||
@ -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:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/components/links/artist_link.dart';
|
import 'package:spotube/components/links/artist_link.dart';
|
||||||
import 'package:spotube/components/ui/button_tile.dart';
|
import 'package:spotube/components/ui/button_tile.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/services/download_manager/download_status.dart';
|
import 'package:spotube/services/download_manager/download_status.dart';
|
||||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||||
|
|
||||||
class DownloadItem extends HookConsumerWidget {
|
class DownloadItem extends HookConsumerWidget {
|
||||||
final Track track;
|
final SpotubeFullTrackObject track;
|
||||||
const DownloadItem({
|
const DownloadItem({
|
||||||
super.key,
|
super.key,
|
||||||
required this.track,
|
required this.track,
|
||||||
@ -29,7 +28,7 @@ class DownloadItem extends HookConsumerWidget {
|
|||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (track is! SourcedTrack) return null;
|
if (track is! SourcedTrack) return null;
|
||||||
final notifier = downloadManager.getStatusNotifier(track as SourcedTrack);
|
final notifier = downloadManager.getStatusNotifier(track);
|
||||||
|
|
||||||
taskStatus.value = notifier?.value;
|
taskStatus.value = notifier?.value;
|
||||||
|
|
||||||
@ -56,18 +55,18 @@ class DownloadItem extends HookConsumerWidget {
|
|||||||
child: UniversalImage(
|
child: UniversalImage(
|
||||||
height: 40,
|
height: 40,
|
||||||
width: 40,
|
width: 40,
|
||||||
path: (track.album?.images).asUrlString(
|
path: track.album.images.asUrlString(
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Text(track.name ?? ''),
|
title: Text(track.name),
|
||||||
subtitle: ArtistLink(
|
subtitle: ArtistLink(
|
||||||
artists: track.artists ?? <Artist>[],
|
artists: track.artists,
|
||||||
mainAxisAlignment: WrapAlignment.start,
|
mainAxisAlignment: WrapAlignment.start,
|
||||||
onOverflowArtistClick: () {
|
onOverflowArtistClick: () {
|
||||||
context.navigateTo(TrackRoute(trackId: track.id!));
|
context.navigateTo(TrackRoute(trackId: track.id));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
trailing: isQueryingSourceInfo
|
trailing: isQueryingSourceInfo
|
||||||
@ -75,8 +74,7 @@ class DownloadItem extends HookConsumerWidget {
|
|||||||
: switch (taskStatus.value!) {
|
: switch (taskStatus.value!) {
|
||||||
DownloadStatus.downloading => HookBuilder(builder: (context) {
|
DownloadStatus.downloading => HookBuilder(builder: (context) {
|
||||||
final taskProgress = useListenable(useMemoized(
|
final taskProgress = useListenable(useMemoized(
|
||||||
() => downloadManager
|
() => downloadManager.getProgressNotifier(track),
|
||||||
.getProgressNotifier(track as SourcedTrack),
|
|
||||||
[track],
|
[track],
|
||||||
));
|
));
|
||||||
return Row(
|
return Row(
|
||||||
@ -88,13 +86,13 @@ class DownloadItem extends HookConsumerWidget {
|
|||||||
IconButton.ghost(
|
IconButton.ghost(
|
||||||
icon: const Icon(SpotubeIcons.pause),
|
icon: const Icon(SpotubeIcons.pause),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
downloadManager.pause(track as SourcedTrack);
|
downloadManager.pause(track);
|
||||||
}),
|
}),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
IconButton.ghost(
|
IconButton.ghost(
|
||||||
icon: const Icon(SpotubeIcons.close),
|
icon: const Icon(SpotubeIcons.close),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
downloadManager.cancel(track as SourcedTrack);
|
downloadManager.cancel(track);
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -105,13 +103,13 @@ class DownloadItem extends HookConsumerWidget {
|
|||||||
IconButton.ghost(
|
IconButton.ghost(
|
||||||
icon: const Icon(SpotubeIcons.play),
|
icon: const Icon(SpotubeIcons.play),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
downloadManager.resume(track as SourcedTrack);
|
downloadManager.resume(track);
|
||||||
}),
|
}),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
IconButton.ghost(
|
IconButton.ghost(
|
||||||
icon: const Icon(SpotubeIcons.close),
|
icon: const Icon(SpotubeIcons.close),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
downloadManager.cancel(track as SourcedTrack);
|
downloadManager.cancel(track);
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -127,7 +125,7 @@ class DownloadItem extends HookConsumerWidget {
|
|||||||
IconButton.ghost(
|
IconButton.ghost(
|
||||||
icon: const Icon(SpotubeIcons.refresh),
|
icon: const Icon(SpotubeIcons.refresh),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
downloadManager.retry(track as SourcedTrack);
|
downloadManager.retry(track);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -138,7 +136,7 @@ class DownloadItem extends HookConsumerWidget {
|
|||||||
DownloadStatus.queued => IconButton.ghost(
|
DownloadStatus.queued => IconButton.ghost(
|
||||||
icon: const Icon(SpotubeIcons.close),
|
icon: const Icon(SpotubeIcons.close),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
downloadManager.removeFromQueue(track as SourcedTrack);
|
downloadManager.removeFromQueue(track);
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import 'package:spotube/collections/assets.gen.dart';
|
|||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/framework/app_pop_scope.dart';
|
import 'package:spotube/components/framework/app_pop_scope.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/modules/player/player_actions.dart';
|
import 'package:spotube/modules/player/player_actions.dart';
|
||||||
import 'package:spotube/modules/player/player_controls.dart';
|
import 'package:spotube/modules/player/player_controls.dart';
|
||||||
import 'package:spotube/modules/player/volume_slider.dart';
|
import 'package:spotube/modules/player/volume_slider.dart';
|
||||||
@ -16,11 +17,8 @@ import 'package:spotube/components/dialogs/track_details_dialog.dart';
|
|||||||
import 'package:spotube/components/links/artist_link.dart';
|
import 'package:spotube/components/links/artist_link.dart';
|
||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/artist_simple.dart';
|
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
|
||||||
import 'package:spotube/models/local_track.dart';
|
|
||||||
import 'package:spotube/modules/root/spotube_navigation_bar.dart';
|
import 'package:spotube/modules/root/spotube_navigation_bar.dart';
|
||||||
import 'package:spotube/provider/authentication/authentication.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
@ -47,8 +45,8 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
final sourcedCurrentTrack = ref.watch(activeTrackSourcesProvider);
|
final sourcedCurrentTrack = ref.watch(activeTrackSourcesProvider);
|
||||||
final currentActiveTrack =
|
final currentActiveTrack =
|
||||||
ref.watch(audioPlayerProvider.select((s) => s.activeTrack));
|
ref.watch(audioPlayerProvider.select((s) => s.activeTrack));
|
||||||
final currentTrack = sourcedCurrentTrack ?? currentActiveTrack;
|
final currentActiveTrackSource = sourcedCurrentTrack.asData?.value?.source;
|
||||||
final isLocalTrack = currentTrack is LocalTrack;
|
final isLocalTrack = currentActiveTrack is SpotubeLocalTrackObject;
|
||||||
final mediaQuery = MediaQuery.sizeOf(context);
|
final mediaQuery = MediaQuery.sizeOf(context);
|
||||||
|
|
||||||
final shouldHide = useState(true);
|
final shouldHide = useState(true);
|
||||||
@ -71,10 +69,10 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
}, [mediaQuery.lgAndUp]);
|
}, [mediaQuery.lgAndUp]);
|
||||||
|
|
||||||
String albumArt = useMemoized(
|
String albumArt = useMemoized(
|
||||||
() => (currentTrack?.album?.images).asUrlString(
|
() => (currentActiveTrack?.album.images).asUrlString(
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
),
|
),
|
||||||
[currentTrack?.album?.images],
|
[currentActiveTrack?.album.images],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
@ -115,7 +113,7 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
trailing: [
|
trailing: [
|
||||||
if (currentTrack is YoutubeSourcedTrack)
|
if (currentActiveTrackSource is YoutubeSourcedTrack)
|
||||||
TextButton(
|
TextButton(
|
||||||
leading: Assets.logos.songlinkTransparent.image(
|
leading: Assets.logos.songlinkTransparent.image(
|
||||||
width: 20,
|
width: 20,
|
||||||
@ -123,31 +121,34 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
color: theme.colorScheme.foreground,
|
color: theme.colorScheme.foreground,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final url = "https://song.link/s/${currentTrack.id}";
|
final url =
|
||||||
|
"https://song.link/s/${currentActiveTrack?.id}";
|
||||||
|
|
||||||
launchUrlString(url);
|
launchUrlString(url);
|
||||||
},
|
},
|
||||||
child: Text(context.l10n.song_link),
|
child: Text(context.l10n.song_link),
|
||||||
),
|
),
|
||||||
Tooltip(
|
if (!isLocalTrack)
|
||||||
tooltip: TooltipContainer(
|
Tooltip(
|
||||||
child: Text(context.l10n.details),
|
tooltip: TooltipContainer(
|
||||||
).call,
|
child: Text(context.l10n.details),
|
||||||
child: IconButton.ghost(
|
).call,
|
||||||
icon: const Icon(SpotubeIcons.info, size: 18),
|
child: IconButton.ghost(
|
||||||
onPressed: currentTrack == null
|
icon: const Icon(SpotubeIcons.info, size: 18),
|
||||||
? null
|
onPressed: currentActiveTrackSource == null
|
||||||
: () {
|
? null
|
||||||
showDialog(
|
: () {
|
||||||
context: context,
|
showDialog(
|
||||||
builder: (context) {
|
context: context,
|
||||||
return TrackDetailsDialog(
|
builder: (context) {
|
||||||
track: currentTrack,
|
return TrackDetailsDialog(
|
||||||
);
|
track: currentActiveTrack
|
||||||
});
|
as SpotubeFullTrackObject,
|
||||||
},
|
);
|
||||||
),
|
});
|
||||||
)
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -190,7 +191,7 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
AutoSizeText(
|
AutoSizeText(
|
||||||
currentTrack?.name ?? context.l10n.not_playing,
|
currentActiveTrack?.name ?? context.l10n.not_playing,
|
||||||
style: const TextStyle(fontSize: 22),
|
style: const TextStyle(fontSize: 22),
|
||||||
maxFontSize: 22,
|
maxFontSize: 22,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
@ -198,13 +199,13 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
if (isLocalTrack)
|
if (isLocalTrack)
|
||||||
Text(
|
Text(
|
||||||
currentTrack.artists?.asString() ?? "",
|
currentActiveTrack.artists.asString(),
|
||||||
style: theme.typography.normal
|
style: theme.typography.normal
|
||||||
.copyWith(fontWeight: FontWeight.bold),
|
.copyWith(fontWeight: FontWeight.bold),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
ArtistLink(
|
ArtistLink(
|
||||||
artists: currentTrack?.artists ?? [],
|
artists: currentActiveTrack?.artists ?? [],
|
||||||
textStyle: theme.typography.normal
|
textStyle: theme.typography.normal
|
||||||
.copyWith(fontWeight: FontWeight.bold),
|
.copyWith(fontWeight: FontWeight.bold),
|
||||||
onRouteChange: (route) {
|
onRouteChange: (route) {
|
||||||
@ -212,7 +213,9 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
context.router.navigateNamed(route);
|
context.router.navigateNamed(route);
|
||||||
},
|
},
|
||||||
onOverflowArtistClick: () => context.navigateTo(
|
onOverflowArtistClick: () => context.navigateTo(
|
||||||
TrackRoute(trackId: currentTrack!.id!),
|
TrackRoute(
|
||||||
|
trackId: currentActiveTrack!.id,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -8,14 +8,13 @@ import 'package:spotube/collections/routes.gr.dart';
|
|||||||
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/modules/player/player_queue.dart';
|
import 'package:spotube/modules/player/player_queue.dart';
|
||||||
import 'package:spotube/modules/player/sibling_tracks_sheet.dart';
|
import 'package:spotube/modules/player/sibling_tracks_sheet.dart';
|
||||||
import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart';
|
import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart';
|
||||||
import 'package:spotube/components/heart_button/heart_button.dart';
|
import 'package:spotube/components/heart_button/heart_button.dart';
|
||||||
import 'package:spotube/extensions/artist_simple.dart';
|
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/duration.dart';
|
import 'package:spotube/extensions/duration.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/authentication/authentication.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
@ -38,12 +37,13 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(audioPlayerProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
final isLocalTrack = playlist.activeTrack is LocalTrack;
|
final isLocalTrack = playlist.activeTrack is SpotubeLocalTrackObject;
|
||||||
ref.watch(downloadManagerProvider);
|
ref.watch(downloadManagerProvider);
|
||||||
final downloader = ref.watch(downloadManagerProvider.notifier);
|
final downloader = ref.watch(downloadManagerProvider.notifier);
|
||||||
final isInQueue = useMemoized(() {
|
final isInQueue = useMemoized(() {
|
||||||
if (playlist.activeTrack == null) return false;
|
if (playlist.activeTrack is! SpotubeFullTrackObject) return false;
|
||||||
return downloader.isActive(playlist.activeTrack!);
|
return downloader
|
||||||
|
.isActive(playlist.activeTrack! as SpotubeFullTrackObject);
|
||||||
}, [
|
}, [
|
||||||
playlist.activeTrack,
|
playlist.activeTrack,
|
||||||
downloader,
|
downloader,
|
||||||
@ -58,9 +58,9 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
return localTracks.any(
|
return localTracks.any(
|
||||||
(element) =>
|
(element) =>
|
||||||
element.name == playlist.activeTrack?.name &&
|
element.name == playlist.activeTrack?.name &&
|
||||||
element.album?.name == playlist.activeTrack?.album?.name &&
|
element.album?.name == playlist.activeTrack?.album.name &&
|
||||||
element.artists?.asString() ==
|
element.artists?.asString() ==
|
||||||
playlist.activeTrack?.artists?.asString(),
|
playlist.activeTrack?.artists.asString(),
|
||||||
) ==
|
) ==
|
||||||
true;
|
true;
|
||||||
}, [localTracks, playlist.activeTrack]);
|
}, [localTracks, playlist.activeTrack]);
|
||||||
@ -168,7 +168,8 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
isDownloaded ? SpotubeIcons.done : SpotubeIcons.download,
|
isDownloaded ? SpotubeIcons.done : SpotubeIcons.download,
|
||||||
),
|
),
|
||||||
onPressed: playlist.activeTrack != null
|
onPressed: playlist.activeTrack != null
|
||||||
? () => downloader.addToQueue(playlist.activeTrack!)
|
? () => downloader.addToQueue(
|
||||||
|
playlist.activeTrack! as SpotubeFullTrackObject)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -7,16 +7,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
|
|
||||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/button/back_button.dart';
|
import 'package:spotube/components/button/back_button.dart';
|
||||||
import 'package:spotube/components/fallbacks/not_found.dart';
|
import 'package:spotube/components/fallbacks/not_found.dart';
|
||||||
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
||||||
import 'package:spotube/components/track_tile/track_tile.dart';
|
import 'package:spotube/components/track_tile/track_tile.dart';
|
||||||
import 'package:spotube/extensions/artist_simple.dart';
|
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart';
|
import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/audio_player/state.dart';
|
import 'package:spotube/provider/audio_player/state.dart';
|
||||||
|
|
||||||
@ -24,7 +23,7 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
final bool floating;
|
final bool floating;
|
||||||
final AudioPlayerState playlist;
|
final AudioPlayerState playlist;
|
||||||
|
|
||||||
final Future<void> Function(Track track) onJump;
|
final Future<void> Function(SpotubeTrackObject track) onJump;
|
||||||
final Future<void> Function(String trackId) onRemove;
|
final Future<void> Function(String trackId) onRemove;
|
||||||
final Future<void> Function(int oldIndex, int newIndex) onReorder;
|
final Future<void> Function(int oldIndex, int newIndex) onReorder;
|
||||||
final Future<void> Function() onStop;
|
final Future<void> Function() onStop;
|
||||||
@ -68,7 +67,7 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
return tracks
|
return tracks
|
||||||
.map((e) => (
|
.map((e) => (
|
||||||
weightedRatio(
|
weightedRatio(
|
||||||
'${e.name!} - ${e.artists?.asString() ?? ""}',
|
'${e.name} - ${e.artists.asString()}',
|
||||||
searchText.value,
|
searchText.value,
|
||||||
),
|
),
|
||||||
e
|
e
|
||||||
@ -161,7 +160,8 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
tooltip: TooltipContainer(
|
tooltip: TooltipContainer(
|
||||||
child: Text(context.l10n.clear_all)).call,
|
child: Text(context.l10n.clear_all))
|
||||||
|
.call,
|
||||||
child: IconButton.outline(
|
child: IconButton.outline(
|
||||||
icon: const Icon(SpotubeIcons.playlistRemove),
|
icon: const Icon(SpotubeIcons.playlistRemove),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -244,7 +244,7 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
icon: const Icon(SpotubeIcons.angleDown),
|
icon: const Icon(SpotubeIcons.angleDown),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
controller.scrollToIndex(
|
controller.scrollToIndex(
|
||||||
playlist.playlist.index,
|
playlist.currentIndex,
|
||||||
preferPosition: AutoScrollPosition.middle,
|
preferPosition: AutoScrollPosition.middle,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,21 +2,19 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
|
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
|
|
||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/components/links/artist_link.dart';
|
import 'package:spotube/components/links/artist_link.dart';
|
||||||
import 'package:spotube/components/links/link_text.dart';
|
import 'package:spotube/components/links/link_text.dart';
|
||||||
import 'package:spotube/extensions/artist_simple.dart';
|
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
|
|
||||||
class PlayerTrackDetails extends HookConsumerWidget {
|
class PlayerTrackDetails extends HookConsumerWidget {
|
||||||
final Color? color;
|
final Color? color;
|
||||||
final Track? track;
|
final SpotubeTrackObject? track;
|
||||||
const PlayerTrackDetails({super.key, this.color, this.track});
|
const PlayerTrackDetails({super.key, this.color, this.track});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -37,7 +35,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
|
|||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
child: UniversalImage(
|
child: UniversalImage(
|
||||||
path: (track?.album?.images)
|
path: (track?.album.images)
|
||||||
.asUrlString(placeholder: ImagePlaceholder.albumArt),
|
.asUrlString(placeholder: ImagePlaceholder.albumArt),
|
||||||
placeholder: Assets.albumPlaceholder.path,
|
placeholder: Assets.albumPlaceholder.path,
|
||||||
),
|
),
|
||||||
@ -59,7 +57,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
playback.activeTrack?.artists?.asString() ?? "",
|
playback.activeTrack?.artists.asString() ?? "",
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: theme.typography.small.copyWith(color: color),
|
style: theme.typography.small.copyWith(color: color),
|
||||||
)
|
)
|
||||||
@ -84,7 +82,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
|
|||||||
context.router.navigateNamed(route);
|
context.router.navigateNamed(route);
|
||||||
},
|
},
|
||||||
onOverflowArtistClick: () =>
|
onOverflowArtistClick: () =>
|
||||||
context.navigateTo(TrackRoute(trackId: track!.id!)),
|
context.navigateTo(TrackRoute(trackId: track!.id)),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -10,30 +10,27 @@ import 'package:spotube/components/button/back_button.dart';
|
|||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
||||||
import 'package:spotube/components/ui/button_tile.dart';
|
import 'package:spotube/components/ui/button_tile.dart';
|
||||||
import 'package:spotube/extensions/artist_simple.dart';
|
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/duration.dart';
|
import 'package:spotube/extensions/duration.dart';
|
||||||
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
|
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
|
||||||
import 'package:spotube/hooks/utils/use_debounce.dart';
|
import 'package:spotube/hooks/utils/use_debounce.dart';
|
||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
|
import 'package:spotube/models/playback/track_sources.dart';
|
||||||
import 'package:spotube/provider/audio_player/querying_track_info.dart';
|
import 'package:spotube/provider/audio_player/querying_track_info.dart';
|
||||||
import 'package:spotube/provider/server/active_track_sources.dart';
|
import 'package:spotube/provider/server/active_track_sources.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/provider/youtube_engine/youtube_engine.dart';
|
import 'package:spotube/provider/youtube_engine/youtube_engine.dart';
|
||||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/models/video_info.dart';
|
import 'package:spotube/services/sourced_track/models/video_info.dart';
|
||||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/sources/invidious.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/sources/jiosaavn.dart';
|
import 'package:spotube/services/sourced_track/sources/jiosaavn.dart';
|
||||||
import 'package:spotube/services/sourced_track/sources/piped.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
final sourceInfoToIconMap = {
|
final sourceInfoToIconMap = {
|
||||||
YoutubeSourceInfo: const Icon(SpotubeIcons.youtube, color: Color(0xFFFF0000)),
|
AudioSource.youtube:
|
||||||
JioSaavnSourceInfo: Container(
|
const Icon(SpotubeIcons.youtube, color: Color(0xFFFF0000)),
|
||||||
|
AudioSource.jiosaavn: Container(
|
||||||
height: 30,
|
height: 30,
|
||||||
width: 30,
|
width: 30,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -44,8 +41,8 @@ final sourceInfoToIconMap = {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PipedSourceInfo: const Icon(SpotubeIcons.piped),
|
AudioSource.piped: const Icon(SpotubeIcons.piped),
|
||||||
InvidiousSourceInfo: Container(
|
AudioSource.invidious: Container(
|
||||||
height: 18,
|
height: 18,
|
||||||
width: 18,
|
width: 18,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -68,25 +65,25 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final playlist = ref.watch(audioPlayerProvider);
|
|
||||||
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
|
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
|
||||||
final preferences = ref.watch(userPreferencesProvider);
|
final preferences = ref.watch(userPreferencesProvider);
|
||||||
final youtubeEngine = ref.watch(youtubeEngineProvider);
|
final youtubeEngine = ref.watch(youtubeEngineProvider);
|
||||||
|
|
||||||
final isSearching = useState(false);
|
final isSearching = useState(false);
|
||||||
final searchMode = useState(preferences.searchMode);
|
final searchMode = useState(preferences.searchMode);
|
||||||
final activeTrackNotifier = ref.watch(activeTrackSourcesProvider.notifier);
|
final activeTrackSources = ref.watch(activeTrackSourcesProvider);
|
||||||
final activeTrack =
|
final activeTrackNotifier = activeTrackSources.asData?.value?.notifier;
|
||||||
ref.watch(activeTrackSourcesProvider) ?? playlist.activeTrack;
|
final activeTrack = activeTrackSources.asData?.value?.track;
|
||||||
|
final activeTrackSource = activeTrackSources.asData?.value?.source;
|
||||||
|
|
||||||
final title = ServiceUtils.getTitle(
|
final title = ServiceUtils.getTitle(
|
||||||
activeTrack?.name ?? "",
|
activeTrack?.name ?? "",
|
||||||
artists: activeTrack?.artists?.map((e) => e.name!).toList() ?? [],
|
artists: activeTrack?.artists.map((e) => e.name).toList() ?? [],
|
||||||
onlyCleanArtist: true,
|
onlyCleanArtist: true,
|
||||||
).trim();
|
).trim();
|
||||||
|
|
||||||
final defaultSearchTerm =
|
final defaultSearchTerm =
|
||||||
"$title - ${activeTrack?.artists?.asString() ?? ""}";
|
"$title - ${activeTrack?.artists.asString() ?? ""}";
|
||||||
final searchController = useShadcnTextEditingController(
|
final searchController = useShadcnTextEditingController(
|
||||||
text: defaultSearchTerm,
|
text: defaultSearchTerm,
|
||||||
);
|
);
|
||||||
@ -99,7 +96,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
|
|
||||||
final searchRequest = useMemoized(() async {
|
final searchRequest = useMemoized(() async {
|
||||||
if (searchTerm.trim().isEmpty) {
|
if (searchTerm.trim().isEmpty) {
|
||||||
return <SourceInfo>[];
|
return <TrackSourceInfo>[];
|
||||||
}
|
}
|
||||||
if (preferences.audioSource == AudioSource.jiosaavn) {
|
if (preferences.audioSource == AudioSource.jiosaavn) {
|
||||||
final resultsJioSaavn =
|
final resultsJioSaavn =
|
||||||
@ -110,7 +107,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
return siblingType.info;
|
return siblingType.info;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
final activeSourceInfo = (activeTrack! as SourcedTrack).sourceInfo;
|
final activeSourceInfo = activeTrackSource as TrackSourceInfo;
|
||||||
|
|
||||||
return results
|
return results
|
||||||
..removeWhere((element) => element.id == activeSourceInfo.id)
|
..removeWhere((element) => element.id == activeSourceInfo.id)
|
||||||
@ -130,7 +127,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
return siblingType.info;
|
return siblingType.info;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
final activeSourceInfo = (activeTrack! as SourcedTrack).sourceInfo;
|
final activeSourceInfo = activeTrackSource as TrackSourceInfo;
|
||||||
return searchResults
|
return searchResults
|
||||||
..removeWhere((element) => element.id == activeSourceInfo.id)
|
..removeWhere((element) => element.id == activeSourceInfo.id)
|
||||||
..insert(
|
..insert(
|
||||||
@ -142,6 +139,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
searchTerm,
|
searchTerm,
|
||||||
searchMode.value,
|
searchMode.value,
|
||||||
activeTrack,
|
activeTrack,
|
||||||
|
activeTrackSource,
|
||||||
preferences.audioSource,
|
preferences.audioSource,
|
||||||
youtubeEngine,
|
youtubeEngine,
|
||||||
]);
|
]);
|
||||||
@ -149,25 +147,25 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
final siblings = useMemoized(
|
final siblings = useMemoized(
|
||||||
() => !isFetchingActiveTrack
|
() => !isFetchingActiveTrack
|
||||||
? [
|
? [
|
||||||
(activeTrack as SourcedTrack).sourceInfo,
|
if (activeTrackSource != null) activeTrackSource.info,
|
||||||
...activeTrack.siblings,
|
...?activeTrackSource?.siblings,
|
||||||
]
|
]
|
||||||
: <SourceInfo>[],
|
: <TrackSourceInfo>[],
|
||||||
[activeTrack, isFetchingActiveTrack],
|
[activeTrackSource, isFetchingActiveTrack],
|
||||||
);
|
);
|
||||||
|
|
||||||
final previousActiveTrack = usePrevious(activeTrack);
|
final previousActiveTrack = usePrevious(activeTrack);
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
/// Populate sibling when active track changes
|
/// Populate sibling when active track changes
|
||||||
if (previousActiveTrack?.id == activeTrack?.id) return;
|
if (previousActiveTrack?.id == activeTrack?.id) return;
|
||||||
if (activeTrack is SourcedTrack && activeTrack.siblings.isEmpty) {
|
if (activeTrackSource != null && activeTrackSource.siblings.isEmpty) {
|
||||||
activeTrackNotifier.populateSibling();
|
activeTrackNotifier?.copyWithSibling();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [activeTrack, previousActiveTrack]);
|
}, [activeTrack, previousActiveTrack]);
|
||||||
|
|
||||||
final itemBuilder = useCallback(
|
final itemBuilder = useCallback(
|
||||||
(SourceInfo sourceInfo) {
|
(TrackSourceInfo sourceInfo) {
|
||||||
final icon = sourceInfoToIconMap[sourceInfo.runtimeType];
|
final icon = sourceInfoToIconMap[sourceInfo.runtimeType];
|
||||||
return ButtonTile(
|
return ButtonTile(
|
||||||
style: ButtonVariance.ghost,
|
style: ButtonVariance.ghost,
|
||||||
@ -182,13 +180,14 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
height: 60,
|
height: 60,
|
||||||
width: 60,
|
width: 60,
|
||||||
),
|
),
|
||||||
trailing: Text(sourceInfo.duration.toHumanReadableString()),
|
trailing: Text(Duration(milliseconds: sourceInfo.durationMs)
|
||||||
|
.toHumanReadableString()),
|
||||||
subtitle: Row(
|
subtitle: Row(
|
||||||
children: [
|
children: [
|
||||||
if (icon != null) icon,
|
if (icon != null) icon,
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
" • ${sourceInfo.artist}",
|
" • ${sourceInfo.artists}",
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@ -197,11 +196,11 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
enabled: !isFetchingActiveTrack,
|
enabled: !isFetchingActiveTrack,
|
||||||
selected: !isFetchingActiveTrack &&
|
selected: !isFetchingActiveTrack &&
|
||||||
sourceInfo.id == (activeTrack as SourcedTrack).sourceInfo.id,
|
sourceInfo.id == activeTrackSource?.info.id,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (!isFetchingActiveTrack &&
|
if (!isFetchingActiveTrack &&
|
||||||
sourceInfo.id != (activeTrack as SourcedTrack).sourceInfo.id) {
|
sourceInfo.id != activeTrackSource?.info.id) {
|
||||||
activeTrackNotifier.swapSibling(sourceInfo);
|
activeTrackNotifier?.swapWithSibling(sourceInfo);
|
||||||
if (MediaQuery.sizeOf(context).mdAndUp) {
|
if (MediaQuery.sizeOf(context).mdAndUp) {
|
||||||
closeOverlay(context);
|
closeOverlay(context);
|
||||||
} else {
|
} else {
|
||||||
@ -211,7 +210,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[activeTrack, siblings],
|
[activeTrackSource, activeTrackNotifier, siblings],
|
||||||
);
|
);
|
||||||
|
|
||||||
final scale = context.theme.scaling;
|
final scale = context.theme.scaling;
|
||||||
|
|||||||
@ -2,8 +2,6 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
||||||
import 'package:spotify/spotify.dart' hide Offset, Image;
|
|
||||||
import 'package:spotube/collections/env.dart';
|
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/components/dialogs/select_device_dialog.dart';
|
import 'package:spotube/components/dialogs/select_device_dialog.dart';
|
||||||
import 'package:spotube/components/playbutton_view/playbutton_card.dart';
|
import 'package:spotube/components/playbutton_view/playbutton_card.dart';
|
||||||
@ -15,9 +13,10 @@ import 'package:spotube/provider/audio_player/querying_track_info.dart';
|
|||||||
import 'package:spotube/provider/connect/connect.dart';
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
import 'package:spotube/provider/history/history.dart';
|
import 'package:spotube/provider/history/history.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/user.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:stroke_text/stroke_text.dart';
|
|
||||||
|
|
||||||
class PlaylistCard extends HookConsumerWidget {
|
class PlaylistCard extends HookConsumerWidget {
|
||||||
final SpotubeSimplePlaylistObject playlist;
|
final SpotubeSimplePlaylistObject playlist;
|
||||||
@ -48,26 +47,30 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final updating = useState(false);
|
final updating = useState(false);
|
||||||
final me = ref.watch(meProvider);
|
final me = ref.watch(metadataPluginUserProvider);
|
||||||
|
|
||||||
Future<List<Track>> fetchInitialTracks() async {
|
Future<List<SpotubeTrackObject>> fetchInitialTracks() async {
|
||||||
if (playlist.id == 'user-liked-tracks') {
|
if (playlist.id == 'user-liked-tracks') {
|
||||||
return await ref.read(likedTracksProvider.future);
|
final tracks = await ref.read(metadataPluginSavedTracksProvider.future);
|
||||||
|
return tracks.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = await ref.read(playlistTracksProvider(playlist.id).future);
|
final result = await ref
|
||||||
|
.read(metadataPluginPlaylistTracksProvider(playlist.id).future);
|
||||||
|
|
||||||
return result.items;
|
return result.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Track>> fetchAllTracks() async {
|
Future<List<SpotubeTrackObject>> fetchAllTracks() async {
|
||||||
final initialTracks = await fetchInitialTracks();
|
final initialTracks = await fetchInitialTracks();
|
||||||
|
|
||||||
if (playlist.id == 'user-liked-tracks') {
|
if (playlist.id == 'user-liked-tracks') {
|
||||||
return initialTracks;
|
return initialTracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ref.read(playlistTracksProvider(playlist.id).notifier).fetchAll();
|
return ref
|
||||||
|
.read(metadataPluginPlaylistTracksProvider(playlist.id).notifier)
|
||||||
|
.fetchAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
void onTap() {
|
void onTap() {
|
||||||
@ -94,14 +97,14 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
final allTracks = await fetchAllTracks();
|
final allTracks = await fetchAllTracks();
|
||||||
await remotePlayback.load(
|
await remotePlayback.load(
|
||||||
WebSocketLoadEventData.playlist(
|
WebSocketLoadEventData.playlist(
|
||||||
tracks: allTracks,
|
tracks: allTracks as List<SpotubeFullTrackObject>,
|
||||||
// collection: playlist,
|
collection: playlist,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await playlistNotifier.load(fetchedInitialTracks, autoPlay: true);
|
await playlistNotifier.load(fetchedInitialTracks, autoPlay: true);
|
||||||
playlistNotifier.addCollection(playlist.id);
|
playlistNotifier.addCollection(playlist.id);
|
||||||
// historyNotifier.addPlaylists([playlist]);
|
historyNotifier.addPlaylists([playlist]);
|
||||||
|
|
||||||
final allTracks = await fetchAllTracks();
|
final allTracks = await fetchAllTracks();
|
||||||
|
|
||||||
@ -126,7 +129,7 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
|
|
||||||
playlistNotifier.addTracks(fetchedInitialTracks);
|
playlistNotifier.addTracks(fetchedInitialTracks);
|
||||||
playlistNotifier.addCollection(playlist.id);
|
playlistNotifier.addCollection(playlist.id);
|
||||||
// historyNotifier.addPlaylists([playlist]);
|
historyNotifier.addPlaylists([playlist]);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showToast(
|
showToast(
|
||||||
context: context,
|
context: context,
|
||||||
@ -141,7 +144,7 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
child: Text(context.l10n.undo),
|
child: Text(context.l10n.undo),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
playlistNotifier
|
playlistNotifier
|
||||||
.removeTracks(fetchedInitialTracks.map((e) => e.id!));
|
.removeTracks(fetchedInitialTracks.map((e) => e.id));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -159,52 +162,15 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
final isLoading =
|
final isLoading =
|
||||||
(isPlaylistPlaying && isFetchingActiveTrack) || updating.value;
|
(isPlaylistPlaying && isFetchingActiveTrack) || updating.value;
|
||||||
final isOwner =
|
final isOwner = playlist.owner.id == me.asData?.value?.id &&
|
||||||
playlist.owner.id == me.asData?.value.id && me.asData?.value.id != null;
|
me.asData?.value?.id != null;
|
||||||
|
|
||||||
final image = playlist.owner.name == "Spotify" && Env.disableSpotifyImages
|
|
||||||
? Consumer(
|
|
||||||
builder: (context, ref, child) {
|
|
||||||
final (:color, :colorBlendMode, :src, :placement) =
|
|
||||||
ref.watch(playlistImageProvider(playlist.id));
|
|
||||||
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
Positioned.fill(
|
|
||||||
child: Image.asset(
|
|
||||||
src,
|
|
||||||
color: color,
|
|
||||||
colorBlendMode: colorBlendMode,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned.fill(
|
|
||||||
top: placement == Alignment.topLeft ? 10 : null,
|
|
||||||
left: 10,
|
|
||||||
bottom: placement == Alignment.bottomLeft ? 10 : null,
|
|
||||||
child: StrokeText(
|
|
||||||
text: playlist.name,
|
|
||||||
strokeColor: Colors.white,
|
|
||||||
strokeWidth: 3,
|
|
||||||
textColor: Colors.black,
|
|
||||||
textStyle: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (_isTile) {
|
if (_isTile) {
|
||||||
return PlaybuttonTile(
|
return PlaybuttonTile(
|
||||||
title: playlist.name,
|
title: playlist.name,
|
||||||
description: playlist.description,
|
description: playlist.description,
|
||||||
image: image,
|
image: null,
|
||||||
imageUrl: image == null ? imageUrl : null,
|
imageUrl: imageUrl,
|
||||||
isPlaying: isPlaylistPlaying,
|
isPlaying: isPlaylistPlaying,
|
||||||
isLoading: isLoading,
|
isLoading: isLoading,
|
||||||
isOwner: isOwner,
|
isOwner: isOwner,
|
||||||
@ -217,8 +183,8 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
return PlaybuttonCard(
|
return PlaybuttonCard(
|
||||||
title: playlist.name,
|
title: playlist.name,
|
||||||
description: playlist.description,
|
description: playlist.description,
|
||||||
image: image,
|
image: null,
|
||||||
imageUrl: image == null ? imageUrl : null,
|
imageUrl: imageUrl,
|
||||||
isPlaying: isPlaylistPlaying,
|
isPlaying: isPlaylistPlaying,
|
||||||
isLoading: isLoading,
|
isLoading: isLoading,
|
||||||
isOwner: isOwner,
|
isOwner: isOwner,
|
||||||
|
|||||||
@ -10,15 +10,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/form/checkbox_form_field.dart';
|
import 'package:spotube/components/form/checkbox_form_field.dart';
|
||||||
import 'package:spotube/components/form/text_form_field.dart';
|
import 'package:spotube/components/form/text_form_field.dart';
|
||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/playlist/playlist.dart';
|
||||||
|
|
||||||
class PlaylistCreateDialog extends HookConsumerWidget {
|
class PlaylistCreateDialog extends HookConsumerWidget {
|
||||||
/// Track ids to add to the playlist
|
/// Track ids to add to the playlist
|
||||||
@ -32,10 +32,11 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final userPlaylists = ref.watch(favoritePlaylistsProvider);
|
final userPlaylists = ref.watch(metadataPluginSavedPlaylistsProvider);
|
||||||
final playlist = ref.watch(playlistProvider(playlistId ?? ""));
|
final playlist =
|
||||||
|
ref.watch(metadataPluginPlaylistProvider(playlistId ?? ""));
|
||||||
final playlistNotifier =
|
final playlistNotifier =
|
||||||
ref.watch(playlistProvider(playlistId ?? "").notifier);
|
ref.watch(metadataPluginPlaylistProvider(playlistId ?? "").notifier);
|
||||||
|
|
||||||
final isSubmitting = useState(false);
|
final isSubmitting = useState(false);
|
||||||
|
|
||||||
@ -55,25 +56,54 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
if (playlist.asData?.value != null) {
|
||||||
|
formKey.currentState?.patchValue({
|
||||||
|
'playlistName': playlist.asData!.value.name,
|
||||||
|
'description': playlist.asData!.value.description,
|
||||||
|
'public': playlist.asData!.value.public,
|
||||||
|
'collaborative': playlist.asData!.value.collaborative,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}, [playlist]);
|
||||||
|
|
||||||
final onError = useCallback((error) {
|
final onError = useCallback((error) {
|
||||||
if (error is SpotifyError || error is SpotifyException) {
|
// if (error is SpotifyError || error is SpotifyException) {
|
||||||
showToast(
|
// showToast(
|
||||||
context: context,
|
// context: context,
|
||||||
location: ToastLocation.topRight,
|
// location: ToastLocation.topRight,
|
||||||
builder: (context, overlay) {
|
// builder: (context, overlay) {
|
||||||
return SurfaceCard(
|
// return SurfaceCard(
|
||||||
child: Basic(
|
// child: Basic(
|
||||||
title: Text(
|
// title: Text(
|
||||||
l10n.error(error.message ?? l10n.epic_failure),
|
// l10n.error(error.message ?? l10n.epic_failure),
|
||||||
style: theme.typography.normal.copyWith(
|
// style: theme.typography.normal.copyWith(
|
||||||
color: theme.colorScheme.destructive,
|
// color: theme.colorScheme.destructive,
|
||||||
),
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
showToast(
|
||||||
|
context: context,
|
||||||
|
location: ToastLocation.topRight,
|
||||||
|
builder: (context, overlay) {
|
||||||
|
return SurfaceCard(
|
||||||
|
child: Basic(
|
||||||
|
title: Text(
|
||||||
|
l10n.error(l10n.epic_failure),
|
||||||
|
style: theme.typography.normal.copyWith(
|
||||||
|
color: theme.colorScheme.destructive,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
);
|
},
|
||||||
}
|
);
|
||||||
}, [l10n, theme]);
|
}, [l10n, theme]);
|
||||||
|
|
||||||
Future<void> onCreate() async {
|
Future<void> onCreate() async {
|
||||||
@ -83,7 +113,7 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
isSubmitting.value = true;
|
isSubmitting.value = true;
|
||||||
final values = formKey.currentState!.value;
|
final values = formKey.currentState!.value;
|
||||||
|
|
||||||
final PlaylistInput payload = (
|
final payload = (
|
||||||
playlistName: values['playlistName'],
|
playlistName: values['playlistName'],
|
||||||
collaborative: values['collaborative'],
|
collaborative: values['collaborative'],
|
||||||
public: values['public'],
|
public: values['public'],
|
||||||
@ -96,9 +126,21 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isUpdatingPlaylist) {
|
if (isUpdatingPlaylist) {
|
||||||
await playlistNotifier.modify(payload, onError);
|
await playlistNotifier.modify(
|
||||||
|
name: payload.playlistName,
|
||||||
|
description: payload.description,
|
||||||
|
public: payload.public,
|
||||||
|
collaborative: payload.collaborative,
|
||||||
|
onError: onError,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await playlistNotifier.create(payload, onError);
|
await playlistNotifier.create(
|
||||||
|
name: payload.playlistName,
|
||||||
|
description: payload.description,
|
||||||
|
public: payload.public,
|
||||||
|
collaborative: payload.collaborative,
|
||||||
|
onError: onError,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackIds.isNotEmpty) {
|
if (trackIds.isNotEmpty) {
|
||||||
@ -107,9 +149,12 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
} finally {
|
} finally {
|
||||||
isSubmitting.value = false;
|
isSubmitting.value = false;
|
||||||
if (context.mounted &&
|
if (context.mounted &&
|
||||||
!ref.read(playlistProvider(playlistId ?? "")).hasError) {
|
!ref
|
||||||
context.router.maybePop<Playlist>(
|
.read(metadataPluginPlaylistProvider(playlistId ?? ""))
|
||||||
await ref.read(playlistProvider(playlistId ?? "").future),
|
.hasError) {
|
||||||
|
context.router.maybePop<SpotubeFullPlaylistObject>(
|
||||||
|
await ref
|
||||||
|
.read(metadataPluginPlaylistProvider(playlistId ?? "").future),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -144,8 +189,8 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
initialValue: {
|
initialValue: {
|
||||||
'playlistName': updatingPlaylist?.name,
|
'playlistName': updatingPlaylist?.name,
|
||||||
'description': updatingPlaylist?.description,
|
'description': updatingPlaylist?.description,
|
||||||
'public': updatingPlaylist?.public ?? false,
|
'public': playlist.asData?.value.public ?? false,
|
||||||
'collaborative': updatingPlaylist?.collaborative ?? false,
|
'collaborative': playlist.asData?.value.collaborative ?? false,
|
||||||
},
|
},
|
||||||
child: ListView(
|
child: ListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
@ -259,7 +304,7 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
class PlaylistCreateDialogButton extends HookConsumerWidget {
|
class PlaylistCreateDialogButton extends HookConsumerWidget {
|
||||||
const PlaylistCreateDialogButton({super.key});
|
const PlaylistCreateDialogButton({super.key});
|
||||||
|
|
||||||
showPlaylistDialog(BuildContext context, SpotifyApiWrapper spotify) {
|
showPlaylistDialog(BuildContext context) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
@ -271,12 +316,10 @@ class PlaylistCreateDialogButton extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final spotify = ref.watch(spotifyProvider);
|
|
||||||
|
|
||||||
return Button.secondary(
|
return Button.secondary(
|
||||||
leading: const Icon(SpotubeIcons.addFilled),
|
leading: const Icon(SpotubeIcons.addFilled),
|
||||||
child: Text(context.l10n.playlist),
|
child: Text(context.l10n.playlist),
|
||||||
onPressed: () => showPlaylistDialog(context, spotify),
|
onPressed: () => showPlaylistDialog(context),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import 'package:spotube/collections/assets.gen.dart';
|
|||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/modules/player/player_actions.dart';
|
import 'package:spotube/modules/player/player_actions.dart';
|
||||||
import 'package:spotube/modules/player/player_overlay.dart';
|
import 'package:spotube/modules/player/player_overlay.dart';
|
||||||
import 'package:spotube/modules/player/player_track_details.dart';
|
import 'package:spotube/modules/player/player_track_details.dart';
|
||||||
@ -15,7 +16,6 @@ import 'package:spotube/modules/player/player_controls.dart';
|
|||||||
import 'package:spotube/modules/player/volume_slider.dart';
|
import 'package:spotube/modules/player/volume_slider.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
|
|
||||||
@ -35,13 +35,13 @@ class BottomPlayer extends HookConsumerWidget {
|
|||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
|
||||||
String albumArt = useMemoized(
|
String albumArt = useMemoized(
|
||||||
() => playlist.activeTrack?.album?.images?.isNotEmpty == true
|
() => playlist.activeTrack?.album.images.isNotEmpty == true
|
||||||
? (playlist.activeTrack?.album?.images).asUrlString(
|
? (playlist.activeTrack?.album.images).asUrlString(
|
||||||
index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1,
|
index: (playlist.activeTrack?.album.images.length ?? 1) - 1,
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
)
|
)
|
||||||
: Assets.albumPlaceholder.path,
|
: Assets.albumPlaceholder.path,
|
||||||
[playlist.activeTrack?.album?.images],
|
[playlist.activeTrack?.album.images],
|
||||||
);
|
);
|
||||||
|
|
||||||
// returning an empty non spacious Container as the overlay will take
|
// returning an empty non spacious Container as the overlay will take
|
||||||
@ -76,7 +76,8 @@ class BottomPlayer extends HookConsumerWidget {
|
|||||||
extraActions: [
|
extraActions: [
|
||||||
Tooltip(
|
Tooltip(
|
||||||
tooltip:
|
tooltip:
|
||||||
TooltipContainer(child: Text(context.l10n.mini_player)).call,
|
TooltipContainer(child: Text(context.l10n.mini_player))
|
||||||
|
.call,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
variance: ButtonVariance.ghost,
|
variance: ButtonVariance.ghost,
|
||||||
icon: const Icon(SpotubeIcons.miniPlayer),
|
icon: const Icon(SpotubeIcons.miniPlayer),
|
||||||
|
|||||||
@ -6,13 +6,13 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
|
|||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/modules/connect/connect_device.dart';
|
import 'package:spotube/modules/connect/connect_device.dart';
|
||||||
import 'package:spotube/provider/authentication/authentication.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/metadata_plugin/user.dart';
|
||||||
|
|
||||||
class SidebarFooter extends HookConsumerWidget implements NavigationBarItem {
|
class SidebarFooter extends HookConsumerWidget implements NavigationBarItem {
|
||||||
const SidebarFooter({
|
const SidebarFooter({
|
||||||
@ -25,11 +25,11 @@ class SidebarFooter extends HookConsumerWidget implements NavigationBarItem {
|
|||||||
final router = AutoRouter.of(context, watch: true);
|
final router = AutoRouter.of(context, watch: true);
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
final downloadCount = ref.watch(downloadManagerProvider).$downloadCount;
|
final downloadCount = ref.watch(downloadManagerProvider).$downloadCount;
|
||||||
final userSnapshot = ref.watch(meProvider);
|
final userSnapshot = ref.watch(metadataPluginUserProvider);
|
||||||
final data = userSnapshot.asData?.value;
|
final data = userSnapshot.asData?.value;
|
||||||
|
|
||||||
final avatarImg = (data?.images).asUrlString(
|
final avatarImg = (data?.images).asUrlString(
|
||||||
index: (data?.images?.length ?? 1) - 1,
|
index: (data?.images.length ?? 1) - 1,
|
||||||
placeholder: ImagePlaceholder.artist,
|
placeholder: ImagePlaceholder.artist,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -102,14 +102,13 @@ class SidebarFooter extends HookConsumerWidget implements NavigationBarItem {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Avatar(
|
Avatar(
|
||||||
initials:
|
initials: Avatar.getInitials(data.name),
|
||||||
Avatar.getInitials(data.displayName ?? "User"),
|
|
||||||
provider: UniversalImage.imageProvider(avatarImg),
|
provider: UniversalImage.imageProvider(avatarImg),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
data.displayName ?? context.l10n.guest,
|
data.name,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
|
|||||||
@ -1,15 +1,14 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
|
import 'package:spotube/components/links/artist_link.dart';
|
||||||
import 'package:spotube/components/ui/button_tile.dart';
|
import 'package:spotube/components/ui/button_tile.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/modules/album/album_card.dart';
|
import 'package:spotube/modules/album/album_card.dart';
|
||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/components/links/artist_link.dart';
|
|
||||||
import 'package:spotube/extensions/image.dart';
|
|
||||||
|
|
||||||
class StatsAlbumItem extends StatelessWidget {
|
class StatsAlbumItem extends StatelessWidget {
|
||||||
final AlbumSimple album;
|
final SpotubeSimpleAlbumObject album;
|
||||||
final Widget info;
|
final Widget info;
|
||||||
const StatsAlbumItem({super.key, required this.album, required this.info});
|
const StatsAlbumItem({super.key, required this.album, required this.info});
|
||||||
|
|
||||||
@ -27,24 +26,24 @@ class StatsAlbumItem extends StatelessWidget {
|
|||||||
height: 40,
|
height: 40,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Text(album.name!),
|
title: Text(album.name),
|
||||||
subtitle: Row(
|
subtitle: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text("${album.albumType?.formatted} • "),
|
Text("${album.albumType.formatted} • "),
|
||||||
// Flexible(
|
Flexible(
|
||||||
// child: ArtistLink(
|
child: ArtistLink(
|
||||||
// artists: album.artists ?? [],
|
artists: album.artists,
|
||||||
// mainAxisAlignment: WrapAlignment.start,
|
mainAxisAlignment: WrapAlignment.start,
|
||||||
// onOverflowArtistClick: () =>
|
onOverflowArtistClick: () =>
|
||||||
// context.navigateTo(AlbumRoute(id: album.id!, album: album)),
|
context.navigateTo(AlbumRoute(id: album.id, album: album)),
|
||||||
// ),
|
),
|
||||||
// ),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
trailing: info,
|
trailing: info,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// context.navigateTo(AlbumRoute(id: album.id!, album: album));
|
context.navigateTo(AlbumRoute(id: album.id, album: album));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/components/ui/button_tile.dart';
|
import 'package:spotube/components/ui/button_tile.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
|
|
||||||
class StatsArtistItem extends StatelessWidget {
|
class StatsArtistItem extends StatelessWidget {
|
||||||
final Artist artist;
|
final SpotubeSimpleArtistObject artist;
|
||||||
final Widget info;
|
final Widget info;
|
||||||
const StatsArtistItem({
|
const StatsArtistItem({
|
||||||
super.key,
|
super.key,
|
||||||
@ -19,9 +18,9 @@ class StatsArtistItem extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ButtonTile(
|
return ButtonTile(
|
||||||
style: ButtonVariance.ghost,
|
style: ButtonVariance.ghost,
|
||||||
title: Text(artist.name!),
|
title: Text(artist.name),
|
||||||
leading: Avatar(
|
leading: Avatar(
|
||||||
initials: artist.name!.substring(0, 1),
|
initials: artist.name.substring(0, 1),
|
||||||
provider: UniversalImage.imageProvider(
|
provider: UniversalImage.imageProvider(
|
||||||
(artist.images).asUrlString(
|
(artist.images).asUrlString(
|
||||||
placeholder: ImagePlaceholder.artist,
|
placeholder: ImagePlaceholder.artist,
|
||||||
@ -30,7 +29,7 @@ class StatsArtistItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
trailing: info,
|
trailing: info,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.navigateTo(ArtistRoute(artistId: artist.id!));
|
context.navigateTo(ArtistRoute(artistId: artist.id));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,11 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
|
||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/components/ui/button_tile.dart';
|
import 'package:spotube/components/ui/button_tile.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
|
||||||
import 'package:spotube/extensions/string.dart';
|
import 'package:spotube/extensions/string.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
|
|
||||||
class StatsPlaylistItem extends StatelessWidget {
|
class StatsPlaylistItem extends StatelessWidget {
|
||||||
final PlaylistSimple playlist;
|
final SpotubeSimplePlaylistObject playlist;
|
||||||
final Widget info;
|
final Widget info;
|
||||||
const StatsPlaylistItem(
|
const StatsPlaylistItem(
|
||||||
{super.key, required this.playlist, required this.info});
|
{super.key, required this.playlist, required this.info});
|
||||||
@ -27,9 +24,9 @@ class StatsPlaylistItem extends StatelessWidget {
|
|||||||
height: 40,
|
height: 40,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Text(playlist.name!),
|
title: Text(playlist.name),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
playlist.description?.unescapeHtml() ?? '',
|
playlist.description.unescapeHtml(),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/components/links/artist_link.dart';
|
import 'package:spotube/components/links/artist_link.dart';
|
||||||
import 'package:spotube/components/ui/button_tile.dart';
|
import 'package:spotube/components/ui/button_tile.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
|
|
||||||
class StatsTrackItem extends StatelessWidget {
|
class StatsTrackItem extends StatelessWidget {
|
||||||
final Track track;
|
final SpotubeTrackObject track;
|
||||||
final Widget info;
|
final Widget info;
|
||||||
const StatsTrackItem({
|
const StatsTrackItem({
|
||||||
super.key,
|
super.key,
|
||||||
@ -23,24 +22,24 @@ class StatsTrackItem extends StatelessWidget {
|
|||||||
leading: ClipRRect(
|
leading: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
child: UniversalImage(
|
child: UniversalImage(
|
||||||
path: (track.album?.images).asUrlString(
|
path: (track.album.images).asUrlString(
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
),
|
),
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Text(track.name!),
|
title: Text(track.name),
|
||||||
subtitle: ArtistLink(
|
subtitle: ArtistLink(
|
||||||
artists: track.artists!,
|
artists: track.artists,
|
||||||
mainAxisAlignment: WrapAlignment.start,
|
mainAxisAlignment: WrapAlignment.start,
|
||||||
onOverflowArtistClick: () {
|
onOverflowArtistClick: () {
|
||||||
context.navigateTo(TrackRoute(trackId: track.id!));
|
context.navigateTo(TrackRoute(trackId: track.id));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
trailing: info,
|
trailing: info,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.navigateTo(TrackRoute(trackId: track.id!));
|
context.navigateTo(TrackRoute(trackId: track.id));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import 'package:spotube/modules/stats/common/album_item.dart';
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/history/top.dart';
|
import 'package:spotube/provider/history/top.dart';
|
||||||
import 'package:spotube/provider/history/top/albums.dart';
|
import 'package:spotube/provider/history/top/albums.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
class TopAlbums extends HookConsumerWidget {
|
class TopAlbums extends HookConsumerWidget {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import 'package:spotube/modules/stats/common/artist_item.dart';
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/history/top.dart';
|
import 'package:spotube/provider/history/top.dart';
|
||||||
import 'package:spotube/provider/history/top/tracks.dart';
|
import 'package:spotube/provider/history/top/tracks.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
class TopArtists extends HookConsumerWidget {
|
class TopArtists extends HookConsumerWidget {
|
||||||
@ -24,14 +24,8 @@ class TopArtists extends HookConsumerWidget {
|
|||||||
final topTracksNotifier =
|
final topTracksNotifier =
|
||||||
ref.watch(historyTopTracksProvider(historyDuration).notifier);
|
ref.watch(historyTopTracksProvider(historyDuration).notifier);
|
||||||
|
|
||||||
final artistsData = useMemoized(
|
final artistsData =
|
||||||
() => topTracks.asData?.value.artists ?? [],
|
useMemoized(() => topTracksNotifier.artists, [topTracks.asData?.value]);
|
||||||
[topTracks.asData?.value],
|
|
||||||
);
|
|
||||||
|
|
||||||
for (final artist in artistsData) {
|
|
||||||
print("${artist.artist.name} has ${artist.artist.images?.length} images");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Skeletonizer.sliver(
|
return Skeletonizer.sliver(
|
||||||
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import 'package:spotube/modules/stats/common/track_item.dart';
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/history/top.dart';
|
import 'package:spotube/provider/history/top.dart';
|
||||||
import 'package:spotube/provider/history/top/tracks.dart';
|
import 'package:spotube/provider/history/top/tracks.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
class TopTracks extends HookConsumerWidget {
|
class TopTracks extends HookConsumerWidget {
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/library/albums.dart';
|
import 'package:spotube/provider/metadata_plugin/library/albums.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/tracks/album.dart';
|
import 'package:spotube/provider/metadata_plugin/tracks/album.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class AlbumPage extends HookConsumerWidget {
|
class AlbumPage extends HookConsumerWidget {
|
||||||
|
|||||||
@ -7,15 +7,17 @@ import 'package:spotube/components/button/back_button.dart';
|
|||||||
|
|
||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
import 'package:spotube/modules/artist/artist_album_list.dart';
|
import 'package:spotube/modules/artist/artist_album_list.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
|
||||||
|
|
||||||
import 'package:spotube/pages/artist/section/footer.dart';
|
import 'package:spotube/pages/artist/section/footer.dart';
|
||||||
import 'package:spotube/pages/artist/section/header.dart';
|
import 'package:spotube/pages/artist/section/header.dart';
|
||||||
// import 'package:spotube/pages/artist/section/related_artists.dart';
|
// import 'package:spotube/pages/artist/section/related_artists.dart';
|
||||||
import 'package:spotube/pages/artist/section/top_tracks.dart';
|
import 'package:spotube/pages/artist/section/top_tracks.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/artist/albums.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/artist/artist.dart';
|
import 'package:spotube/provider/metadata_plugin/artist/artist.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/artist/top_tracks.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/artist/wikipedia.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/library/artists.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class ArtistPage extends HookConsumerWidget {
|
class ArtistPage extends HookConsumerWidget {
|
||||||
@ -30,7 +32,6 @@ class ArtistPage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final scrollController = useScrollController();
|
final scrollController = useScrollController();
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
final artistQuery = ref.watch(metadataPluginArtistProvider(artistId));
|
final artistQuery = ref.watch(metadataPluginArtistProvider(artistId));
|
||||||
|
|
||||||
@ -46,14 +47,15 @@ class ArtistPage extends HookConsumerWidget {
|
|||||||
floatingHeader: true,
|
floatingHeader: true,
|
||||||
child: material.RefreshIndicator.adaptive(
|
child: material.RefreshIndicator.adaptive(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
ref.invalidate(artistProvider(artistId));
|
ref.invalidate(metadataPluginArtistProvider(artistId));
|
||||||
ref.invalidate(relatedArtistsProvider(artistId));
|
// ref.invalidate(relatedArtistsProvider(artistId));
|
||||||
ref.invalidate(artistAlbumsProvider(artistId));
|
ref.invalidate(metadataPluginArtistAlbumsProvider(artistId));
|
||||||
ref.invalidate(artistIsFollowingProvider(artistId));
|
ref.invalidate(metadataPluginIsSavedArtistProvider(artistId));
|
||||||
ref.invalidate(artistTopTracksProvider(artistId));
|
ref.invalidate(metadataPluginArtistTopTracksProvider(artistId));
|
||||||
if (artistQuery.hasValue) {
|
if (artistQuery.hasValue) {
|
||||||
ref.invalidate(
|
ref.invalidate(
|
||||||
artistWikipediaSummaryProvider(artistQuery.asData!.value));
|
artistWikipediaSummaryProvider(artistQuery.asData!.value),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Builder(builder: (context) {
|
child: Builder(builder: (context) {
|
||||||
|
|||||||
@ -5,8 +5,7 @@ import 'package:spotube/collections/spotube_icons.dart';
|
|||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/metadata_plugin/artist/wikipedia.dart';
|
||||||
|
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class ArtistPageFooter extends ConsumerWidget {
|
class ArtistPageFooter extends ConsumerWidget {
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import 'package:spotube/models/metadata/metadata.dart';
|
|||||||
import 'package:spotube/provider/authentication/authentication.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/artist/artist.dart';
|
import 'package:spotube/provider/metadata_plugin/artist/artist.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/metadata_plugin/library/artists.dart';
|
||||||
import 'package:spotube/utils/primitive_utils.dart';
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
|
|
||||||
class ArtistPageHeader extends HookConsumerWidget {
|
class ArtistPageHeader extends HookConsumerWidget {
|
||||||
@ -31,7 +31,7 @@ class ArtistPageHeader extends HookConsumerWidget {
|
|||||||
final auth = ref.watch(authenticationProvider);
|
final auth = ref.watch(authenticationProvider);
|
||||||
ref.watch(blacklistProvider);
|
ref.watch(blacklistProvider);
|
||||||
final blacklistNotifier = ref.watch(blacklistProvider.notifier);
|
final blacklistNotifier = ref.watch(blacklistProvider.notifier);
|
||||||
final isBlackListed = /* blacklistNotifier.containsArtist(artist) */ false;
|
final isBlackListed = blacklistNotifier.containsArtist(artist.id);
|
||||||
|
|
||||||
final image = artist.images.asUrlString(
|
final image = artist.images.asUrlString(
|
||||||
placeholder: ImagePlaceholder.artist,
|
placeholder: ImagePlaceholder.artist,
|
||||||
@ -45,11 +45,10 @@ class ArtistPageHeader extends HookConsumerWidget {
|
|||||||
Consumer(
|
Consumer(
|
||||||
builder: (context, ref, _) {
|
builder: (context, ref, _) {
|
||||||
final isFollowingQuery = ref.watch(
|
final isFollowingQuery = ref.watch(
|
||||||
artistIsFollowingProvider(artist.id!),
|
metadataPluginIsSavedArtistProvider(artist.id),
|
||||||
);
|
|
||||||
final followingArtistNotifier = ref.watch(
|
|
||||||
followedArtistsProvider.notifier,
|
|
||||||
);
|
);
|
||||||
|
final followingArtistNotifier =
|
||||||
|
ref.watch(metadataPluginSavedArtistsProvider.notifier);
|
||||||
|
|
||||||
return switch (isFollowingQuery) {
|
return switch (isFollowingQuery) {
|
||||||
AsyncData(value: final following) => Builder(
|
AsyncData(value: final following) => Builder(
|
||||||
@ -58,7 +57,7 @@ class ArtistPageHeader extends HookConsumerWidget {
|
|||||||
return Button.outline(
|
return Button.outline(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await followingArtistNotifier
|
await followingArtistNotifier
|
||||||
.removeArtists([artist.id!]);
|
.removeFavorite([artist]);
|
||||||
},
|
},
|
||||||
child: Text(context.l10n.following),
|
child: Text(context.l10n.following),
|
||||||
);
|
);
|
||||||
@ -66,8 +65,7 @@ class ArtistPageHeader extends HookConsumerWidget {
|
|||||||
|
|
||||||
return Button.primary(
|
return Button.primary(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await followingArtistNotifier
|
await followingArtistNotifier.addFavorite([artist]);
|
||||||
.saveArtists([artist.id!]);
|
|
||||||
},
|
},
|
||||||
child: Text(context.l10n.follow),
|
child: Text(context.l10n.follow),
|
||||||
);
|
);
|
||||||
@ -96,12 +94,12 @@ class ArtistPageHeader extends HookConsumerWidget {
|
|||||||
: ButtonVariance.ghost,
|
: ButtonVariance.ghost,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (isBlackListed) {
|
if (isBlackListed) {
|
||||||
await ref.read(blacklistProvider.notifier).remove(artist.id!);
|
await ref.read(blacklistProvider.notifier).remove(artist.id);
|
||||||
} else {
|
} else {
|
||||||
await ref.read(blacklistProvider.notifier).add(
|
await ref.read(blacklistProvider.notifier).add(
|
||||||
BlacklistTableCompanion.insert(
|
BlacklistTableCompanion.insert(
|
||||||
name: artist.name!,
|
name: artist.name,
|
||||||
elementId: artist.id!,
|
elementId: artist.id,
|
||||||
elementType: BlacklistedType.artist,
|
elementType: BlacklistedType.artist,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -184,7 +182,7 @@ class ArtistPageHeader extends HookConsumerWidget {
|
|||||||
const Gap(10),
|
const Gap(10),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: AutoSizeText(
|
child: AutoSizeText(
|
||||||
artist.name!,
|
artist.name,
|
||||||
style: constrains.smAndDown
|
style: constrains.smAndDown
|
||||||
? typography.h4
|
? typography.h4
|
||||||
: typography.h3,
|
: typography.h3,
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/modules/artist/artist_card.dart';
|
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
|
||||||
|
|
||||||
@Deprecated("Related artists are no longer supported by Spotube")
|
@Deprecated("Related artists are no longer supported by Spotube")
|
||||||
class ArtistPageRelatedArtists extends ConsumerWidget {
|
class ArtistPageRelatedArtists extends ConsumerWidget {
|
||||||
@ -13,38 +11,39 @@ class ArtistPageRelatedArtists extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final relatedArtists = ref.watch(relatedArtistsProvider(artistId));
|
return const SizedBox.shrink();
|
||||||
|
// final relatedArtists = ref.watch(relatedArtistsProvider(artistId));
|
||||||
|
|
||||||
return switch (relatedArtists) {
|
// return switch (relatedArtists) {
|
||||||
AsyncData(value: final artists) => SliverPadding(
|
// AsyncData(value: final artists) => SliverPadding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
// padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
sliver: SliverGrid.builder(
|
// sliver: SliverGrid.builder(
|
||||||
itemCount: artists.length,
|
// itemCount: artists.length,
|
||||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
// gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
maxCrossAxisExtent: 200,
|
// maxCrossAxisExtent: 200,
|
||||||
mainAxisExtent: 250,
|
// mainAxisExtent: 250,
|
||||||
mainAxisSpacing: 10,
|
// mainAxisSpacing: 10,
|
||||||
crossAxisSpacing: 10,
|
// crossAxisSpacing: 10,
|
||||||
childAspectRatio: 0.8,
|
// childAspectRatio: 0.8,
|
||||||
),
|
// ),
|
||||||
itemBuilder: (context, index) {
|
// itemBuilder: (context, index) {
|
||||||
final artist = artists.elementAt(index);
|
// final artist = artists.elementAt(index);
|
||||||
return SizedBox(
|
// return SizedBox(
|
||||||
width: 180,
|
// width: 180,
|
||||||
// child: ArtistCard(artist),
|
// // child: ArtistCard(artist),
|
||||||
);
|
// );
|
||||||
// return ArtistCard(artist);
|
// // return ArtistCard(artist);
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
AsyncError(:final error) => SliverToBoxAdapter(
|
// AsyncError(:final error) => SliverToBoxAdapter(
|
||||||
child: Center(
|
// child: Center(
|
||||||
child: Text(error.toString()),
|
// child: Text(error.toString()),
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
_ => const SliverToBoxAdapter(
|
// _ => const SliverToBoxAdapter(
|
||||||
child: Center(child: CircularProgressIndicator()),
|
// child: Center(child: CircularProgressIndicator()),
|
||||||
),
|
// ),
|
||||||
};
|
// };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/fake.dart';
|
import 'package:spotube/collections/fake.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/dialogs/select_device_dialog.dart';
|
import 'package:spotube/components/dialogs/select_device_dialog.dart';
|
||||||
import 'package:spotube/components/track_tile/track_tile.dart';
|
import 'package:spotube/components/track_tile/track_tile.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/connect/connect.dart';
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/metadata_plugin/artist/top_tracks.dart';
|
||||||
|
|
||||||
class ArtistPageTopTracks extends HookConsumerWidget {
|
class ArtistPageTopTracks extends HookConsumerWidget {
|
||||||
final String artistId;
|
final String artistId;
|
||||||
@ -22,10 +22,11 @@ class ArtistPageTopTracks extends HookConsumerWidget {
|
|||||||
|
|
||||||
final playlist = ref.watch(audioPlayerProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
||||||
final topTracksQuery = ref.watch(artistTopTracksProvider(artistId));
|
final topTracksQuery =
|
||||||
|
ref.watch(metadataPluginArtistTopTracksProvider(artistId));
|
||||||
|
|
||||||
final isPlaylistPlaying = playlist.containsTracks(
|
final isPlaylistPlaying = playlist.containsTracks(
|
||||||
topTracksQuery.asData?.value ?? <Track>[],
|
topTracksQuery.asData?.value.items ?? <SpotubeTrackObject>[],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (topTracksQuery.hasError) {
|
if (topTracksQuery.hasError) {
|
||||||
@ -36,10 +37,11 @@ class ArtistPageTopTracks extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final topTracks = topTracksQuery.asData?.value ??
|
final topTracks = topTracksQuery.asData?.value.items ??
|
||||||
List.generate(10, (index) => FakeData.track);
|
List.generate(10, (index) => FakeData.track);
|
||||||
|
|
||||||
void playPlaylist(List<Track> tracks, {Track? currentTrack}) async {
|
void playPlaylist(List<SpotubeFullTrackObject> tracks,
|
||||||
|
{SpotubeTrackObject? currentTrack}) async {
|
||||||
currentTrack ??= tracks.first;
|
currentTrack ??= tracks.first;
|
||||||
|
|
||||||
final isRemoteDevice = await showSelectDeviceDialog(context, ref);
|
final isRemoteDevice = await showSelectDeviceDialog(context, ref);
|
||||||
@ -61,7 +63,6 @@ class ArtistPageTopTracks extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (isPlaylistPlaying &&
|
} else if (isPlaylistPlaying &&
|
||||||
currentTrack.id != null &&
|
|
||||||
currentTrack.id != remotePlaylist.activeTrack?.id) {
|
currentTrack.id != remotePlaylist.activeTrack?.id) {
|
||||||
final index = playlist.tracks
|
final index = playlist.tracks
|
||||||
.toList()
|
.toList()
|
||||||
@ -76,7 +77,6 @@ class ArtistPageTopTracks extends HookConsumerWidget {
|
|||||||
autoPlay: true,
|
autoPlay: true,
|
||||||
);
|
);
|
||||||
} else if (isPlaylistPlaying &&
|
} else if (isPlaylistPlaying &&
|
||||||
currentTrack.id != null &&
|
|
||||||
currentTrack.id != playlist.activeTrack?.id) {
|
currentTrack.id != playlist.activeTrack?.id) {
|
||||||
await playlistNotifier.jumpToTrack(currentTrack);
|
await playlistNotifier.jumpToTrack(currentTrack);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
|||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/modules/player/player_queue.dart';
|
import 'package:spotube/modules/player/player_queue.dart';
|
||||||
import 'package:spotube/modules/player/volume_slider.dart';
|
import 'package:spotube/modules/player/volume_slider.dart';
|
||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
@ -17,7 +18,6 @@ import 'package:spotube/components/titlebar/titlebar.dart';
|
|||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/duration.dart';
|
import 'package:spotube/extensions/duration.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
|
||||||
import 'package:spotube/provider/connect/clients.dart';
|
import 'package:spotube/provider/connect/clients.dart';
|
||||||
import 'package:spotube/provider/connect/connect.dart';
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
import 'package:media_kit/media_kit.dart' hide Track;
|
import 'package:media_kit/media_kit.dart' hide Track;
|
||||||
@ -120,7 +120,7 @@ class ConnectControlPage extends HookConsumerWidget {
|
|||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: UniversalImage(
|
child: UniversalImage(
|
||||||
path: (playlist.activeTrack?.album?.images)
|
path: (playlist.activeTrack?.album.images)
|
||||||
.asUrlString(
|
.asUrlString(
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
),
|
),
|
||||||
@ -140,8 +140,7 @@ class ConnectControlPage extends HookConsumerWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
if (playlist.activeTrack == null) return;
|
if (playlist.activeTrack == null) return;
|
||||||
context.navigateTo(
|
context.navigateTo(
|
||||||
TrackRoute(
|
TrackRoute(trackId: playlist.activeTrack!.id),
|
||||||
trackId: playlist.activeTrack!.id!),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -152,7 +151,7 @@ class ConnectControlPage extends HookConsumerWidget {
|
|||||||
textStyle: typography.normal,
|
textStyle: typography.normal,
|
||||||
mainAxisAlignment: WrapAlignment.start,
|
mainAxisAlignment: WrapAlignment.start,
|
||||||
onOverflowArtistClick: () => context.navigateTo(
|
onOverflowArtistClick: () => context.navigateTo(
|
||||||
TrackRoute(trackId: playlist.activeTrack!.id!),
|
TrackRoute(trackId: playlist.activeTrack!.id),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/language_codes.dart';
|
import 'package:spotube/collections/language_codes.dart';
|
||||||
import 'package:spotube/collections/spotify_markets.dart';
|
import 'package:spotube/collections/spotify_markets.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
@ -14,7 +13,7 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget {
|
|||||||
const GettingStartedPageLanguageRegionSection(
|
const GettingStartedPageLanguageRegionSection(
|
||||||
{super.key, required this.onNext});
|
{super.key, required this.onNext});
|
||||||
|
|
||||||
bool filterMarkets(Market item, String query) {
|
bool filterMarkets(dynamic item, String query) {
|
||||||
final market = spotifyMarkets
|
final market = spotifyMarkets
|
||||||
.firstWhere((element) => element.$1 == item)
|
.firstWhere((element) => element.$1 == item)
|
||||||
.$2
|
.$2
|
||||||
@ -64,7 +63,7 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget {
|
|||||||
const Gap(8),
|
const Gap(8),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Select<Market>(
|
child: Select(
|
||||||
value: preferences.market,
|
value: preferences.market,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
|
|||||||
@ -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/connect/connect_device.dart';
|
||||||
import 'package:spotube/modules/home/sections/featured.dart';
|
import 'package:spotube/modules/home/sections/featured.dart';
|
||||||
import 'package:spotube/modules/home/sections/sections.dart';
|
import 'package:spotube/modules/home/sections/sections.dart';
|
||||||
import 'package:spotube/modules/home/sections/friends.dart';
|
|
||||||
import 'package:spotube/modules/home/sections/genres/genres.dart';
|
|
||||||
import 'package:spotube/modules/home/sections/made_for_user.dart';
|
|
||||||
import 'package:spotube/modules/home/sections/new_releases.dart';
|
import 'package:spotube/modules/home/sections/new_releases.dart';
|
||||||
import 'package:spotube/modules/home/sections/recent.dart';
|
import 'package:spotube/modules/home/sections/recent.dart';
|
||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
@ -76,18 +73,18 @@ class HomePage extends HookConsumerWidget {
|
|||||||
else if (kIsMacOS)
|
else if (kIsMacOS)
|
||||||
const SliverGap(10),
|
const SliverGap(10),
|
||||||
const SliverGap(10),
|
const SliverGap(10),
|
||||||
// SliverList.builder(
|
SliverList.builder(
|
||||||
// itemCount: 5,
|
itemCount: 3,
|
||||||
// itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
// return switch (index) {
|
return switch (index) {
|
||||||
// 0 => const HomeGenresSection(),
|
// 0 => const HomeGenresSection(),
|
||||||
// 1 => const HomeRecentlyPlayedSection(),
|
0 => const HomeRecentlyPlayedSection(),
|
||||||
// 2 => const HomeFeaturedSection(),
|
1 => const HomeFeaturedSection(),
|
||||||
// 3 => const HomePageFriendsSection(),
|
// 3 => const HomePageFriendsSection(),
|
||||||
// _ => const HomeNewReleasesSection()
|
_ => const HomeNewReleasesSection()
|
||||||
// };
|
};
|
||||||
// },
|
},
|
||||||
// ),
|
),
|
||||||
const HomePageBrowseSection(),
|
const HomePageBrowseSection(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/library/albums.dart';
|
import 'package:spotube/provider/metadata_plugin/library/albums.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
@ -61,7 +60,7 @@ class UserAlbumsPage extends HookConsumerWidget {
|
|||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
child: material.RefreshIndicator.adaptive(
|
child: material.RefreshIndicator.adaptive(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
ref.invalidate(favoriteAlbumsProvider);
|
ref.invalidate(metadataPluginSavedAlbumsProvider);
|
||||||
},
|
},
|
||||||
child: InterScrollbar(
|
child: InterScrollbar(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import 'package:spotube/extensions/constrains.dart';
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/library/artists.dart';
|
import 'package:spotube/provider/metadata_plugin/library/artists.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
@ -65,7 +64,7 @@ class UserArtistsPage extends HookConsumerWidget {
|
|||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
child: material.RefreshIndicator.adaptive(
|
child: material.RefreshIndicator.adaptive(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
ref.invalidate(followedArtistsProvider);
|
ref.invalidate(metadataPluginSavedArtistsProvider);
|
||||||
},
|
},
|
||||||
child: InterScrollbar(
|
child: InterScrollbar(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
|||||||
@ -16,10 +16,7 @@ class UserDownloadsPage extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final downloadManager = ref.watch(downloadManagerProvider);
|
final downloadManager = ref.watch(downloadManagerProvider);
|
||||||
|
|
||||||
final history = [
|
final history = downloadManager.$backHistory;
|
||||||
...downloadManager.$history,
|
|
||||||
...downloadManager.$backHistory,
|
|
||||||
];
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -51,7 +48,7 @@ class UserDownloadsPage extends HookConsumerWidget {
|
|||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
itemCount: history.length,
|
itemCount: history.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return DownloadItem(track: history[index]);
|
return DownloadItem(track: history.elementAt(index));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import 'package:spotube/components/button/back_button.dart';
|
|||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/string.dart';
|
import 'package:spotube/extensions/string.dart';
|
||||||
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
|
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/modules/library/local_folder/cache_export_dialog.dart';
|
import 'package:spotube/modules/library/local_folder/cache_export_dialog.dart';
|
||||||
import 'package:spotube/pages/library/user_local_tracks/user_local_tracks.dart';
|
import 'package:spotube/pages/library/user_local_tracks/user_local_tracks.dart';
|
||||||
import 'package:spotube/components/expandable_search/expandable_search.dart';
|
import 'package:spotube/components/expandable_search/expandable_search.dart';
|
||||||
@ -24,9 +25,7 @@ import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
|||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
import 'package:spotube/components/track_presentation/sort_tracks_dropdown.dart';
|
import 'package:spotube/components/track_presentation/sort_tracks_dropdown.dart';
|
||||||
import 'package:spotube/components/track_tile/track_tile.dart';
|
import 'package:spotube/components/track_tile/track_tile.dart';
|
||||||
import 'package:spotube/extensions/artist_simple.dart';
|
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
|
||||||
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
@ -49,8 +48,8 @@ class LocalLibraryPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
Future<void> playLocalTracks(
|
Future<void> playLocalTracks(
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
List<LocalTrack> tracks, {
|
List<SpotubeLocalTrackObject> tracks, {
|
||||||
LocalTrack? currentTrack,
|
SpotubeLocalTrackObject? currentTrack,
|
||||||
}) async {
|
}) async {
|
||||||
final playlist = ref.read(audioPlayerProvider);
|
final playlist = ref.read(audioPlayerProvider);
|
||||||
final playback = ref.read(audioPlayerProvider.notifier);
|
final playback = ref.read(audioPlayerProvider.notifier);
|
||||||
@ -64,7 +63,6 @@ class LocalLibraryPage extends HookConsumerWidget {
|
|||||||
autoPlay: true,
|
autoPlay: true,
|
||||||
);
|
);
|
||||||
} else if (isPlaylistPlaying &&
|
} else if (isPlaylistPlaying &&
|
||||||
currentTrack.id != null &&
|
|
||||||
currentTrack.id != playlist.activeTrack?.id) {
|
currentTrack.id != playlist.activeTrack?.id) {
|
||||||
await playback.jumpToTrack(currentTrack);
|
await playback.jumpToTrack(currentTrack);
|
||||||
}
|
}
|
||||||
@ -296,7 +294,8 @@ class LocalLibraryPage extends HookConsumerWidget {
|
|||||||
data: (tracks) {
|
data: (tracks) {
|
||||||
final sortedTracks = useMemoized(() {
|
final sortedTracks = useMemoized(() {
|
||||||
return ServiceUtils.sortTracks(
|
return ServiceUtils.sortTracks(
|
||||||
tracks[location] ?? <LocalTrack>[],
|
tracks[location] ??
|
||||||
|
<SpotubeLocalTrackObject>[],
|
||||||
sortBy.value);
|
sortBy.value);
|
||||||
}, [sortBy.value, tracks]);
|
}, [sortBy.value, tracks]);
|
||||||
|
|
||||||
@ -307,7 +306,7 @@ class LocalLibraryPage extends HookConsumerWidget {
|
|||||||
return sortedTracks
|
return sortedTracks
|
||||||
.map((e) => (
|
.map((e) => (
|
||||||
weightedRatio(
|
weightedRatio(
|
||||||
"${e.name} - ${e.artists?.asString() ?? ""}",
|
"${e.name} - ${e.artists.asString()}",
|
||||||
searchController.text,
|
searchController.text,
|
||||||
),
|
),
|
||||||
e,
|
e,
|
||||||
|
|||||||
@ -6,13 +6,13 @@ import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
|||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
|
||||||
import 'package:spotube/hooks/utils/use_palette_color.dart';
|
import 'package:spotube/hooks/utils/use_palette_color.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
|
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
|
||||||
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
|
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
|
import 'package:spotube/provider/lyrics/synced.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
@ -25,11 +25,11 @@ class LyricsPage extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(audioPlayerProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
String albumArt = useMemoized(
|
String albumArt = useMemoized(
|
||||||
() => (playlist.activeTrack?.album?.images).asUrlString(
|
() => (playlist.activeTrack?.album.images).asUrlString(
|
||||||
index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1,
|
index: (playlist.activeTrack?.album.images.length ?? 1) - 1,
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
),
|
),
|
||||||
[playlist.activeTrack?.album?.images],
|
[playlist.activeTrack?.album.images],
|
||||||
);
|
);
|
||||||
final palette = usePaletteColor(albumArt, ref);
|
final palette = usePaletteColor(albumArt, ref);
|
||||||
final selectedIndex = useState(0);
|
final selectedIndex = useState(0);
|
||||||
|
|||||||
@ -5,14 +5,14 @@ import 'package:palette_generator/palette_generator.dart';
|
|||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/modules/lyrics/zoom_controls.dart';
|
import 'package:spotube/modules/lyrics/zoom_controls.dart';
|
||||||
import 'package:spotube/components/shimmers/shimmer_lyrics.dart';
|
import 'package:spotube/components/shimmers/shimmer_lyrics.dart';
|
||||||
import 'package:spotube/extensions/artist_simple.dart';
|
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/lyrics/synced.dart';
|
||||||
|
|
||||||
class PlainLyrics extends HookConsumerWidget {
|
class PlainLyrics extends HookConsumerWidget {
|
||||||
final PaletteColor palette;
|
final PaletteColor palette;
|
||||||
@ -52,7 +52,7 @@ class PlainLyrics extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
Center(
|
Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
playlist.activeTrack?.artists?.asString() ?? "",
|
playlist.activeTrack?.artists.asString() ?? "",
|
||||||
style: (mediaQuery.mdAndUp ? typography.h4 : typography.large)
|
style: (mediaQuery.mdAndUp ? typography.h4 : typography.large)
|
||||||
.copyWith(
|
.copyWith(
|
||||||
color: palette.bodyTextColor,
|
color: palette.bodyTextColor,
|
||||||
|
|||||||
@ -5,16 +5,16 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:palette_generator/palette_generator.dart';
|
import 'package:palette_generator/palette_generator.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/modules/lyrics/zoom_controls.dart';
|
import 'package:spotube/modules/lyrics/zoom_controls.dart';
|
||||||
import 'package:spotube/components/shimmers/shimmer_lyrics.dart';
|
import 'package:spotube/components/shimmers/shimmer_lyrics.dart';
|
||||||
import 'package:spotube/extensions/artist_simple.dart';
|
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart';
|
import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart';
|
||||||
import 'package:spotube/modules/lyrics/use_synced_lyrics.dart';
|
import 'package:spotube/modules/lyrics/use_synced_lyrics.dart';
|
||||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/lyrics/synced.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ class SyncedLyrics extends HookConsumerWidget {
|
|||||||
bottom: PreferredSize(
|
bottom: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(40),
|
preferredSize: const Size.fromHeight(40),
|
||||||
child: Text(
|
child: Text(
|
||||||
playlist.activeTrack?.artists?.asString() ?? "",
|
playlist.activeTrack?.artists.asString() ?? "",
|
||||||
style:
|
style:
|
||||||
mediaQuery.mdAndUp ? typography.h4 : typography.x2Large,
|
mediaQuery.mdAndUp ? typography.h4 : typography.x2Large,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
|
|||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/button/back_button.dart';
|
import 'package:spotube/components/button/back_button.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
|
||||||
import 'package:spotube/hooks/utils/use_palette_color.dart';
|
import 'package:spotube/hooks/utils/use_palette_color.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
|
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
|
||||||
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
|
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
@ -19,11 +19,11 @@ class PlayerLyricsPage extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(audioPlayerProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
String albumArt = useMemoized(
|
String albumArt = useMemoized(
|
||||||
() => (playlist.activeTrack?.album?.images).asUrlString(
|
() => (playlist.activeTrack?.album.images).asUrlString(
|
||||||
index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1,
|
index: (playlist.activeTrack?.album.images.length ?? 1) - 1,
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
),
|
),
|
||||||
[playlist.activeTrack?.album?.images],
|
[playlist.activeTrack?.album.images],
|
||||||
);
|
);
|
||||||
final selectedIndex = useState(0);
|
final selectedIndex = useState(0);
|
||||||
final palette = usePaletteColor(albumArt, ref);
|
final palette = usePaletteColor(albumArt, ref);
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import 'package:flutter/material.dart' as material;
|
import 'package:flutter/material.dart' as material;
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/components/track_presentation/presentation_props.dart';
|
import 'package:spotube/components/track_presentation/presentation_props.dart';
|
||||||
import 'package:spotube/components/track_presentation/track_presentation.dart';
|
import 'package:spotube/components/track_presentation/track_presentation.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/pages/playlist/playlist.dart';
|
import 'package:spotube/pages/playlist/playlist.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
@ -21,12 +20,12 @@ class LikedPlaylistPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final likedTracks = ref.watch(likedTracksProvider);
|
final likedTracks = ref.watch(metadataPluginSavedTracksProvider);
|
||||||
final tracks = likedTracks.asData?.value ?? <Track>[];
|
final tracks = likedTracks.asData?.value.items ?? [];
|
||||||
|
|
||||||
return material.RefreshIndicator.adaptive(
|
return material.RefreshIndicator.adaptive(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
ref.invalidate(likedTracksProvider);
|
ref.invalidate(metadataPluginSavedTracksProvider);
|
||||||
},
|
},
|
||||||
child: TrackPresentation(
|
child: TrackPresentation(
|
||||||
options: TrackPresentationOptions(
|
options: TrackPresentationOptions(
|
||||||
@ -40,7 +39,7 @@ class LikedPlaylistPage extends HookConsumerWidget {
|
|||||||
return tracks.toList();
|
return tracks.toList();
|
||||||
},
|
},
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
ref.invalidate(likedTracksProvider);
|
ref.invalidate(metadataPluginSavedTracksProvider);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
title: playlist.name,
|
title: playlist.name,
|
||||||
|
|||||||
@ -9,8 +9,9 @@ import 'package:spotube/components/track_presentation/use_is_user_playlist.dart'
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
|
import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class PlaylistPage extends HookConsumerWidget {
|
class PlaylistPage extends HookConsumerWidget {
|
||||||
@ -30,8 +31,8 @@ class PlaylistPage extends HookConsumerWidget {
|
|||||||
.watch(
|
.watch(
|
||||||
metadataPluginSavedPlaylistsProvider.select(
|
metadataPluginSavedPlaylistsProvider.select(
|
||||||
(value) => value.whenData(
|
(value) => value.whenData(
|
||||||
(value) => (value.items as List<SpotubeSimplePlaylistObject>)
|
(value) =>
|
||||||
.firstWhereOrNull((s) => s.id == _playlist.id),
|
value.items.firstWhereOrNull((s) => s.id == _playlist.id),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -39,11 +40,11 @@ class PlaylistPage extends HookConsumerWidget {
|
|||||||
?.value ??
|
?.value ??
|
||||||
_playlist;
|
_playlist;
|
||||||
|
|
||||||
final tracks = ref.watch(playlistTracksProvider(playlist.id));
|
final tracks = ref.watch(metadataPluginPlaylistTracksProvider(playlist.id));
|
||||||
final tracksNotifier =
|
final tracksNotifier =
|
||||||
ref.watch(playlistTracksProvider(playlist.id).notifier);
|
ref.watch(metadataPluginPlaylistTracksProvider(playlist.id).notifier);
|
||||||
final isFavoritePlaylist =
|
final isFavoritePlaylist =
|
||||||
ref.watch(isFavoritePlaylistProvider(playlist.id));
|
ref.watch(metadataPluginIsSavedPlaylistProvider(playlist.id));
|
||||||
|
|
||||||
final favoritePlaylistsNotifier =
|
final favoritePlaylistsNotifier =
|
||||||
ref.watch(metadataPluginSavedPlaylistsProvider.notifier);
|
ref.watch(metadataPluginSavedPlaylistsProvider.notifier);
|
||||||
@ -52,9 +53,9 @@ class PlaylistPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
return material.RefreshIndicator.adaptive(
|
return material.RefreshIndicator.adaptive(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
ref.invalidate(playlistTracksProvider(playlist.id));
|
ref.invalidate(metadataPluginPlaylistTracksProvider(playlist.id));
|
||||||
ref.invalidate(isFavoritePlaylistProvider(playlist.id));
|
ref.invalidate(metadataPluginSavedPlaylistsProvider);
|
||||||
ref.invalidate(favoritePlaylistsProvider);
|
ref.invalidate(metadataPluginIsSavedPlaylistProvider(playlist.id));
|
||||||
},
|
},
|
||||||
child: TrackPresentation(
|
child: TrackPresentation(
|
||||||
options: TrackPresentationOptions(
|
options: TrackPresentationOptions(
|
||||||
@ -67,7 +68,7 @@ class PlaylistPage extends HookConsumerWidget {
|
|||||||
isLoading: tracks.isLoading || tracks.isLoadingNextPage,
|
isLoading: tracks.isLoading || tracks.isLoadingNextPage,
|
||||||
onFetchMore: tracksNotifier.fetchMore,
|
onFetchMore: tracksNotifier.fetchMore,
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
ref.invalidate(playlistTracksProvider(playlist.id));
|
ref.invalidate(metadataPluginPlaylistTracksProvider(playlist.id));
|
||||||
},
|
},
|
||||||
onFetchAll: () async {
|
onFetchAll: () async {
|
||||||
return await tracksNotifier.fetchAll();
|
return await tracksNotifier.fetchAll();
|
||||||
|
|||||||
@ -1,16 +1,14 @@
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:sliver_tools/sliver_tools.dart';
|
import 'package:sliver_tools/sliver_tools.dart';
|
||||||
import 'package:spotube/collections/fake.dart';
|
import 'package:spotube/collections/fake.dart';
|
||||||
import 'package:spotube/collections/spotify_markets.dart';
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/metadata_plugin/user.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
|
||||||
@ -22,22 +20,22 @@ class ProfilePage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final me = ref.watch(meProvider);
|
final me = ref.watch(metadataPluginUserProvider);
|
||||||
final meData = me.asData?.value ?? FakeData.user;
|
final meData = me.asData?.value ?? FakeData.user;
|
||||||
|
|
||||||
final userProperties = useMemoized(
|
// final userProperties = useMemoized(
|
||||||
() => {
|
// () => {
|
||||||
context.l10n.email: meData.email ?? "N/A",
|
// context.l10n.email: meData.email ?? "N/A",
|
||||||
context.l10n.profile_followers:
|
// context.l10n.profile_followers:
|
||||||
meData.followers?.total.toString() ?? "N/A",
|
// meData.followers?.total.toString() ?? "N/A",
|
||||||
context.l10n.birthday: meData.birthdate ?? context.l10n.not_born,
|
// context.l10n.birthday: meData.birthdate ?? context.l10n.not_born,
|
||||||
context.l10n.country: spotifyMarkets
|
// context.l10n.country: spotifyMarkets
|
||||||
.firstWhere((market) => market.$1 == meData.country)
|
// .firstWhere((market) => market.$1 == meData.country)
|
||||||
.$2,
|
// .$2,
|
||||||
context.l10n.subscription: meData.product ?? context.l10n.hacker,
|
// context.l10n.subscription: meData.product ?? context.l10n.hacker,
|
||||||
},
|
// },
|
||||||
[meData],
|
// [meData],
|
||||||
);
|
// );
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
@ -72,7 +70,7 @@ class ProfilePage extends HookConsumerWidget {
|
|||||||
const SliverGap(10),
|
const SliverGap(10),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Text(
|
child: Text(
|
||||||
meData.displayName ?? context.l10n.no_name,
|
meData.name,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
).h4(),
|
).h4(),
|
||||||
),
|
),
|
||||||
@ -97,42 +95,42 @@ class ProfilePage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverCrossAxisConstrained(
|
// SliverCrossAxisConstrained(
|
||||||
maxCrossAxisExtent: 500,
|
// maxCrossAxisExtent: 500,
|
||||||
child: SliverToBoxAdapter(
|
// child: SliverToBoxAdapter(
|
||||||
child: Card(
|
// child: Card(
|
||||||
child: Padding(
|
// child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
// padding: const EdgeInsets.all(8.0),
|
||||||
child: Table(
|
// child: Table(
|
||||||
columnWidths: const {
|
// columnWidths: const {
|
||||||
0: FixedTableSize(120),
|
// 0: FixedTableSize(120),
|
||||||
},
|
// },
|
||||||
defaultRowHeight: const FixedTableSize(40),
|
// defaultRowHeight: const FixedTableSize(40),
|
||||||
rows: [
|
// rows: [
|
||||||
for (final MapEntry(:key, :value)
|
// for (final MapEntry(:key, :value)
|
||||||
in userProperties.entries)
|
// in userProperties.entries)
|
||||||
TableRow(
|
// TableRow(
|
||||||
cells: [
|
// cells: [
|
||||||
TableCell(
|
// TableCell(
|
||||||
child: Padding(
|
// child: Padding(
|
||||||
padding: const EdgeInsets.all(6),
|
// padding: const EdgeInsets.all(6),
|
||||||
child: Text(key).large(),
|
// child: Text(key).large(),
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
TableCell(
|
// TableCell(
|
||||||
child: Padding(
|
// child: Padding(
|
||||||
padding: const EdgeInsets.all(6),
|
// padding: const EdgeInsets.all(6),
|
||||||
child: Text(value),
|
// child: Text(value),
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
],
|
// ],
|
||||||
)
|
// )
|
||||||
],
|
// ],
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
const SliverGap(200),
|
const SliverGap(200),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -19,10 +19,13 @@ import 'package:spotube/pages/search/sections/artists.dart';
|
|||||||
import 'package:spotube/pages/search/sections/playlists.dart';
|
import 'package:spotube/pages/search/sections/playlists.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/search/all.dart';
|
import 'package:spotube/provider/metadata_plugin/search/all.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
|
||||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
|
||||||
|
final searchTermStateProvider = StateProvider<String>((ref) {
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class SearchPage extends HookConsumerWidget {
|
class SearchPage extends HookConsumerWidget {
|
||||||
static const name = "search";
|
static const name = "search";
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
|
|||||||
|
|
||||||
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/pages/search/search.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/search/all.dart';
|
import 'package:spotube/provider/metadata_plugin/search/all.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
|
||||||
|
|
||||||
class SearchAlbumsSection extends HookConsumerWidget {
|
class SearchAlbumsSection extends HookConsumerWidget {
|
||||||
const SearchAlbumsSection({
|
const SearchAlbumsSection({
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
|
|||||||
|
|
||||||
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/pages/search/search.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/search/all.dart';
|
import 'package:spotube/provider/metadata_plugin/search/all.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
|
||||||
|
|
||||||
class SearchArtistsSection extends HookConsumerWidget {
|
class SearchArtistsSection extends HookConsumerWidget {
|
||||||
const SearchArtistsSection({
|
const SearchArtistsSection({
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user