mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-02-03 23:52:52 +00:00
feat: remove hetu and use rquickjs based frb bindings for plugin methods
This commit is contained in:
parent
2b9c5730c9
commit
bd2275a89f
@ -3,13 +3,15 @@ import 'package:spotube/models/metadata/metadata.dart';
|
|||||||
import 'package:spotube/provider/history/summary.dart';
|
import 'package:spotube/provider/history/summary.dart';
|
||||||
|
|
||||||
abstract class FakeData {
|
abstract class FakeData {
|
||||||
static final SpotubeImageObject image = SpotubeImageObject(
|
static const SpotubeImageObject image = SpotubeImageObject(
|
||||||
|
typeName: "image",
|
||||||
height: 100,
|
height: 100,
|
||||||
width: 100,
|
width: 100,
|
||||||
url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
|
url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
|
||||||
);
|
);
|
||||||
|
|
||||||
static final SpotubeFullArtistObject artist = SpotubeFullArtistObject(
|
static const SpotubeFullArtistObject artist = SpotubeFullArtistObject(
|
||||||
|
typeName: "artist_full",
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "What an artist",
|
name: "What an artist",
|
||||||
externalUri: "https://example.com",
|
externalUri: "https://example.com",
|
||||||
@ -17,6 +19,7 @@ abstract class FakeData {
|
|||||||
genres: ["genre"],
|
genres: ["genre"],
|
||||||
images: [
|
images: [
|
||||||
SpotubeImageObject(
|
SpotubeImageObject(
|
||||||
|
typeName: "image",
|
||||||
height: 100,
|
height: 100,
|
||||||
width: 100,
|
width: 100,
|
||||||
url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
|
url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
|
||||||
@ -24,7 +27,8 @@ abstract class FakeData {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
static final SpotubeFullAlbumObject album = SpotubeFullAlbumObject(
|
static const SpotubeFullAlbumObject album = SpotubeFullAlbumObject(
|
||||||
|
typeName: "album_full",
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "A good album",
|
name: "A good album",
|
||||||
externalUri: "https://example.com",
|
externalUri: "https://example.com",
|
||||||
@ -37,15 +41,17 @@ abstract class FakeData {
|
|||||||
recordLabel: "Record Label",
|
recordLabel: "Record Label",
|
||||||
);
|
);
|
||||||
|
|
||||||
static final SpotubeSimpleArtistObject artistSimple =
|
static const SpotubeSimpleArtistObject artistSimple =
|
||||||
SpotubeSimpleArtistObject(
|
SpotubeSimpleArtistObject(
|
||||||
|
typeName: "artist_simple",
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "What an artist",
|
name: "What an artist",
|
||||||
externalUri: "https://example.com",
|
externalUri: "https://example.com",
|
||||||
images: null,
|
images: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
static final SpotubeSimpleAlbumObject albumSimple = SpotubeSimpleAlbumObject(
|
static const SpotubeSimpleAlbumObject albumSimple = SpotubeSimpleAlbumObject(
|
||||||
|
typeName: "album_simple",
|
||||||
albumType: SpotubeAlbumType.album,
|
albumType: SpotubeAlbumType.album,
|
||||||
artists: [],
|
artists: [],
|
||||||
externalUri: "https://example.com",
|
externalUri: "https://example.com",
|
||||||
@ -54,6 +60,7 @@ abstract class FakeData {
|
|||||||
releaseDate: "2021-01-01",
|
releaseDate: "2021-01-01",
|
||||||
images: [
|
images: [
|
||||||
SpotubeImageObject(
|
SpotubeImageObject(
|
||||||
|
typeName: "image",
|
||||||
height: 1,
|
height: 1,
|
||||||
width: 1,
|
width: 1,
|
||||||
url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
|
url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
|
||||||
@ -61,7 +68,9 @@ abstract class FakeData {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
static final SpotubeFullTrackObject track = SpotubeTrackObject.full(
|
static const SpotubeTrackObject track = SpotubeTrackObject.full(
|
||||||
|
SpotubeFullTrackObject(
|
||||||
|
typeName: "track",
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "A good track",
|
name: "A good track",
|
||||||
externalUri: "https://example.com",
|
externalUri: "https://example.com",
|
||||||
@ -69,16 +78,20 @@ abstract class FakeData {
|
|||||||
durationMs: 3 * 60 * 1000, // 3 minutes
|
durationMs: 3 * 60 * 1000, // 3 minutes
|
||||||
isrc: "USUM72112345",
|
isrc: "USUM72112345",
|
||||||
explicit: false,
|
explicit: false,
|
||||||
) as SpotubeFullTrackObject;
|
artists: [artistSimple],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
static final SpotubeUserObject user = SpotubeUserObject(
|
static const SpotubeUserObject user = SpotubeUserObject(
|
||||||
|
typeName: "user",
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "User Name",
|
name: "User Name",
|
||||||
externalUri: "https://example.com",
|
externalUri: "https://example.com",
|
||||||
images: [image],
|
images: [image],
|
||||||
);
|
);
|
||||||
|
|
||||||
static final SpotubeFullPlaylistObject playlist = SpotubeFullPlaylistObject(
|
static const SpotubeFullPlaylistObject playlist = SpotubeFullPlaylistObject(
|
||||||
|
typeName: "playlist_full",
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "A good playlist",
|
name: "A good playlist",
|
||||||
description: "A very good playlist description",
|
description: "A very good playlist description",
|
||||||
@ -89,8 +102,9 @@ abstract class FakeData {
|
|||||||
images: [image],
|
images: [image],
|
||||||
collaborators: [user]);
|
collaborators: [user]);
|
||||||
|
|
||||||
static final SpotubeSimplePlaylistObject playlistSimple =
|
static const SpotubeSimplePlaylistObject playlistSimple =
|
||||||
SpotubeSimplePlaylistObject(
|
SpotubeSimplePlaylistObject(
|
||||||
|
typeName: "playlist_simple",
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "A good playlist",
|
name: "A good playlist",
|
||||||
description: "A very good playlist description",
|
description: "A very good playlist description",
|
||||||
@ -99,13 +113,18 @@ abstract class FakeData {
|
|||||||
images: [image],
|
images: [image],
|
||||||
);
|
);
|
||||||
|
|
||||||
static final SpotubeBrowseSectionObject browseSection =
|
static const SpotubeBrowseSectionObject browseSection =
|
||||||
SpotubeBrowseSectionObject(
|
SpotubeBrowseSectionObject(
|
||||||
|
typeName: "browse_section",
|
||||||
id: "section-id",
|
id: "section-id",
|
||||||
title: "Browse Section",
|
title: "Browse Section",
|
||||||
browseMore: true,
|
browseMore: true,
|
||||||
externalUri: "https://example.com/browse/section",
|
externalUri: "https://example.com/browse/section",
|
||||||
items: [playlistSimple, playlistSimple, playlistSimple]);
|
items: [
|
||||||
|
SpotubeBrowseSectionResponseObjectItem.playlistSimple(playlistSimple),
|
||||||
|
SpotubeBrowseSectionResponseObjectItem.playlistSimple(playlistSimple),
|
||||||
|
SpotubeBrowseSectionResponseObjectItem.playlistSimple(playlistSimple),
|
||||||
|
]);
|
||||||
|
|
||||||
static const historySummary = PlaybackHistorySummary(
|
static const historySummary = PlaybackHistorySummary(
|
||||||
albums: 1,
|
albums: 1,
|
||||||
|
|||||||
@ -225,7 +225,7 @@ class HomeBrowseSectionItemsRoute
|
|||||||
HomeBrowseSectionItemsRoute({
|
HomeBrowseSectionItemsRoute({
|
||||||
_i44.Key? key,
|
_i44.Key? key,
|
||||||
required String sectionId,
|
required String sectionId,
|
||||||
required _i43.SpotubeBrowseSectionObject<Object> section,
|
required _i43.SpotubeBrowseSectionObject section,
|
||||||
List<_i41.PageRouteInfo>? children,
|
List<_i41.PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
HomeBrowseSectionItemsRoute.name,
|
HomeBrowseSectionItemsRoute.name,
|
||||||
@ -264,7 +264,7 @@ class HomeBrowseSectionItemsRouteArgs {
|
|||||||
|
|
||||||
final String sectionId;
|
final String sectionId;
|
||||||
|
|
||||||
final _i43.SpotubeBrowseSectionObject<Object> section;
|
final _i43.SpotubeBrowseSectionObject section;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@ -632,7 +632,7 @@ class SettingsMetadataProviderFormRoute
|
|||||||
SettingsMetadataProviderFormRoute({
|
SettingsMetadataProviderFormRoute({
|
||||||
_i44.Key? key,
|
_i44.Key? key,
|
||||||
required String title,
|
required String title,
|
||||||
required List<_i43.MetadataFormFieldObject> fields,
|
required List<void> fields,
|
||||||
List<_i41.PageRouteInfo>? children,
|
List<_i41.PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
SettingsMetadataProviderFormRoute.name,
|
SettingsMetadataProviderFormRoute.name,
|
||||||
@ -670,7 +670,7 @@ class SettingsMetadataProviderFormRouteArgs {
|
|||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
final List<_i43.MetadataFormFieldObject> fields;
|
final List<void> fields;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
|
|||||||
@ -7,7 +7,7 @@ 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 SpotubeTrackObject track;
|
final SpotubeFullTrackObject track;
|
||||||
const ReplaceDownloadedDialog({required this.track, super.key});
|
const ReplaceDownloadedDialog({required this.track, super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -37,7 +37,8 @@ class TrackDetailsDialog extends HookConsumerWidget {
|
|||||||
// style: const TextStyle(color: Colors.blue),
|
// style: const TextStyle(color: Colors.blue),
|
||||||
// ),
|
// ),
|
||||||
context.l10n.duration: sourcedTrack.asData != null
|
context.l10n.duration: sourcedTrack.asData != null
|
||||||
? sourcedTrack.asData!.value.info.duration.toHumanReadableString()
|
? Duration(milliseconds: sourcedTrack.asData!.value.info.duration)
|
||||||
|
.toHumanReadableString()
|
||||||
: Duration(milliseconds: track.durationMs).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,
|
||||||
|
|||||||
@ -65,7 +65,7 @@ class HeartButton extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class TrackHeartButton extends HookConsumerWidget {
|
class TrackHeartButton extends HookConsumerWidget {
|
||||||
final SpotubeTrackObject track;
|
final SpotubeFullTrackObject track;
|
||||||
const TrackHeartButton({
|
const TrackHeartButton({
|
||||||
super.key,
|
super.key,
|
||||||
required this.track,
|
required this.track,
|
||||||
|
|||||||
@ -5,10 +5,11 @@ import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
|
|||||||
typedef UseTrackToggleLike = ({
|
typedef UseTrackToggleLike = ({
|
||||||
bool isLiked,
|
bool isLiked,
|
||||||
bool isLoading,
|
bool isLoading,
|
||||||
Future<void> Function(SpotubeTrackObject track) toggleTrackLike,
|
Future<void> Function(SpotubeFullTrackObject track) toggleTrackLike,
|
||||||
});
|
});
|
||||||
|
|
||||||
UseTrackToggleLike useTrackToggleLike(SpotubeTrackObject track, WidgetRef ref) {
|
UseTrackToggleLike useTrackToggleLike(
|
||||||
|
SpotubeFullTrackObject track, WidgetRef ref) {
|
||||||
final savedTracksNotifier =
|
final savedTracksNotifier =
|
||||||
ref.watch(metadataPluginSavedTracksProvider.notifier);
|
ref.watch(metadataPluginSavedTracksProvider.notifier);
|
||||||
|
|
||||||
|
|||||||
@ -80,7 +80,7 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
|
|||||||
required String action,
|
required String action,
|
||||||
}) async {
|
}) async {
|
||||||
final fullTrackObjects =
|
final fullTrackObjects =
|
||||||
tracks.whereType<SpotubeFullTrackObject>().toList();
|
tracks.whereType<SpotubeTrackObject_Full>().toList();
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
@ -89,7 +89,7 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
|
|||||||
) ??
|
) ??
|
||||||
false;
|
false;
|
||||||
if (confirmed != true) return;
|
if (confirmed != true) return;
|
||||||
downloader.addAllToQueue(fullTrackObjects);
|
downloader.addAllToQueue(fullTrackObjects.map((e) => e.field0).toList());
|
||||||
notifier.deselectAllTracks();
|
notifier.deselectAllTracks();
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
showToastForAction(context, action, fullTrackObjects.length);
|
showToastForAction(context, action, fullTrackObjects.length);
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import 'package:spotube/components/track_presentation/use_track_tile_play_callba
|
|||||||
import 'package:spotube/components/track_tile/track_tile.dart';
|
import 'package:spotube/components/track_tile/track_tile.dart';
|
||||||
import 'package:spotube/components/track_presentation/use_is_user_playlist.dart';
|
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/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ class PaginationProps {
|
|||||||
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<SpotubeFullTrackObject>> Function() onFetchAll;
|
final Future<List<SpotubeTrackObject>> 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<SpotubeFullTrackObject> tracks;
|
final List<SpotubeTrackObject> tracks;
|
||||||
final PaginationProps pagination;
|
final PaginationProps pagination;
|
||||||
final bool isLiked;
|
final bool isLiked;
|
||||||
final String? shareUrl;
|
final String? shareUrl;
|
||||||
|
|||||||
@ -44,7 +44,7 @@ class PresentationStateNotifier
|
|||||||
next.whenData((value) {
|
next.whenData((value) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
presentationTracks: ServiceUtils.sortTracks(
|
presentationTracks: ServiceUtils.sortTracks(
|
||||||
value.items,
|
value.items.union(),
|
||||||
state.sortBy,
|
state.sortBy,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -62,7 +62,7 @@ class PresentationStateNotifier
|
|||||||
next.whenData((value) {
|
next.whenData((value) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
presentationTracks: ServiceUtils.sortTracks(
|
presentationTracks: ServiceUtils.sortTracks(
|
||||||
value.items,
|
value.items.union(),
|
||||||
state.sortBy,
|
state.sortBy,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -109,7 +109,7 @@ class PresentationStateNotifier
|
|||||||
} ??
|
} ??
|
||||||
<SpotubeFullTrackObject>[];
|
<SpotubeFullTrackObject>[];
|
||||||
|
|
||||||
return tracks;
|
return tracks.union();
|
||||||
}
|
}
|
||||||
|
|
||||||
void selectTrack(SpotubeTrackObject track) {
|
void selectTrack(SpotubeTrackObject track) {
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import 'package:spotube/components/track_presentation/use_action_callbacks.dart'
|
|||||||
import 'package:spotube/components/track_presentation/use_is_user_playlist.dart';
|
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/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
|
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
|
||||||
|
|
||||||
class TrackPresentationTopSection extends HookConsumerWidget {
|
class TrackPresentationTopSection extends HookConsumerWidget {
|
||||||
|
|||||||
@ -10,7 +10,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/track_options/track_options_provider.dart';
|
import 'package:spotube/provider/track_options/track_options_provider.dart';
|
||||||
|
|
||||||
/// [track] must be a [SpotubeFullTrackObject] or [SpotubeLocalTrackObject]
|
/// [track] must be a [SpotubeTrackObject] or [SpotubeLocalTrackObject]
|
||||||
class TrackOptions extends HookConsumerWidget {
|
class TrackOptions extends HookConsumerWidget {
|
||||||
final SpotubeTrackObject track;
|
final SpotubeTrackObject track;
|
||||||
final bool userPlaylist;
|
final bool userPlaylist;
|
||||||
@ -26,8 +26,8 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
this.icon,
|
this.icon,
|
||||||
this.onTapItem,
|
this.onTapItem,
|
||||||
}) : assert(
|
}) : assert(
|
||||||
track is SpotubeFullTrackObject || track is SpotubeLocalTrackObject,
|
track is SpotubeTrackObject || track is SpotubeLocalTrackObject,
|
||||||
"Track must be a SpotubeFullTrackObject, SpotubeLocalTrackObject",
|
"Track must be a SpotubeTrackObject, SpotubeLocalTrackObject",
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.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';
|
||||||
@ -24,12 +25,15 @@ void useEndlessPlayback(WidgetRef ref) {
|
|||||||
|
|
||||||
final track = playlist.tracks.last;
|
final track = playlist.tracks.last;
|
||||||
|
|
||||||
final tracks = await (await metadataPlugin)?.track.radio(track.id);
|
final tracks = await metadataPlugin.then(
|
||||||
|
(plugin) async =>
|
||||||
|
plugin?.track.radio(id: track.id, mpscTx: plugin.sender),
|
||||||
|
);
|
||||||
|
|
||||||
if (tracks == null || tracks.isEmpty) return;
|
if (tracks == null || tracks.isEmpty) return;
|
||||||
|
|
||||||
await playback.addTracks(
|
await playback.addTracks(
|
||||||
tracks.toList()
|
tracks.union()
|
||||||
..removeWhere((e) {
|
..removeWhere((e) {
|
||||||
final playlist = ref.read(audioPlayerProvider);
|
final playlist = ref.read(audioPlayerProvider);
|
||||||
final isDuplicate = playlist.tracks.any((t) => t.id == e.id);
|
final isDuplicate = playlist.tracks.any((t) => t.id == e.id);
|
||||||
|
|||||||
@ -33,23 +33,23 @@ WebSocketLoadEventData _$WebSocketLoadEventDataFromJson(
|
|||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$WebSocketLoadEventData {
|
mixin _$WebSocketLoadEventData {
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> get tracks => throw _privateConstructorUsedError;
|
List<SpotubeTrackObject> get tracks => throw _privateConstructorUsedError;
|
||||||
Object? get collection => throw _privateConstructorUsedError;
|
Object? get collection => throw _privateConstructorUsedError;
|
||||||
int? get initialIndex => throw _privateConstructorUsedError;
|
int? get initialIndex => throw _privateConstructorUsedError;
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult when<TResult extends Object?>({
|
TResult when<TResult extends Object?>({
|
||||||
required TResult Function(
|
required TResult Function(
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> tracks,
|
List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimplePlaylistObject? collection,
|
SpotubeSimplePlaylistObject? collection,
|
||||||
int? initialIndex)
|
int? initialIndex)
|
||||||
playlist,
|
playlist,
|
||||||
required TResult Function(
|
required TResult Function(
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> tracks,
|
List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimpleAlbumObject? collection,
|
SpotubeSimpleAlbumObject? collection,
|
||||||
int? initialIndex)
|
int? initialIndex)
|
||||||
@ -59,15 +59,15 @@ mixin _$WebSocketLoadEventData {
|
|||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult? whenOrNull<TResult extends Object?>({
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
TResult? Function(
|
TResult? Function(
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> tracks,
|
List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimplePlaylistObject? collection,
|
SpotubeSimplePlaylistObject? collection,
|
||||||
int? initialIndex)?
|
int? initialIndex)?
|
||||||
playlist,
|
playlist,
|
||||||
TResult? Function(
|
TResult? Function(
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> tracks,
|
List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimpleAlbumObject? collection,
|
SpotubeSimpleAlbumObject? collection,
|
||||||
int? initialIndex)?
|
int? initialIndex)?
|
||||||
@ -77,15 +77,15 @@ mixin _$WebSocketLoadEventData {
|
|||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult maybeWhen<TResult extends Object?>({
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
TResult Function(
|
TResult Function(
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> tracks,
|
List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimplePlaylistObject? collection,
|
SpotubeSimplePlaylistObject? collection,
|
||||||
int? initialIndex)?
|
int? initialIndex)?
|
||||||
playlist,
|
playlist,
|
||||||
TResult Function(
|
TResult Function(
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> tracks,
|
List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimpleAlbumObject? collection,
|
SpotubeSimpleAlbumObject? collection,
|
||||||
int? initialIndex)?
|
int? initialIndex)?
|
||||||
@ -130,8 +130,8 @@ abstract class $WebSocketLoadEventDataCopyWith<$Res> {
|
|||||||
_$WebSocketLoadEventDataCopyWithImpl<$Res, WebSocketLoadEventData>;
|
_$WebSocketLoadEventDataCopyWithImpl<$Res, WebSocketLoadEventData>;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call(
|
$Res call(
|
||||||
{@Assert("tracks is List<SpotubeFullTrackObject>",
|
{@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> tracks,
|
List<SpotubeTrackObject> tracks,
|
||||||
int? initialIndex});
|
int? initialIndex});
|
||||||
}
|
}
|
||||||
@ -178,8 +178,8 @@ abstract class _$$WebSocketLoadEventDataPlaylistImplCopyWith<$Res>
|
|||||||
@override
|
@override
|
||||||
@useResult
|
@useResult
|
||||||
$Res call(
|
$Res call(
|
||||||
{@Assert("tracks is List<SpotubeFullTrackObject>",
|
{@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> tracks,
|
List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimplePlaylistObject? collection,
|
SpotubeSimplePlaylistObject? collection,
|
||||||
int? initialIndex});
|
int? initialIndex});
|
||||||
@ -243,8 +243,8 @@ class __$$WebSocketLoadEventDataPlaylistImplCopyWithImpl<$Res>
|
|||||||
class _$WebSocketLoadEventDataPlaylistImpl
|
class _$WebSocketLoadEventDataPlaylistImpl
|
||||||
extends WebSocketLoadEventDataPlaylist {
|
extends WebSocketLoadEventDataPlaylist {
|
||||||
_$WebSocketLoadEventDataPlaylistImpl(
|
_$WebSocketLoadEventDataPlaylistImpl(
|
||||||
{@Assert("tracks is List<SpotubeFullTrackObject>",
|
{@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
required final List<SpotubeTrackObject> tracks,
|
required final List<SpotubeTrackObject> tracks,
|
||||||
this.collection,
|
this.collection,
|
||||||
this.initialIndex,
|
this.initialIndex,
|
||||||
@ -259,8 +259,8 @@ class _$WebSocketLoadEventDataPlaylistImpl
|
|||||||
|
|
||||||
final List<SpotubeTrackObject> _tracks;
|
final List<SpotubeTrackObject> _tracks;
|
||||||
@override
|
@override
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> get tracks {
|
List<SpotubeTrackObject> get tracks {
|
||||||
if (_tracks is EqualUnmodifiableListView) return _tracks;
|
if (_tracks is EqualUnmodifiableListView) return _tracks;
|
||||||
// ignore: implicit_dynamic_type
|
// ignore: implicit_dynamic_type
|
||||||
@ -311,15 +311,15 @@ class _$WebSocketLoadEventDataPlaylistImpl
|
|||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult when<TResult extends Object?>({
|
TResult when<TResult extends Object?>({
|
||||||
required TResult Function(
|
required TResult Function(
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> tracks,
|
List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimplePlaylistObject? collection,
|
SpotubeSimplePlaylistObject? collection,
|
||||||
int? initialIndex)
|
int? initialIndex)
|
||||||
playlist,
|
playlist,
|
||||||
required TResult Function(
|
required TResult Function(
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> tracks,
|
List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimpleAlbumObject? collection,
|
SpotubeSimpleAlbumObject? collection,
|
||||||
int? initialIndex)
|
int? initialIndex)
|
||||||
@ -332,15 +332,15 @@ class _$WebSocketLoadEventDataPlaylistImpl
|
|||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult? whenOrNull<TResult extends Object?>({
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
TResult? Function(
|
TResult? Function(
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> tracks,
|
List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimplePlaylistObject? collection,
|
SpotubeSimplePlaylistObject? collection,
|
||||||
int? initialIndex)?
|
int? initialIndex)?
|
||||||
playlist,
|
playlist,
|
||||||
TResult? Function(
|
TResult? Function(
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> tracks,
|
List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimpleAlbumObject? collection,
|
SpotubeSimpleAlbumObject? collection,
|
||||||
int? initialIndex)?
|
int? initialIndex)?
|
||||||
@ -353,15 +353,15 @@ class _$WebSocketLoadEventDataPlaylistImpl
|
|||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult maybeWhen<TResult extends Object?>({
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
TResult Function(
|
TResult Function(
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> tracks,
|
List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimplePlaylistObject? collection,
|
SpotubeSimplePlaylistObject? collection,
|
||||||
int? initialIndex)?
|
int? initialIndex)?
|
||||||
playlist,
|
playlist,
|
||||||
TResult Function(
|
TResult Function(
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> tracks,
|
List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimpleAlbumObject? collection,
|
SpotubeSimpleAlbumObject? collection,
|
||||||
int? initialIndex)?
|
int? initialIndex)?
|
||||||
@ -415,8 +415,8 @@ class _$WebSocketLoadEventDataPlaylistImpl
|
|||||||
|
|
||||||
abstract class WebSocketLoadEventDataPlaylist extends WebSocketLoadEventData {
|
abstract class WebSocketLoadEventDataPlaylist extends WebSocketLoadEventData {
|
||||||
factory WebSocketLoadEventDataPlaylist(
|
factory WebSocketLoadEventDataPlaylist(
|
||||||
{@Assert("tracks is List<SpotubeFullTrackObject>",
|
{@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
required final List<SpotubeTrackObject> tracks,
|
required final List<SpotubeTrackObject> tracks,
|
||||||
final SpotubeSimplePlaylistObject? collection,
|
final SpotubeSimplePlaylistObject? collection,
|
||||||
final int? initialIndex}) = _$WebSocketLoadEventDataPlaylistImpl;
|
final int? initialIndex}) = _$WebSocketLoadEventDataPlaylistImpl;
|
||||||
@ -426,8 +426,8 @@ abstract class WebSocketLoadEventDataPlaylist extends WebSocketLoadEventData {
|
|||||||
_$WebSocketLoadEventDataPlaylistImpl.fromJson;
|
_$WebSocketLoadEventDataPlaylistImpl.fromJson;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> get tracks;
|
List<SpotubeTrackObject> get tracks;
|
||||||
@override
|
@override
|
||||||
SpotubeSimplePlaylistObject? get collection;
|
SpotubeSimplePlaylistObject? get collection;
|
||||||
@ -453,8 +453,8 @@ abstract class _$$WebSocketLoadEventDataAlbumImplCopyWith<$Res>
|
|||||||
@override
|
@override
|
||||||
@useResult
|
@useResult
|
||||||
$Res call(
|
$Res call(
|
||||||
{@Assert("tracks is List<SpotubeFullTrackObject>",
|
{@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> tracks,
|
List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimpleAlbumObject? collection,
|
SpotubeSimpleAlbumObject? collection,
|
||||||
int? initialIndex});
|
int? initialIndex});
|
||||||
@ -516,8 +516,8 @@ class __$$WebSocketLoadEventDataAlbumImplCopyWithImpl<$Res>
|
|||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
|
class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
|
||||||
_$WebSocketLoadEventDataAlbumImpl(
|
_$WebSocketLoadEventDataAlbumImpl(
|
||||||
{@Assert("tracks is List<SpotubeFullTrackObject>",
|
{@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
required final List<SpotubeTrackObject> tracks,
|
required final List<SpotubeTrackObject> tracks,
|
||||||
this.collection,
|
this.collection,
|
||||||
this.initialIndex,
|
this.initialIndex,
|
||||||
@ -532,8 +532,8 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
|
|||||||
|
|
||||||
final List<SpotubeTrackObject> _tracks;
|
final List<SpotubeTrackObject> _tracks;
|
||||||
@override
|
@override
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> get tracks {
|
List<SpotubeTrackObject> get tracks {
|
||||||
if (_tracks is EqualUnmodifiableListView) return _tracks;
|
if (_tracks is EqualUnmodifiableListView) return _tracks;
|
||||||
// ignore: implicit_dynamic_type
|
// ignore: implicit_dynamic_type
|
||||||
@ -583,15 +583,15 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
|
|||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult when<TResult extends Object?>({
|
TResult when<TResult extends Object?>({
|
||||||
required TResult Function(
|
required TResult Function(
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> tracks,
|
List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimplePlaylistObject? collection,
|
SpotubeSimplePlaylistObject? collection,
|
||||||
int? initialIndex)
|
int? initialIndex)
|
||||||
playlist,
|
playlist,
|
||||||
required TResult Function(
|
required TResult Function(
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> tracks,
|
List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimpleAlbumObject? collection,
|
SpotubeSimpleAlbumObject? collection,
|
||||||
int? initialIndex)
|
int? initialIndex)
|
||||||
@ -604,15 +604,15 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
|
|||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult? whenOrNull<TResult extends Object?>({
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
TResult? Function(
|
TResult? Function(
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> tracks,
|
List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimplePlaylistObject? collection,
|
SpotubeSimplePlaylistObject? collection,
|
||||||
int? initialIndex)?
|
int? initialIndex)?
|
||||||
playlist,
|
playlist,
|
||||||
TResult? Function(
|
TResult? Function(
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> tracks,
|
List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimpleAlbumObject? collection,
|
SpotubeSimpleAlbumObject? collection,
|
||||||
int? initialIndex)?
|
int? initialIndex)?
|
||||||
@ -625,15 +625,15 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
|
|||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult maybeWhen<TResult extends Object?>({
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
TResult Function(
|
TResult Function(
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> tracks,
|
List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimplePlaylistObject? collection,
|
SpotubeSimplePlaylistObject? collection,
|
||||||
int? initialIndex)?
|
int? initialIndex)?
|
||||||
playlist,
|
playlist,
|
||||||
TResult Function(
|
TResult Function(
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> tracks,
|
List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimpleAlbumObject? collection,
|
SpotubeSimpleAlbumObject? collection,
|
||||||
int? initialIndex)?
|
int? initialIndex)?
|
||||||
@ -687,8 +687,8 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
|
|||||||
|
|
||||||
abstract class WebSocketLoadEventDataAlbum extends WebSocketLoadEventData {
|
abstract class WebSocketLoadEventDataAlbum extends WebSocketLoadEventData {
|
||||||
factory WebSocketLoadEventDataAlbum(
|
factory WebSocketLoadEventDataAlbum(
|
||||||
{@Assert("tracks is List<SpotubeFullTrackObject>",
|
{@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
required final List<SpotubeTrackObject> tracks,
|
required final List<SpotubeTrackObject> tracks,
|
||||||
final SpotubeSimpleAlbumObject? collection,
|
final SpotubeSimpleAlbumObject? collection,
|
||||||
final int? initialIndex}) = _$WebSocketLoadEventDataAlbumImpl;
|
final int? initialIndex}) = _$WebSocketLoadEventDataAlbumImpl;
|
||||||
@ -698,8 +698,8 @@ abstract class WebSocketLoadEventDataAlbum extends WebSocketLoadEventData {
|
|||||||
_$WebSocketLoadEventDataAlbumImpl.fromJson;
|
_$WebSocketLoadEventDataAlbumImpl.fromJson;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
@Assert("tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject")
|
"tracks must be a list of SpotubeTrackObject")
|
||||||
List<SpotubeTrackObject> get tracks;
|
List<SpotubeTrackObject> get tracks;
|
||||||
@override
|
@override
|
||||||
SpotubeSimpleAlbumObject? get collection;
|
SpotubeSimpleAlbumObject? get collection;
|
||||||
|
|||||||
@ -6,8 +6,8 @@ class WebSocketLoadEventData with _$WebSocketLoadEventData {
|
|||||||
|
|
||||||
factory WebSocketLoadEventData.playlist({
|
factory WebSocketLoadEventData.playlist({
|
||||||
@Assert(
|
@Assert(
|
||||||
"tracks is List<SpotubeFullTrackObject>",
|
"tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject",
|
"tracks must be a list of SpotubeTrackObject",
|
||||||
)
|
)
|
||||||
required List<SpotubeTrackObject> tracks,
|
required List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimplePlaylistObject? collection,
|
SpotubeSimplePlaylistObject? collection,
|
||||||
@ -16,8 +16,8 @@ class WebSocketLoadEventData with _$WebSocketLoadEventData {
|
|||||||
|
|
||||||
factory WebSocketLoadEventData.album({
|
factory WebSocketLoadEventData.album({
|
||||||
@Assert(
|
@Assert(
|
||||||
"tracks is List<SpotubeFullTrackObject>",
|
"tracks is List<SpotubeTrackObject>",
|
||||||
"tracks must be a list of SpotubeFullTrackObject",
|
"tracks must be a list of SpotubeTrackObject",
|
||||||
)
|
)
|
||||||
required List<SpotubeTrackObject> tracks,
|
required List<SpotubeTrackObject> tracks,
|
||||||
SpotubeSimpleAlbumObject? collection,
|
SpotubeSimpleAlbumObject? collection,
|
||||||
|
|||||||
@ -338,14 +338,14 @@ class WebSocketRemoveTrackEvent extends WebSocketEvent<String> {
|
|||||||
WebSocketRemoveTrackEvent(String data) : super(WsEvent.removeTrack, data);
|
WebSocketRemoveTrackEvent(String data) : super(WsEvent.removeTrack, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebSocketAddTrackEvent extends WebSocketEvent<SpotubeFullTrackObject> {
|
class WebSocketAddTrackEvent extends WebSocketEvent<SpotubeTrackObject> {
|
||||||
WebSocketAddTrackEvent(SpotubeFullTrackObject data)
|
WebSocketAddTrackEvent(SpotubeTrackObject data)
|
||||||
: super(WsEvent.addTrack, data);
|
: super(WsEvent.addTrack, data);
|
||||||
|
|
||||||
WebSocketAddTrackEvent.fromJson(Map<String, dynamic> json)
|
WebSocketAddTrackEvent.fromJson(Map<String, dynamic> json)
|
||||||
: super(
|
: super(
|
||||||
WsEvent.addTrack,
|
WsEvent.addTrack,
|
||||||
SpotubeFullTrackObject.fromJson(
|
SpotubeTrackObject.fromJson(
|
||||||
json["data"] as Map<String, dynamic>,
|
json["data"] as Map<String, dynamic>,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,42 +0,0 @@
|
|||||||
part of 'metadata.dart';
|
|
||||||
|
|
||||||
enum SpotubeAlbumType {
|
|
||||||
album,
|
|
||||||
single,
|
|
||||||
compilation,
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class SpotubeFullAlbumObject with _$SpotubeFullAlbumObject {
|
|
||||||
factory SpotubeFullAlbumObject({
|
|
||||||
required String id,
|
|
||||||
required String name,
|
|
||||||
required List<SpotubeSimpleArtistObject> artists,
|
|
||||||
@Default([]) List<SpotubeImageObject> images,
|
|
||||||
required String releaseDate,
|
|
||||||
required String externalUri,
|
|
||||||
required int totalTracks,
|
|
||||||
required SpotubeAlbumType albumType,
|
|
||||||
String? recordLabel,
|
|
||||||
List<String>? genres,
|
|
||||||
}) = _SpotubeFullAlbumObject;
|
|
||||||
|
|
||||||
factory SpotubeFullAlbumObject.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SpotubeFullAlbumObjectFromJson(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class SpotubeSimpleAlbumObject with _$SpotubeSimpleAlbumObject {
|
|
||||||
factory SpotubeSimpleAlbumObject({
|
|
||||||
required String id,
|
|
||||||
required String name,
|
|
||||||
required String externalUri,
|
|
||||||
required List<SpotubeSimpleArtistObject> artists,
|
|
||||||
@Default([]) List<SpotubeImageObject> images,
|
|
||||||
required SpotubeAlbumType albumType,
|
|
||||||
String? releaseDate,
|
|
||||||
}) = _SpotubeSimpleAlbumObject;
|
|
||||||
|
|
||||||
factory SpotubeSimpleAlbumObject.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SpotubeSimpleAlbumObjectFromJson(json);
|
|
||||||
}
|
|
||||||
@ -1,33 +1,5 @@
|
|||||||
part of 'metadata.dart';
|
part of 'metadata.dart';
|
||||||
|
|
||||||
@freezed
|
|
||||||
class SpotubeFullArtistObject with _$SpotubeFullArtistObject {
|
|
||||||
factory SpotubeFullArtistObject({
|
|
||||||
required String id,
|
|
||||||
required String name,
|
|
||||||
required String externalUri,
|
|
||||||
@Default([]) List<SpotubeImageObject> images,
|
|
||||||
List<String>? genres,
|
|
||||||
int? followers,
|
|
||||||
}) = _SpotubeFullArtistObject;
|
|
||||||
|
|
||||||
factory SpotubeFullArtistObject.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SpotubeFullArtistObjectFromJson(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class SpotubeSimpleArtistObject with _$SpotubeSimpleArtistObject {
|
|
||||||
factory SpotubeSimpleArtistObject({
|
|
||||||
required String id,
|
|
||||||
required String name,
|
|
||||||
required String externalUri,
|
|
||||||
List<SpotubeImageObject>? images,
|
|
||||||
}) = _SpotubeSimpleArtistObject;
|
|
||||||
|
|
||||||
factory SpotubeSimpleArtistObject.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SpotubeSimpleArtistObjectFromJson(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SpotubeFullArtistObjectAsString on List<SpotubeFullArtistObject> {
|
extension SpotubeFullArtistObjectAsString on List<SpotubeFullArtistObject> {
|
||||||
String asString() {
|
String asString() {
|
||||||
return map((e) => e.name).join(", ");
|
return map((e) => e.name).join(", ");
|
||||||
|
|||||||
@ -7,29 +7,7 @@ enum SpotubeMediaCompressionType {
|
|||||||
lossless,
|
lossless,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Freezed(unionKey: 'type')
|
extension GetFileExtension on SpotubeAudioSourceContainerPreset {
|
||||||
class SpotubeAudioSourceContainerPreset
|
|
||||||
with _$SpotubeAudioSourceContainerPreset {
|
|
||||||
const SpotubeAudioSourceContainerPreset._();
|
|
||||||
|
|
||||||
@FreezedUnionValue("lossy")
|
|
||||||
factory SpotubeAudioSourceContainerPreset.lossy({
|
|
||||||
required SpotubeMediaCompressionType type,
|
|
||||||
required String name,
|
|
||||||
required List<SpotubeAudioLossyContainerQuality> qualities,
|
|
||||||
}) = SpotubeAudioSourceContainerPresetLossy;
|
|
||||||
|
|
||||||
@FreezedUnionValue("lossless")
|
|
||||||
factory SpotubeAudioSourceContainerPreset.lossless({
|
|
||||||
required SpotubeMediaCompressionType type,
|
|
||||||
required String name,
|
|
||||||
required List<SpotubeAudioLosslessContainerQuality> qualities,
|
|
||||||
}) = SpotubeAudioSourceContainerPresetLossless;
|
|
||||||
|
|
||||||
factory SpotubeAudioSourceContainerPreset.fromJson(
|
|
||||||
Map<String, dynamic> json) =>
|
|
||||||
_$SpotubeAudioSourceContainerPresetFromJson(json);
|
|
||||||
|
|
||||||
String getFileExtension() {
|
String getFileExtension() {
|
||||||
return switch (name) {
|
return switch (name) {
|
||||||
"mp4" => "m4a",
|
"mp4" => "m4a",
|
||||||
@ -39,72 +17,16 @@ class SpotubeAudioSourceContainerPreset
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
extension ToStringSpotubeAudioLossyContainerQuality
|
||||||
class SpotubeAudioLossyContainerQuality
|
on SpotubeAudioLossyContainerQuality {
|
||||||
with _$SpotubeAudioLossyContainerQuality {
|
toFormattedString() {
|
||||||
const SpotubeAudioLossyContainerQuality._();
|
|
||||||
|
|
||||||
factory SpotubeAudioLossyContainerQuality({
|
|
||||||
required int bitrate, // bits per second
|
|
||||||
}) = _SpotubeAudioLossyContainerQuality;
|
|
||||||
|
|
||||||
factory SpotubeAudioLossyContainerQuality.fromJson(
|
|
||||||
Map<String, dynamic> json) =>
|
|
||||||
_$SpotubeAudioLossyContainerQualityFromJson(json);
|
|
||||||
|
|
||||||
@override
|
|
||||||
toString() {
|
|
||||||
return "${oneOptionalDecimalFormatter.format(bitrate / 1000)}kbps";
|
return "${oneOptionalDecimalFormatter.format(bitrate / 1000)}kbps";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
extension ToStringSpotubeAudioLosslessContainerQuality
|
||||||
class SpotubeAudioLosslessContainerQuality
|
on SpotubeAudioLosslessContainerQuality {
|
||||||
with _$SpotubeAudioLosslessContainerQuality {
|
toFormattedString() {
|
||||||
const SpotubeAudioLosslessContainerQuality._();
|
|
||||||
|
|
||||||
factory SpotubeAudioLosslessContainerQuality({
|
|
||||||
required int bitDepth, // bit
|
|
||||||
required int sampleRate, // hz
|
|
||||||
}) = _SpotubeAudioLosslessContainerQuality;
|
|
||||||
|
|
||||||
factory SpotubeAudioLosslessContainerQuality.fromJson(
|
|
||||||
Map<String, dynamic> json) =>
|
|
||||||
_$SpotubeAudioLosslessContainerQualityFromJson(json);
|
|
||||||
|
|
||||||
@override
|
|
||||||
toString() {
|
|
||||||
return "${bitDepth}bit • ${oneOptionalDecimalFormatter.format(sampleRate / 1000)}kHz";
|
return "${bitDepth}bit • ${oneOptionalDecimalFormatter.format(sampleRate / 1000)}kHz";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
|
||||||
class SpotubeAudioSourceMatchObject with _$SpotubeAudioSourceMatchObject {
|
|
||||||
factory SpotubeAudioSourceMatchObject({
|
|
||||||
required String id,
|
|
||||||
required String title,
|
|
||||||
required List<String> artists,
|
|
||||||
required Duration duration,
|
|
||||||
String? thumbnail,
|
|
||||||
required String externalUri,
|
|
||||||
}) = _SpotubeAudioSourceMatchObject;
|
|
||||||
|
|
||||||
factory SpotubeAudioSourceMatchObject.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SpotubeAudioSourceMatchObjectFromJson(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class SpotubeAudioSourceStreamObject with _$SpotubeAudioSourceStreamObject {
|
|
||||||
factory SpotubeAudioSourceStreamObject({
|
|
||||||
required String url,
|
|
||||||
required String container,
|
|
||||||
required SpotubeMediaCompressionType type,
|
|
||||||
String? codec,
|
|
||||||
double? bitrate,
|
|
||||||
int? bitDepth,
|
|
||||||
double? sampleRate,
|
|
||||||
}) = _SpotubeAudioSourceStreamObject;
|
|
||||||
|
|
||||||
factory SpotubeAudioSourceStreamObject.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SpotubeAudioSourceStreamObjectFromJson(json);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,21 +1,79 @@
|
|||||||
part of 'metadata.dart';
|
part of 'metadata.dart';
|
||||||
|
|
||||||
@Freezed(genericArgumentFactories: true)
|
class SpotubeFlattenedBrowseSectionObject<T> {
|
||||||
class SpotubeBrowseSectionObject<T> with _$SpotubeBrowseSectionObject<T> {
|
final String id;
|
||||||
factory SpotubeBrowseSectionObject({
|
final String title;
|
||||||
required String id,
|
final String externalUri;
|
||||||
required String title,
|
final bool browseMore;
|
||||||
required String externalUri,
|
final List<T> items;
|
||||||
required bool browseMore,
|
|
||||||
required List<T> items,
|
|
||||||
}) = _SpotubeBrowseSectionObject<T>;
|
|
||||||
|
|
||||||
factory SpotubeBrowseSectionObject.fromJson(
|
SpotubeFlattenedBrowseSectionObject({
|
||||||
Map<String, Object?> json,
|
required this.id,
|
||||||
T Function(Map<String, dynamic> json) fromJsonT,
|
required this.title,
|
||||||
) =>
|
required this.browseMore,
|
||||||
_$SpotubeBrowseSectionObjectFromJson<T>(
|
required this.externalUri,
|
||||||
json,
|
required this.items,
|
||||||
(json) => fromJsonT(json as Map<String, dynamic>),
|
});
|
||||||
|
|
||||||
|
static SpotubeFlattenedBrowseSectionObject<T> from<T>(
|
||||||
|
SpotubeBrowseSectionObject browseSection,
|
||||||
|
T Function(SpotubeBrowseSectionResponseObjectItem item) parse,
|
||||||
|
) {
|
||||||
|
return SpotubeFlattenedBrowseSectionObject<T>(
|
||||||
|
browseMore: browseSection.browseMore,
|
||||||
|
id: browseSection.id,
|
||||||
|
title: browseSection.title,
|
||||||
|
externalUri: browseSection.externalUri,
|
||||||
|
items: browseSection.items
|
||||||
|
.map((item) => parse(item))
|
||||||
|
.toList(growable: false),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SpotubeFlattenedBrowseSectionObject<T> copyWith({
|
||||||
|
String? id,
|
||||||
|
String? title,
|
||||||
|
String? externalUri,
|
||||||
|
bool? browseMore,
|
||||||
|
List<T>? items,
|
||||||
|
}) {
|
||||||
|
return SpotubeFlattenedBrowseSectionObject<T>(
|
||||||
|
id: id ?? this.id,
|
||||||
|
title: title ?? this.title,
|
||||||
|
externalUri: externalUri ?? this.externalUri,
|
||||||
|
browseMore: browseMore ?? this.browseMore,
|
||||||
|
items: items ?? this.items,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SpotubeBrowseSectionObjectExtension on SpotubeBrowseSectionObject {
|
||||||
|
SpotubeFlattenedBrowseSectionObject<T> flatten<T>() {
|
||||||
|
return SpotubeFlattenedBrowseSectionObject.from<T>(
|
||||||
|
this,
|
||||||
|
(item) => switch (T) {
|
||||||
|
SpotubeSimpleAlbumObject() =>
|
||||||
|
(item as SpotubeBrowseSectionResponseObjectItem_AlbumSimple).field0
|
||||||
|
as T,
|
||||||
|
SpotubeFullAlbumObject() =>
|
||||||
|
(item as SpotubeBrowseSectionResponseObjectItem_AlbumFull).field0
|
||||||
|
as T,
|
||||||
|
SpotubeSimpleArtistObject() =>
|
||||||
|
(item as SpotubeBrowseSectionResponseObjectItem_ArtistSimple).field0
|
||||||
|
as T,
|
||||||
|
SpotubeFullArtistObject() =>
|
||||||
|
(item as SpotubeBrowseSectionResponseObjectItem_ArtistFull).field0
|
||||||
|
as T,
|
||||||
|
SpotubeTrackObject() =>
|
||||||
|
(item as SpotubeBrowseSectionResponseObjectItem_Track).field0 as T,
|
||||||
|
SpotubeSimplePlaylistObject() =>
|
||||||
|
(item as SpotubeBrowseSectionResponseObjectItem_PlaylistSimple).field0
|
||||||
|
as T,
|
||||||
|
SpotubeFullPlaylistObject() =>
|
||||||
|
(item as SpotubeBrowseSectionResponseObjectItem_PlaylistFull).field0
|
||||||
|
as T,
|
||||||
|
_ => throw Exception("Unsupported type: $T"),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,17 +1,5 @@
|
|||||||
part of 'metadata.dart';
|
part of 'metadata.dart';
|
||||||
|
|
||||||
@freezed
|
|
||||||
class SpotubeImageObject with _$SpotubeImageObject {
|
|
||||||
factory SpotubeImageObject({
|
|
||||||
required String url,
|
|
||||||
int? width,
|
|
||||||
int? height,
|
|
||||||
}) = _SpotubeImageObject;
|
|
||||||
|
|
||||||
factory SpotubeImageObject.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SpotubeImageObjectFromJson(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ImagePlaceholder {
|
enum ImagePlaceholder {
|
||||||
albumArt,
|
albumArt,
|
||||||
artist,
|
artist,
|
||||||
|
|||||||
@ -10,23 +10,31 @@ import 'package:metadata_god/metadata_god.dart';
|
|||||||
import 'package:mime/mime.dart';
|
import 'package:mime/mime.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.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';
|
||||||
|
|
||||||
|
export "package:spotube/src/rust/api/plugin/models/album.dart";
|
||||||
|
export "package:spotube/src/rust/api/plugin/models/audio_source.dart";
|
||||||
|
export "package:spotube/src/rust/api/plugin/models/artist.dart";
|
||||||
|
export "package:spotube/src/rust/api/plugin/models/auth.dart";
|
||||||
|
export "package:spotube/src/rust/api/plugin/models/browse.dart";
|
||||||
|
export "package:spotube/src/rust/api/plugin/models/core.dart";
|
||||||
|
export "package:spotube/src/rust/api/plugin/models/playlist.dart";
|
||||||
|
export "package:spotube/src/rust/api/plugin/models/track.dart";
|
||||||
|
export "package:spotube/src/rust/api/plugin/models/user.dart";
|
||||||
|
export "package:spotube/src/rust/api/plugin/models/image.dart";
|
||||||
|
export "package:spotube/src/rust/api/plugin/models/pagination.dart";
|
||||||
|
export "package:spotube/src/rust/api/plugin/models/search.dart";
|
||||||
|
|
||||||
part 'metadata.g.dart';
|
part 'metadata.g.dart';
|
||||||
part 'metadata.freezed.dart';
|
part 'metadata.freezed.dart';
|
||||||
|
|
||||||
part 'audio_source.dart';
|
|
||||||
part 'album.dart';
|
|
||||||
part 'artist.dart';
|
part 'artist.dart';
|
||||||
|
part 'audio_source.dart';
|
||||||
part 'browse.dart';
|
part 'browse.dart';
|
||||||
part 'fields.dart';
|
part 'fields.dart';
|
||||||
part 'image.dart';
|
part 'image.dart';
|
||||||
part 'pagination.dart';
|
part 'pagination.dart';
|
||||||
part 'playlist.dart';
|
|
||||||
part 'search.dart';
|
|
||||||
part 'track.dart';
|
part 'track.dart';
|
||||||
part 'user.dart';
|
|
||||||
|
|
||||||
part 'plugin.dart';
|
|
||||||
part 'repository.dart';
|
part 'repository.dart';
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -6,270 +6,6 @@ part of 'metadata.dart';
|
|||||||
// JsonSerializableGenerator
|
// JsonSerializableGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
_$SpotubeAudioSourceContainerPresetLossyImpl
|
|
||||||
_$$SpotubeAudioSourceContainerPresetLossyImplFromJson(Map json) =>
|
|
||||||
_$SpotubeAudioSourceContainerPresetLossyImpl(
|
|
||||||
type: $enumDecode(_$SpotubeMediaCompressionTypeEnumMap, json['type']),
|
|
||||||
name: json['name'] as String,
|
|
||||||
qualities: (json['qualities'] as List<dynamic>)
|
|
||||||
.map((e) => SpotubeAudioLossyContainerQuality.fromJson(
|
|
||||||
Map<String, dynamic>.from(e as Map)))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubeAudioSourceContainerPresetLossyImplToJson(
|
|
||||||
_$SpotubeAudioSourceContainerPresetLossyImpl instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'type': _$SpotubeMediaCompressionTypeEnumMap[instance.type]!,
|
|
||||||
'name': instance.name,
|
|
||||||
'qualities': instance.qualities.map((e) => e.toJson()).toList(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const _$SpotubeMediaCompressionTypeEnumMap = {
|
|
||||||
SpotubeMediaCompressionType.lossy: 'lossy',
|
|
||||||
SpotubeMediaCompressionType.lossless: 'lossless',
|
|
||||||
};
|
|
||||||
|
|
||||||
_$SpotubeAudioSourceContainerPresetLosslessImpl
|
|
||||||
_$$SpotubeAudioSourceContainerPresetLosslessImplFromJson(Map json) =>
|
|
||||||
_$SpotubeAudioSourceContainerPresetLosslessImpl(
|
|
||||||
type: $enumDecode(_$SpotubeMediaCompressionTypeEnumMap, json['type']),
|
|
||||||
name: json['name'] as String,
|
|
||||||
qualities: (json['qualities'] as List<dynamic>)
|
|
||||||
.map((e) => SpotubeAudioLosslessContainerQuality.fromJson(
|
|
||||||
Map<String, dynamic>.from(e as Map)))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubeAudioSourceContainerPresetLosslessImplToJson(
|
|
||||||
_$SpotubeAudioSourceContainerPresetLosslessImpl instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'type': _$SpotubeMediaCompressionTypeEnumMap[instance.type]!,
|
|
||||||
'name': instance.name,
|
|
||||||
'qualities': instance.qualities.map((e) => e.toJson()).toList(),
|
|
||||||
};
|
|
||||||
|
|
||||||
_$SpotubeAudioLossyContainerQualityImpl
|
|
||||||
_$$SpotubeAudioLossyContainerQualityImplFromJson(Map json) =>
|
|
||||||
_$SpotubeAudioLossyContainerQualityImpl(
|
|
||||||
bitrate: (json['bitrate'] as num).toInt(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubeAudioLossyContainerQualityImplToJson(
|
|
||||||
_$SpotubeAudioLossyContainerQualityImpl instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'bitrate': instance.bitrate,
|
|
||||||
};
|
|
||||||
|
|
||||||
_$SpotubeAudioLosslessContainerQualityImpl
|
|
||||||
_$$SpotubeAudioLosslessContainerQualityImplFromJson(Map json) =>
|
|
||||||
_$SpotubeAudioLosslessContainerQualityImpl(
|
|
||||||
bitDepth: (json['bitDepth'] as num).toInt(),
|
|
||||||
sampleRate: (json['sampleRate'] as num).toInt(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubeAudioLosslessContainerQualityImplToJson(
|
|
||||||
_$SpotubeAudioLosslessContainerQualityImpl instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'bitDepth': instance.bitDepth,
|
|
||||||
'sampleRate': instance.sampleRate,
|
|
||||||
};
|
|
||||||
|
|
||||||
_$SpotubeAudioSourceMatchObjectImpl
|
|
||||||
_$$SpotubeAudioSourceMatchObjectImplFromJson(Map json) =>
|
|
||||||
_$SpotubeAudioSourceMatchObjectImpl(
|
|
||||||
id: json['id'] as String,
|
|
||||||
title: json['title'] as String,
|
|
||||||
artists: (json['artists'] as List<dynamic>)
|
|
||||||
.map((e) => e as String)
|
|
||||||
.toList(),
|
|
||||||
duration: Duration(microseconds: (json['duration'] as num).toInt()),
|
|
||||||
thumbnail: json['thumbnail'] as String?,
|
|
||||||
externalUri: json['externalUri'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubeAudioSourceMatchObjectImplToJson(
|
|
||||||
_$SpotubeAudioSourceMatchObjectImpl instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'id': instance.id,
|
|
||||||
'title': instance.title,
|
|
||||||
'artists': instance.artists,
|
|
||||||
'duration': instance.duration.inMicroseconds,
|
|
||||||
'thumbnail': instance.thumbnail,
|
|
||||||
'externalUri': instance.externalUri,
|
|
||||||
};
|
|
||||||
|
|
||||||
_$SpotubeAudioSourceStreamObjectImpl
|
|
||||||
_$$SpotubeAudioSourceStreamObjectImplFromJson(Map json) =>
|
|
||||||
_$SpotubeAudioSourceStreamObjectImpl(
|
|
||||||
url: json['url'] as String,
|
|
||||||
container: json['container'] as String,
|
|
||||||
type: $enumDecode(_$SpotubeMediaCompressionTypeEnumMap, json['type']),
|
|
||||||
codec: json['codec'] as String?,
|
|
||||||
bitrate: (json['bitrate'] as num?)?.toDouble(),
|
|
||||||
bitDepth: (json['bitDepth'] as num?)?.toInt(),
|
|
||||||
sampleRate: (json['sampleRate'] as num?)?.toDouble(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubeAudioSourceStreamObjectImplToJson(
|
|
||||||
_$SpotubeAudioSourceStreamObjectImpl instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'url': instance.url,
|
|
||||||
'container': instance.container,
|
|
||||||
'type': _$SpotubeMediaCompressionTypeEnumMap[instance.type]!,
|
|
||||||
'codec': instance.codec,
|
|
||||||
'bitrate': instance.bitrate,
|
|
||||||
'bitDepth': instance.bitDepth,
|
|
||||||
'sampleRate': instance.sampleRate,
|
|
||||||
};
|
|
||||||
|
|
||||||
_$SpotubeFullAlbumObjectImpl _$$SpotubeFullAlbumObjectImplFromJson(Map json) =>
|
|
||||||
_$SpotubeFullAlbumObjectImpl(
|
|
||||||
id: json['id'] as String,
|
|
||||||
name: json['name'] as String,
|
|
||||||
artists: (json['artists'] as List<dynamic>)
|
|
||||||
.map((e) => SpotubeSimpleArtistObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(e as Map)))
|
|
||||||
.toList(),
|
|
||||||
images: (json['images'] as List<dynamic>?)
|
|
||||||
?.map((e) => SpotubeImageObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(e as Map)))
|
|
||||||
.toList() ??
|
|
||||||
const [],
|
|
||||||
releaseDate: json['releaseDate'] as String,
|
|
||||||
externalUri: json['externalUri'] as String,
|
|
||||||
totalTracks: (json['totalTracks'] as num).toInt(),
|
|
||||||
albumType: $enumDecode(_$SpotubeAlbumTypeEnumMap, json['albumType']),
|
|
||||||
recordLabel: json['recordLabel'] as String?,
|
|
||||||
genres:
|
|
||||||
(json['genres'] as List<dynamic>?)?.map((e) => e as String).toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubeFullAlbumObjectImplToJson(
|
|
||||||
_$SpotubeFullAlbumObjectImpl instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'id': instance.id,
|
|
||||||
'name': instance.name,
|
|
||||||
'artists': instance.artists.map((e) => e.toJson()).toList(),
|
|
||||||
'images': instance.images.map((e) => e.toJson()).toList(),
|
|
||||||
'releaseDate': instance.releaseDate,
|
|
||||||
'externalUri': instance.externalUri,
|
|
||||||
'totalTracks': instance.totalTracks,
|
|
||||||
'albumType': _$SpotubeAlbumTypeEnumMap[instance.albumType]!,
|
|
||||||
'recordLabel': instance.recordLabel,
|
|
||||||
'genres': instance.genres,
|
|
||||||
};
|
|
||||||
|
|
||||||
const _$SpotubeAlbumTypeEnumMap = {
|
|
||||||
SpotubeAlbumType.album: 'album',
|
|
||||||
SpotubeAlbumType.single: 'single',
|
|
||||||
SpotubeAlbumType.compilation: 'compilation',
|
|
||||||
};
|
|
||||||
|
|
||||||
_$SpotubeSimpleAlbumObjectImpl _$$SpotubeSimpleAlbumObjectImplFromJson(
|
|
||||||
Map json) =>
|
|
||||||
_$SpotubeSimpleAlbumObjectImpl(
|
|
||||||
id: json['id'] as String,
|
|
||||||
name: json['name'] as String,
|
|
||||||
externalUri: json['externalUri'] as String,
|
|
||||||
artists: (json['artists'] as List<dynamic>)
|
|
||||||
.map((e) => SpotubeSimpleArtistObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(e as Map)))
|
|
||||||
.toList(),
|
|
||||||
images: (json['images'] as List<dynamic>?)
|
|
||||||
?.map((e) => SpotubeImageObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(e as Map)))
|
|
||||||
.toList() ??
|
|
||||||
const [],
|
|
||||||
albumType: $enumDecode(_$SpotubeAlbumTypeEnumMap, json['albumType']),
|
|
||||||
releaseDate: json['releaseDate'] as String?,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubeSimpleAlbumObjectImplToJson(
|
|
||||||
_$SpotubeSimpleAlbumObjectImpl instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'id': instance.id,
|
|
||||||
'name': instance.name,
|
|
||||||
'externalUri': instance.externalUri,
|
|
||||||
'artists': instance.artists.map((e) => e.toJson()).toList(),
|
|
||||||
'images': instance.images.map((e) => e.toJson()).toList(),
|
|
||||||
'albumType': _$SpotubeAlbumTypeEnumMap[instance.albumType]!,
|
|
||||||
'releaseDate': instance.releaseDate,
|
|
||||||
};
|
|
||||||
|
|
||||||
_$SpotubeFullArtistObjectImpl _$$SpotubeFullArtistObjectImplFromJson(
|
|
||||||
Map json) =>
|
|
||||||
_$SpotubeFullArtistObjectImpl(
|
|
||||||
id: json['id'] as String,
|
|
||||||
name: json['name'] as String,
|
|
||||||
externalUri: json['externalUri'] as String,
|
|
||||||
images: (json['images'] as List<dynamic>?)
|
|
||||||
?.map((e) => SpotubeImageObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(e as Map)))
|
|
||||||
.toList() ??
|
|
||||||
const [],
|
|
||||||
genres:
|
|
||||||
(json['genres'] as List<dynamic>?)?.map((e) => e as String).toList(),
|
|
||||||
followers: (json['followers'] as num?)?.toInt(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubeFullArtistObjectImplToJson(
|
|
||||||
_$SpotubeFullArtistObjectImpl instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'id': instance.id,
|
|
||||||
'name': instance.name,
|
|
||||||
'externalUri': instance.externalUri,
|
|
||||||
'images': instance.images.map((e) => e.toJson()).toList(),
|
|
||||||
'genres': instance.genres,
|
|
||||||
'followers': instance.followers,
|
|
||||||
};
|
|
||||||
|
|
||||||
_$SpotubeSimpleArtistObjectImpl _$$SpotubeSimpleArtistObjectImplFromJson(
|
|
||||||
Map json) =>
|
|
||||||
_$SpotubeSimpleArtistObjectImpl(
|
|
||||||
id: json['id'] as String,
|
|
||||||
name: json['name'] as String,
|
|
||||||
externalUri: json['externalUri'] as String,
|
|
||||||
images: (json['images'] as List<dynamic>?)
|
|
||||||
?.map((e) =>
|
|
||||||
SpotubeImageObject.fromJson(Map<String, dynamic>.from(e as Map)))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubeSimpleArtistObjectImplToJson(
|
|
||||||
_$SpotubeSimpleArtistObjectImpl instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'id': instance.id,
|
|
||||||
'name': instance.name,
|
|
||||||
'externalUri': instance.externalUri,
|
|
||||||
'images': instance.images?.map((e) => e.toJson()).toList(),
|
|
||||||
};
|
|
||||||
|
|
||||||
_$SpotubeBrowseSectionObjectImpl<T>
|
|
||||||
_$$SpotubeBrowseSectionObjectImplFromJson<T>(
|
|
||||||
Map json,
|
|
||||||
T Function(Object? json) fromJsonT,
|
|
||||||
) =>
|
|
||||||
_$SpotubeBrowseSectionObjectImpl<T>(
|
|
||||||
id: json['id'] as String,
|
|
||||||
title: json['title'] as String,
|
|
||||||
externalUri: json['externalUri'] as String,
|
|
||||||
browseMore: json['browseMore'] as bool,
|
|
||||||
items: (json['items'] as List<dynamic>).map(fromJsonT).toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubeBrowseSectionObjectImplToJson<T>(
|
|
||||||
_$SpotubeBrowseSectionObjectImpl<T> instance,
|
|
||||||
Object? Function(T value) toJsonT,
|
|
||||||
) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'id': instance.id,
|
|
||||||
'title': instance.title,
|
|
||||||
'externalUri': instance.externalUri,
|
|
||||||
'browseMore': instance.browseMore,
|
|
||||||
'items': instance.items.map(toJsonT).toList(),
|
|
||||||
};
|
|
||||||
|
|
||||||
_$MetadataFormFieldInputObjectImpl _$$MetadataFormFieldInputObjectImplFromJson(
|
_$MetadataFormFieldInputObjectImpl _$$MetadataFormFieldInputObjectImplFromJson(
|
||||||
Map json) =>
|
Map json) =>
|
||||||
_$MetadataFormFieldInputObjectImpl(
|
_$MetadataFormFieldInputObjectImpl(
|
||||||
@ -316,286 +52,6 @@ Map<String, dynamic> _$$MetadataFormFieldTextObjectImplToJson(
|
|||||||
'text': instance.text,
|
'text': instance.text,
|
||||||
};
|
};
|
||||||
|
|
||||||
_$SpotubeImageObjectImpl _$$SpotubeImageObjectImplFromJson(Map json) =>
|
|
||||||
_$SpotubeImageObjectImpl(
|
|
||||||
url: json['url'] as String,
|
|
||||||
width: (json['width'] as num?)?.toInt(),
|
|
||||||
height: (json['height'] as num?)?.toInt(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubeImageObjectImplToJson(
|
|
||||||
_$SpotubeImageObjectImpl instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'url': instance.url,
|
|
||||||
'width': instance.width,
|
|
||||||
'height': instance.height,
|
|
||||||
};
|
|
||||||
|
|
||||||
_$SpotubePaginationResponseObjectImpl<T>
|
|
||||||
_$$SpotubePaginationResponseObjectImplFromJson<T>(
|
|
||||||
Map json,
|
|
||||||
T Function(Object? json) fromJsonT,
|
|
||||||
) =>
|
|
||||||
_$SpotubePaginationResponseObjectImpl<T>(
|
|
||||||
limit: (json['limit'] as num).toInt(),
|
|
||||||
nextOffset: (json['nextOffset'] as num?)?.toInt(),
|
|
||||||
total: (json['total'] as num).toInt(),
|
|
||||||
hasMore: json['hasMore'] as bool,
|
|
||||||
items: (json['items'] as List<dynamic>).map(fromJsonT).toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubePaginationResponseObjectImplToJson<T>(
|
|
||||||
_$SpotubePaginationResponseObjectImpl<T> instance,
|
|
||||||
Object? Function(T value) toJsonT,
|
|
||||||
) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'limit': instance.limit,
|
|
||||||
'nextOffset': instance.nextOffset,
|
|
||||||
'total': instance.total,
|
|
||||||
'hasMore': instance.hasMore,
|
|
||||||
'items': instance.items.map(toJsonT).toList(),
|
|
||||||
};
|
|
||||||
|
|
||||||
_$SpotubeFullPlaylistObjectImpl _$$SpotubeFullPlaylistObjectImplFromJson(
|
|
||||||
Map json) =>
|
|
||||||
_$SpotubeFullPlaylistObjectImpl(
|
|
||||||
id: json['id'] as String,
|
|
||||||
name: json['name'] as String,
|
|
||||||
description: json['description'] as String,
|
|
||||||
externalUri: json['externalUri'] as String,
|
|
||||||
owner: SpotubeUserObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(json['owner'] as Map)),
|
|
||||||
images: (json['images'] as List<dynamic>?)
|
|
||||||
?.map((e) => SpotubeImageObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(e as Map)))
|
|
||||||
.toList() ??
|
|
||||||
const [],
|
|
||||||
collaborators: (json['collaborators'] as List<dynamic>?)
|
|
||||||
?.map((e) => SpotubeUserObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(e as Map)))
|
|
||||||
.toList() ??
|
|
||||||
const [],
|
|
||||||
collaborative: json['collaborative'] as bool? ?? false,
|
|
||||||
public: json['public'] as bool? ?? false,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubeFullPlaylistObjectImplToJson(
|
|
||||||
_$SpotubeFullPlaylistObjectImpl instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'id': instance.id,
|
|
||||||
'name': instance.name,
|
|
||||||
'description': instance.description,
|
|
||||||
'externalUri': instance.externalUri,
|
|
||||||
'owner': instance.owner.toJson(),
|
|
||||||
'images': instance.images.map((e) => e.toJson()).toList(),
|
|
||||||
'collaborators': instance.collaborators.map((e) => e.toJson()).toList(),
|
|
||||||
'collaborative': instance.collaborative,
|
|
||||||
'public': instance.public,
|
|
||||||
};
|
|
||||||
|
|
||||||
_$SpotubeSimplePlaylistObjectImpl _$$SpotubeSimplePlaylistObjectImplFromJson(
|
|
||||||
Map json) =>
|
|
||||||
_$SpotubeSimplePlaylistObjectImpl(
|
|
||||||
id: json['id'] as String,
|
|
||||||
name: json['name'] as String,
|
|
||||||
description: json['description'] as String,
|
|
||||||
externalUri: json['externalUri'] as String,
|
|
||||||
owner: SpotubeUserObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(json['owner'] as Map)),
|
|
||||||
images: (json['images'] as List<dynamic>?)
|
|
||||||
?.map((e) => SpotubeImageObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(e as Map)))
|
|
||||||
.toList() ??
|
|
||||||
const [],
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubeSimplePlaylistObjectImplToJson(
|
|
||||||
_$SpotubeSimplePlaylistObjectImpl instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'id': instance.id,
|
|
||||||
'name': instance.name,
|
|
||||||
'description': instance.description,
|
|
||||||
'externalUri': instance.externalUri,
|
|
||||||
'owner': instance.owner.toJson(),
|
|
||||||
'images': instance.images.map((e) => e.toJson()).toList(),
|
|
||||||
};
|
|
||||||
|
|
||||||
_$SpotubeSearchResponseObjectImpl _$$SpotubeSearchResponseObjectImplFromJson(
|
|
||||||
Map json) =>
|
|
||||||
_$SpotubeSearchResponseObjectImpl(
|
|
||||||
albums: (json['albums'] as List<dynamic>)
|
|
||||||
.map((e) => SpotubeSimpleAlbumObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(e as Map)))
|
|
||||||
.toList(),
|
|
||||||
artists: (json['artists'] as List<dynamic>)
|
|
||||||
.map((e) => SpotubeFullArtistObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(e as Map)))
|
|
||||||
.toList(),
|
|
||||||
playlists: (json['playlists'] as List<dynamic>)
|
|
||||||
.map((e) => SpotubeSimplePlaylistObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(e as Map)))
|
|
||||||
.toList(),
|
|
||||||
tracks: (json['tracks'] as List<dynamic>)
|
|
||||||
.map((e) => SpotubeFullTrackObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(e as Map)))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubeSearchResponseObjectImplToJson(
|
|
||||||
_$SpotubeSearchResponseObjectImpl instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'albums': instance.albums.map((e) => e.toJson()).toList(),
|
|
||||||
'artists': instance.artists.map((e) => e.toJson()).toList(),
|
|
||||||
'playlists': instance.playlists.map((e) => e.toJson()).toList(),
|
|
||||||
'tracks': instance.tracks.map((e) => e.toJson()).toList(),
|
|
||||||
};
|
|
||||||
|
|
||||||
_$SpotubeLocalTrackObjectImpl _$$SpotubeLocalTrackObjectImplFromJson(
|
|
||||||
Map json) =>
|
|
||||||
_$SpotubeLocalTrackObjectImpl(
|
|
||||||
id: json['id'] as String,
|
|
||||||
name: json['name'] as String,
|
|
||||||
externalUri: json['externalUri'] as String,
|
|
||||||
artists: (json['artists'] as List<dynamic>?)
|
|
||||||
?.map((e) => SpotubeSimpleArtistObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(e as Map)))
|
|
||||||
.toList() ??
|
|
||||||
const [],
|
|
||||||
album: SpotubeSimpleAlbumObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(json['album'] as Map)),
|
|
||||||
durationMs: (json['durationMs'] as num).toInt(),
|
|
||||||
path: json['path'] as String,
|
|
||||||
$type: json['runtimeType'] as String?,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubeLocalTrackObjectImplToJson(
|
|
||||||
_$SpotubeLocalTrackObjectImpl instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'id': instance.id,
|
|
||||||
'name': instance.name,
|
|
||||||
'externalUri': instance.externalUri,
|
|
||||||
'artists': instance.artists.map((e) => e.toJson()).toList(),
|
|
||||||
'album': instance.album.toJson(),
|
|
||||||
'durationMs': instance.durationMs,
|
|
||||||
'path': instance.path,
|
|
||||||
'runtimeType': instance.$type,
|
|
||||||
};
|
|
||||||
|
|
||||||
_$SpotubeFullTrackObjectImpl _$$SpotubeFullTrackObjectImplFromJson(Map json) =>
|
|
||||||
_$SpotubeFullTrackObjectImpl(
|
|
||||||
id: json['id'] as String,
|
|
||||||
name: json['name'] as String,
|
|
||||||
externalUri: json['externalUri'] as String,
|
|
||||||
artists: (json['artists'] as List<dynamic>?)
|
|
||||||
?.map((e) => SpotubeSimpleArtistObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(e as Map)))
|
|
||||||
.toList() ??
|
|
||||||
const [],
|
|
||||||
album: SpotubeSimpleAlbumObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(json['album'] as Map)),
|
|
||||||
durationMs: (json['durationMs'] as num).toInt(),
|
|
||||||
isrc: json['isrc'] as String,
|
|
||||||
explicit: json['explicit'] as bool,
|
|
||||||
$type: json['runtimeType'] as String?,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubeFullTrackObjectImplToJson(
|
|
||||||
_$SpotubeFullTrackObjectImpl instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'id': instance.id,
|
|
||||||
'name': instance.name,
|
|
||||||
'externalUri': instance.externalUri,
|
|
||||||
'artists': instance.artists.map((e) => e.toJson()).toList(),
|
|
||||||
'album': instance.album.toJson(),
|
|
||||||
'durationMs': instance.durationMs,
|
|
||||||
'isrc': instance.isrc,
|
|
||||||
'explicit': instance.explicit,
|
|
||||||
'runtimeType': instance.$type,
|
|
||||||
};
|
|
||||||
|
|
||||||
_$SpotubeUserObjectImpl _$$SpotubeUserObjectImplFromJson(Map json) =>
|
|
||||||
_$SpotubeUserObjectImpl(
|
|
||||||
id: json['id'] as String,
|
|
||||||
name: json['name'] as String,
|
|
||||||
images: (json['images'] as List<dynamic>?)
|
|
||||||
?.map((e) => SpotubeImageObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(e as Map)))
|
|
||||||
.toList() ??
|
|
||||||
const [],
|
|
||||||
externalUri: json['externalUri'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubeUserObjectImplToJson(
|
|
||||||
_$SpotubeUserObjectImpl instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'id': instance.id,
|
|
||||||
'name': instance.name,
|
|
||||||
'images': instance.images.map((e) => e.toJson()).toList(),
|
|
||||||
'externalUri': instance.externalUri,
|
|
||||||
};
|
|
||||||
|
|
||||||
_$PluginConfigurationImpl _$$PluginConfigurationImplFromJson(Map json) =>
|
|
||||||
_$PluginConfigurationImpl(
|
|
||||||
name: json['name'] as String,
|
|
||||||
description: json['description'] as String,
|
|
||||||
version: json['version'] as String,
|
|
||||||
author: json['author'] as String,
|
|
||||||
entryPoint: json['entryPoint'] as String,
|
|
||||||
pluginApiVersion: json['pluginApiVersion'] as String,
|
|
||||||
apis: (json['apis'] as List<dynamic>?)
|
|
||||||
?.map((e) => $enumDecode(_$PluginApisEnumMap, e))
|
|
||||||
.toList() ??
|
|
||||||
const [],
|
|
||||||
abilities: (json['abilities'] as List<dynamic>?)
|
|
||||||
?.map((e) => $enumDecode(_$PluginAbilitiesEnumMap, e))
|
|
||||||
.toList() ??
|
|
||||||
const [],
|
|
||||||
repository: json['repository'] as String?,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$PluginConfigurationImplToJson(
|
|
||||||
_$PluginConfigurationImpl instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'name': instance.name,
|
|
||||||
'description': instance.description,
|
|
||||||
'version': instance.version,
|
|
||||||
'author': instance.author,
|
|
||||||
'entryPoint': instance.entryPoint,
|
|
||||||
'pluginApiVersion': instance.pluginApiVersion,
|
|
||||||
'apis': instance.apis.map((e) => _$PluginApisEnumMap[e]!).toList(),
|
|
||||||
'abilities':
|
|
||||||
instance.abilities.map((e) => _$PluginAbilitiesEnumMap[e]!).toList(),
|
|
||||||
'repository': instance.repository,
|
|
||||||
};
|
|
||||||
|
|
||||||
const _$PluginApisEnumMap = {
|
|
||||||
PluginApis.webview: 'webview',
|
|
||||||
PluginApis.localstorage: 'localstorage',
|
|
||||||
PluginApis.timezone: 'timezone',
|
|
||||||
};
|
|
||||||
|
|
||||||
const _$PluginAbilitiesEnumMap = {
|
|
||||||
PluginAbilities.authentication: 'authentication',
|
|
||||||
PluginAbilities.scrobbling: 'scrobbling',
|
|
||||||
PluginAbilities.metadata: 'metadata',
|
|
||||||
PluginAbilities.audioSource: 'audio-source',
|
|
||||||
};
|
|
||||||
|
|
||||||
_$PluginUpdateAvailableImpl _$$PluginUpdateAvailableImplFromJson(Map json) =>
|
|
||||||
_$PluginUpdateAvailableImpl(
|
|
||||||
downloadUrl: json['downloadUrl'] as String,
|
|
||||||
version: json['version'] as String,
|
|
||||||
changelog: json['changelog'] as String?,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$$PluginUpdateAvailableImplToJson(
|
|
||||||
_$PluginUpdateAvailableImpl instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'downloadUrl': instance.downloadUrl,
|
|
||||||
'version': instance.version,
|
|
||||||
'changelog': instance.changelog,
|
|
||||||
};
|
|
||||||
|
|
||||||
_$MetadataPluginRepositoryImpl _$$MetadataPluginRepositoryImplFromJson(
|
_$MetadataPluginRepositoryImpl _$$MetadataPluginRepositoryImplFromJson(
|
||||||
Map json) =>
|
Map json) =>
|
||||||
_$MetadataPluginRepositoryImpl(
|
_$MetadataPluginRepositoryImpl(
|
||||||
|
|||||||
@ -1,22 +1,78 @@
|
|||||||
part of 'metadata.dart';
|
part of 'metadata.dart';
|
||||||
|
|
||||||
@Freezed(genericArgumentFactories: true)
|
class SpotubeFlattenedPaginationObject<T> {
|
||||||
class SpotubePaginationResponseObject<T>
|
final int limit;
|
||||||
with _$SpotubePaginationResponseObject<T> {
|
final int? nextOffset;
|
||||||
factory SpotubePaginationResponseObject({
|
final int total;
|
||||||
required int limit,
|
final bool hasMore;
|
||||||
required int? nextOffset,
|
final List<T> items;
|
||||||
required int total,
|
|
||||||
required bool hasMore,
|
|
||||||
required List<T> items,
|
|
||||||
}) = _SpotubePaginationResponseObject<T>;
|
|
||||||
|
|
||||||
factory SpotubePaginationResponseObject.fromJson(
|
SpotubeFlattenedPaginationObject({
|
||||||
Map<String, Object?> json,
|
required this.limit,
|
||||||
T Function(Map<String, dynamic> json) fromJsonT,
|
required this.nextOffset,
|
||||||
) =>
|
required this.total,
|
||||||
_$SpotubePaginationResponseObjectFromJson<T>(
|
required this.hasMore,
|
||||||
json,
|
required this.items,
|
||||||
(json) => fromJsonT(json as Map<String, dynamic>),
|
});
|
||||||
|
|
||||||
|
static SpotubeFlattenedPaginationObject<T> from<T>(
|
||||||
|
SpotubePaginationResponseObject response,
|
||||||
|
T Function(SpotubePaginationResponseObjectItem item) parse,
|
||||||
|
) {
|
||||||
|
return SpotubeFlattenedPaginationObject<T>(
|
||||||
|
limit: response.limit,
|
||||||
|
nextOffset: response.nextOffset,
|
||||||
|
total: response.total,
|
||||||
|
hasMore: response.hasMore,
|
||||||
|
items: response.items.map((item) => parse(item)).toList(growable: false),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SpotubeFlattenedPaginationObject<T> copyWith({
|
||||||
|
int? limit,
|
||||||
|
int? nextOffset,
|
||||||
|
int? total,
|
||||||
|
bool? hasMore,
|
||||||
|
List<T>? items,
|
||||||
|
}) {
|
||||||
|
return SpotubeFlattenedPaginationObject<T>(
|
||||||
|
limit: limit ?? this.limit,
|
||||||
|
nextOffset: nextOffset ?? this.nextOffset,
|
||||||
|
total: total ?? this.total,
|
||||||
|
hasMore: hasMore ?? this.hasMore,
|
||||||
|
items: items ?? this.items,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SpotubePaginationResponseObjectExtension
|
||||||
|
on SpotubePaginationResponseObject {
|
||||||
|
SpotubeFlattenedPaginationObject<T> flatten<T>() {
|
||||||
|
return SpotubeFlattenedPaginationObject.from<T>(
|
||||||
|
this,
|
||||||
|
(item) => switch (T) {
|
||||||
|
SpotubeSimpleAlbumObject() =>
|
||||||
|
(item as SpotubePaginationResponseObjectItem_AlbumSimple).field0 as T,
|
||||||
|
SpotubeFullAlbumObject() =>
|
||||||
|
(item as SpotubePaginationResponseObjectItem_AlbumFull).field0 as T,
|
||||||
|
SpotubeSimpleArtistObject() =>
|
||||||
|
(item as SpotubePaginationResponseObjectItem_ArtistSimple).field0
|
||||||
|
as T,
|
||||||
|
SpotubeFullArtistObject() =>
|
||||||
|
(item as SpotubePaginationResponseObjectItem_ArtistFull).field0 as T,
|
||||||
|
SpotubeTrackObject() =>
|
||||||
|
(item as SpotubePaginationResponseObjectItem_Track).field0 as T,
|
||||||
|
SpotubeSimplePlaylistObject() =>
|
||||||
|
(item as SpotubePaginationResponseObjectItem_PlaylistSimple).field0
|
||||||
|
as T,
|
||||||
|
SpotubeFullPlaylistObject() =>
|
||||||
|
(item as SpotubePaginationResponseObjectItem_PlaylistFull).field0
|
||||||
|
as T,
|
||||||
|
SpotubeBrowseSectionObject() =>
|
||||||
|
(item as SpotubePaginationResponseObjectItem_BrowseSection).field0
|
||||||
|
as T,
|
||||||
|
_ => throw Exception("Unsupported type: $T"),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
part of 'metadata.dart';
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class SpotubeFullPlaylistObject with _$SpotubeFullPlaylistObject {
|
|
||||||
factory SpotubeFullPlaylistObject({
|
|
||||||
required String id,
|
|
||||||
required String name,
|
|
||||||
required String description,
|
|
||||||
required String externalUri,
|
|
||||||
required SpotubeUserObject owner,
|
|
||||||
@Default([]) List<SpotubeImageObject> images,
|
|
||||||
@Default([]) List<SpotubeUserObject> collaborators,
|
|
||||||
@Default(false) bool collaborative,
|
|
||||||
@Default(false) bool public,
|
|
||||||
}) = _SpotubeFullPlaylistObject;
|
|
||||||
|
|
||||||
factory SpotubeFullPlaylistObject.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SpotubeFullPlaylistObjectFromJson(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class SpotubeSimplePlaylistObject with _$SpotubeSimplePlaylistObject {
|
|
||||||
factory SpotubeSimplePlaylistObject({
|
|
||||||
required String id,
|
|
||||||
required String name,
|
|
||||||
required String description,
|
|
||||||
required String externalUri,
|
|
||||||
required SpotubeUserObject owner,
|
|
||||||
@Default([]) List<SpotubeImageObject> images,
|
|
||||||
}) = _SpotubeSimplePlaylistObject;
|
|
||||||
|
|
||||||
factory SpotubeSimplePlaylistObject.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SpotubeSimplePlaylistObjectFromJson(json);
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
part of 'metadata.dart';
|
|
||||||
|
|
||||||
enum PluginApis { webview, localstorage, timezone }
|
|
||||||
|
|
||||||
enum PluginAbilities {
|
|
||||||
authentication,
|
|
||||||
scrobbling,
|
|
||||||
metadata,
|
|
||||||
@JsonValue('audio-source')
|
|
||||||
audioSource,
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class PluginConfiguration with _$PluginConfiguration {
|
|
||||||
const PluginConfiguration._();
|
|
||||||
|
|
||||||
factory PluginConfiguration({
|
|
||||||
required String name,
|
|
||||||
required String description,
|
|
||||||
required String version,
|
|
||||||
required String author,
|
|
||||||
required String entryPoint,
|
|
||||||
required String pluginApiVersion,
|
|
||||||
@Default([]) List<PluginApis> apis,
|
|
||||||
@Default([]) List<PluginAbilities> abilities,
|
|
||||||
String? repository,
|
|
||||||
}) = _PluginConfiguration;
|
|
||||||
|
|
||||||
factory PluginConfiguration.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$PluginConfigurationFromJson(json);
|
|
||||||
|
|
||||||
String get slug => name.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]+'), '-');
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class PluginUpdateAvailable with _$PluginUpdateAvailable {
|
|
||||||
factory PluginUpdateAvailable({
|
|
||||||
required String downloadUrl,
|
|
||||||
required String version,
|
|
||||||
String? changelog,
|
|
||||||
}) = _PluginUpdateAvailable;
|
|
||||||
|
|
||||||
factory PluginUpdateAvailable.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$PluginUpdateAvailableFromJson(json);
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
part of 'metadata.dart';
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class SpotubeSearchResponseObject with _$SpotubeSearchResponseObject {
|
|
||||||
factory SpotubeSearchResponseObject({
|
|
||||||
required List<SpotubeSimpleAlbumObject> albums,
|
|
||||||
required List<SpotubeFullArtistObject> artists,
|
|
||||||
required List<SpotubeSimplePlaylistObject> playlists,
|
|
||||||
required List<SpotubeFullTrackObject> tracks,
|
|
||||||
}) = _SpotubeSearchResponseObject;
|
|
||||||
|
|
||||||
factory SpotubeSearchResponseObject.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SpotubeSearchResponseObjectFromJson(json);
|
|
||||||
}
|
|
||||||
@ -1,94 +1,37 @@
|
|||||||
part of 'metadata.dart';
|
part of 'metadata.dart';
|
||||||
|
|
||||||
@freezed
|
|
||||||
class SpotubeTrackObject with _$SpotubeTrackObject {
|
|
||||||
factory SpotubeTrackObject.local({
|
|
||||||
required String id,
|
|
||||||
required String name,
|
|
||||||
required String externalUri,
|
|
||||||
@Default([]) List<SpotubeSimpleArtistObject> artists,
|
|
||||||
required SpotubeSimpleAlbumObject album,
|
|
||||||
required int durationMs,
|
|
||||||
required String path,
|
|
||||||
}) = SpotubeLocalTrackObject;
|
|
||||||
|
|
||||||
factory SpotubeTrackObject.full({
|
|
||||||
required String id,
|
|
||||||
required String name,
|
|
||||||
required String externalUri,
|
|
||||||
@Default([]) List<SpotubeSimpleArtistObject> artists,
|
|
||||||
required SpotubeSimpleAlbumObject album,
|
|
||||||
required int durationMs,
|
|
||||||
required String isrc,
|
|
||||||
required bool explicit,
|
|
||||||
}) = SpotubeFullTrackObject;
|
|
||||||
|
|
||||||
factory SpotubeTrackObject.localTrackFromFile(
|
|
||||||
File file, {
|
|
||||||
Metadata? metadata,
|
|
||||||
String? art,
|
|
||||||
}) {
|
|
||||||
return SpotubeLocalTrackObject(
|
|
||||||
id: file.absolute.path,
|
|
||||||
name: metadata?.title ?? basenameWithoutExtension(file.path),
|
|
||||||
externalUri: "file://${file.absolute.path}",
|
|
||||||
artists: metadata?.artist?.split(",").map((a) {
|
|
||||||
return SpotubeSimpleArtistObject(
|
|
||||||
id: a.trim(),
|
|
||||||
name: a.trim(),
|
|
||||||
externalUri: "file://${file.absolute.path}",
|
|
||||||
);
|
|
||||||
}).toList() ??
|
|
||||||
[
|
|
||||||
SpotubeSimpleArtistObject(
|
|
||||||
id: "unknown",
|
|
||||||
name: "Unknown Artist",
|
|
||||||
externalUri: "file://${file.absolute.path}",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
album: SpotubeSimpleAlbumObject(
|
|
||||||
albumType: SpotubeAlbumType.album,
|
|
||||||
id: metadata?.album ?? "unknown",
|
|
||||||
name: metadata?.album ?? "Unknown Album",
|
|
||||||
externalUri: "file://${file.absolute.path}",
|
|
||||||
artists: [
|
|
||||||
SpotubeSimpleArtistObject(
|
|
||||||
id: metadata?.albumArtist ?? "unknown",
|
|
||||||
name: metadata?.albumArtist ?? "Unknown Artist",
|
|
||||||
externalUri: "file://${file.absolute.path}",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
releaseDate:
|
|
||||||
metadata?.year != null ? "${metadata!.year}-01-01" : "1970-01-01",
|
|
||||||
images: [
|
|
||||||
if (art != null)
|
|
||||||
SpotubeImageObject(
|
|
||||||
url: art,
|
|
||||||
width: 300,
|
|
||||||
height: 300,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
durationMs: metadata?.durationMs?.toInt() ?? 0,
|
|
||||||
path: file.path,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
factory SpotubeTrackObject.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SpotubeTrackObjectFromJson(
|
|
||||||
json.containsKey("path")
|
|
||||||
? {...json, "runtimeType": "local"}
|
|
||||||
: {...json, "runtimeType": "full"},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AsMediaListSpotubeTrackObject on Iterable<SpotubeTrackObject> {
|
extension AsMediaListSpotubeTrackObject on Iterable<SpotubeTrackObject> {
|
||||||
List<SpotubeMedia> asMediaList() {
|
List<SpotubeMedia> asMediaList() {
|
||||||
return map((track) => SpotubeMedia(track)).toList();
|
return map((track) => SpotubeMedia(track)).toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ToMetadataSpotubeFullTrackObject on SpotubeFullTrackObject {
|
extension FullAsPartialSpotubeTrackObject on Iterable<SpotubeFullTrackObject>? {
|
||||||
|
List<SpotubeTrackObject>? union() {
|
||||||
|
return this?.map((track) => SpotubeTrackObject.full(track)).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FullAsSpotubeTrackObject on Iterable<SpotubeFullTrackObject> {
|
||||||
|
List<SpotubeTrackObject> union() {
|
||||||
|
return map((track) => SpotubeTrackObject.full(track)).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LocalAsPartialSpotubeTrackObject
|
||||||
|
on Iterable<SpotubeLocalTrackObject>? {
|
||||||
|
List<SpotubeTrackObject>? union() {
|
||||||
|
return this?.map((track) => SpotubeTrackObject.local(track)).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LocalAsSpotubeTrackObject on Iterable<SpotubeLocalTrackObject> {
|
||||||
|
List<SpotubeTrackObject> union() {
|
||||||
|
return map((track) => SpotubeTrackObject.local(track)).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ToMetadataSpotubeFullTrackObject on SpotubeTrackObject {
|
||||||
Metadata toMetadata({
|
Metadata toMetadata({
|
||||||
required int fileLength,
|
required int fileLength,
|
||||||
Uint8List? imageBytes,
|
Uint8List? imageBytes,
|
||||||
@ -117,3 +60,91 @@ extension ToMetadataSpotubeFullTrackObject on SpotubeFullTrackObject {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension CommonTrackProperties on SpotubeTrackObject {
|
||||||
|
String get id => when(
|
||||||
|
full: (track) => track.id,
|
||||||
|
local: (track) => track.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
String get name => when(
|
||||||
|
full: (track) => track.name,
|
||||||
|
local: (track) => track.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
String get externalUri => when(
|
||||||
|
full: (track) => track.externalUri,
|
||||||
|
local: (track) => track.externalUri,
|
||||||
|
);
|
||||||
|
|
||||||
|
int get durationMs => when(
|
||||||
|
full: (track) => track.durationMs,
|
||||||
|
local: (track) => track.durationMs,
|
||||||
|
);
|
||||||
|
|
||||||
|
SpotubeSimpleAlbumObject get album => when(
|
||||||
|
full: (track) => track.album,
|
||||||
|
local: (track) => track.album,
|
||||||
|
);
|
||||||
|
List<SpotubeSimpleArtistObject> get artists => when(
|
||||||
|
full: (track) => track.artists,
|
||||||
|
local: (track) => track.artists,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SpotubeLocalTrackObject localTrackFromFile(
|
||||||
|
File file, {
|
||||||
|
Metadata? metadata,
|
||||||
|
String? art,
|
||||||
|
}) {
|
||||||
|
return SpotubeLocalTrackObject(
|
||||||
|
typeName: "track_local",
|
||||||
|
id: file.absolute.path,
|
||||||
|
name: metadata?.title ?? basenameWithoutExtension(file.path),
|
||||||
|
externalUri: "file://${file.absolute.path}",
|
||||||
|
artists: metadata?.artist?.split(",").map((a) {
|
||||||
|
return SpotubeSimpleArtistObject(
|
||||||
|
typeName: "artist_simple",
|
||||||
|
id: a.trim(),
|
||||||
|
name: a.trim(),
|
||||||
|
externalUri: "file://${file.absolute.path}",
|
||||||
|
);
|
||||||
|
}).toList() ??
|
||||||
|
[
|
||||||
|
SpotubeSimpleArtistObject(
|
||||||
|
typeName: "artist_simple",
|
||||||
|
id: "unknown",
|
||||||
|
name: "Unknown Artist",
|
||||||
|
externalUri: "file://${file.absolute.path}",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
album: SpotubeSimpleAlbumObject(
|
||||||
|
typeName: "album_simple",
|
||||||
|
albumType: SpotubeAlbumType.album,
|
||||||
|
id: metadata?.album ?? "unknown",
|
||||||
|
name: metadata?.album ?? "Unknown Album",
|
||||||
|
externalUri: "file://${file.absolute.path}",
|
||||||
|
artists: [
|
||||||
|
SpotubeSimpleArtistObject(
|
||||||
|
typeName: "artist_simple",
|
||||||
|
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",
|
||||||
|
images: [
|
||||||
|
if (art != null)
|
||||||
|
SpotubeImageObject(
|
||||||
|
typeName: "image",
|
||||||
|
url: art,
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
durationMs: metadata?.durationMs?.toInt() ?? 0,
|
||||||
|
path: file.path,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
part of 'metadata.dart';
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class SpotubeUserObject with _$SpotubeUserObject {
|
|
||||||
factory SpotubeUserObject({
|
|
||||||
required final String id,
|
|
||||||
required final String name,
|
|
||||||
@Default([]) final List<SpotubeImageObject> images,
|
|
||||||
required final String externalUri,
|
|
||||||
}) = _SpotubeUserObject;
|
|
||||||
|
|
||||||
factory SpotubeUserObject.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SpotubeUserObjectFromJson(json);
|
|
||||||
}
|
|
||||||
@ -7,7 +7,7 @@ part of 'track_sources.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
BasicSourcedTrack _$BasicSourcedTrackFromJson(Map json) => BasicSourcedTrack(
|
BasicSourcedTrack _$BasicSourcedTrackFromJson(Map json) => BasicSourcedTrack(
|
||||||
query: SpotubeFullTrackObject.fromJson(
|
query: SpotubeTrackObject.fromJson(
|
||||||
Map<String, dynamic>.from(json['query'] as Map)),
|
Map<String, dynamic>.from(json['query'] as Map)),
|
||||||
source: json['source'] as String,
|
source: json['source'] as String,
|
||||||
info: SpotubeAudioSourceMatchObject.fromJson(
|
info: SpotubeAudioSourceMatchObject.fromJson(
|
||||||
|
|||||||
@ -88,12 +88,12 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
final remotePlayback = ref.read(connectProvider.notifier);
|
final remotePlayback = ref.read(connectProvider.notifier);
|
||||||
await remotePlayback.load(
|
await remotePlayback.load(
|
||||||
WebSocketLoadEventData.album(
|
WebSocketLoadEventData.album(
|
||||||
tracks: fetchedTracks,
|
tracks: fetchedTracks.union(),
|
||||||
collection: album,
|
collection: album,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await playlistNotifier.load(fetchedTracks, autoPlay: true);
|
await playlistNotifier.load(fetchedTracks.union(), autoPlay: true);
|
||||||
playlistNotifier.addCollection(album.id);
|
playlistNotifier.addCollection(album.id);
|
||||||
historyNotifier.addAlbums([album]);
|
historyNotifier.addAlbums([album]);
|
||||||
}
|
}
|
||||||
@ -123,7 +123,9 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
final fetchedTracks = await fetchAllTrack();
|
final fetchedTracks = await fetchAllTrack();
|
||||||
|
|
||||||
if (fetchedTracks.isEmpty) return;
|
if (fetchedTracks.isEmpty) return;
|
||||||
playlistNotifier.addTracks(fetchedTracks);
|
playlistNotifier.addTracks(
|
||||||
|
fetchedTracks.union(),
|
||||||
|
);
|
||||||
playlistNotifier.addCollection(album.id);
|
playlistNotifier.addCollection(album.id);
|
||||||
historyNotifier.addAlbums([album]);
|
historyNotifier.addAlbums([album]);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
|
|||||||
@ -14,8 +14,8 @@ import 'package:spotube/provider/metadata_plugin/updater/update_checker.dart';
|
|||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
final validAbilities = {
|
final validAbilities = {
|
||||||
PluginAbilities.metadata: ("Metadata", SpotubeIcons.album),
|
PluginAbility.metadata: ("Metadata", SpotubeIcons.album),
|
||||||
PluginAbilities.audioSource: ("Audio Source", SpotubeIcons.music),
|
PluginAbility.audioSource: ("Audio Source", SpotubeIcons.music),
|
||||||
};
|
};
|
||||||
|
|
||||||
class MetadataInstalledPluginItem extends HookConsumerWidget {
|
class MetadataInstalledPluginItem extends HookConsumerWidget {
|
||||||
@ -44,9 +44,9 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
|
|||||||
final pluginsNotifier = ref.watch(metadataPluginsProvider.notifier);
|
final pluginsNotifier = ref.watch(metadataPluginsProvider.notifier);
|
||||||
|
|
||||||
final requiresAuth = (isDefaultMetadata || isDefaultAudioSource) &&
|
final requiresAuth = (isDefaultMetadata || isDefaultAudioSource) &&
|
||||||
plugin.abilities.contains(PluginAbilities.authentication);
|
plugin.abilities.contains(PluginAbility.authentication);
|
||||||
final supportsScrobbling = isDefaultMetadata &&
|
final supportsScrobbling = isDefaultMetadata &&
|
||||||
plugin.abilities.contains(PluginAbilities.scrobbling);
|
plugin.abilities.contains(PluginAbility.scrobbling);
|
||||||
|
|
||||||
final isMetadataAuthenticatedSnapshot =
|
final isMetadataAuthenticatedSnapshot =
|
||||||
ref.watch(metadataPluginAuthenticatedProvider);
|
ref.watch(metadataPluginAuthenticatedProvider);
|
||||||
@ -253,7 +253,7 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
|
|||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
children: [
|
children: [
|
||||||
if (plugin.abilities.contains(PluginAbilities.metadata))
|
if (plugin.abilities.contains(PluginAbility.metadata))
|
||||||
Button.secondary(
|
Button.secondary(
|
||||||
enabled: !isDefaultMetadata,
|
enabled: !isDefaultMetadata,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@ -265,7 +265,7 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
|
|||||||
: context.l10n.set_default_metadata_source,
|
: context.l10n.set_default_metadata_source,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (plugin.abilities.contains(PluginAbilities.audioSource))
|
if (plugin.abilities.contains(PluginAbility.audioSource))
|
||||||
Button.secondary(
|
Button.secondary(
|
||||||
enabled: !isDefaultAudioSource,
|
enabled: !isDefaultAudioSource,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@ -378,8 +378,13 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
|
|||||||
!isAuthenticated)
|
!isAuthenticated)
|
||||||
Button.primary(
|
Button.primary(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await pluginSnapshot?.asData?.value?.auth
|
if ((pluginSnapshot?.hasValue ?? false) == false) {
|
||||||
.authenticate();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await pluginSnapshot!.value!.auth.authenticate(
|
||||||
|
mpscTx: pluginSnapshot.value!.sender,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
leading: const Icon(SpotubeIcons.login),
|
leading: const Icon(SpotubeIcons.login),
|
||||||
child: Text(context.l10n.login),
|
child: Text(context.l10n.login),
|
||||||
@ -389,7 +394,13 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
|
|||||||
isAuthenticated)
|
isAuthenticated)
|
||||||
Button.destructive(
|
Button.destructive(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await pluginSnapshot?.asData?.value?.auth.logout();
|
if ((pluginSnapshot?.hasValue ?? false) == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await pluginSnapshot!.value!.auth.logout(
|
||||||
|
mpscTx: pluginSnapshot.value!.sender,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
leading: const Icon(SpotubeIcons.logout),
|
leading: const Icon(SpotubeIcons.logout),
|
||||||
child: Text(context.l10n.logout),
|
child: Text(context.l10n.logout),
|
||||||
|
|||||||
@ -123,7 +123,7 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return TrackDetailsDialog(
|
return TrackDetailsDialog(
|
||||||
track: currentActiveTrack
|
track: currentActiveTrack?.field0
|
||||||
as SpotubeFullTrackObject,
|
as SpotubeFullTrackObject,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -180,7 +180,7 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
if (isLocalTrack)
|
if (isLocalTrack)
|
||||||
Text(
|
Text(
|
||||||
currentActiveTrack.artists.asString(),
|
currentActiveTrack?.artists.asString() ?? "",
|
||||||
style: theme.typography.normal
|
style: theme.typography.normal
|
||||||
.copyWith(fontWeight: FontWeight.bold),
|
.copyWith(fontWeight: FontWeight.bold),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -38,11 +38,12 @@ 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 SpotubeLocalTrackObject;
|
final isLocalTrack =
|
||||||
|
playlist.activeTrack?.field0 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 is! SpotubeFullTrackObject) return false;
|
if (playlist.activeTrack is! SpotubeTrackObject) return false;
|
||||||
final downloadTask =
|
final downloadTask =
|
||||||
downloader.getTaskByTrackId(playlist.activeTrack!.id);
|
downloader.getTaskByTrackId(playlist.activeTrack!.id);
|
||||||
return const [
|
return const [
|
||||||
@ -172,16 +173,19 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
icon: Icon(
|
icon: Icon(
|
||||||
isDownloaded ? SpotubeIcons.done : SpotubeIcons.download,
|
isDownloaded ? SpotubeIcons.done : SpotubeIcons.download,
|
||||||
),
|
),
|
||||||
onPressed: playlist.activeTrack != null
|
onPressed: playlist.activeTrack != null && !isLocalTrack
|
||||||
? () => downloader.addToQueue(
|
? () => downloader.addToQueue(
|
||||||
playlist.activeTrack! as SpotubeFullTrackObject)
|
playlist.activeTrack?.field0
|
||||||
|
as SpotubeFullTrackObject,
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (playlist.activeTrack != null &&
|
if (playlist.activeTrack != null &&
|
||||||
!isLocalTrack &&
|
!isLocalTrack &&
|
||||||
authenticated.asData?.value == true)
|
authenticated.asData?.value == true)
|
||||||
TrackHeartButton(track: playlist.activeTrack!),
|
TrackHeartButton(
|
||||||
|
track: playlist.activeTrack?.field0 as SpotubeFullTrackObject),
|
||||||
AdaptivePopSheetList<Duration>(
|
AdaptivePopSheetList<Duration>(
|
||||||
tooltip: context.l10n.sleep_timer,
|
tooltip: context.l10n.sleep_timer,
|
||||||
offset: Offset(0, -50 * (sleepTimerEntries.values.length + 2)),
|
offset: Offset(0, -50 * (sleepTimerEntries.values.length + 2)),
|
||||||
|
|||||||
@ -26,14 +26,14 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
final activeTrack =
|
final activeTrack =
|
||||||
ref.watch(audioPlayerProvider.select((e) => e.activeTrack));
|
ref.watch(audioPlayerProvider.select((e) => e.activeTrack));
|
||||||
|
|
||||||
if (activeTrack == null || activeTrack is! SpotubeFullTrackObject) {
|
if (activeTrack is! SpotubeTrackObject_Full) {
|
||||||
return const SafeArea(child: NotFound());
|
return const SafeArea(child: NotFound());
|
||||||
}
|
}
|
||||||
|
|
||||||
return HookBuilder(builder: (context) {
|
return HookBuilder(builder: (context) {
|
||||||
final sourcedTrack = ref.watch(sourcedTrackProvider(activeTrack));
|
final sourcedTrack = ref.watch(sourcedTrackProvider(activeTrack.field0));
|
||||||
final sourcedTrackNotifier =
|
final sourcedTrackNotifier =
|
||||||
ref.watch(sourcedTrackProvider(activeTrack).notifier);
|
ref.watch(sourcedTrackProvider(activeTrack.field0).notifier);
|
||||||
|
|
||||||
final siblings = useMemoized<List<SpotubeAudioSourceMatchObject>>(
|
final siblings = useMemoized<List<SpotubeAudioSourceMatchObject>>(
|
||||||
() => !sourcedTrack.isLoading
|
() => !sourcedTrack.isLoading
|
||||||
@ -112,8 +112,10 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
width: 60,
|
width: 60,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
trailing:
|
trailing: Text(
|
||||||
Text(sourceInfo.duration.toHumanReadableString()),
|
Duration(milliseconds: sourceInfo.duration)
|
||||||
|
.toHumanReadableString(),
|
||||||
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
sourceInfo.artists.join(", "),
|
sourceInfo.artists.join(", "),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
|
|||||||
@ -98,19 +98,23 @@ 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.union(),
|
||||||
collection: playlist,
|
collection: playlist,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await playlistNotifier.load(fetchedInitialTracks, autoPlay: true);
|
await playlistNotifier.load(
|
||||||
|
fetchedInitialTracks.union(),
|
||||||
|
autoPlay: true,
|
||||||
|
);
|
||||||
playlistNotifier.addCollection(playlist.id);
|
playlistNotifier.addCollection(playlist.id);
|
||||||
historyNotifier.addPlaylists([playlist]);
|
historyNotifier.addPlaylists([playlist]);
|
||||||
|
|
||||||
final allTracks = await fetchAllTracks();
|
final allTracks = await fetchAllTracks();
|
||||||
|
|
||||||
await playlistNotifier
|
await playlistNotifier.addTracks(
|
||||||
.addTracks(allTracks.sublist(fetchedInitialTracks.length));
|
allTracks.sublist(fetchedInitialTracks.length).union(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
@ -142,7 +146,9 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (fetchedInitialTracks.isEmpty) return;
|
if (fetchedInitialTracks.isEmpty) return;
|
||||||
|
|
||||||
playlistNotifier.addTracks(fetchedInitialTracks);
|
playlistNotifier.addTracks(
|
||||||
|
fetchedInitialTracks.union(),
|
||||||
|
);
|
||||||
playlistNotifier.addCollection(playlist.id);
|
playlistNotifier.addCollection(playlist.id);
|
||||||
historyNotifier.addPlaylists([playlist]);
|
historyNotifier.addPlaylists([playlist]);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
|
|||||||
@ -7,10 +7,12 @@ 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/pages/search/search.dart';
|
import 'package:spotube/pages/search/search.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/metadata_plugin/search/all.dart';
|
import 'package:spotube/provider/metadata_plugin/search/all.dart';
|
||||||
|
import 'package:spotube/src/rust/api/plugin/models/track.dart';
|
||||||
|
|
||||||
class SearchTracksSection extends HookConsumerWidget {
|
class SearchTracksSection extends HookConsumerWidget {
|
||||||
const SearchTracksSection({
|
const SearchTracksSection({
|
||||||
@ -41,7 +43,8 @@ class SearchTracksSection extends HookConsumerWidget {
|
|||||||
if (search.isLoading)
|
if (search.isLoading)
|
||||||
const CircularProgressIndicator()
|
const CircularProgressIndicator()
|
||||||
else
|
else
|
||||||
...tracks.mapIndexed((i, track) {
|
...tracks.mapIndexed((i, mehTrack) {
|
||||||
|
final track = SpotubeTrackObject.full(mehTrack);
|
||||||
return TrackTile(
|
return TrackTile(
|
||||||
index: i,
|
index: i,
|
||||||
track: track,
|
track: track,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart' as material;
|
import 'package:flutter/material.dart' as material;
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.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/components/track_presentation/presentation_props.dart';
|
import 'package:spotube/components/track_presentation/presentation_props.dart';
|
||||||
@ -31,6 +32,10 @@ class AlbumPage extends HookConsumerWidget {
|
|||||||
ref.watch(metadataPluginSavedAlbumsProvider.notifier);
|
ref.watch(metadataPluginSavedAlbumsProvider.notifier);
|
||||||
final isSavedAlbum =
|
final isSavedAlbum =
|
||||||
ref.watch(metadataPluginIsSavedAlbumProvider(album.id));
|
ref.watch(metadataPluginIsSavedAlbumProvider(album.id));
|
||||||
|
final tracksUnion = useMemoized(
|
||||||
|
() => tracks.asData?.value.items.union() ?? [],
|
||||||
|
[tracks.asData?.value.items],
|
||||||
|
);
|
||||||
|
|
||||||
return material.RefreshIndicator.adaptive(
|
return material.RefreshIndicator.adaptive(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
@ -47,7 +52,7 @@ class AlbumPage extends HookConsumerWidget {
|
|||||||
title: album.name,
|
title: album.name,
|
||||||
description:
|
description:
|
||||||
"${context.l10n.released} • ${album.releaseDate} • ${album.artists.first.name}",
|
"${context.l10n.released} • ${album.releaseDate} • ${album.artists.first.name}",
|
||||||
tracks: tracks.asData?.value.items ?? [],
|
tracks: tracksUnion,
|
||||||
error: tracks.error,
|
error: tracks.error,
|
||||||
pagination: PaginationProps(
|
pagination: PaginationProps(
|
||||||
hasNextPage: tracks.asData?.value.hasMore ?? false,
|
hasNextPage: tracks.asData?.value.hasMore ?? false,
|
||||||
@ -56,7 +61,9 @@ class AlbumPage extends HookConsumerWidget {
|
|||||||
await tracksNotifier.fetchMore();
|
await tracksNotifier.fetchMore();
|
||||||
},
|
},
|
||||||
onFetchAll: () async {
|
onFetchAll: () async {
|
||||||
return tracksNotifier.fetchAll();
|
final res = await tracksNotifier.fetchAll();
|
||||||
|
|
||||||
|
return res.union();
|
||||||
},
|
},
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
ref.invalidate(metadataPluginAlbumTracksProvider(album.id));
|
ref.invalidate(metadataPluginAlbumTracksProvider(album.id));
|
||||||
|
|||||||
@ -44,7 +44,7 @@ class ArtistPageTopTracks extends HookConsumerWidget {
|
|||||||
List.generate(10, (index) => FakeData.track);
|
List.generate(10, (index) => FakeData.track);
|
||||||
|
|
||||||
void playPlaylist(
|
void playPlaylist(
|
||||||
List<SpotubeFullTrackObject> tracks, {
|
List<SpotubeTrackObject> tracks, {
|
||||||
SpotubeTrackObject? currentTrack,
|
SpotubeTrackObject? currentTrack,
|
||||||
}) async {
|
}) async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|||||||
@ -28,7 +28,7 @@ class HomeBrowseSectionItemsPage extends HookConsumerWidget {
|
|||||||
static const name = "home_browse_section_items";
|
static const name = "home_browse_section_items";
|
||||||
|
|
||||||
final String sectionId;
|
final String sectionId;
|
||||||
final SpotubeBrowseSectionObject<Object> section;
|
final SpotubeBrowseSectionObject section;
|
||||||
const HomeBrowseSectionItemsPage({
|
const HomeBrowseSectionItemsPage({
|
||||||
super.key,
|
super.key,
|
||||||
@PathParam("sectionId") required this.sectionId,
|
@PathParam("sectionId") required this.sectionId,
|
||||||
|
|||||||
@ -55,17 +55,17 @@ class LocalLibraryPage extends HookConsumerWidget {
|
|||||||
final playlist = ref.read(audioPlayerProvider);
|
final playlist = ref.read(audioPlayerProvider);
|
||||||
final playback = ref.read(audioPlayerProvider.notifier);
|
final playback = ref.read(audioPlayerProvider.notifier);
|
||||||
currentTrack ??= tracks.first;
|
currentTrack ??= tracks.first;
|
||||||
final isPlaylistPlaying = playlist.containsTracks(tracks);
|
final isPlaylistPlaying = playlist.containsTracks(tracks.union());
|
||||||
if (!isPlaylistPlaying) {
|
if (!isPlaylistPlaying) {
|
||||||
var indexWhere = tracks.indexWhere((s) => s.id == currentTrack?.id);
|
var indexWhere = tracks.indexWhere((s) => s.id == currentTrack?.id);
|
||||||
await playback.load(
|
await playback.load(
|
||||||
tracks,
|
tracks.union(),
|
||||||
initialIndex: indexWhere,
|
initialIndex: indexWhere,
|
||||||
autoPlay: true,
|
autoPlay: true,
|
||||||
);
|
);
|
||||||
} else if (isPlaylistPlaying &&
|
} else if (isPlaylistPlaying &&
|
||||||
currentTrack.id != playlist.activeTrack?.id) {
|
currentTrack.id != playlist.activeTrack?.id) {
|
||||||
await playback.jumpToTrack(currentTrack);
|
await playback.jumpToTrack(SpotubeTrackObject.local(currentTrack));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,12 +75,12 @@ class LocalLibraryPage extends HookConsumerWidget {
|
|||||||
) async {
|
) async {
|
||||||
final playlist = ref.read(audioPlayerProvider);
|
final playlist = ref.read(audioPlayerProvider);
|
||||||
final playback = ref.read(audioPlayerProvider.notifier);
|
final playback = ref.read(audioPlayerProvider.notifier);
|
||||||
final isPlaylistPlaying = playlist.containsTracks(tracks);
|
final isPlaylistPlaying = playlist.containsTracks(tracks.union());
|
||||||
final shuffledTracks = tracks.shuffled();
|
final shuffledTracks = tracks.shuffled();
|
||||||
if (isPlaylistPlaying) return;
|
if (isPlaylistPlaying) return;
|
||||||
|
|
||||||
await playback.load(
|
await playback.load(
|
||||||
shuffledTracks,
|
shuffledTracks.union(),
|
||||||
initialIndex: 0,
|
initialIndex: 0,
|
||||||
autoPlay: true,
|
autoPlay: true,
|
||||||
);
|
);
|
||||||
@ -93,9 +93,9 @@ class LocalLibraryPage extends HookConsumerWidget {
|
|||||||
) async {
|
) async {
|
||||||
final playlist = ref.read(audioPlayerProvider);
|
final playlist = ref.read(audioPlayerProvider);
|
||||||
final playback = ref.read(audioPlayerProvider.notifier);
|
final playback = ref.read(audioPlayerProvider.notifier);
|
||||||
final isPlaylistPlaying = playlist.containsTracks(tracks);
|
final isPlaylistPlaying = playlist.containsTracks(tracks.union());
|
||||||
if (isPlaylistPlaying) return;
|
if (isPlaylistPlaying) return;
|
||||||
await playback.addTracks(tracks);
|
await playback.addTracks(tracks.union());
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
showToastForAction(context, "add-to-queue", tracks.length);
|
showToastForAction(context, "add-to-queue", tracks.length);
|
||||||
}
|
}
|
||||||
@ -109,7 +109,7 @@ class LocalLibraryPage extends HookConsumerWidget {
|
|||||||
final trackSnapshot = ref.watch(localTracksProvider);
|
final trackSnapshot = ref.watch(localTracksProvider);
|
||||||
final isPlaylistPlaying = useMemoized(
|
final isPlaylistPlaying = useMemoized(
|
||||||
() => playlist.containsTracks(
|
() => playlist.containsTracks(
|
||||||
trackSnapshot.asData?.value[location] ?? [],
|
trackSnapshot.asData?.value[location]?.union() ?? [],
|
||||||
),
|
),
|
||||||
[playlist, trackSnapshot, location],
|
[playlist, trackSnapshot, location],
|
||||||
);
|
);
|
||||||
@ -382,7 +382,7 @@ class LocalLibraryPage extends HookConsumerWidget {
|
|||||||
data: (tracks) {
|
data: (tracks) {
|
||||||
final sortedTracks = useMemoized(() {
|
final sortedTracks = useMemoized(() {
|
||||||
return ServiceUtils.sortTracks(
|
return ServiceUtils.sortTracks(
|
||||||
tracks[location] ?? <SpotubeLocalTrackObject>[],
|
tracks[location]?.union() ?? <SpotubeTrackObject>[],
|
||||||
sortBy.value);
|
sortBy.value);
|
||||||
}, [sortBy.value, tracks]);
|
}, [sortBy.value, tracks]);
|
||||||
|
|
||||||
@ -463,8 +463,12 @@ class LocalLibraryPage extends HookConsumerWidget {
|
|||||||
onTap: () async {
|
onTap: () async {
|
||||||
await playLocalTracks(
|
await playLocalTracks(
|
||||||
ref,
|
ref,
|
||||||
sortedTracks,
|
sortedTracks
|
||||||
currentTrack: track,
|
.map((e) => e.field0
|
||||||
|
as SpotubeLocalTrackObject)
|
||||||
|
.toList(),
|
||||||
|
currentTrack: track.field0
|
||||||
|
as SpotubeLocalTrackObject,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -43,6 +43,7 @@ class UserPlaylistsPage extends HookConsumerWidget {
|
|||||||
() => me.asData?.value == null
|
() => me.asData?.value == null
|
||||||
? null
|
? null
|
||||||
: SpotubeSimplePlaylistObject(
|
: SpotubeSimplePlaylistObject(
|
||||||
|
typeName: "playlist_simple",
|
||||||
id: "user-liked-tracks",
|
id: "user-liked-tracks",
|
||||||
name: context.l10n.liked_tracks,
|
name: context.l10n.liked_tracks,
|
||||||
description: context.l10n.liked_tracks_description,
|
description: context.l10n.liked_tracks_description,
|
||||||
@ -50,6 +51,7 @@ class UserPlaylistsPage extends HookConsumerWidget {
|
|||||||
owner: me.asData!.value!,
|
owner: me.asData!.value!,
|
||||||
images: [
|
images: [
|
||||||
SpotubeImageObject(
|
SpotubeImageObject(
|
||||||
|
typeName: "image",
|
||||||
url: Assets.images.likedTracks.path,
|
url: Assets.images.likedTracks.path,
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 300,
|
height: 300,
|
||||||
|
|||||||
@ -2,10 +2,11 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
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' hide Consumer;
|
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/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/metadata/metadata.dart';
|
||||||
import 'package:spotube/modules/player/player_controls.dart';
|
import 'package:spotube/modules/player/player_controls.dart';
|
||||||
import 'package:spotube/modules/player/player_queue.dart';
|
import 'package:spotube/modules/player/player_queue.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
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:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
import 'package:spotube/components/track_presentation/presentation_props.dart';
|
import 'package:spotube/components/track_presentation/presentation_props.dart';
|
||||||
@ -25,7 +26,9 @@ class LikedPlaylistPage extends HookConsumerWidget {
|
|||||||
final likedTracks = ref.watch(metadataPluginSavedTracksProvider);
|
final likedTracks = ref.watch(metadataPluginSavedTracksProvider);
|
||||||
final likedTracksNotifier =
|
final likedTracksNotifier =
|
||||||
ref.watch(metadataPluginSavedTracksProvider.notifier);
|
ref.watch(metadataPluginSavedTracksProvider.notifier);
|
||||||
final tracks = likedTracks.asData?.value.items ?? [];
|
final tracks = useMemoized(
|
||||||
|
() => likedTracks.asData?.value.items.union() ?? [],
|
||||||
|
[likedTracks.asData?.value]);
|
||||||
|
|
||||||
return material.RefreshIndicator.adaptive(
|
return material.RefreshIndicator.adaptive(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
@ -42,7 +45,9 @@ class LikedPlaylistPage extends HookConsumerWidget {
|
|||||||
await likedTracksNotifier.fetchMore();
|
await likedTracksNotifier.fetchMore();
|
||||||
},
|
},
|
||||||
onFetchAll: () async {
|
onFetchAll: () async {
|
||||||
return await likedTracksNotifier.fetchAll();
|
final res = await likedTracksNotifier.fetchAll();
|
||||||
|
|
||||||
|
return res.union();
|
||||||
},
|
},
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
ref.invalidate(metadataPluginSavedTracksProvider);
|
ref.invalidate(metadataPluginSavedTracksProvider);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart' as material;
|
import 'package:flutter/material.dart' as material;
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart' hide Page;
|
import 'package:flutter/material.dart' hide Page;
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/components/dialogs/prompt_dialog.dart';
|
import 'package:spotube/components/dialogs/prompt_dialog.dart';
|
||||||
import 'package:spotube/components/track_presentation/presentation_props.dart';
|
import 'package:spotube/components/track_presentation/presentation_props.dart';
|
||||||
@ -50,6 +51,10 @@ class PlaylistPage extends HookConsumerWidget {
|
|||||||
ref.watch(metadataPluginSavedPlaylistsProvider.notifier);
|
ref.watch(metadataPluginSavedPlaylistsProvider.notifier);
|
||||||
|
|
||||||
final isUserPlaylist = useIsUserPlaylist(ref, playlist.id);
|
final isUserPlaylist = useIsUserPlaylist(ref, playlist.id);
|
||||||
|
final tracksMemoized = useMemoized(
|
||||||
|
() => tracks.asData?.value.items.union() ?? [],
|
||||||
|
[tracks.asData?.value],
|
||||||
|
);
|
||||||
|
|
||||||
return material.RefreshIndicator.adaptive(
|
return material.RefreshIndicator.adaptive(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
@ -71,14 +76,15 @@ class PlaylistPage extends HookConsumerWidget {
|
|||||||
ref.invalidate(metadataPluginPlaylistTracksProvider(playlist.id));
|
ref.invalidate(metadataPluginPlaylistTracksProvider(playlist.id));
|
||||||
},
|
},
|
||||||
onFetchAll: () async {
|
onFetchAll: () async {
|
||||||
return await tracksNotifier.fetchAll();
|
final res = await tracksNotifier.fetchAll();
|
||||||
|
return res.union();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
title: playlist.name,
|
title: playlist.name,
|
||||||
description: playlist.description,
|
description: playlist.description,
|
||||||
owner: playlist.owner.name,
|
owner: playlist.owner.name,
|
||||||
ownerImage: playlist.owner.images.lastOrNull?.url,
|
ownerImage: playlist.owner.images.lastOrNull?.url,
|
||||||
tracks: tracks.asData?.value.items ?? [],
|
tracks: tracksMemoized,
|
||||||
error: tracks.error,
|
error: tracks.error,
|
||||||
routePath: '/playlist/${playlist.id}',
|
routePath: '/playlist/${playlist.id}',
|
||||||
isLiked: isFavoritePlaylist.asData?.value ?? false,
|
isLiked: isFavoritePlaylist.asData?.value ?? false,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import 'package:spotube/components/fallbacks/error_box.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/modules/search/loading.dart';
|
import 'package:spotube/modules/search/loading.dart';
|
||||||
import 'package:spotube/pages/search/search.dart';
|
import 'package:spotube/pages/search/search.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
|
|||||||
@ -82,8 +82,8 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
|
|||||||
return plugins.asData?.value.plugins.where((d) {
|
return plugins.asData?.value.plugins.where((d) {
|
||||||
return d.abilities.contains(
|
return d.abilities.contains(
|
||||||
tabState.value == 1
|
tabState.value == 1
|
||||||
? PluginAbilities.metadata
|
? PluginAbility.metadata
|
||||||
: PluginAbilities.audioSource,
|
: PluginAbility.audioSource,
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
}, [tabState.value, plugins.asData?.value]);
|
}, [tabState.value, plugins.asData?.value]);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
|
|||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotube/collections/formatters.dart';
|
import 'package:spotube/collections/formatters.dart';
|
||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/modules/stats/common/track_item.dart';
|
import 'package:spotube/modules/stats/common/track_item.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
|
||||||
|
|||||||
@ -44,7 +44,8 @@ class TrackPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
final trackQuery = ref.watch(metadataPluginTrackProvider(trackId));
|
final trackQuery = ref.watch(metadataPluginTrackProvider(trackId));
|
||||||
|
|
||||||
final track = trackQuery.asData?.value ?? FakeData.track;
|
final track = SpotubeTrackObject.full(trackQuery.asData?.value ??
|
||||||
|
FakeData.track.field0 as SpotubeFullTrackObject);
|
||||||
|
|
||||||
void onPlay() async {
|
void onPlay() async {
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
@ -230,7 +231,10 @@ class TrackPage extends HookConsumerWidget {
|
|||||||
const Spacer()
|
const Spacer()
|
||||||
else
|
else
|
||||||
const Gap(20),
|
const Gap(20),
|
||||||
TrackHeartButton(track: track),
|
TrackHeartButton(
|
||||||
|
track: track.field0
|
||||||
|
as SpotubeFullTrackObject,
|
||||||
|
),
|
||||||
TrackOptionsButton(
|
TrackOptionsButton(
|
||||||
track: track,
|
track: track,
|
||||||
userPlaylist: false,
|
userPlaylist: false,
|
||||||
|
|||||||
@ -22,16 +22,16 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
|||||||
assert(
|
assert(
|
||||||
tracks.every(
|
tracks.every(
|
||||||
(track) =>
|
(track) =>
|
||||||
track is SpotubeFullTrackObject || track is SpotubeLocalTrackObject,
|
track is SpotubeTrackObject || track is SpotubeLocalTrackObject,
|
||||||
),
|
),
|
||||||
'All tracks must be either SpotubeFullTrackObject or SpotubeLocalTrackObject',
|
'All tracks must be either SpotubeTrackObject or SpotubeLocalTrackObject',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _assertAllowedTrack(SpotubeTrackObject tracks) {
|
void _assertAllowedTrack(SpotubeTrackObject tracks) {
|
||||||
assert(
|
assert(
|
||||||
tracks is SpotubeFullTrackObject || tracks is SpotubeLocalTrackObject,
|
tracks is SpotubeTrackObject || tracks is SpotubeLocalTrackObject,
|
||||||
'Track must be either SpotubeFullTrackObject or SpotubeLocalTrackObject',
|
'Track must be either SpotubeTrackObject or SpotubeLocalTrackObject',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,9 +345,12 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return a is SpotubeLocalTrackObject && b is SpotubeLocalTrackObject
|
return switch ((a.field0, b.field0)) {
|
||||||
? a.path == b.path
|
(SpotubeLocalTrackObject(), SpotubeLocalTrackObject()) =>
|
||||||
: a.id == b.id;
|
(a.field0 as SpotubeLocalTrackObject).path ==
|
||||||
|
(b.field0 as SpotubeLocalTrackObject).path,
|
||||||
|
_ => a.id == b.id,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> load(
|
Future<void> load(
|
||||||
@ -366,12 +369,10 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
|||||||
// Giving the initial track a boost so MediaKit won't skip
|
// Giving the initial track a boost so MediaKit won't skip
|
||||||
// because of timeout
|
// because of timeout
|
||||||
final intendedActiveTrack = medias.elementAt(initialIndex);
|
final intendedActiveTrack = medias.elementAt(initialIndex);
|
||||||
if (intendedActiveTrack.track is! SpotubeLocalTrackObject) {
|
if (intendedActiveTrack.track is SpotubeTrackObject_Full) {
|
||||||
ref.read(
|
ref.read(sourcedTrackProvider(
|
||||||
sourcedTrackProvider(
|
intendedActiveTrack.track.field0 as SpotubeFullTrackObject)
|
||||||
intendedActiveTrack.track as SpotubeFullTrackObject,
|
.future);
|
||||||
).future,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (medias.isEmpty) return;
|
if (medias.isEmpty) return;
|
||||||
@ -398,7 +399,7 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> swapActiveSource() async {
|
Future<void> swapActiveSource() async {
|
||||||
if (state.tracks.isEmpty || state.activeTrack is! SpotubeFullTrackObject) {
|
if (state.tracks.isEmpty || state.activeTrack is! SpotubeTrackObject) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -103,7 +103,8 @@ class AudioPlayerStreamListeners {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
scrobbler.scrobble(audioPlayerState.activeTrack!);
|
scrobbler.scrobble(
|
||||||
|
audioPlayerState.activeTrack!.field0 as SpotubeFullTrackObject);
|
||||||
ref
|
ref
|
||||||
.read(metadataPluginScrobbleProvider.notifier)
|
.read(metadataPluginScrobbleProvider.notifier)
|
||||||
.scrobble(audioPlayerState.activeTrack!);
|
.scrobble(audioPlayerState.activeTrack!);
|
||||||
@ -115,13 +116,28 @@ class AudioPlayerStreamListeners {
|
|||||||
if (activeTrack.artists.any((a) => a.images == null)) {
|
if (activeTrack.artists.any((a) => a.images == null)) {
|
||||||
final metadataPlugin = await ref.read(metadataPluginProvider.future);
|
final metadataPlugin = await ref.read(metadataPluginProvider.future);
|
||||||
final artists = await Future.wait(
|
final artists = await Future.wait(
|
||||||
activeTrack.artists
|
activeTrack.artists.map(
|
||||||
.map((artist) => metadataPlugin!.artist.getArtist(artist.id)),
|
(artist) => metadataPlugin!.artist.getArtist(
|
||||||
|
id: artist.id,
|
||||||
|
mpscTx: metadataPlugin.sender,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
activeTrack = activeTrack.copyWith(
|
activeTrack = activeTrack.when(
|
||||||
|
full: (field0) => SpotubeTrackObject.full(
|
||||||
|
field0.copyWith(
|
||||||
artists: artists
|
artists: artists
|
||||||
.map((e) => SpotubeSimpleArtistObject.fromJson(e.toJson()))
|
.map((e) => SpotubeSimpleArtistObject.fromJson(e.toJson()))
|
||||||
.toList(),
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
local: (field0) => SpotubeTrackObject.local(
|
||||||
|
field0.copyWith(
|
||||||
|
artists: artists
|
||||||
|
.map((e) => SpotubeSimpleArtistObject.fromJson(e.toJson()))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,7 +171,8 @@ class AudioPlayerStreamListeners {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await ref.read(
|
await ref.read(
|
||||||
sourcedTrackProvider(nextTrack as SpotubeFullTrackObject).future,
|
sourcedTrackProvider(nextTrack.field0 as SpotubeFullTrackObject)
|
||||||
|
.future,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
lastTrack = nextTrack.id;
|
lastTrack = nextTrack.id;
|
||||||
|
|||||||
@ -10,14 +10,14 @@ final queryingTrackInfoProvider = Provider<bool>((ref) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioPlayer.activeTrack is! SpotubeFullTrackObject) {
|
if (audioPlayer.activeTrack is! SpotubeTrackObject_Full) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ref
|
return ref
|
||||||
.watch(
|
.watch(
|
||||||
sourcedTrackProvider(
|
sourcedTrackProvider(
|
||||||
audioPlayer.activeTrack! as SpotubeFullTrackObject),
|
audioPlayer.activeTrack?.field0 as SpotubeFullTrackObject),
|
||||||
)
|
)
|
||||||
.isLoading;
|
.isLoading;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -27,9 +27,8 @@ class AudioPlayerState with _$AudioPlayerState {
|
|||||||
List<SpotubeTrackObject> tracks = const [],
|
List<SpotubeTrackObject> tracks = const [],
|
||||||
}) {
|
}) {
|
||||||
assert(
|
assert(
|
||||||
tracks.every((track) =>
|
tracks.every((track) => track is SpotubeTrackObject_Local),
|
||||||
track is SpotubeFullTrackObject || track is SpotubeLocalTrackObject),
|
'All tracks must be either SpotubeTrackObject or SpotubeLocalTrackObject',
|
||||||
'All tracks must be either SpotubeFullTrackObject or SpotubeLocalTrackObject',
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return AudioPlayerState._inner(
|
return AudioPlayerState._inner(
|
||||||
@ -53,10 +52,12 @@ class AudioPlayerState with _$AudioPlayerState {
|
|||||||
bool containsTrack(SpotubeTrackObject track) {
|
bool containsTrack(SpotubeTrackObject track) {
|
||||||
return tracks.isNotEmpty &&
|
return tracks.isNotEmpty &&
|
||||||
tracks.any(
|
tracks.any(
|
||||||
(t) =>
|
(t) => switch ((t.field0, track.field0)) {
|
||||||
t is SpotubeLocalTrackObject && track is SpotubeLocalTrackObject
|
(SpotubeLocalTrackObject(), SpotubeLocalTrackObject()) =>
|
||||||
? t.path == track.path
|
(t.field0 as SpotubeLocalTrackObject).path ==
|
||||||
: t.id == track.id,
|
(track.field0 as SpotubeLocalTrackObject).path,
|
||||||
|
_ => t.id == track.id,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -208,7 +208,7 @@ class ConnectNotifier extends AsyncNotifier<ConnectState?> {
|
|||||||
emit(WebSocketLoopEvent(value));
|
emit(WebSocketLoopEvent(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addTrack(SpotubeFullTrackObject data) async {
|
Future<void> addTrack(SpotubeTrackObject data) async {
|
||||||
emit(WebSocketAddTrackEvent(data));
|
emit(WebSocketAddTrackEvent(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -249,7 +249,7 @@ class DownloadManagerNotifier extends Notifier<List<DownloadTask>> {
|
|||||||
);
|
);
|
||||||
await MetadataGod.writeMetadata(
|
await MetadataGod.writeMetadata(
|
||||||
file: savePath,
|
file: savePath,
|
||||||
metadata: task.track.toMetadata(
|
metadata: SpotubeTrackObject.full(task.track).toMetadata(
|
||||||
fileLength: await savePathFile.length(),
|
fileLength: await savePathFile.length(),
|
||||||
imageBytes: imageBytes,
|
imageBytes: imageBytes,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -69,7 +69,7 @@ class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier<
|
|||||||
|
|
||||||
final items = getAlbumsWithCount(await albumsQuery.get());
|
final items = getAlbumsWithCount(await albumsQuery.get());
|
||||||
|
|
||||||
return SpotubePaginationResponseObject(
|
return SpotubeFlattenedPaginationObject(
|
||||||
items: items,
|
items: items,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
hasMore: items.length == limit,
|
hasMore: items.length == limit,
|
||||||
@ -110,7 +110,7 @@ class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier<
|
|||||||
|
|
||||||
final historyTopAlbumsProvider = AsyncNotifierProviderFamily<
|
final historyTopAlbumsProvider = AsyncNotifierProviderFamily<
|
||||||
HistoryTopAlbumsNotifier,
|
HistoryTopAlbumsNotifier,
|
||||||
SpotubePaginationResponseObject<PlaybackHistoryAlbum>,
|
SpotubeFlattenedPaginationObject<PlaybackHistoryAlbum>,
|
||||||
HistoryDuration>(
|
HistoryDuration>(
|
||||||
() => HistoryTopAlbumsNotifier(),
|
() => HistoryTopAlbumsNotifier(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -36,7 +36,7 @@ class HistoryTopPlaylistsNotifier extends FamilyPaginatedAsyncNotifier<
|
|||||||
|
|
||||||
final items = getPlaylistsWithCount(await playlistsQuery.get());
|
final items = getPlaylistsWithCount(await playlistsQuery.get());
|
||||||
|
|
||||||
return SpotubePaginationResponseObject(
|
return SpotubeFlattenedPaginationObject(
|
||||||
items: items,
|
items: items,
|
||||||
nextOffset: offset + limit,
|
nextOffset: offset + limit,
|
||||||
total: items.length,
|
total: items.length,
|
||||||
@ -80,7 +80,7 @@ class HistoryTopPlaylistsNotifier extends FamilyPaginatedAsyncNotifier<
|
|||||||
|
|
||||||
final historyTopPlaylistsProvider = AsyncNotifierProviderFamily<
|
final historyTopPlaylistsProvider = AsyncNotifierProviderFamily<
|
||||||
HistoryTopPlaylistsNotifier,
|
HistoryTopPlaylistsNotifier,
|
||||||
SpotubePaginationResponseObject<PlaybackHistoryPlaylist>,
|
SpotubeFlattenedPaginationObject<PlaybackHistoryPlaylist>,
|
||||||
HistoryDuration>(
|
HistoryDuration>(
|
||||||
() => HistoryTopPlaylistsNotifier(),
|
() => HistoryTopPlaylistsNotifier(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -81,9 +81,12 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier<
|
|||||||
.nonNulls
|
.nonNulls
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
track = track.copyWith(artists: includedArtists);
|
final updatedTrack = track.when(
|
||||||
|
full: (field0) => field0.copyWith(artists: includedArtists).toJson(),
|
||||||
|
local: (field0) => field0.copyWith(artists: includedArtists).toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
return e.copyWith(data: track.toJson());
|
return e.copyWith(data: updatedTrack);
|
||||||
});
|
});
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
@ -109,7 +112,7 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier<
|
|||||||
|
|
||||||
final items = getTracksWithCount(entries);
|
final items = getTracksWithCount(entries);
|
||||||
|
|
||||||
return SpotubePaginationResponseObject<PlaybackHistoryTrack>(
|
return SpotubeFlattenedPaginationObject<PlaybackHistoryTrack>(
|
||||||
items: items,
|
items: items,
|
||||||
nextOffset: offset + limit,
|
nextOffset: offset + limit,
|
||||||
total: items.length,
|
total: items.length,
|
||||||
@ -190,7 +193,7 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier<
|
|||||||
|
|
||||||
final historyTopTracksProvider = AsyncNotifierProviderFamily<
|
final historyTopTracksProvider = AsyncNotifierProviderFamily<
|
||||||
HistoryTopTracksNotifier,
|
HistoryTopTracksNotifier,
|
||||||
SpotubePaginationResponseObject<PlaybackHistoryTrack>,
|
SpotubeFlattenedPaginationObject<PlaybackHistoryTrack>,
|
||||||
HistoryDuration>(
|
HistoryDuration>(
|
||||||
() => HistoryTopTracksNotifier(),
|
() => HistoryTopTracksNotifier(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -130,11 +130,11 @@ final localTracksProvider =
|
|||||||
|
|
||||||
final tracksFromMetadata = filesWithMetadata
|
final tracksFromMetadata = filesWithMetadata
|
||||||
.map(
|
.map(
|
||||||
(fileWithMetadata) => SpotubeTrackObject.localTrackFromFile(
|
(fileWithMetadata) => localTrackFromFile(
|
||||||
fileWithMetadata.file,
|
fileWithMetadata.file,
|
||||||
metadata: fileWithMetadata.metadata,
|
metadata: fileWithMetadata.metadata,
|
||||||
art: fileWithMetadata.art,
|
art: fileWithMetadata.art,
|
||||||
) as SpotubeLocalTrackObject,
|
),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,6 @@ final metadataPluginAlbumProvider =
|
|||||||
throw MetadataPluginException.noDefaultMetadataPlugin();
|
throw MetadataPluginException.noDefaultMetadataPlugin();
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadataPlugin.album.getAlbum(id);
|
return metadataPlugin.album.getAlbum(id: id, mpscTx: metadataPlugin.sender);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,13 +6,14 @@ import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
|
|||||||
class MetadataPluginAlbumReleasesNotifier
|
class MetadataPluginAlbumReleasesNotifier
|
||||||
extends PaginatedAsyncNotifier<SpotubeSimpleAlbumObject> {
|
extends PaginatedAsyncNotifier<SpotubeSimpleAlbumObject> {
|
||||||
@override
|
@override
|
||||||
Future<SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>> fetch(
|
Future<SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>> fetch(
|
||||||
int offset,
|
int offset,
|
||||||
int limit,
|
int limit,
|
||||||
) async {
|
) async {
|
||||||
return await (await metadataPlugin)
|
return await (await metadataPlugin)
|
||||||
.album
|
.album
|
||||||
.releases(limit: limit, offset: offset);
|
.releases(mpscTx: await mpscTx, limit: limit, offset: offset)
|
||||||
|
.then((a) => a.flatten());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -24,6 +25,6 @@ class MetadataPluginAlbumReleasesNotifier
|
|||||||
|
|
||||||
final metadataPluginAlbumReleasesProvider = AsyncNotifierProvider<
|
final metadataPluginAlbumReleasesProvider = AsyncNotifierProvider<
|
||||||
MetadataPluginAlbumReleasesNotifier,
|
MetadataPluginAlbumReleasesNotifier,
|
||||||
SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>>(
|
SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>>(
|
||||||
() => MetadataPluginAlbumReleasesNotifier(),
|
() => MetadataPluginAlbumReleasesNotifier(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,15 +6,19 @@ import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart';
|
|||||||
class MetadataPluginArtistAlbumNotifier
|
class MetadataPluginArtistAlbumNotifier
|
||||||
extends FamilyPaginatedAsyncNotifier<SpotubeSimpleAlbumObject, String> {
|
extends FamilyPaginatedAsyncNotifier<SpotubeSimpleAlbumObject, String> {
|
||||||
@override
|
@override
|
||||||
Future<SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>> fetch(
|
Future<SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>> fetch(
|
||||||
int offset,
|
int offset,
|
||||||
int limit,
|
int limit,
|
||||||
) async {
|
) async {
|
||||||
return await (await metadataPlugin).artist.albums(
|
return await (await metadataPlugin)
|
||||||
arg,
|
.artist
|
||||||
|
.albums(
|
||||||
|
mpscTx: await mpscTx,
|
||||||
|
id: arg,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
);
|
)
|
||||||
|
.then((a) => a.flatten());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -26,7 +30,7 @@ class MetadataPluginArtistAlbumNotifier
|
|||||||
|
|
||||||
final metadataPluginArtistAlbumsProvider = AsyncNotifierProviderFamily<
|
final metadataPluginArtistAlbumsProvider = AsyncNotifierProviderFamily<
|
||||||
MetadataPluginArtistAlbumNotifier,
|
MetadataPluginArtistAlbumNotifier,
|
||||||
SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>,
|
SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>,
|
||||||
String>(
|
String>(
|
||||||
() => MetadataPluginArtistAlbumNotifier(),
|
() => MetadataPluginArtistAlbumNotifier(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -15,6 +15,7 @@ final metadataPluginArtistProvider =
|
|||||||
throw MetadataPluginException.noDefaultMetadataPlugin();
|
throw MetadataPluginException.noDefaultMetadataPlugin();
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadataPlugin.artist.getArtist(artistId);
|
return metadataPlugin.artist
|
||||||
|
.getArtist(id: artistId, mpscTx: metadataPlugin.sender);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,15 +6,19 @@ import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart';
|
|||||||
class MetadataPluginArtistRelatedArtistsNotifier
|
class MetadataPluginArtistRelatedArtistsNotifier
|
||||||
extends FamilyPaginatedAsyncNotifier<SpotubeFullArtistObject, String> {
|
extends FamilyPaginatedAsyncNotifier<SpotubeFullArtistObject, String> {
|
||||||
@override
|
@override
|
||||||
Future<SpotubePaginationResponseObject<SpotubeFullArtistObject>> fetch(
|
Future<SpotubeFlattenedPaginationObject<SpotubeFullArtistObject>> fetch(
|
||||||
int offset,
|
int offset,
|
||||||
int limit,
|
int limit,
|
||||||
) async {
|
) async {
|
||||||
return await (await metadataPlugin).artist.related(
|
return await (await metadataPlugin)
|
||||||
arg,
|
.artist
|
||||||
|
.related(
|
||||||
|
id: arg,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
);
|
mpscTx: await mpscTx,
|
||||||
|
)
|
||||||
|
.then((a) => a.flatten());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -26,7 +30,7 @@ class MetadataPluginArtistRelatedArtistsNotifier
|
|||||||
|
|
||||||
final metadataPluginArtistRelatedArtistsProvider = AsyncNotifierProviderFamily<
|
final metadataPluginArtistRelatedArtistsProvider = AsyncNotifierProviderFamily<
|
||||||
MetadataPluginArtistRelatedArtistsNotifier,
|
MetadataPluginArtistRelatedArtistsNotifier,
|
||||||
SpotubePaginationResponseObject<SpotubeFullArtistObject>,
|
SpotubeFlattenedPaginationObject<SpotubeFullArtistObject>,
|
||||||
String>(
|
String>(
|
||||||
() => MetadataPluginArtistRelatedArtistsNotifier(),
|
() => MetadataPluginArtistRelatedArtistsNotifier(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,19 +5,20 @@ import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart';
|
|||||||
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||||
|
|
||||||
class MetadataPluginArtistTopTracksNotifier
|
class MetadataPluginArtistTopTracksNotifier
|
||||||
extends AutoDisposeFamilyPaginatedAsyncNotifier<SpotubeFullTrackObject,
|
extends AutoDisposeFamilyPaginatedAsyncNotifier<SpotubeTrackObject,
|
||||||
String> {
|
String> {
|
||||||
MetadataPluginArtistTopTracksNotifier() : super();
|
MetadataPluginArtistTopTracksNotifier() : super();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
fetch(offset, limit) async {
|
fetch(offset, limit) async {
|
||||||
final tracks = await (await metadataPlugin).artist.topTracks(
|
final tracks = await (await metadataPlugin).artist.topTracks(
|
||||||
arg,
|
id: arg,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
|
mpscTx: await mpscTx,
|
||||||
);
|
);
|
||||||
|
|
||||||
return tracks;
|
return tracks.flatten();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -32,7 +33,7 @@ class MetadataPluginArtistTopTracksNotifier
|
|||||||
final metadataPluginArtistTopTracksProvider =
|
final metadataPluginArtistTopTracksProvider =
|
||||||
AutoDisposeAsyncNotifierProviderFamily<
|
AutoDisposeAsyncNotifierProviderFamily<
|
||||||
MetadataPluginArtistTopTracksNotifier,
|
MetadataPluginArtistTopTracksNotifier,
|
||||||
SpotubePaginationResponseObject<SpotubeFullTrackObject>,
|
SpotubeFlattenedPaginationObject<SpotubeTrackObject>,
|
||||||
String>(
|
String>(
|
||||||
() => MetadataPluginArtistTopTracksNotifier(),
|
() => MetadataPluginArtistTopTracksNotifier(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -41,10 +41,10 @@ class AudioSourceAvailableQualityPresetsNotifier
|
|||||||
listenSelf((previous, next) {
|
listenSelf((previous, next) {
|
||||||
final isNewLossless =
|
final isNewLossless =
|
||||||
next.presets.elementAtOrNull(next.selectedStreamingContainerIndex)
|
next.presets.elementAtOrNull(next.selectedStreamingContainerIndex)
|
||||||
is SpotubeAudioSourceContainerPresetLossless;
|
is SpotubeAudioSourceContainerPreset_Lossless;
|
||||||
final isOldLossless = previous?.presets
|
final isOldLossless = previous?.presets
|
||||||
.elementAtOrNull(previous.selectedStreamingContainerIndex)
|
.elementAtOrNull(previous.selectedStreamingContainerIndex)
|
||||||
is SpotubeAudioSourceContainerPresetLossless;
|
is SpotubeAudioSourceContainerPreset_Lossless;
|
||||||
if (!isOldLossless && isNewLossless) {
|
if (!isOldLossless && isNewLossless) {
|
||||||
audioPlayer.setDemuxerBufferSize(6 * 1024 * 1024); // 6MB
|
audioPlayer.setDemuxerBufferSize(6 * 1024 * 1024); // 6MB
|
||||||
} else if (isOldLossless && !isNewLossless) {
|
} else if (isOldLossless && !isNewLossless) {
|
||||||
@ -72,11 +72,13 @@ class AudioSourceAvailableQualityPresetsNotifier
|
|||||||
state =
|
state =
|
||||||
AudioSourcePresetsState.fromJson(jsonDecode(persistedStateStr))
|
AudioSourcePresetsState.fromJson(jsonDecode(persistedStateStr))
|
||||||
.copyWith(
|
.copyWith(
|
||||||
presets: audioSource.audioSource.supportedPresets,
|
presets: await audioSource.audioSource
|
||||||
|
.supportedPresets(mpscTx: audioSource.sender),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
state = AudioSourcePresetsState(
|
state = AudioSourcePresetsState(
|
||||||
presets: audioSource.audioSource.supportedPresets,
|
presets: await audioSource.audioSource
|
||||||
|
.supportedPresets(mpscTx: audioSource.sender),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,15 +6,19 @@ import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart';
|
|||||||
class MetadataPluginBrowseSectionItemsNotifier
|
class MetadataPluginBrowseSectionItemsNotifier
|
||||||
extends FamilyPaginatedAsyncNotifier<Object, String> {
|
extends FamilyPaginatedAsyncNotifier<Object, String> {
|
||||||
@override
|
@override
|
||||||
Future<SpotubePaginationResponseObject<Object>> fetch(
|
Future<SpotubeFlattenedPaginationObject<Object>> fetch(
|
||||||
int offset,
|
int offset,
|
||||||
int limit,
|
int limit,
|
||||||
) async {
|
) async {
|
||||||
return await (await metadataPlugin).browse.sectionItems(
|
return await (await metadataPlugin)
|
||||||
arg,
|
.browse
|
||||||
|
.sectionItems(
|
||||||
|
id: arg,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
);
|
mpscTx: await mpscTx,
|
||||||
|
)
|
||||||
|
.then((value) => value.flatten());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -26,7 +30,7 @@ class MetadataPluginBrowseSectionItemsNotifier
|
|||||||
|
|
||||||
final metadataPluginBrowseSectionItemsProvider = AsyncNotifierProviderFamily<
|
final metadataPluginBrowseSectionItemsProvider = AsyncNotifierProviderFamily<
|
||||||
MetadataPluginBrowseSectionItemsNotifier,
|
MetadataPluginBrowseSectionItemsNotifier,
|
||||||
SpotubePaginationResponseObject<Object>,
|
SpotubeFlattenedPaginationObject<Object>,
|
||||||
String>(
|
String>(
|
||||||
() => MetadataPluginBrowseSectionItemsNotifier(),
|
() => MetadataPluginBrowseSectionItemsNotifier(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,17 +4,20 @@ import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
|||||||
import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
|
import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
|
||||||
|
|
||||||
class MetadataPluginBrowseSectionsNotifier
|
class MetadataPluginBrowseSectionsNotifier
|
||||||
extends PaginatedAsyncNotifier<SpotubeBrowseSectionObject<Object>> {
|
extends PaginatedAsyncNotifier<SpotubeBrowseSectionObject> {
|
||||||
@override
|
@override
|
||||||
Future<SpotubePaginationResponseObject<SpotubeBrowseSectionObject<Object>>>
|
Future<SpotubeFlattenedPaginationObject<SpotubeBrowseSectionObject>> fetch(
|
||||||
fetch(
|
|
||||||
int offset,
|
int offset,
|
||||||
int limit,
|
int limit,
|
||||||
) async {
|
) async {
|
||||||
return await (await metadataPlugin).browse.sections(
|
return await (await metadataPlugin)
|
||||||
|
.browse
|
||||||
|
.sections(
|
||||||
limit: limit,
|
limit: limit,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
);
|
mpscTx: await mpscTx,
|
||||||
|
)
|
||||||
|
.then((value) => value.flatten());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -26,6 +29,6 @@ class MetadataPluginBrowseSectionsNotifier
|
|||||||
|
|
||||||
final metadataPluginBrowseSectionsProvider = AsyncNotifierProvider<
|
final metadataPluginBrowseSectionsProvider = AsyncNotifierProvider<
|
||||||
MetadataPluginBrowseSectionsNotifier,
|
MetadataPluginBrowseSectionsNotifier,
|
||||||
SpotubePaginationResponseObject<SpotubeBrowseSectionObject<Object>>>(
|
SpotubeFlattenedPaginationObject<SpotubeBrowseSectionObject>>(
|
||||||
() => MetadataPluginBrowseSectionsNotifier(),
|
() => MetadataPluginBrowseSectionsNotifier(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -9,7 +9,7 @@ class MetadataPluginAuthenticatedNotifier extends AsyncNotifier<bool> {
|
|||||||
FutureOr<bool> build() async {
|
FutureOr<bool> build() async {
|
||||||
final defaultPluginConfig = ref.watch(metadataPluginsProvider);
|
final defaultPluginConfig = ref.watch(metadataPluginsProvider);
|
||||||
if (defaultPluginConfig.asData?.value.defaultMetadataPluginConfig?.abilities
|
if (defaultPluginConfig.asData?.value.defaultMetadataPluginConfig?.abilities
|
||||||
.contains(PluginAbilities.authentication) !=
|
.contains(PluginAbility.authentication) !=
|
||||||
true) {
|
true) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -19,15 +19,16 @@ class MetadataPluginAuthenticatedNotifier extends AsyncNotifier<bool> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final sub = defaultPlugin.auth.authStateStream.listen((event) {
|
final sub = defaultPlugin.authState().listen((event) async {
|
||||||
state = AsyncData(defaultPlugin.auth.isAuthenticated());
|
state = AsyncData(await defaultPlugin.auth
|
||||||
|
.isAuthenticated(mpscTx: defaultPlugin.sender));
|
||||||
});
|
});
|
||||||
|
|
||||||
ref.onDispose(() {
|
ref.onDispose(() {
|
||||||
sub.cancel();
|
sub.cancel();
|
||||||
});
|
});
|
||||||
|
|
||||||
return defaultPlugin.auth.isAuthenticated();
|
return defaultPlugin.auth.isAuthenticated(mpscTx: defaultPlugin.sender);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,7 +43,7 @@ class AudioSourcePluginAuthenticatedNotifier extends AsyncNotifier<bool> {
|
|||||||
final defaultPluginConfig = ref.watch(metadataPluginsProvider);
|
final defaultPluginConfig = ref.watch(metadataPluginsProvider);
|
||||||
if (defaultPluginConfig
|
if (defaultPluginConfig
|
||||||
.asData?.value.defaultAudioSourcePluginConfig?.abilities
|
.asData?.value.defaultAudioSourcePluginConfig?.abilities
|
||||||
.contains(PluginAbilities.authentication) !=
|
.contains(PluginAbility.authentication) !=
|
||||||
true) {
|
true) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -52,15 +53,16 @@ class AudioSourcePluginAuthenticatedNotifier extends AsyncNotifier<bool> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final sub = defaultPlugin.auth.authStateStream.listen((event) {
|
final sub = defaultPlugin.authState().listen((event) async {
|
||||||
state = AsyncData(defaultPlugin.auth.isAuthenticated());
|
state = AsyncData(await defaultPlugin.auth
|
||||||
|
.isAuthenticated(mpscTx: defaultPlugin.sender));
|
||||||
});
|
});
|
||||||
|
|
||||||
ref.onDispose(() {
|
ref.onDispose(() {
|
||||||
sub.cancel();
|
sub.cancel();
|
||||||
});
|
});
|
||||||
|
|
||||||
return defaultPlugin.auth.isAuthenticated();
|
return defaultPlugin.auth.isAuthenticated(mpscTx: defaultPlugin.sender);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -62,7 +62,7 @@ class MetadataPluginRepositoriesNotifier
|
|||||||
return _hasMore[response.requestOptions.uri.host] ?? false;
|
return _hasMore[response.requestOptions.uri.host] ?? false;
|
||||||
});
|
});
|
||||||
|
|
||||||
return SpotubePaginationResponseObject(
|
return SpotubeFlattenedPaginationObject(
|
||||||
items: repos,
|
items: repos,
|
||||||
total: responses.fold<int>(
|
total: responses.fold<int>(
|
||||||
0,
|
0,
|
||||||
@ -85,6 +85,6 @@ class MetadataPluginRepositoriesNotifier
|
|||||||
|
|
||||||
final metadataPluginRepositoriesProvider = AsyncNotifierProvider<
|
final metadataPluginRepositoriesProvider = AsyncNotifierProvider<
|
||||||
MetadataPluginRepositoriesNotifier,
|
MetadataPluginRepositoriesNotifier,
|
||||||
SpotubePaginationResponseObject<MetadataPluginRepository>>(
|
SpotubeFlattenedPaginationObject<MetadataPluginRepository>>(
|
||||||
() => MetadataPluginRepositoriesNotifier(),
|
() => MetadataPluginRepositoriesNotifier(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -17,7 +17,7 @@ class MetadataPluginScrobbleNotifier
|
|||||||
|
|
||||||
if (metadataPlugin.valueOrNull == null ||
|
if (metadataPlugin.valueOrNull == null ||
|
||||||
pluginConfig == null ||
|
pluginConfig == null ||
|
||||||
!pluginConfig.abilities.contains(PluginAbilities.scrobbling)) {
|
!pluginConfig.abilities.contains(PluginAbility.scrobbling)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,23 +25,46 @@ class MetadataPluginScrobbleNotifier
|
|||||||
|
|
||||||
final subscription = controller.stream.listen((event) async {
|
final subscription = controller.stream.listen((event) async {
|
||||||
try {
|
try {
|
||||||
await metadataPlugin.valueOrNull?.core.scrobble({
|
final details = switch (event) {
|
||||||
"id": event.id,
|
SpotubeTrackObject_Full(:final field0) => ScrobbleDetails(
|
||||||
"title": event.name,
|
id: field0.id,
|
||||||
"artists": event.artists
|
title: field0.name,
|
||||||
.map((artist) => {
|
artists: field0.artists
|
||||||
"id": artist.id,
|
.map((artist) => ScrobbleArtist(
|
||||||
"name": artist.name,
|
id: artist.id,
|
||||||
})
|
name: artist.name,
|
||||||
|
))
|
||||||
.toList(),
|
.toList(),
|
||||||
"album": {
|
album: ScrobbleAlbum(
|
||||||
"id": event.album.id,
|
id: field0.album.id,
|
||||||
"name": event.album.name,
|
name: field0.album.name,
|
||||||
},
|
),
|
||||||
"timestamp": DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||||
"duration_ms": event.durationMs,
|
durationMs: field0.durationMs,
|
||||||
"isrc": event is SpotubeFullTrackObject ? event.isrc : null,
|
isrc: field0.isrc,
|
||||||
});
|
),
|
||||||
|
SpotubeTrackObject_Local(:final field0) => ScrobbleDetails(
|
||||||
|
id: field0.id,
|
||||||
|
title: field0.name,
|
||||||
|
artists: field0.artists
|
||||||
|
.map((artist) => ScrobbleArtist(
|
||||||
|
id: artist.id,
|
||||||
|
name: artist.name,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
album: ScrobbleAlbum(
|
||||||
|
id: field0.album.id,
|
||||||
|
name: field0.album.name,
|
||||||
|
),
|
||||||
|
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||||
|
durationMs: field0.durationMs,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
await metadataPlugin.valueOrNull?.core.scrobble(
|
||||||
|
mpscTx: metadataPlugin.valueOrNull!.sender,
|
||||||
|
details: details,
|
||||||
|
);
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
AppLogger.reportError(e, stack);
|
AppLogger.reportError(e, stack);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ final metadataPluginSupportTextProvider = FutureProvider<String>((ref) async {
|
|||||||
if (metadataPlugin == null) {
|
if (metadataPlugin == null) {
|
||||||
throw 'No metadata plugin available';
|
throw 'No metadata plugin available';
|
||||||
}
|
}
|
||||||
return await metadataPlugin.core.support;
|
return await metadataPlugin.core.support(mpscTx: metadataPlugin.sender);
|
||||||
});
|
});
|
||||||
|
|
||||||
final audioSourcePluginSupportTextProvider =
|
final audioSourcePluginSupportTextProvider =
|
||||||
@ -17,5 +17,5 @@ final audioSourcePluginSupportTextProvider =
|
|||||||
if (audioSourcePlugin == null) {
|
if (audioSourcePlugin == null) {
|
||||||
throw 'No metadata plugin available';
|
throw 'No metadata plugin available';
|
||||||
}
|
}
|
||||||
return await audioSourcePlugin.core.support;
|
return await audioSourcePlugin.core.support(mpscTx: audioSourcePlugin.sender);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -12,6 +12,6 @@ final metadataPluginUserProvider = FutureProvider<SpotubeUserObject?>(
|
|||||||
if (!authenticated || metadataPlugin == null) {
|
if (!authenticated || metadataPlugin == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return metadataPlugin.user.me();
|
return metadataPlugin.user.me(mpscTx: metadataPlugin.sender);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,14 +6,18 @@ import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
|
|||||||
class MetadataPluginSavedAlbumNotifier
|
class MetadataPluginSavedAlbumNotifier
|
||||||
extends PaginatedAsyncNotifier<SpotubeSimpleAlbumObject> {
|
extends PaginatedAsyncNotifier<SpotubeSimpleAlbumObject> {
|
||||||
@override
|
@override
|
||||||
Future<SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>> fetch(
|
Future<SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>> fetch(
|
||||||
int offset,
|
int offset,
|
||||||
int limit,
|
int limit,
|
||||||
) async {
|
) async {
|
||||||
return await (await metadataPlugin).user.savedAlbums(
|
return await (await metadataPlugin)
|
||||||
|
.user
|
||||||
|
.savedAlbums(
|
||||||
limit: limit,
|
limit: limit,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
);
|
mpscTx: await mpscTx,
|
||||||
|
)
|
||||||
|
.then((a) => a.flatten());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -35,7 +39,10 @@ class MetadataPluginSavedAlbumNotifier
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await (await metadataPlugin).album.save(albums.map((e) => e.id).toList());
|
await (await metadataPlugin).album.save(
|
||||||
|
ids: albums.map((e) => e.id).toList(),
|
||||||
|
mpscTx: await mpscTx,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
state = AsyncData(oldState!);
|
state = AsyncData(oldState!);
|
||||||
rethrow;
|
rethrow;
|
||||||
@ -58,7 +65,9 @@ class MetadataPluginSavedAlbumNotifier
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await (await metadataPlugin).album.unsave(albumIds);
|
await (await metadataPlugin)
|
||||||
|
.album
|
||||||
|
.unsave(ids: albumIds, mpscTx: await mpscTx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
state = AsyncData(oldState!);
|
state = AsyncData(oldState!);
|
||||||
rethrow;
|
rethrow;
|
||||||
@ -68,7 +77,7 @@ class MetadataPluginSavedAlbumNotifier
|
|||||||
|
|
||||||
final metadataPluginSavedAlbumsProvider = AsyncNotifierProvider<
|
final metadataPluginSavedAlbumsProvider = AsyncNotifierProvider<
|
||||||
MetadataPluginSavedAlbumNotifier,
|
MetadataPluginSavedAlbumNotifier,
|
||||||
SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>>(
|
SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>>(
|
||||||
() => MetadataPluginSavedAlbumNotifier(),
|
() => MetadataPluginSavedAlbumNotifier(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -6,16 +6,17 @@ import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
|
|||||||
class MetadataPluginSavedArtistNotifier
|
class MetadataPluginSavedArtistNotifier
|
||||||
extends PaginatedAsyncNotifier<SpotubeFullArtistObject> {
|
extends PaginatedAsyncNotifier<SpotubeFullArtistObject> {
|
||||||
@override
|
@override
|
||||||
Future<SpotubePaginationResponseObject<SpotubeFullArtistObject>> fetch(
|
Future<SpotubeFlattenedPaginationObject<SpotubeFullArtistObject>> fetch(
|
||||||
int offset,
|
int offset,
|
||||||
int limit,
|
int limit,
|
||||||
) async {
|
) async {
|
||||||
final artists = await (await metadataPlugin).user.savedArtists(
|
final artists = await (await metadataPlugin).user.savedArtists(
|
||||||
limit: limit,
|
limit: limit,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
|
mpscTx: await mpscTx,
|
||||||
);
|
);
|
||||||
|
|
||||||
return artists;
|
return artists.flatten();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -39,7 +40,7 @@ class MetadataPluginSavedArtistNotifier
|
|||||||
try {
|
try {
|
||||||
await (await metadataPlugin)
|
await (await metadataPlugin)
|
||||||
.artist
|
.artist
|
||||||
.save(artists.map((e) => e.id).toList());
|
.save(ids: artists.map((e) => e.id).toList(), mpscTx: await mpscTx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
state = AsyncData(oldState!);
|
state = AsyncData(oldState!);
|
||||||
rethrow;
|
rethrow;
|
||||||
@ -63,7 +64,9 @@ class MetadataPluginSavedArtistNotifier
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await (await metadataPlugin).artist.unsave(artistIds);
|
await (await metadataPlugin)
|
||||||
|
.artist
|
||||||
|
.unsave(ids: artistIds, mpscTx: await mpscTx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
state = AsyncData(oldState!);
|
state = AsyncData(oldState!);
|
||||||
rethrow;
|
rethrow;
|
||||||
@ -73,7 +76,7 @@ class MetadataPluginSavedArtistNotifier
|
|||||||
|
|
||||||
final metadataPluginSavedArtistsProvider = AsyncNotifierProvider<
|
final metadataPluginSavedArtistsProvider = AsyncNotifierProvider<
|
||||||
MetadataPluginSavedArtistNotifier,
|
MetadataPluginSavedArtistNotifier,
|
||||||
SpotubePaginationResponseObject<SpotubeFullArtistObject>>(
|
SpotubeFlattenedPaginationObject<SpotubeFullArtistObject>>(
|
||||||
() => MetadataPluginSavedArtistNotifier(),
|
() => MetadataPluginSavedArtistNotifier(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -15,9 +15,9 @@ class MetadataPluginSavedPlaylistsNotifier
|
|||||||
fetch(int offset, int limit) async {
|
fetch(int offset, int limit) async {
|
||||||
final playlists = await (await metadataPlugin)
|
final playlists = await (await metadataPlugin)
|
||||||
.user
|
.user
|
||||||
.savedPlaylists(limit: limit, offset: offset);
|
.savedPlaylists(limit: limit, offset: offset, mpscTx: await mpscTx);
|
||||||
|
|
||||||
return playlists;
|
return playlists.flatten();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -58,7 +58,9 @@ class MetadataPluginSavedPlaylistsNotifier
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await (await metadataPlugin).playlist.save(playlist.id);
|
await (await metadataPlugin)
|
||||||
|
.playlist
|
||||||
|
.save(playlistId: playlist.id, mpscTx: await mpscTx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
state = AsyncData(oldState!);
|
state = AsyncData(oldState!);
|
||||||
rethrow;
|
rethrow;
|
||||||
@ -76,7 +78,9 @@ class MetadataPluginSavedPlaylistsNotifier
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await (await metadataPlugin).playlist.unsave(playlist.id);
|
await (await metadataPlugin)
|
||||||
|
.playlist
|
||||||
|
.unsave(playlistId: playlist.id, mpscTx: await mpscTx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
state = AsyncData(oldState!);
|
state = AsyncData(oldState!);
|
||||||
rethrow;
|
rethrow;
|
||||||
@ -88,7 +92,9 @@ class MetadataPluginSavedPlaylistsNotifier
|
|||||||
final oldState = state;
|
final oldState = state;
|
||||||
try {
|
try {
|
||||||
state = const AsyncLoading();
|
state = const AsyncLoading();
|
||||||
await (await metadataPlugin).playlist.deletePlaylist(playlistId);
|
await (await metadataPlugin)
|
||||||
|
.playlist
|
||||||
|
.deletePlaylist(playlistId: playlistId, mpscTx: await mpscTx);
|
||||||
ref.invalidateSelf();
|
ref.invalidateSelf();
|
||||||
ref.invalidate(metadataPluginIsSavedPlaylistProvider(playlistId));
|
ref.invalidate(metadataPluginIsSavedPlaylistProvider(playlistId));
|
||||||
ref.invalidate(metadataPluginPlaylistTracksProvider(playlistId));
|
ref.invalidate(metadataPluginPlaylistTracksProvider(playlistId));
|
||||||
@ -101,9 +107,8 @@ class MetadataPluginSavedPlaylistsNotifier
|
|||||||
Future<void> addTracks(String playlistId, List<String> trackIds) async {
|
Future<void> addTracks(String playlistId, List<String> trackIds) async {
|
||||||
if (state.value == null) return;
|
if (state.value == null) return;
|
||||||
|
|
||||||
await (await metadataPlugin)
|
await (await metadataPlugin).playlist.addTracks(
|
||||||
.playlist
|
playlistId: playlistId, trackIds: trackIds, mpscTx: await mpscTx);
|
||||||
.addTracks(playlistId, trackIds: trackIds);
|
|
||||||
|
|
||||||
ref.invalidate(metadataPluginPlaylistTracksProvider(playlistId));
|
ref.invalidate(metadataPluginPlaylistTracksProvider(playlistId));
|
||||||
}
|
}
|
||||||
@ -111,9 +116,8 @@ class MetadataPluginSavedPlaylistsNotifier
|
|||||||
Future<void> removeTracks(String playlistId, List<String> trackIds) async {
|
Future<void> removeTracks(String playlistId, List<String> trackIds) async {
|
||||||
if (state.value == null) return;
|
if (state.value == null) return;
|
||||||
|
|
||||||
await (await metadataPlugin)
|
await (await metadataPlugin).playlist.removeTracks(
|
||||||
.playlist
|
playlistId: playlistId, trackIds: trackIds, mpscTx: await mpscTx);
|
||||||
.removeTracks(playlistId, trackIds: trackIds);
|
|
||||||
|
|
||||||
ref.invalidate(metadataPluginPlaylistTracksProvider(playlistId));
|
ref.invalidate(metadataPluginPlaylistTracksProvider(playlistId));
|
||||||
}
|
}
|
||||||
@ -121,7 +125,7 @@ class MetadataPluginSavedPlaylistsNotifier
|
|||||||
|
|
||||||
final metadataPluginSavedPlaylistsProvider = AsyncNotifierProvider<
|
final metadataPluginSavedPlaylistsProvider = AsyncNotifierProvider<
|
||||||
MetadataPluginSavedPlaylistsNotifier,
|
MetadataPluginSavedPlaylistsNotifier,
|
||||||
SpotubePaginationResponseObject<SpotubeSimplePlaylistObject>>(
|
SpotubeFlattenedPaginationObject<SpotubeSimplePlaylistObject>>(
|
||||||
() => MetadataPluginSavedPlaylistsNotifier(),
|
() => MetadataPluginSavedPlaylistsNotifier(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -13,9 +13,10 @@ class MetadataPluginSavedTracksNotifier
|
|||||||
final tracks = await (await metadataPlugin).user.savedTracks(
|
final tracks = await (await metadataPlugin).user.savedTracks(
|
||||||
offset: offset,
|
offset: offset,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
|
mpscTx: await mpscTx,
|
||||||
);
|
);
|
||||||
|
|
||||||
return tracks;
|
return tracks.flatten();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -26,7 +27,7 @@ class MetadataPluginSavedTracksNotifier
|
|||||||
return await fetch(0, 20);
|
return await fetch(0, 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addFavorite(List<SpotubeTrackObject> tracks) async {
|
Future<void> addFavorite(List<SpotubeFullTrackObject> tracks) async {
|
||||||
if (state.value == null) {
|
if (state.value == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -34,22 +35,21 @@ class MetadataPluginSavedTracksNotifier
|
|||||||
final oldState = state.value;
|
final oldState = state.value;
|
||||||
state = AsyncData(
|
state = AsyncData(
|
||||||
state.value!.copyWith(
|
state.value!.copyWith(
|
||||||
items: [
|
items: [...tracks, ...state.value!.items],
|
||||||
...tracks.whereType<SpotubeFullTrackObject>(),
|
|
||||||
...state.value!.items
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await (await metadataPlugin).track.save(tracks.map((e) => e.id).toList());
|
await (await metadataPlugin)
|
||||||
|
.track
|
||||||
|
.save(ids: tracks.map((e) => e.id).toList(), mpscTx: await mpscTx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
state = AsyncData(oldState!);
|
state = AsyncData(oldState!);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeFavorite(List<SpotubeTrackObject> tracks) async {
|
Future<void> removeFavorite(List<SpotubeFullTrackObject> tracks) async {
|
||||||
if (state.value == null) {
|
if (state.value == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -68,7 +68,7 @@ class MetadataPluginSavedTracksNotifier
|
|||||||
try {
|
try {
|
||||||
await (await metadataPlugin)
|
await (await metadataPlugin)
|
||||||
.track
|
.track
|
||||||
.unsave(tracks.map((e) => e.id).toList());
|
.unsave(ids: tracks.map((e) => e.id).toList(), mpscTx: await mpscTx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
state = AsyncData(oldState!);
|
state = AsyncData(oldState!);
|
||||||
rethrow;
|
rethrow;
|
||||||
@ -78,7 +78,7 @@ class MetadataPluginSavedTracksNotifier
|
|||||||
|
|
||||||
final metadataPluginSavedTracksProvider = AutoDisposeAsyncNotifierProvider<
|
final metadataPluginSavedTracksProvider = AutoDisposeAsyncNotifierProvider<
|
||||||
MetadataPluginSavedTracksNotifier,
|
MetadataPluginSavedTracksNotifier,
|
||||||
SpotubePaginationResponseObject<SpotubeFullTrackObject>>(
|
SpotubeFlattenedPaginationObject<SpotubeFullTrackObject>>(
|
||||||
() => MetadataPluginSavedTracksNotifier(),
|
() => MetadataPluginSavedTracksNotifier(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import 'package:path_provider/path_provider.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/provider/database/database.dart';
|
import 'package:spotube/provider/database/database.dart';
|
||||||
import 'package:spotube/provider/youtube_engine/youtube_engine.dart';
|
import 'package:spotube/provider/server/server.dart';
|
||||||
import 'package:spotube/services/dio/dio.dart';
|
import 'package:spotube/services/dio/dio.dart';
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||||
@ -24,6 +24,8 @@ final allowedDomainsRegex = RegExp(
|
|||||||
r"^(https?:\/\/)?(www\.)?(github\.com|codeberg\.org)\/.+",
|
r"^(https?:\/\/)?(www\.)?(github\.com|codeberg\.org)\/.+",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final kPluginApiVersion = Version.parse("2.0.0");
|
||||||
|
|
||||||
class MetadataPluginState {
|
class MetadataPluginState {
|
||||||
final List<PluginConfiguration> plugins;
|
final List<PluginConfiguration> plugins;
|
||||||
final int defaultMetadataPlugin;
|
final int defaultMetadataPlugin;
|
||||||
@ -129,7 +131,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
|||||||
repository: plugin.repository,
|
repository: plugin.repository,
|
||||||
apis: plugin.apis
|
apis: plugin.apis
|
||||||
.map(
|
.map(
|
||||||
(e) => PluginApis.values.firstWhereOrNull(
|
(e) => PluginApi.values.firstWhereOrNull(
|
||||||
(api) => api.name == e,
|
(api) => api.name == e,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -137,7 +139,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
|||||||
.toList(),
|
.toList(),
|
||||||
abilities: plugin.abilities
|
abilities: plugin.abilities
|
||||||
.map(
|
.map(
|
||||||
(e) => PluginAbilities.values.firstWhereOrNull(
|
(e) => PluginAbility.values.firstWhereOrNull(
|
||||||
(ability) => ability.name == e,
|
(ability) => ability.name == e,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -149,7 +151,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
|||||||
final pluginJsonFile =
|
final pluginJsonFile =
|
||||||
File(join(pluginExtractionDir.path, "plugin.json"));
|
File(join(pluginExtractionDir.path, "plugin.json"));
|
||||||
final pluginBinaryFile =
|
final pluginBinaryFile =
|
||||||
File(join(pluginExtractionDir.path, "plugin.out"));
|
File(join(pluginExtractionDir.path, "plugin.js"));
|
||||||
|
|
||||||
if (!await pluginExtractionDir.exists() ||
|
if (!await pluginExtractionDir.exists() ||
|
||||||
!await pluginJsonFile.exists() ||
|
!await pluginJsonFile.exists() ||
|
||||||
@ -374,13 +376,12 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
|||||||
|
|
||||||
bool validatePluginApiCompatibility(PluginConfiguration plugin) {
|
bool validatePluginApiCompatibility(PluginConfiguration plugin) {
|
||||||
final configPluginApiVersion = Version.parse(plugin.pluginApiVersion);
|
final configPluginApiVersion = Version.parse(plugin.pluginApiVersion);
|
||||||
final appPluginApiVersion = MetadataPlugin.pluginApiVersion;
|
|
||||||
|
|
||||||
// Plugin API's major version must match the app's major version
|
// Plugin API's major version must match the app's major version
|
||||||
if (configPluginApiVersion.major != appPluginApiVersion.major) {
|
if (configPluginApiVersion.major != kPluginApiVersion.major) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return configPluginApiVersion >= appPluginApiVersion;
|
return configPluginApiVersion >= kPluginApiVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _assertPluginApiCompatibility(PluginConfiguration plugin) {
|
void _assertPluginApiCompatibility(PluginConfiguration plugin) {
|
||||||
@ -419,18 +420,18 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
|||||||
selectedForMetadata: Value(
|
selectedForMetadata: Value(
|
||||||
(state.valueOrNull?.plugins
|
(state.valueOrNull?.plugins
|
||||||
.where(
|
.where(
|
||||||
(d) => d.abilities.contains(PluginAbilities.metadata))
|
(d) => d.abilities.contains(PluginAbility.metadata))
|
||||||
.isEmpty ??
|
.isEmpty ??
|
||||||
true) &&
|
true) &&
|
||||||
plugin.abilities.contains(PluginAbilities.metadata),
|
plugin.abilities.contains(PluginAbility.metadata),
|
||||||
),
|
),
|
||||||
selectedForAudioSource: Value(
|
selectedForAudioSource: Value(
|
||||||
(state.valueOrNull?.plugins
|
(state.valueOrNull?.plugins
|
||||||
.where((d) =>
|
.where((d) =>
|
||||||
d.abilities.contains(PluginAbilities.audioSource))
|
d.abilities.contains(PluginAbility.audioSource))
|
||||||
.isEmpty ??
|
.isEmpty ??
|
||||||
true) &&
|
true) &&
|
||||||
plugin.abilities.contains(PluginAbilities.audioSource),
|
plugin.abilities.contains(PluginAbility.audioSource),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -450,8 +451,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
|||||||
// only when there is 1 remaining plugin
|
// only when there is 1 remaining plugin
|
||||||
if (state.valueOrNull?.defaultMetadataPluginConfig == plugin) {
|
if (state.valueOrNull?.defaultMetadataPluginConfig == plugin) {
|
||||||
final remainingPlugins = state.valueOrNull?.plugins.where(
|
final remainingPlugins = state.valueOrNull?.plugins.where(
|
||||||
(p) =>
|
(p) => p != plugin && p.abilities.contains(PluginAbility.metadata),
|
||||||
p != plugin && p.abilities.contains(PluginAbilities.metadata),
|
|
||||||
) ??
|
) ??
|
||||||
[];
|
[];
|
||||||
if (remainingPlugins.length == 1) {
|
if (remainingPlugins.length == 1) {
|
||||||
@ -462,8 +462,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
|||||||
if (state.valueOrNull?.defaultAudioSourcePluginConfig == plugin) {
|
if (state.valueOrNull?.defaultAudioSourcePluginConfig == plugin) {
|
||||||
final remainingPlugins = state.valueOrNull?.plugins.where(
|
final remainingPlugins = state.valueOrNull?.plugins.where(
|
||||||
(p) =>
|
(p) =>
|
||||||
p != plugin &&
|
p != plugin && p.abilities.contains(PluginAbility.audioSource),
|
||||||
p.abilities.contains(PluginAbilities.audioSource),
|
|
||||||
) ??
|
) ??
|
||||||
[];
|
[];
|
||||||
if (remainingPlugins.length == 1) {
|
if (remainingPlugins.length == 1) {
|
||||||
@ -523,7 +522,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
|||||||
|
|
||||||
Future<void> setDefaultMetadataPlugin(PluginConfiguration plugin) async {
|
Future<void> setDefaultMetadataPlugin(PluginConfiguration plugin) async {
|
||||||
assert(
|
assert(
|
||||||
plugin.abilities.contains(PluginAbilities.metadata),
|
plugin.abilities.contains(PluginAbility.metadata),
|
||||||
"Must be a metadata plugin",
|
"Must be a metadata plugin",
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -541,7 +540,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
|||||||
|
|
||||||
Future<void> setDefaultAudioSourcePlugin(PluginConfiguration plugin) async {
|
Future<void> setDefaultAudioSourcePlugin(PluginConfiguration plugin) async {
|
||||||
assert(
|
assert(
|
||||||
plugin.abilities.contains(PluginAbilities.audioSource),
|
plugin.abilities.contains(PluginAbility.audioSource),
|
||||||
"Must be an audio-source plugin",
|
"Must be an audio-source plugin",
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -556,16 +555,16 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uint8List> getPluginByteCode(PluginConfiguration plugin) async {
|
Future<String> getPluginSourceCode(PluginConfiguration plugin) async {
|
||||||
final pluginExtractionDirPath = await _getPluginExtractionDir(plugin);
|
final pluginExtractionDirPath = await _getPluginExtractionDir(plugin);
|
||||||
|
|
||||||
final libraryFile = File(join(pluginExtractionDirPath.path, "plugin.out"));
|
final libraryFile = File(join(pluginExtractionDirPath.path, "plugin.js"));
|
||||||
|
|
||||||
if (!libraryFile.existsSync()) {
|
if (!libraryFile.existsSync()) {
|
||||||
throw MetadataPluginException.pluginByteCodeFileNotFound();
|
throw MetadataPluginException.pluginSourceCodeFileNotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return await libraryFile.readAsBytes();
|
return await libraryFile.readAsString();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<File?> getLogoPath(PluginConfiguration plugin) async {
|
Future<File?> getLogoPath(PluginConfiguration plugin) async {
|
||||||
@ -586,27 +585,41 @@ final metadataPluginsProvider =
|
|||||||
MetadataPluginNotifier.new,
|
MetadataPluginNotifier.new,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final _pluginProvider =
|
||||||
|
FutureProvider.family<MetadataPlugin?, PluginConfiguration?>(
|
||||||
|
(ref, config) async {
|
||||||
|
final (:server, :port) = await ref.watch(serverProvider.future);
|
||||||
|
final serverSecret = ref.watch(serverRandomSecretProvider);
|
||||||
|
|
||||||
|
if (config == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final pluginsNotifier = ref.read(metadataPluginsProvider.notifier);
|
||||||
|
final pluginSourceCode = await pluginsNotifier.getPluginSourceCode(config);
|
||||||
|
|
||||||
|
final plugin = MetadataPlugin(
|
||||||
|
pluginScript: pluginSourceCode,
|
||||||
|
pluginConfig: config,
|
||||||
|
serverEndpointUrl: "http://${server.address.host}:$port",
|
||||||
|
serverSecret: serverSecret,
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
plugin.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
return plugin;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
final metadataPluginProvider = FutureProvider<MetadataPlugin?>(
|
final metadataPluginProvider = FutureProvider<MetadataPlugin?>(
|
||||||
(ref) async {
|
(ref) async {
|
||||||
final defaultPlugin = await ref.watch(
|
final defaultPlugin = await ref.watch(
|
||||||
metadataPluginsProvider
|
metadataPluginsProvider
|
||||||
.selectAsync((data) => data.defaultMetadataPluginConfig),
|
.selectAsync((data) => data.defaultMetadataPluginConfig),
|
||||||
);
|
);
|
||||||
final youtubeEngine = ref.read(youtubeEngineProvider);
|
return await ref.watch(_pluginProvider(defaultPlugin).future);
|
||||||
|
|
||||||
if (defaultPlugin == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final pluginsNotifier = ref.read(metadataPluginsProvider.notifier);
|
|
||||||
final pluginByteCode =
|
|
||||||
await pluginsNotifier.getPluginByteCode(defaultPlugin);
|
|
||||||
|
|
||||||
return await MetadataPlugin.create(
|
|
||||||
youtubeEngine,
|
|
||||||
defaultPlugin,
|
|
||||||
pluginByteCode,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -616,20 +629,6 @@ final audioSourcePluginProvider = FutureProvider<MetadataPlugin?>(
|
|||||||
metadataPluginsProvider
|
metadataPluginsProvider
|
||||||
.selectAsync((data) => data.defaultAudioSourcePluginConfig),
|
.selectAsync((data) => data.defaultAudioSourcePluginConfig),
|
||||||
);
|
);
|
||||||
final youtubeEngine = ref.watch(youtubeEngineProvider);
|
return await ref.watch(_pluginProvider(defaultPlugin).future);
|
||||||
|
|
||||||
if (defaultPlugin == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final pluginsNotifier = ref.read(metadataPluginsProvider.notifier);
|
|
||||||
final pluginByteCode =
|
|
||||||
await pluginsNotifier.getPluginByteCode(defaultPlugin);
|
|
||||||
|
|
||||||
return await MetadataPlugin.create(
|
|
||||||
youtubeEngine,
|
|
||||||
defaultPlugin,
|
|
||||||
pluginByteCode,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import 'package:spotube/provider/metadata_plugin/core/user.dart';
|
|||||||
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||||
import 'package:spotube/services/metadata/metadata.dart';
|
import 'package:spotube/services/metadata/metadata.dart';
|
||||||
|
import 'package:spotube/src/rust/api/plugin/plugin.dart';
|
||||||
|
|
||||||
class MetadataPluginPlaylistNotifier
|
class MetadataPluginPlaylistNotifier
|
||||||
extends AutoDisposeFamilyAsyncNotifier<SpotubeFullPlaylistObject, String> {
|
extends AutoDisposeFamilyAsyncNotifier<SpotubeFullPlaylistObject, String> {
|
||||||
@ -19,11 +20,17 @@ class MetadataPluginPlaylistNotifier
|
|||||||
return metadataPlugin;
|
return metadataPlugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<OpaqueSender> get mpscTx async {
|
||||||
|
return (await metadataPlugin).sender;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
build(playlistId) async {
|
build(playlistId) async {
|
||||||
ref.cacheFor();
|
ref.cacheFor();
|
||||||
|
|
||||||
return (await metadataPlugin).playlist.getPlaylist(playlistId);
|
return (await metadataPlugin)
|
||||||
|
.playlist
|
||||||
|
.getPlaylist(id: playlistId, mpscTx: await mpscTx);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> create({
|
Future<void> create({
|
||||||
@ -40,12 +47,13 @@ class MetadataPluginPlaylistNotifier
|
|||||||
}
|
}
|
||||||
state = const AsyncValue.loading();
|
state = const AsyncValue.loading();
|
||||||
try {
|
try {
|
||||||
final playlist = await (await metadataPlugin).playlist.create(
|
final playlist = await (await metadataPlugin).playlist.createPlaylist(
|
||||||
userId,
|
userId: userId,
|
||||||
name: name,
|
name: name,
|
||||||
description: description,
|
description: description,
|
||||||
public: public,
|
public: public,
|
||||||
collaborative: collaborative,
|
collaborative: collaborative,
|
||||||
|
mpscTx: await mpscTx,
|
||||||
);
|
);
|
||||||
if (playlist != null) {
|
if (playlist != null) {
|
||||||
state = AsyncValue.data(playlist);
|
state = AsyncValue.data(playlist);
|
||||||
@ -71,12 +79,13 @@ class MetadataPluginPlaylistNotifier
|
|||||||
collaborative == null) {
|
collaborative == null) {
|
||||||
throw Exception('No modifications provided.');
|
throw Exception('No modifications provided.');
|
||||||
}
|
}
|
||||||
await (await metadataPlugin).playlist.update(
|
await (await metadataPlugin).playlist.updatePlaylist(
|
||||||
arg,
|
playlistId: arg,
|
||||||
name: name,
|
name: name,
|
||||||
description: description,
|
description: description,
|
||||||
public: public,
|
public: public,
|
||||||
collaborative: collaborative,
|
collaborative: collaborative,
|
||||||
|
mpscTx: await mpscTx,
|
||||||
);
|
);
|
||||||
ref.invalidateSelf();
|
ref.invalidateSelf();
|
||||||
} on Exception catch (e) {
|
} on Exception catch (e) {
|
||||||
|
|||||||
@ -12,7 +12,7 @@ class MetadataPluginSearchAlbumsNotifier
|
|||||||
@override
|
@override
|
||||||
fetch(offset, limit) async {
|
fetch(offset, limit) async {
|
||||||
if (arg.isEmpty) {
|
if (arg.isEmpty) {
|
||||||
return SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>(
|
return SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>(
|
||||||
limit: limit,
|
limit: limit,
|
||||||
nextOffset: null,
|
nextOffset: null,
|
||||||
total: 0,
|
total: 0,
|
||||||
@ -22,12 +22,13 @@ class MetadataPluginSearchAlbumsNotifier
|
|||||||
}
|
}
|
||||||
|
|
||||||
final res = await (await metadataPlugin).search.albums(
|
final res = await (await metadataPlugin).search.albums(
|
||||||
arg,
|
query: arg,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
|
mpscTx: await mpscTx,
|
||||||
);
|
);
|
||||||
|
|
||||||
return res;
|
return res.flatten();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -41,6 +42,6 @@ class MetadataPluginSearchAlbumsNotifier
|
|||||||
|
|
||||||
final metadataPluginSearchAlbumsProvider =
|
final metadataPluginSearchAlbumsProvider =
|
||||||
AutoDisposeAsyncNotifierProviderFamily<MetadataPluginSearchAlbumsNotifier,
|
AutoDisposeAsyncNotifierProviderFamily<MetadataPluginSearchAlbumsNotifier,
|
||||||
SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>, String>(
|
SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>, String>(
|
||||||
() => MetadataPluginSearchAlbumsNotifier(),
|
() => MetadataPluginSearchAlbumsNotifier(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -12,7 +12,8 @@ final metadataPluginSearchAllProvider =
|
|||||||
throw MetadataPluginException.noDefaultMetadataPlugin();
|
throw MetadataPluginException.noDefaultMetadataPlugin();
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadataPlugin.search.all(query);
|
return metadataPlugin.search
|
||||||
|
.all(query: query, mpscTx: metadataPlugin.sender);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -22,5 +23,5 @@ final metadataPluginSearchChipsProvider = FutureProvider((ref) async {
|
|||||||
if (metadataPlugin == null) {
|
if (metadataPlugin == null) {
|
||||||
throw MetadataPluginException.noDefaultMetadataPlugin();
|
throw MetadataPluginException.noDefaultMetadataPlugin();
|
||||||
}
|
}
|
||||||
return metadataPlugin.search.chips;
|
return metadataPlugin.search.chips(mpscTx: metadataPlugin.sender);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -12,7 +12,7 @@ class MetadataPluginSearchArtistsNotifier
|
|||||||
@override
|
@override
|
||||||
fetch(offset, limit) async {
|
fetch(offset, limit) async {
|
||||||
if (arg.isEmpty) {
|
if (arg.isEmpty) {
|
||||||
return SpotubePaginationResponseObject<SpotubeFullArtistObject>(
|
return SpotubeFlattenedPaginationObject<SpotubeFullArtistObject>(
|
||||||
limit: limit,
|
limit: limit,
|
||||||
nextOffset: null,
|
nextOffset: null,
|
||||||
total: 0,
|
total: 0,
|
||||||
@ -22,12 +22,13 @@ class MetadataPluginSearchArtistsNotifier
|
|||||||
}
|
}
|
||||||
|
|
||||||
final res = await (await metadataPlugin).search.artists(
|
final res = await (await metadataPlugin).search.artists(
|
||||||
arg,
|
query: arg,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
|
mpscTx: await mpscTx,
|
||||||
);
|
);
|
||||||
|
|
||||||
return res;
|
return res.flatten();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -41,6 +42,6 @@ class MetadataPluginSearchArtistsNotifier
|
|||||||
|
|
||||||
final metadataPluginSearchArtistsProvider =
|
final metadataPluginSearchArtistsProvider =
|
||||||
AutoDisposeAsyncNotifierProviderFamily<MetadataPluginSearchArtistsNotifier,
|
AutoDisposeAsyncNotifierProviderFamily<MetadataPluginSearchArtistsNotifier,
|
||||||
SpotubePaginationResponseObject<SpotubeFullArtistObject>, String>(
|
SpotubeFlattenedPaginationObject<SpotubeFullArtistObject>, String>(
|
||||||
() => MetadataPluginSearchArtistsNotifier(),
|
() => MetadataPluginSearchArtistsNotifier(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -12,7 +12,7 @@ class MetadataPluginSearchPlaylistsNotifier
|
|||||||
@override
|
@override
|
||||||
fetch(offset, limit) async {
|
fetch(offset, limit) async {
|
||||||
if (arg.isEmpty) {
|
if (arg.isEmpty) {
|
||||||
return SpotubePaginationResponseObject<SpotubeSimplePlaylistObject>(
|
return SpotubeFlattenedPaginationObject<SpotubeSimplePlaylistObject>(
|
||||||
limit: limit,
|
limit: limit,
|
||||||
nextOffset: null,
|
nextOffset: null,
|
||||||
total: 0,
|
total: 0,
|
||||||
@ -22,12 +22,13 @@ class MetadataPluginSearchPlaylistsNotifier
|
|||||||
}
|
}
|
||||||
|
|
||||||
final res = await (await metadataPlugin).search.playlists(
|
final res = await (await metadataPlugin).search.playlists(
|
||||||
arg,
|
query: arg,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
|
mpscTx: await mpscTx,
|
||||||
);
|
);
|
||||||
|
|
||||||
return res;
|
return res.flatten();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -42,7 +43,7 @@ class MetadataPluginSearchPlaylistsNotifier
|
|||||||
final metadataPluginSearchPlaylistsProvider =
|
final metadataPluginSearchPlaylistsProvider =
|
||||||
AutoDisposeAsyncNotifierProviderFamily<
|
AutoDisposeAsyncNotifierProviderFamily<
|
||||||
MetadataPluginSearchPlaylistsNotifier,
|
MetadataPluginSearchPlaylistsNotifier,
|
||||||
SpotubePaginationResponseObject<SpotubeSimplePlaylistObject>,
|
SpotubeFlattenedPaginationObject<SpotubeSimplePlaylistObject>,
|
||||||
String>(
|
String>(
|
||||||
() => MetadataPluginSearchPlaylistsNotifier(),
|
() => MetadataPluginSearchPlaylistsNotifier(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,14 +5,14 @@ import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
|||||||
import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart';
|
import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart';
|
||||||
|
|
||||||
class MetadataPluginSearchTracksNotifier
|
class MetadataPluginSearchTracksNotifier
|
||||||
extends AutoDisposeFamilyPaginatedAsyncNotifier<SpotubeFullTrackObject,
|
extends AutoDisposeFamilyPaginatedAsyncNotifier<SpotubeTrackObject,
|
||||||
String> {
|
String> {
|
||||||
MetadataPluginSearchTracksNotifier() : super();
|
MetadataPluginSearchTracksNotifier() : super();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
fetch(offset, limit) async {
|
fetch(offset, limit) async {
|
||||||
if (arg.isEmpty) {
|
if (arg.isEmpty) {
|
||||||
return SpotubePaginationResponseObject<SpotubeFullTrackObject>(
|
return SpotubeFlattenedPaginationObject<SpotubeTrackObject>(
|
||||||
limit: limit,
|
limit: limit,
|
||||||
nextOffset: null,
|
nextOffset: null,
|
||||||
total: 0,
|
total: 0,
|
||||||
@ -22,12 +22,13 @@ class MetadataPluginSearchTracksNotifier
|
|||||||
}
|
}
|
||||||
|
|
||||||
final tracks = await (await metadataPlugin).search.tracks(
|
final tracks = await (await metadataPlugin).search.tracks(
|
||||||
arg,
|
query: arg,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
|
mpscTx: await mpscTx,
|
||||||
);
|
);
|
||||||
|
|
||||||
return tracks;
|
return tracks.flatten();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -41,6 +42,6 @@ class MetadataPluginSearchTracksNotifier
|
|||||||
|
|
||||||
final metadataPluginSearchTracksProvider =
|
final metadataPluginSearchTracksProvider =
|
||||||
AutoDisposeAsyncNotifierProviderFamily<MetadataPluginSearchTracksNotifier,
|
AutoDisposeAsyncNotifierProviderFamily<MetadataPluginSearchTracksNotifier,
|
||||||
SpotubePaginationResponseObject<SpotubeFullTrackObject>, String>(
|
SpotubeFlattenedPaginationObject<SpotubeTrackObject>, String>(
|
||||||
() => MetadataPluginSearchTracksNotifier(),
|
() => MetadataPluginSearchTracksNotifier(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -12,12 +12,13 @@ class MetadataPluginAlbumTracksNotifier
|
|||||||
@override
|
@override
|
||||||
fetch(offset, limit) async {
|
fetch(offset, limit) async {
|
||||||
final tracks = await (await metadataPlugin).album.tracks(
|
final tracks = await (await metadataPlugin).album.tracks(
|
||||||
arg,
|
id: arg,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
|
mpscTx: await mpscTx,
|
||||||
);
|
);
|
||||||
|
|
||||||
return tracks;
|
return tracks.flatten();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -31,6 +32,6 @@ class MetadataPluginAlbumTracksNotifier
|
|||||||
|
|
||||||
final metadataPluginAlbumTracksProvider =
|
final metadataPluginAlbumTracksProvider =
|
||||||
AutoDisposeAsyncNotifierProviderFamily<MetadataPluginAlbumTracksNotifier,
|
AutoDisposeAsyncNotifierProviderFamily<MetadataPluginAlbumTracksNotifier,
|
||||||
SpotubePaginationResponseObject<SpotubeFullTrackObject>, String>(
|
SpotubeFlattenedPaginationObject<SpotubeFullTrackObject>, String>(
|
||||||
() => MetadataPluginAlbumTracksNotifier(),
|
() => MetadataPluginAlbumTracksNotifier(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -12,12 +12,13 @@ class MetadataPluginPlaylistTracksNotifier
|
|||||||
@override
|
@override
|
||||||
fetch(offset, limit) async {
|
fetch(offset, limit) async {
|
||||||
final tracks = await (await metadataPlugin).playlist.tracks(
|
final tracks = await (await metadataPlugin).playlist.tracks(
|
||||||
arg,
|
id: arg,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
|
mpscTx: await mpscTx,
|
||||||
);
|
);
|
||||||
|
|
||||||
return tracks;
|
return tracks.flatten();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -31,6 +32,6 @@ class MetadataPluginPlaylistTracksNotifier
|
|||||||
|
|
||||||
final metadataPluginPlaylistTracksProvider =
|
final metadataPluginPlaylistTracksProvider =
|
||||||
AutoDisposeAsyncNotifierProviderFamily<MetadataPluginPlaylistTracksNotifier,
|
AutoDisposeAsyncNotifierProviderFamily<MetadataPluginPlaylistTracksNotifier,
|
||||||
SpotubePaginationResponseObject<SpotubeFullTrackObject>, String>(
|
SpotubeFlattenedPaginationObject<SpotubeFullTrackObject>, String>(
|
||||||
() => MetadataPluginPlaylistTracksNotifier(),
|
() => MetadataPluginPlaylistTracksNotifier(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -11,5 +11,7 @@ final metadataPluginTrackProvider =
|
|||||||
throw MetadataPluginException.noDefaultMetadataPlugin();
|
throw MetadataPluginException.noDefaultMetadataPlugin();
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadataPlugin.track.getTrack(trackId);
|
return await metadataPlugin.track
|
||||||
|
.getTrack(id: trackId, mpscTx: metadataPlugin.sender);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -12,8 +12,10 @@ final metadataPluginUpdateCheckerProvider =
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadataPlugin.core
|
return metadataPlugin.core.checkUpdate(
|
||||||
.checkUpdate(metadataPluginConfigs.defaultMetadataPluginConfig!);
|
pluginConfig: metadataPluginConfigs.defaultMetadataPluginConfig!,
|
||||||
|
mpscTx: metadataPlugin.sender,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
final audioSourcePluginUpdateCheckerProvider =
|
final audioSourcePluginUpdateCheckerProvider =
|
||||||
@ -27,6 +29,8 @@ final audioSourcePluginUpdateCheckerProvider =
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return audioSourcePlugin.core
|
return audioSourcePlugin.core.checkUpdate(
|
||||||
.checkUpdate(audioSourcePluginConfigs.defaultAudioSourcePluginConfig!);
|
pluginConfig: audioSourcePluginConfigs.defaultAudioSourcePluginConfig!,
|
||||||
|
mpscTx: audioSourcePlugin.sender,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import 'package:spotube/models/metadata/metadata.dart';
|
|||||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||||
import 'package:spotube/services/metadata/metadata.dart';
|
import 'package:spotube/services/metadata/metadata.dart';
|
||||||
|
import 'package:spotube/src/rust/api/plugin/plugin.dart';
|
||||||
|
|
||||||
extension PaginationExtension<T> on AsyncValue<T> {
|
extension PaginationExtension<T> on AsyncValue<T> {
|
||||||
bool get isLoadingNextPage => this is AsyncData && this is AsyncLoadingNext;
|
bool get isLoadingNextPage => this is AsyncData && this is AsyncLoadingNext;
|
||||||
@ -15,7 +16,7 @@ extension PaginationExtension<T> on AsyncValue<T> {
|
|||||||
|
|
||||||
mixin MetadataPluginMixin<K>
|
mixin MetadataPluginMixin<K>
|
||||||
// ignore: invalid_use_of_internal_member
|
// ignore: invalid_use_of_internal_member
|
||||||
on AsyncNotifierBase<SpotubePaginationResponseObject<K>> {
|
on AsyncNotifierBase<SpotubeFlattenedPaginationObject<K>> {
|
||||||
Future<MetadataPlugin> get metadataPlugin async {
|
Future<MetadataPlugin> get metadataPlugin async {
|
||||||
final plugin = await ref.read(metadataPluginProvider.future);
|
final plugin = await ref.read(metadataPluginProvider.future);
|
||||||
|
|
||||||
@ -25,6 +26,11 @@ mixin MetadataPluginMixin<K>
|
|||||||
|
|
||||||
return plugin;
|
return plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<OpaqueSender> get mpscTx async {
|
||||||
|
final plugin = await metadataPlugin;
|
||||||
|
return plugin.sender;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AutoDisposeAsyncNotifierCacheFor
|
extension AutoDisposeAsyncNotifierCacheFor
|
||||||
|
|||||||
@ -7,9 +7,9 @@ import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
|||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
|
|
||||||
abstract class FamilyPaginatedAsyncNotifier<K, A>
|
abstract class FamilyPaginatedAsyncNotifier<K, A>
|
||||||
extends FamilyAsyncNotifier<SpotubePaginationResponseObject<K>, A>
|
extends FamilyAsyncNotifier<SpotubeFlattenedPaginationObject<K>, A>
|
||||||
with MetadataPluginMixin<K> {
|
with MetadataPluginMixin<K> {
|
||||||
Future<SpotubePaginationResponseObject<K>> fetch(int offset, int limit);
|
Future<SpotubeFlattenedPaginationObject<K>> fetch(int offset, int limit);
|
||||||
|
|
||||||
Future<void> fetchMore() async {
|
Future<void> fetchMore() async {
|
||||||
if (state.value == null || !state.value!.hasMore) return;
|
if (state.value == null || !state.value!.hasMore) return;
|
||||||
@ -74,9 +74,9 @@ abstract class FamilyPaginatedAsyncNotifier<K, A>
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract class AutoDisposeFamilyPaginatedAsyncNotifier<K, A>
|
abstract class AutoDisposeFamilyPaginatedAsyncNotifier<K, A>
|
||||||
extends AutoDisposeFamilyAsyncNotifier<SpotubePaginationResponseObject<K>,
|
extends AutoDisposeFamilyAsyncNotifier<SpotubeFlattenedPaginationObject<K>,
|
||||||
A> with MetadataPluginMixin<K> {
|
A> with MetadataPluginMixin<K> {
|
||||||
Future<SpotubePaginationResponseObject<K>> fetch(int offset, int limit);
|
Future<SpotubeFlattenedPaginationObject<K>> fetch(int offset, int limit);
|
||||||
|
|
||||||
Future<void> fetchMore() async {
|
Future<void> fetchMore() async {
|
||||||
if (state.value == null || !state.value!.hasMore) return;
|
if (state.value == null || !state.value!.hasMore) return;
|
||||||
|
|||||||
@ -10,8 +10,8 @@ import 'package:spotube/services/logger/logger.dart';
|
|||||||
|
|
||||||
mixin PaginatedAsyncNotifierMixin<K>
|
mixin PaginatedAsyncNotifierMixin<K>
|
||||||
// ignore: invalid_use_of_internal_member
|
// ignore: invalid_use_of_internal_member
|
||||||
on AsyncNotifierBase<SpotubePaginationResponseObject<K>> {
|
on AsyncNotifierBase<SpotubeFlattenedPaginationObject<K>> {
|
||||||
Future<SpotubePaginationResponseObject<K>> fetch(int offset, int limit);
|
Future<SpotubeFlattenedPaginationObject<K>> fetch(int offset, int limit);
|
||||||
|
|
||||||
Future<void> fetchMore() async {
|
Future<void> fetchMore() async {
|
||||||
if (state.value == null || !state.value!.hasMore) return;
|
if (state.value == null || !state.value!.hasMore) return;
|
||||||
@ -75,9 +75,9 @@ mixin PaginatedAsyncNotifierMixin<K>
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract class PaginatedAsyncNotifier<K>
|
abstract class PaginatedAsyncNotifier<K>
|
||||||
extends AsyncNotifier<SpotubePaginationResponseObject<K>>
|
extends AsyncNotifier<SpotubeFlattenedPaginationObject<K>>
|
||||||
with PaginatedAsyncNotifierMixin<K>, MetadataPluginMixin<K> {}
|
with PaginatedAsyncNotifierMixin<K>, MetadataPluginMixin<K> {}
|
||||||
|
|
||||||
abstract class AutoDisposePaginatedAsyncNotifier<K>
|
abstract class AutoDisposePaginatedAsyncNotifier<K>
|
||||||
extends AutoDisposeAsyncNotifier<SpotubePaginationResponseObject<K>>
|
extends AutoDisposeAsyncNotifier<SpotubeFlattenedPaginationObject<K>>
|
||||||
with PaginatedAsyncNotifierMixin<K>, MetadataPluginMixin<K> {}
|
with PaginatedAsyncNotifierMixin<K>, MetadataPluginMixin<K> {}
|
||||||
|
|||||||
@ -10,8 +10,8 @@ import 'package:spotube/provider/database/database.dart';
|
|||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
|
|
||||||
class ScrobblerNotifier extends AsyncNotifier<Scrobblenaut?> {
|
class ScrobblerNotifier extends AsyncNotifier<Scrobblenaut?> {
|
||||||
final StreamController<SpotubeTrackObject> _scrobbleController =
|
final _scrobbleController =
|
||||||
StreamController<SpotubeTrackObject>.broadcast();
|
StreamController<SpotubeFullTrackObject>.broadcast();
|
||||||
@override
|
@override
|
||||||
build() async {
|
build() async {
|
||||||
final database = ref.watch(databaseProvider);
|
final database = ref.watch(databaseProvider);
|
||||||
@ -107,18 +107,18 @@ class ScrobblerNotifier extends AsyncNotifier<Scrobblenaut?> {
|
|||||||
await database.delete(database.scrobblerTable).go();
|
await database.delete(database.scrobblerTable).go();
|
||||||
}
|
}
|
||||||
|
|
||||||
void scrobble(SpotubeTrackObject track) {
|
void scrobble(SpotubeFullTrackObject track) {
|
||||||
_scrobbleController.add(track);
|
_scrobbleController.add(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> love(SpotubeTrackObject track) async {
|
Future<void> love(SpotubeFullTrackObject track) async {
|
||||||
await state.asData?.value?.track.love(
|
await state.asData?.value?.track.love(
|
||||||
artist: track.artists.asString(),
|
artist: track.artists.asString(),
|
||||||
track: track.name,
|
track: track.name,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> unlove(SpotubeTrackObject track) async {
|
Future<void> unlove(SpotubeFullTrackObject track) async {
|
||||||
await state.asData?.value?.track.unLove(
|
await state.asData?.value?.track.unLove(
|
||||||
artist: track.artists.asString(),
|
artist: track.artists.asString(),
|
||||||
track: track.name,
|
track: track.name,
|
||||||
|
|||||||
@ -16,7 +16,7 @@ final activeTrackSourcesProvider = FutureProvider<
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioPlayerState.activeTrack is SpotubeLocalTrackObject) {
|
if (audioPlayerState.activeTrack is SpotubeTrackObject_Local) {
|
||||||
return (
|
return (
|
||||||
source: null,
|
source: null,
|
||||||
notifier: null,
|
notifier: null,
|
||||||
@ -26,12 +26,12 @@ final activeTrackSourcesProvider = FutureProvider<
|
|||||||
|
|
||||||
final sourcedTrack = await ref.watch(
|
final sourcedTrack = await ref.watch(
|
||||||
sourcedTrackProvider(
|
sourcedTrackProvider(
|
||||||
audioPlayerState.activeTrack! as SpotubeFullTrackObject,
|
audioPlayerState.activeTrack?.field0 as SpotubeFullTrackObject,
|
||||||
).future,
|
).future,
|
||||||
);
|
);
|
||||||
final sourcedTrackNotifier = ref.watch(
|
final sourcedTrackNotifier = ref.watch(
|
||||||
sourcedTrackProvider(
|
sourcedTrackProvider(
|
||||||
audioPlayerState.activeTrack! as SpotubeFullTrackObject,
|
audioPlayerState.activeTrack?.field0 as SpotubeFullTrackObject,
|
||||||
).notifier,
|
).notifier,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -161,7 +161,7 @@ class ServerConnectRoutes {
|
|||||||
|
|
||||||
event.onLoad((event) async {
|
event.onLoad((event) async {
|
||||||
await audioPlayerNotifier.load(
|
await audioPlayerNotifier.load(
|
||||||
event.data.tracks.cast<SpotubeFullTrackObject>().toList(),
|
event.data.tracks.cast<SpotubeTrackObject>().toList(),
|
||||||
autoPlay: true,
|
autoPlay: true,
|
||||||
initialIndex: event.data.initialIndex ?? 0,
|
initialIndex: event.data.initialIndex ?? 0,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -70,8 +70,9 @@ class ServerPlaybackRoutes {
|
|||||||
final sourcedTrack = activeSourcedTrack?.track.id == track.id
|
final sourcedTrack = activeSourcedTrack?.track.id == track.id
|
||||||
? activeSourcedTrack?.source
|
? activeSourcedTrack?.source
|
||||||
: await ref.read(
|
: await ref.read(
|
||||||
sourcedTrackProvider(spotubeMedia.track as SpotubeFullTrackObject)
|
sourcedTrackProvider(
|
||||||
.future,
|
spotubeMedia.track.field0 as SpotubeFullTrackObject,
|
||||||
|
).future,
|
||||||
);
|
);
|
||||||
|
|
||||||
return sourcedTrack;
|
return sourcedTrack;
|
||||||
@ -258,7 +259,7 @@ class ServerPlaybackRoutes {
|
|||||||
|
|
||||||
await MetadataGod.writeMetadata(
|
await MetadataGod.writeMetadata(
|
||||||
file: trackCacheFile.path,
|
file: trackCacheFile.path,
|
||||||
metadata: track.query.toMetadata(
|
metadata: SpotubeTrackObject.full(track.query).toMetadata(
|
||||||
imageBytes: imageBytes,
|
imageBytes: imageBytes,
|
||||||
fileLength: fileLength,
|
fileLength: fileLength,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -98,7 +98,8 @@ class TrackOptionsActions {
|
|||||||
throw MetadataPluginException.noDefaultMetadataPlugin();
|
throw MetadataPluginException.noDefaultMetadataPlugin();
|
||||||
}
|
}
|
||||||
|
|
||||||
final tracks = await metadataPlugin.track.radio(track.id);
|
final tracks = await metadataPlugin.track
|
||||||
|
.radio(id: track.id, mpscTx: metadataPlugin.sender);
|
||||||
|
|
||||||
bool replaceQueue = false;
|
bool replaceQueue = false;
|
||||||
|
|
||||||
@ -123,7 +124,7 @@ class TrackOptionsActions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await playback.addTracks(
|
await playback.addTracks(
|
||||||
tracks.toList()
|
tracks.union()
|
||||||
..removeWhere((e) {
|
..removeWhere((e) {
|
||||||
final isDuplicate = playlist.tracks.any((t) => t.id == e.id);
|
final isDuplicate = playlist.tracks.any((t) => t.id == e.id);
|
||||||
return e.id == track.id || isDuplicate;
|
return e.id == track.id || isDuplicate;
|
||||||
@ -202,14 +203,17 @@ class TrackOptionsActions {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TrackOptionValue.favorite:
|
case TrackOptionValue.favorite:
|
||||||
|
if (track is SpotubeTrackObject_Local) break;
|
||||||
final isLikedTrack = await ref.read(
|
final isLikedTrack = await ref.read(
|
||||||
metadataPluginIsSavedTrackProvider(track.id).future,
|
metadataPluginIsSavedTrackProvider(track.id).future,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isLikedTrack) {
|
if (isLikedTrack) {
|
||||||
await favoriteTracks.removeFavorite([track]);
|
await favoriteTracks
|
||||||
|
.removeFavorite([track.field0 as SpotubeFullTrackObject]);
|
||||||
} else {
|
} else {
|
||||||
await favoriteTracks.addFavorite([track]);
|
await favoriteTracks
|
||||||
|
.addFavorite([track.field0 as SpotubeFullTrackObject]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TrackOptionValue.addToPlaylist:
|
case TrackOptionValue.addToPlaylist:
|
||||||
@ -236,18 +240,19 @@ class TrackOptionsActions {
|
|||||||
actionShare(context);
|
actionShare(context);
|
||||||
break;
|
break;
|
||||||
case TrackOptionValue.details:
|
case TrackOptionValue.details:
|
||||||
if (track is! SpotubeFullTrackObject) break;
|
if (track is! SpotubeTrackObject_Full) 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 as SpotubeFullTrackObject),
|
child: TrackDetailsDialog(
|
||||||
|
track: track.field0 as SpotubeFullTrackObject),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case TrackOptionValue.download:
|
case TrackOptionValue.download:
|
||||||
if (track is SpotubeLocalTrackObject) break;
|
if (track is SpotubeTrackObject_Local) break;
|
||||||
downloadManager.addToQueue(track as SpotubeFullTrackObject);
|
downloadManager.addToQueue(track.field0 as SpotubeFullTrackObject);
|
||||||
break;
|
break;
|
||||||
case TrackOptionValue.startRadio:
|
case TrackOptionValue.startRadio:
|
||||||
actionStartRadio(context);
|
actionStartRadio(context);
|
||||||
|
|||||||
@ -24,13 +24,14 @@ class SpotubeMedia extends mk.Media {
|
|||||||
final SpotubeTrackObject track;
|
final SpotubeTrackObject track;
|
||||||
SpotubeMedia(this.track)
|
SpotubeMedia(this.track)
|
||||||
: assert(
|
: assert(
|
||||||
track is SpotubeLocalTrackObject || track is SpotubeFullTrackObject,
|
track is SpotubeTrackObject_Local ||
|
||||||
|
((track.field0 as SpotubeFullTrackObject).isrc.isNotEmpty),
|
||||||
"Track must be a either a local track or a full track object with ISRC",
|
"Track must be a either a local track or a full track object with ISRC",
|
||||||
),
|
),
|
||||||
// If the track is a local track, use its path, otherwise use the server URL
|
// If the track is a local track, use its path, otherwise use the server URL
|
||||||
super(
|
super(
|
||||||
track is SpotubeLocalTrackObject
|
track is SpotubeTrackObject_Local
|
||||||
? track.path
|
? track.field0.path
|
||||||
: "http://$_host:$serverPort/stream/${track.id}",
|
: "http://$_host:$serverPort/stream/${track.id}",
|
||||||
extras: track.toJson(),
|
extras: track.toJson(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,78 +0,0 @@
|
|||||||
import 'package:hetu_spotube_plugin/hetu_spotube_plugin.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
|
|
||||||
class SharedPreferencesLocalStorage implements Localstorage {
|
|
||||||
final SharedPreferences _prefs;
|
|
||||||
final String pluginSlug;
|
|
||||||
|
|
||||||
SharedPreferencesLocalStorage(this._prefs, this.pluginSlug);
|
|
||||||
|
|
||||||
String prefix(String key) {
|
|
||||||
return 'spotube_plugin.$pluginSlug.$key';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> clear() {
|
|
||||||
return _prefs.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> containsKey(String key) async {
|
|
||||||
return _prefs.containsKey(prefix(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool?> getBool(String key) async {
|
|
||||||
return _prefs.getBool(prefix(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<double?> getDouble(String key) async {
|
|
||||||
return _prefs.getDouble(prefix(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<int?> getInt(String key) async {
|
|
||||||
return _prefs.getInt(prefix(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getString(String key) async {
|
|
||||||
return _prefs.getString(prefix(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List<String>?> getStringList(String key) async {
|
|
||||||
return _prefs.getStringList(prefix(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> remove(String key) async {
|
|
||||||
await _prefs.remove(prefix(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> setBool(String key, bool value) async {
|
|
||||||
await _prefs.setBool(prefix(key), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> setDouble(String key, double value) async {
|
|
||||||
await _prefs.setDouble(prefix(key), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> setInt(String key, int value) async {
|
|
||||||
await _prefs.setInt(prefix(key), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> setString(String key, String value) async {
|
|
||||||
await _prefs.setString(prefix(key), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> setStringList(String key, List<String> value) async {
|
|
||||||
await _prefs.setStringList(prefix(key), value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
import 'package:hetu_script/hetu_script.dart';
|
|
||||||
import 'package:hetu_script/values.dart';
|
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
|
||||||
|
|
||||||
class MetadataPluginAlbumEndpoint {
|
|
||||||
final Hetu hetu;
|
|
||||||
MetadataPluginAlbumEndpoint(this.hetu);
|
|
||||||
|
|
||||||
HTInstance get hetuMetadataAlbum =>
|
|
||||||
(hetu.fetch("metadataPlugin") as HTInstance).memberGet("album")
|
|
||||||
as HTInstance;
|
|
||||||
|
|
||||||
Future<SpotubeFullAlbumObject> getAlbum(String id) async {
|
|
||||||
final raw =
|
|
||||||
await hetuMetadataAlbum.invoke("getAlbum", positionalArgs: [id]) as Map;
|
|
||||||
|
|
||||||
return SpotubeFullAlbumObject.fromJson(
|
|
||||||
raw.cast<String, dynamic>(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<SpotubePaginationResponseObject<SpotubeFullTrackObject>> tracks(
|
|
||||||
String id, {
|
|
||||||
int? offset,
|
|
||||||
int? limit,
|
|
||||||
}) async {
|
|
||||||
final raw = await hetuMetadataAlbum.invoke(
|
|
||||||
"tracks",
|
|
||||||
positionalArgs: [id],
|
|
||||||
namedArgs: {
|
|
||||||
"offset": offset,
|
|
||||||
"limit": limit,
|
|
||||||
}..removeWhere((key, value) => value == null),
|
|
||||||
) as Map;
|
|
||||||
|
|
||||||
return SpotubePaginationResponseObject<SpotubeFullTrackObject>.fromJson(
|
|
||||||
raw.cast<String, dynamic>(),
|
|
||||||
(Map json) =>
|
|
||||||
SpotubeFullTrackObject.fromJson(json.cast<String, dynamic>()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>> releases({
|
|
||||||
int? offset,
|
|
||||||
int? limit,
|
|
||||||
}) async {
|
|
||||||
final raw = await hetuMetadataAlbum.invoke(
|
|
||||||
"releases",
|
|
||||||
namedArgs: {
|
|
||||||
"offset": offset,
|
|
||||||
"limit": limit,
|
|
||||||
}..removeWhere((key, value) => value == null),
|
|
||||||
) as Map;
|
|
||||||
|
|
||||||
return SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>.fromJson(
|
|
||||||
raw.cast<String, dynamic>(),
|
|
||||||
(Map json) =>
|
|
||||||
SpotubeSimpleAlbumObject.fromJson(json.cast<String, dynamic>()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> save(List<String> ids) async {
|
|
||||||
await hetuMetadataAlbum.invoke(
|
|
||||||
"save",
|
|
||||||
positionalArgs: [ids],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> unsave(List<String> ids) async {
|
|
||||||
await hetuMetadataAlbum.invoke(
|
|
||||||
"unsave",
|
|
||||||
positionalArgs: [ids],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user