mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Compare commits
4 Commits
005355e267
...
d22b5349a3
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d22b5349a3 | ||
![]() |
83172f198c | ||
![]() |
f870e12011 | ||
![]() |
345c6ac714 |
@ -38,6 +38,7 @@ class HeartButton extends HookConsumerWidget {
|
|||||||
child: IconButton(
|
child: IconButton(
|
||||||
variance: variance,
|
variance: variance,
|
||||||
size: size,
|
size: size,
|
||||||
|
enabled: onPressed != null,
|
||||||
icon: AnimatedSwitcher(
|
icon: AnimatedSwitcher(
|
||||||
switchInCurve: Curves.fastOutSlowIn,
|
switchInCurve: Curves.fastOutSlowIn,
|
||||||
switchOutCurve: Curves.fastOutSlowIn,
|
switchOutCurve: Curves.fastOutSlowIn,
|
||||||
@ -74,7 +75,8 @@ class TrackHeartButton extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final savedTracks = ref.watch(metadataPluginSavedTracksProvider);
|
final savedTracks = ref.watch(metadataPluginSavedTracksProvider);
|
||||||
final me = ref.watch(metadataPluginUserProvider);
|
final me = ref.watch(metadataPluginUserProvider);
|
||||||
final (:isLiked, :toggleTrackLike) = useTrackToggleLike(track, ref);
|
final (:isLiked, :isLoading, :toggleTrackLike) =
|
||||||
|
useTrackToggleLike(track, ref);
|
||||||
|
|
||||||
if (me.isLoading) {
|
if (me.isLoading) {
|
||||||
return const CircularProgressIndicator();
|
return const CircularProgressIndicator();
|
||||||
@ -85,11 +87,11 @@ class TrackHeartButton extends HookConsumerWidget {
|
|||||||
? context.l10n.remove_from_favorites
|
? context.l10n.remove_from_favorites
|
||||||
: context.l10n.save_as_favorite,
|
: context.l10n.save_as_favorite,
|
||||||
isLiked: isLiked,
|
isLiked: isLiked,
|
||||||
onPressed: savedTracks.asData?.value != null
|
onPressed: savedTracks.asData?.value == null || isLoading
|
||||||
? () {
|
? null
|
||||||
|
: () {
|
||||||
toggleTrackLike(track);
|
toggleTrackLike(track);
|
||||||
}
|
},
|
||||||
: null,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
|
|||||||
|
|
||||||
typedef UseTrackToggleLike = ({
|
typedef UseTrackToggleLike = ({
|
||||||
bool isLiked,
|
bool isLiked,
|
||||||
|
bool isLoading,
|
||||||
Future<void> Function(SpotubeTrackObject track) toggleTrackLike,
|
Future<void> Function(SpotubeTrackObject track) toggleTrackLike,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -11,12 +12,11 @@ UseTrackToggleLike useTrackToggleLike(SpotubeTrackObject track, WidgetRef ref) {
|
|||||||
final savedTracksNotifier =
|
final savedTracksNotifier =
|
||||||
ref.watch(metadataPluginSavedTracksProvider.notifier);
|
ref.watch(metadataPluginSavedTracksProvider.notifier);
|
||||||
|
|
||||||
final isSavedTrack = ref.watch(
|
final isSavedTrack = ref.watch(metadataPluginIsSavedTrackProvider(track.id));
|
||||||
metadataPluginIsSavedTrackProvider(track.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isLiked: isSavedTrack.asData?.value ?? false,
|
isLiked: isSavedTrack.asData?.value ?? false,
|
||||||
|
isLoading: isSavedTrack.isLoading,
|
||||||
toggleTrackLike: (track) async {
|
toggleTrackLike: (track) async {
|
||||||
final isLikedTrack = await ref.read(
|
final isLikedTrack = await ref.read(
|
||||||
metadataPluginIsSavedTrackProvider(track.id).future,
|
metadataPluginIsSavedTrackProvider(track.id).future,
|
||||||
|
@ -14,6 +14,7 @@ import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
|||||||
class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
||||||
final Widget title;
|
final Widget title;
|
||||||
final List<T> items;
|
final List<T> items;
|
||||||
|
final Widget? error;
|
||||||
final VoidCallback onFetchMore;
|
final VoidCallback onFetchMore;
|
||||||
final bool isLoadingNextPage;
|
final bool isLoadingNextPage;
|
||||||
final bool hasNextPage;
|
final bool hasNextPage;
|
||||||
@ -26,6 +27,7 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
|||||||
required this.onFetchMore,
|
required this.onFetchMore,
|
||||||
required this.isLoadingNextPage,
|
required this.isLoadingNextPage,
|
||||||
this.titleTrailing,
|
this.titleTrailing,
|
||||||
|
this.error,
|
||||||
super.key,
|
super.key,
|
||||||
}) : assert(
|
}) : assert(
|
||||||
items.every(
|
items.every(
|
||||||
@ -64,54 +66,57 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
|||||||
if (titleTrailing != null) titleTrailing!,
|
if (titleTrailing != null) titleTrailing!,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(
|
if (error != null)
|
||||||
height: isArtist ? 250 : 225,
|
error!
|
||||||
child: NotificationListener(
|
else
|
||||||
// disable multiple scrollbar to use this
|
SizedBox(
|
||||||
onNotification: (notification) => true,
|
height: isArtist ? 250 : 225,
|
||||||
child: ScrollConfiguration(
|
child: NotificationListener(
|
||||||
behavior: ScrollConfiguration.of(context).copyWith(
|
// disable multiple scrollbar to use this
|
||||||
dragDevices: PointerDeviceKind.values.toSet(),
|
onNotification: (notification) => true,
|
||||||
),
|
child: ScrollConfiguration(
|
||||||
child: items.isEmpty
|
behavior: ScrollConfiguration.of(context).copyWith(
|
||||||
? ListView.builder(
|
dragDevices: PointerDeviceKind.values.toSet(),
|
||||||
scrollDirection: Axis.horizontal,
|
),
|
||||||
itemCount: 5,
|
child: items.isEmpty
|
||||||
itemBuilder: (context, index) {
|
? ListView.builder(
|
||||||
return AlbumCard(FakeData.albumSimple);
|
scrollDirection: Axis.horizontal,
|
||||||
},
|
itemCount: 5,
|
||||||
)
|
itemBuilder: (context, index) {
|
||||||
: InfiniteList(
|
return AlbumCard(FakeData.albumSimple);
|
||||||
scrollController: scrollController,
|
},
|
||||||
scrollDirection: Axis.horizontal,
|
)
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
: InfiniteList(
|
||||||
itemCount: items.length,
|
scrollController: scrollController,
|
||||||
onFetchData: onFetchMore,
|
scrollDirection: Axis.horizontal,
|
||||||
loadingBuilder: (context) => Skeletonizer(
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
enabled: true,
|
itemCount: items.length,
|
||||||
child: isArtist
|
onFetchData: onFetchMore,
|
||||||
? ArtistCard(FakeData.artist)
|
loadingBuilder: (context) => Skeletonizer(
|
||||||
: AlbumCard(FakeData.albumSimple),
|
enabled: true,
|
||||||
),
|
child: isArtist
|
||||||
isLoading: isLoadingNextPage,
|
? ArtistCard(FakeData.artist)
|
||||||
hasReachedMax: !hasNextPage,
|
: AlbumCard(FakeData.albumSimple),
|
||||||
separatorBuilder: (context, index) => Gap(12 * scale),
|
),
|
||||||
itemBuilder: (context, index) {
|
isLoading: isLoadingNextPage,
|
||||||
final item = items[index];
|
hasReachedMax: !hasNextPage,
|
||||||
|
separatorBuilder: (context, index) => Gap(12 * scale),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = items[index];
|
||||||
|
|
||||||
return switch (item) {
|
return switch (item) {
|
||||||
SpotubeSimplePlaylistObject() =>
|
SpotubeSimplePlaylistObject() => PlaylistCard(
|
||||||
PlaylistCard(item as SpotubeSimplePlaylistObject),
|
item as SpotubeSimplePlaylistObject),
|
||||||
SpotubeSimpleAlbumObject() =>
|
SpotubeSimpleAlbumObject() =>
|
||||||
AlbumCard(item as SpotubeSimpleAlbumObject),
|
AlbumCard(item as SpotubeSimpleAlbumObject),
|
||||||
SpotubeFullArtistObject() =>
|
SpotubeFullArtistObject() =>
|
||||||
ArtistCard(item as SpotubeFullArtistObject),
|
ArtistCard(item as SpotubeFullArtistObject),
|
||||||
_ => const SizedBox.shrink(),
|
_ => const SizedBox.shrink(),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -6,6 +6,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
|
|||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotube/collections/fake.dart';
|
import 'package:spotube/collections/fake.dart';
|
||||||
|
import 'package:spotube/components/fallbacks/error_box.dart';
|
||||||
import 'package:spotube/components/track_presentation/presentation_props.dart';
|
import 'package:spotube/components/track_presentation/presentation_props.dart';
|
||||||
import 'package:spotube/components/track_presentation/presentation_state.dart';
|
import 'package:spotube/components/track_presentation/presentation_state.dart';
|
||||||
import 'package:spotube/components/track_presentation/use_track_tile_play_callback.dart';
|
import 'package:spotube/components/track_presentation/use_track_tile_play_callback.dart';
|
||||||
@ -30,6 +31,19 @@ class PresentationListSection extends HookConsumerWidget {
|
|||||||
final onTileTap = useTrackTilePlayCallback(ref);
|
final onTileTap = useTrackTilePlayCallback(ref);
|
||||||
|
|
||||||
if (state.presentationTracks.isEmpty && !options.pagination.isLoading) {
|
if (state.presentationTracks.isEmpty && !options.pagination.isLoading) {
|
||||||
|
if (options.error != null) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: ErrorBox(
|
||||||
|
error: options.error!,
|
||||||
|
onRetry: options.pagination.onRefresh,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
@ -50,6 +50,7 @@ class TrackPresentationOptions {
|
|||||||
final PaginationProps pagination;
|
final PaginationProps pagination;
|
||||||
final bool isLiked;
|
final bool isLiked;
|
||||||
final String? shareUrl;
|
final String? shareUrl;
|
||||||
|
final Object? error;
|
||||||
|
|
||||||
// events
|
// events
|
||||||
final FutureOr<bool?> Function()? onHeart; // if null heart button will hidden
|
final FutureOr<bool?> Function()? onHeart; // if null heart button will hidden
|
||||||
@ -67,6 +68,7 @@ class TrackPresentationOptions {
|
|||||||
this.shareUrl,
|
this.shareUrl,
|
||||||
this.isLiked = false,
|
this.isLiked = false,
|
||||||
this.onHeart,
|
this.onHeart,
|
||||||
|
this.error,
|
||||||
}) : assert(collection is SpotubeSimpleAlbumObject ||
|
}) : assert(collection is SpotubeSimpleAlbumObject ||
|
||||||
collection is SpotubeSimplePlaylistObject);
|
collection is SpotubeSimplePlaylistObject);
|
||||||
|
|
||||||
@ -90,7 +92,8 @@ class TrackPresentationOptions {
|
|||||||
other.pagination == pagination &&
|
other.pagination == pagination &&
|
||||||
other.isLiked == isLiked &&
|
other.isLiked == isLiked &&
|
||||||
other.shareUrl == shareUrl &&
|
other.shareUrl == shareUrl &&
|
||||||
other.onHeart == onHeart;
|
other.onHeart == onHeart &&
|
||||||
|
other.error == error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -105,5 +108,6 @@ class TrackPresentationOptions {
|
|||||||
pagination.hashCode ^
|
pagination.hashCode ^
|
||||||
isLiked.hashCode ^
|
isLiked.hashCode ^
|
||||||
shareUrl.hashCode ^
|
shareUrl.hashCode ^
|
||||||
onHeart.hashCode;
|
onHeart.hashCode ^
|
||||||
|
error.hashCode;
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
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/fallbacks/error_box.dart';
|
||||||
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/album/releases.dart';
|
import 'package:spotube/provider/metadata_plugin/album/releases.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
import 'package:spotube/provider/metadata_plugin/core/auth.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';
|
||||||
|
|
||||||
class HomeNewReleasesSection extends HookConsumerWidget {
|
class HomeNewReleasesSection extends HookConsumerWidget {
|
||||||
const HomeNewReleasesSection({super.key});
|
const HomeNewReleasesSection({super.key});
|
||||||
@ -24,12 +26,30 @@ class HomeNewReleasesSection extends HookConsumerWidget {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newReleases.error
|
||||||
|
case MetadataPluginException(
|
||||||
|
errorCode: MetadataPluginErrorCode.noDefaultPlugin,
|
||||||
|
message: _,
|
||||||
|
)) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
return HorizontalPlaybuttonCardView<SpotubeSimpleAlbumObject>(
|
return HorizontalPlaybuttonCardView<SpotubeSimpleAlbumObject>(
|
||||||
items: newReleases.asData?.value.items ?? [],
|
items: newReleases.asData?.value.items ?? [],
|
||||||
title: Text(context.l10n.new_releases),
|
title: Text(context.l10n.new_releases),
|
||||||
isLoadingNextPage: newReleases.isLoadingNextPage,
|
isLoadingNextPage: newReleases.isLoadingNextPage,
|
||||||
hasNextPage: newReleases.asData?.value.hasMore ?? false,
|
hasNextPage: newReleases.asData?.value.hasMore ?? false,
|
||||||
onFetchMore: newReleasesNotifier.fetchMore,
|
onFetchMore: newReleasesNotifier.fetchMore,
|
||||||
|
error: newReleases.hasError
|
||||||
|
? Center(
|
||||||
|
child: ErrorBox(
|
||||||
|
error: newReleases.error!,
|
||||||
|
onRetry: () {
|
||||||
|
ref.invalidate(metadataPluginAlbumReleasesProvider);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,7 @@ class AlbumPage extends HookConsumerWidget {
|
|||||||
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: tracks.asData?.value.items ?? [],
|
||||||
|
error: tracks.error,
|
||||||
pagination: PaginationProps(
|
pagination: PaginationProps(
|
||||||
hasNextPage: tracks.asData?.value.hasMore ?? false,
|
hasNextPage: tracks.asData?.value.hasMore ?? false,
|
||||||
isLoading: tracks.isLoading || tracks.isLoadingNextPage,
|
isLoading: tracks.isLoading || tracks.isLoadingNextPage,
|
||||||
|
@ -99,7 +99,7 @@ class UserAlbumsPage extends HookConsumerWidget {
|
|||||||
features: const [
|
features: const [
|
||||||
InputFeature.leading(Icon(SpotubeIcons.filter))
|
InputFeature.leading(Icon(SpotubeIcons.filter))
|
||||||
],
|
],
|
||||||
placeholder: Text(context.l10n.filter_artist),
|
placeholder: Text(context.l10n.filter_albums),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -121,7 +121,7 @@ class UserAlbumsPage extends HookConsumerWidget {
|
|||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
context.l10n.not_following_artists,
|
context.l10n.no_favorite_albums_yet,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
).muted().small()
|
).muted().small()
|
||||||
],
|
],
|
||||||
|
@ -8,6 +8,7 @@ import 'package:spotube/models/metadata/metadata.dart';
|
|||||||
import 'package:spotube/pages/playlist/playlist.dart';
|
import 'package:spotube/pages/playlist/playlist.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
|
import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class LikedPlaylistPage extends HookConsumerWidget {
|
class LikedPlaylistPage extends HookConsumerWidget {
|
||||||
@ -22,6 +23,8 @@ class LikedPlaylistPage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final likedTracks = ref.watch(metadataPluginSavedTracksProvider);
|
final likedTracks = ref.watch(metadataPluginSavedTracksProvider);
|
||||||
|
final likedTracksNotifier =
|
||||||
|
ref.watch(metadataPluginSavedTracksProvider.notifier);
|
||||||
final tracks = likedTracks.asData?.value.items ?? [];
|
final tracks = likedTracks.asData?.value.items ?? [];
|
||||||
|
|
||||||
return material.RefreshIndicator.adaptive(
|
return material.RefreshIndicator.adaptive(
|
||||||
@ -33,11 +36,13 @@ class LikedPlaylistPage extends HookConsumerWidget {
|
|||||||
collection: playlist,
|
collection: playlist,
|
||||||
image: Assets.images.likedTracks.path,
|
image: Assets.images.likedTracks.path,
|
||||||
pagination: PaginationProps(
|
pagination: PaginationProps(
|
||||||
hasNextPage: false,
|
hasNextPage: likedTracks.asData?.value.hasMore ?? false,
|
||||||
isLoading: likedTracks.isLoading,
|
isLoading: likedTracks.isLoadingNextPage && !likedTracks.isLoading,
|
||||||
onFetchMore: () {},
|
onFetchMore: () async {
|
||||||
|
await likedTracksNotifier.fetchMore();
|
||||||
|
},
|
||||||
onFetchAll: () async {
|
onFetchAll: () async {
|
||||||
return tracks.toList();
|
return await likedTracksNotifier.fetchAll();
|
||||||
},
|
},
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
ref.invalidate(metadataPluginSavedTracksProvider);
|
ref.invalidate(metadataPluginSavedTracksProvider);
|
||||||
@ -46,6 +51,7 @@ class LikedPlaylistPage extends HookConsumerWidget {
|
|||||||
title: playlist.name,
|
title: playlist.name,
|
||||||
description: playlist.description,
|
description: playlist.description,
|
||||||
tracks: tracks,
|
tracks: tracks,
|
||||||
|
error: likedTracks.error,
|
||||||
routePath: '/playlist/${playlist.id}',
|
routePath: '/playlist/${playlist.id}',
|
||||||
isLiked: false,
|
isLiked: false,
|
||||||
shareUrl: null,
|
shareUrl: null,
|
||||||
|
@ -79,6 +79,7 @@ class PlaylistPage extends HookConsumerWidget {
|
|||||||
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: tracks.asData?.value.items ?? [],
|
||||||
|
error: tracks.error,
|
||||||
routePath: '/playlist/${playlist.id}',
|
routePath: '/playlist/${playlist.id}',
|
||||||
isLiked: isFavoritePlaylist.asData?.value ?? false,
|
isLiked: isFavoritePlaylist.asData?.value ?? false,
|
||||||
shareUrl: playlist.externalUri,
|
shareUrl: playlist.externalUri,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:riverpod/riverpod.dart';
|
import 'package:riverpod/riverpod.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
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 MetadataPluginSavedAlbumNotifier
|
class MetadataPluginSavedAlbumNotifier
|
||||||
@ -18,38 +18,50 @@ class MetadataPluginSavedAlbumNotifier
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
build() async {
|
build() async {
|
||||||
ref.watch(metadataPluginProvider);
|
await ref.watch(metadataPluginAuthenticatedProvider.future);
|
||||||
return await fetch(0, 20);
|
return await fetch(0, 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addFavorite(List<SpotubeSimpleAlbumObject> albums) async {
|
Future<void> addFavorite(List<SpotubeSimpleAlbumObject> albums) async {
|
||||||
await update((state) async {
|
if (albums.isEmpty || state.value == null) return;
|
||||||
(await metadataPlugin).album.save(albums.map((e) => e.id).toList());
|
final oldState = state.value;
|
||||||
return state.copyWith(
|
|
||||||
items: [...state.items, ...albums],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (final album in albums) {
|
state = AsyncData(
|
||||||
ref.invalidate(metadataPluginIsSavedAlbumProvider(album.id));
|
state.value!.copyWith(
|
||||||
|
items: [
|
||||||
|
...albums,
|
||||||
|
...state.value!.items,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await (await metadataPlugin).album.save(albums.map((e) => e.id).toList());
|
||||||
|
} catch (e) {
|
||||||
|
state = AsyncData(oldState!);
|
||||||
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeFavorite(List<SpotubeSimpleAlbumObject> albums) async {
|
Future<void> removeFavorite(List<SpotubeSimpleAlbumObject> albums) async {
|
||||||
await update((state) async {
|
if (albums.isEmpty || state.value == null) return;
|
||||||
final albumIds = albums.map((e) => e.id).toList();
|
|
||||||
(await metadataPlugin).album.unsave(albumIds);
|
final oldState = state.value;
|
||||||
return state.copyWith(
|
|
||||||
items: state.items
|
final albumIds = albums.map((e) => e.id).toList();
|
||||||
|
state = AsyncData(
|
||||||
|
state.value!.copyWith(
|
||||||
|
items: state.value!.items
|
||||||
.where(
|
.where(
|
||||||
(e) => albumIds.contains((e).id) == false,
|
(e) => albumIds.contains((e).id) == false,
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
),
|
||||||
});
|
);
|
||||||
|
try {
|
||||||
for (final album in albums) {
|
await (await metadataPlugin).album.unsave(albumIds);
|
||||||
ref.invalidate(metadataPluginIsSavedAlbumProvider(album.id));
|
} catch (e) {
|
||||||
|
state = AsyncData(oldState!);
|
||||||
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -63,9 +75,14 @@ final metadataPluginSavedAlbumsProvider = AsyncNotifierProvider<
|
|||||||
final metadataPluginIsSavedAlbumProvider =
|
final metadataPluginIsSavedAlbumProvider =
|
||||||
FutureProvider.autoDispose.family<bool, String>(
|
FutureProvider.autoDispose.family<bool, String>(
|
||||||
(ref, albumId) async {
|
(ref, albumId) async {
|
||||||
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
final savedAlbums =
|
||||||
|
await ref.watch(metadataPluginSavedAlbumsProvider.future);
|
||||||
|
final savedAlbumsNotifier =
|
||||||
|
ref.read(metadataPluginSavedAlbumsProvider.notifier);
|
||||||
|
final allSavedAlbums = savedAlbums.hasMore
|
||||||
|
? await savedAlbumsNotifier.fetchAll()
|
||||||
|
: savedAlbums.items;
|
||||||
|
|
||||||
return metadataPlugin!.user
|
return allSavedAlbums.any((element) => element.id == albumId);
|
||||||
.isSavedAlbums([albumId]).then((value) => value.first);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:riverpod/riverpod.dart';
|
import 'package:riverpod/riverpod.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
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 MetadataPluginSavedArtistNotifier
|
class MetadataPluginSavedArtistNotifier
|
||||||
@ -20,38 +20,53 @@ class MetadataPluginSavedArtistNotifier
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
build() async {
|
build() async {
|
||||||
ref.watch(metadataPluginProvider);
|
await ref.watch(metadataPluginAuthenticatedProvider.future);
|
||||||
return await fetch(0, 20);
|
return await fetch(0, 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addFavorite(List<SpotubeFullArtistObject> artists) async {
|
Future<void> addFavorite(List<SpotubeFullArtistObject> artists) async {
|
||||||
await update((state) async {
|
if (artists.isEmpty || state.value == null) return;
|
||||||
(await metadataPlugin).artist.save(artists.map((e) => e.id).toList());
|
final oldState = state.value;
|
||||||
return state.copyWith(
|
|
||||||
items: [...state.items, ...artists],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (final artist in artists) {
|
state = AsyncData(
|
||||||
ref.invalidate(metadataPluginIsSavedArtistProvider(artist.id));
|
state.value!.copyWith(
|
||||||
|
items: [
|
||||||
|
...artists,
|
||||||
|
...state.value!.items,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await (await metadataPlugin)
|
||||||
|
.artist
|
||||||
|
.save(artists.map((e) => e.id).toList());
|
||||||
|
} catch (e) {
|
||||||
|
state = AsyncData(oldState!);
|
||||||
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeFavorite(List<SpotubeFullArtistObject> artists) async {
|
Future<void> removeFavorite(List<SpotubeFullArtistObject> artists) async {
|
||||||
await update((state) async {
|
if (artists.isEmpty || state.value == null) return;
|
||||||
final artistIds = artists.map((e) => e.id).toList();
|
|
||||||
(await metadataPlugin).artist.unsave(artistIds);
|
final oldState = state.value;
|
||||||
return state.copyWith(
|
|
||||||
items: state.items
|
final artistIds = artists.map((e) => e.id).toList();
|
||||||
|
state = AsyncData(
|
||||||
|
state.value!.copyWith(
|
||||||
|
items: state.value!.items
|
||||||
.where(
|
.where(
|
||||||
(e) => artistIds.contains((e).id) == false,
|
(e) => artistIds.contains((e).id) == false,
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
),
|
||||||
});
|
);
|
||||||
|
|
||||||
for (final artist in artists) {
|
try {
|
||||||
ref.invalidate(metadataPluginIsSavedArtistProvider(artist.id));
|
await (await metadataPlugin).artist.unsave(artistIds);
|
||||||
|
} catch (e) {
|
||||||
|
state = AsyncData(oldState!);
|
||||||
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -65,9 +80,15 @@ final metadataPluginSavedArtistsProvider = AsyncNotifierProvider<
|
|||||||
final metadataPluginIsSavedArtistProvider =
|
final metadataPluginIsSavedArtistProvider =
|
||||||
FutureProvider.autoDispose.family<bool, String>(
|
FutureProvider.autoDispose.family<bool, String>(
|
||||||
(ref, artistId) async {
|
(ref, artistId) async {
|
||||||
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
final savedArtists =
|
||||||
|
await ref.watch(metadataPluginSavedArtistsProvider.future);
|
||||||
|
final savedArtistsNotifier =
|
||||||
|
ref.read(metadataPluginSavedArtistsProvider.notifier);
|
||||||
|
|
||||||
return metadataPlugin!.user
|
final allSavedArtists = savedArtists.hasMore
|
||||||
.isSavedArtists([artistId]).then((value) => value.first);
|
? await savedArtistsNotifier.fetchAll()
|
||||||
|
: savedArtists.items;
|
||||||
|
|
||||||
|
return allSavedArtists.any((element) => element.id == artistId);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart';
|
import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
|
import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
|
||||||
@ -21,7 +22,8 @@ class MetadataPluginSavedPlaylistsNotifier
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
build() async {
|
build() async {
|
||||||
ref.watch(metadataPluginProvider);
|
await ref.watch(metadataPluginAuthenticatedProvider.future);
|
||||||
|
|
||||||
final playlists = await fetch(0, 20);
|
final playlists = await fetch(0, 20);
|
||||||
|
|
||||||
return playlists;
|
return playlists;
|
||||||
@ -42,25 +44,43 @@ class MetadataPluginSavedPlaylistsNotifier
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addFavorite(SpotubeSimplePlaylistObject playlist) async {
|
Future<void> addFavorite(SpotubeSimplePlaylistObject playlist) async {
|
||||||
await update((state) async {
|
if (state.value == null) return;
|
||||||
(await metadataPlugin).playlist.save(playlist.id);
|
|
||||||
return state.copyWith(
|
|
||||||
items: [...state.items, playlist],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ref.invalidate(metadataPluginIsSavedPlaylistProvider(playlist.id));
|
final oldState = state.value;
|
||||||
|
|
||||||
|
state = AsyncData(
|
||||||
|
state.value!.copyWith(
|
||||||
|
items: [
|
||||||
|
playlist,
|
||||||
|
...state.value!.items,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await (await metadataPlugin).playlist.save(playlist.id);
|
||||||
|
} catch (e) {
|
||||||
|
state = AsyncData(oldState!);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeFavorite(SpotubeSimplePlaylistObject playlist) async {
|
Future<void> removeFavorite(SpotubeSimplePlaylistObject playlist) async {
|
||||||
await update((state) async {
|
if (state.value == null) return;
|
||||||
(await metadataPlugin).playlist.unsave(playlist.id);
|
|
||||||
return state.copyWith(
|
|
||||||
items: state.items.where((e) => (e).id != playlist.id).toList(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ref.invalidate(metadataPluginIsSavedPlaylistProvider(playlist.id));
|
final oldState = state.value;
|
||||||
|
state = AsyncData(
|
||||||
|
state.value!.copyWith(
|
||||||
|
items: state.value!.items.where((e) => (e).id != playlist.id).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await (await metadataPlugin).playlist.unsave(playlist.id);
|
||||||
|
} catch (e) {
|
||||||
|
state = AsyncData(oldState!);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> delete(String playlistId) async {
|
Future<void> delete(String playlistId) async {
|
||||||
@ -114,8 +134,16 @@ final metadataPluginIsSavedPlaylistProvider =
|
|||||||
throw MetadataPluginException.noDefaultPlugin();
|
throw MetadataPluginException.noDefaultPlugin();
|
||||||
}
|
}
|
||||||
|
|
||||||
final follows = await plugin.user.isSavedPlaylist(id);
|
final savedPlaylists =
|
||||||
|
await ref.watch(metadataPluginSavedPlaylistsProvider.future);
|
||||||
|
|
||||||
return follows;
|
final savedPlaylistsNotifier =
|
||||||
|
ref.read(metadataPluginSavedPlaylistsProvider.notifier);
|
||||||
|
|
||||||
|
final allSavedPlaylists = savedPlaylists.hasMore
|
||||||
|
? await savedPlaylistsNotifier.fetchAll()
|
||||||
|
: savedPlaylists.items;
|
||||||
|
|
||||||
|
return allSavedPlaylists.any((element) => element.id == id);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
|
import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
|
||||||
|
|
||||||
@ -22,20 +22,57 @@ class MetadataPluginSavedTracksNotifier
|
|||||||
build() async {
|
build() async {
|
||||||
ref.cacheFor();
|
ref.cacheFor();
|
||||||
|
|
||||||
ref.watch(metadataPluginProvider);
|
await ref.watch(metadataPluginAuthenticatedProvider.future);
|
||||||
return await fetch(0, 20);
|
return await fetch(0, 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addFavorite(List<SpotubeTrackObject> tracks) async {
|
Future<void> addFavorite(List<SpotubeTrackObject> tracks) async {
|
||||||
await (await metadataPlugin).track.save(tracks.map((e) => e.id).toList());
|
if (state.value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ref.invalidateSelf();
|
final oldState = state.value;
|
||||||
|
state = AsyncData(
|
||||||
|
state.value!.copyWith(
|
||||||
|
items: [
|
||||||
|
...tracks.whereType<SpotubeFullTrackObject>(),
|
||||||
|
...state.value!.items
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await (await metadataPlugin).track.save(tracks.map((e) => e.id).toList());
|
||||||
|
} catch (e) {
|
||||||
|
state = AsyncData(oldState!);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeFavorite(List<SpotubeTrackObject> tracks) async {
|
Future<void> removeFavorite(List<SpotubeTrackObject> tracks) async {
|
||||||
await (await metadataPlugin).track.unsave(tracks.map((e) => e.id).toList());
|
if (state.value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ref.invalidateSelf();
|
final oldState = state.value;
|
||||||
|
state = AsyncData(
|
||||||
|
state.value!.copyWith(
|
||||||
|
items: state.value!.items
|
||||||
|
.where(
|
||||||
|
(savedTrack) => !tracks.any((track) => track.id == savedTrack.id),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await (await metadataPlugin)
|
||||||
|
.track
|
||||||
|
.unsave(tracks.map((e) => e.id).toList());
|
||||||
|
} catch (e) {
|
||||||
|
state = AsyncData(oldState!);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,9 +85,11 @@ final metadataPluginSavedTracksProvider = AutoDisposeAsyncNotifierProvider<
|
|||||||
final metadataPluginIsSavedTrackProvider =
|
final metadataPluginIsSavedTrackProvider =
|
||||||
FutureProvider.autoDispose.family<bool, String>(
|
FutureProvider.autoDispose.family<bool, String>(
|
||||||
(ref, trackId) async {
|
(ref, trackId) async {
|
||||||
await ref.watch(metadataPluginSavedTracksProvider.future);
|
final savedTracks =
|
||||||
final allSavedTracks =
|
await ref.watch(metadataPluginSavedTracksProvider.future);
|
||||||
await ref.read(metadataPluginSavedTracksProvider.notifier).fetchAll();
|
final allSavedTracks = savedTracks.hasMore
|
||||||
|
? await ref.read(metadataPluginSavedTracksProvider.notifier).fetchAll()
|
||||||
|
: savedTracks.items;
|
||||||
|
|
||||||
return allSavedTracks.any((track) => track.id == trackId);
|
return allSavedTracks.any((track) => track.id == trackId);
|
||||||
},
|
},
|
||||||
|
@ -12,22 +12,24 @@ abstract class FamilyPaginatedAsyncNotifier<K, A>
|
|||||||
Future<void> fetchMore() async {
|
Future<void> fetchMore() async {
|
||||||
if (state.value == null || !state.value!.hasMore) return;
|
if (state.value == null || !state.value!.hasMore) return;
|
||||||
|
|
||||||
state = AsyncLoadingNext(state.asData!.value);
|
final oldState = state.value;
|
||||||
|
|
||||||
state = await AsyncValue.guard(
|
try {
|
||||||
() async {
|
state = AsyncLoadingNext(state.asData!.value);
|
||||||
final newState = await fetch(
|
|
||||||
state.value!.nextOffset!,
|
|
||||||
state.value!.limit,
|
|
||||||
);
|
|
||||||
|
|
||||||
final oldItems =
|
final newState = await fetch(
|
||||||
state.value!.items.isEmpty ? <K>[] : state.value!.items.cast<K>();
|
state.value!.nextOffset!,
|
||||||
final items = newState.items.isEmpty ? <K>[] : newState.items.cast<K>();
|
state.value!.limit,
|
||||||
|
);
|
||||||
|
|
||||||
return newState.copyWith(items: <K>[...oldItems, ...items]);
|
final oldItems =
|
||||||
},
|
state.value!.items.isEmpty ? <K>[] : state.value!.items.cast<K>();
|
||||||
);
|
final items = newState.items.isEmpty ? <K>[] : newState.items.cast<K>();
|
||||||
|
|
||||||
|
state = AsyncData(newState.copyWith(items: <K>[...oldItems, ...items]));
|
||||||
|
} finally {
|
||||||
|
state = AsyncData(oldState!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<K>> fetchAll() async {
|
Future<List<K>> fetchAll() async {
|
||||||
@ -60,21 +62,25 @@ abstract class AutoDisposeFamilyPaginatedAsyncNotifier<K, A>
|
|||||||
|
|
||||||
Future<void> fetchMore() async {
|
Future<void> fetchMore() async {
|
||||||
if (state.value == null || !state.value!.hasMore) return;
|
if (state.value == null || !state.value!.hasMore) return;
|
||||||
|
final oldState = state.value;
|
||||||
|
|
||||||
state = AsyncLoadingNext(state.asData!.value);
|
try {
|
||||||
|
state = AsyncLoadingNext(state.value!);
|
||||||
|
|
||||||
state = await AsyncValue.guard(
|
final newState = await fetch(
|
||||||
() async {
|
state.value!.nextOffset!,
|
||||||
final newState = await fetch(
|
state.value!.limit,
|
||||||
state.value!.nextOffset!,
|
);
|
||||||
state.value!.limit,
|
|
||||||
);
|
state = AsyncData(
|
||||||
return newState.copyWith(items: [
|
newState.copyWith(items: [
|
||||||
...state.value!.items.cast<K>(),
|
...state.value!.items.cast<K>(),
|
||||||
...newState.items.cast<K>(),
|
...newState.items.cast<K>(),
|
||||||
]);
|
]),
|
||||||
},
|
);
|
||||||
);
|
} finally {
|
||||||
|
state = AsyncData(oldState!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<K>> fetchAll() async {
|
Future<List<K>> fetchAll() async {
|
||||||
|
@ -14,22 +14,23 @@ mixin PaginatedAsyncNotifierMixin<K>
|
|||||||
Future<void> fetchMore() async {
|
Future<void> fetchMore() async {
|
||||||
if (state.value == null || !state.value!.hasMore) return;
|
if (state.value == null || !state.value!.hasMore) return;
|
||||||
|
|
||||||
state = AsyncLoadingNext(state.asData!.value);
|
final oldState = state.value;
|
||||||
|
try {
|
||||||
|
state = AsyncLoadingNext(state.asData!.value);
|
||||||
|
|
||||||
state = await AsyncValue.guard(
|
final newState = await fetch(
|
||||||
() async {
|
state.value!.nextOffset!,
|
||||||
final newState = await fetch(
|
state.value!.limit,
|
||||||
state.value!.nextOffset!,
|
);
|
||||||
state.value!.limit,
|
|
||||||
);
|
|
||||||
|
|
||||||
final oldItems =
|
final oldItems =
|
||||||
state.value!.items.isEmpty ? <K>[] : state.value!.items.cast<K>();
|
state.value!.items.isEmpty ? <K>[] : state.value!.items.cast<K>();
|
||||||
final items = newState.items.isEmpty ? <K>[] : newState.items.cast<K>();
|
final items = newState.items.isEmpty ? <K>[] : newState.items.cast<K>();
|
||||||
|
|
||||||
return newState.copyWith(items: <K>[...oldItems, ...items]);
|
state = AsyncData(newState.copyWith(items: <K>[...oldItems, ...items]));
|
||||||
},
|
} finally {
|
||||||
);
|
state = AsyncData(oldState!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<K>> fetchAll() async {
|
Future<List<K>> fetchAll() async {
|
||||||
|
@ -48,6 +48,7 @@ class ServerPlaybackRoutes {
|
|||||||
|
|
||||||
Future<({dio_lib.Response<Uint8List> response, Uint8List? bytes})>
|
Future<({dio_lib.Response<Uint8List> response, Uint8List? bytes})>
|
||||||
streamTrack(
|
streamTrack(
|
||||||
|
Request request,
|
||||||
SourcedTrack track,
|
SourcedTrack track,
|
||||||
Map<String, dynamic> headers,
|
Map<String, dynamic> headers,
|
||||||
) async {
|
) async {
|
||||||
@ -59,35 +60,6 @@ class ServerPlaybackRoutes {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
final trackPartialCacheFile = File("${trackCacheFile.path}.part");
|
|
||||||
|
|
||||||
String? url = track.url;
|
|
||||||
|
|
||||||
url ??= await ref
|
|
||||||
.read(trackSourcesProvider(track.query).notifier)
|
|
||||||
.swapWithNextSibling()
|
|
||||||
.then((track) => track.url!);
|
|
||||||
|
|
||||||
var options = Options(
|
|
||||||
headers: {
|
|
||||||
...headers,
|
|
||||||
"user-agent": _randomUserAgent,
|
|
||||||
"Cache-Control": "max-age=3600",
|
|
||||||
"Connection": "keep-alive",
|
|
||||||
"host": Uri.parse(url!).host,
|
|
||||||
},
|
|
||||||
responseType: ResponseType.bytes,
|
|
||||||
validateStatus: (status) => status! < 400,
|
|
||||||
);
|
|
||||||
|
|
||||||
final headersRes = await Future<dio_lib.Response?>.value(
|
|
||||||
dio.head(
|
|
||||||
url,
|
|
||||||
options: options,
|
|
||||||
),
|
|
||||||
).catchError((_) async => null);
|
|
||||||
|
|
||||||
final contentLength = headersRes?.headers.value("content-length");
|
|
||||||
|
|
||||||
if (await trackCacheFile.exists() && userPreferences.cacheMusic) {
|
if (await trackCacheFile.exists() && userPreferences.cacheMusic) {
|
||||||
final bytes = await trackCacheFile.readAsBytes();
|
final bytes = await trackCacheFile.readAsBytes();
|
||||||
@ -102,12 +74,51 @@ class ServerPlaybackRoutes {
|
|||||||
"accept-ranges": ["bytes"],
|
"accept-ranges": ["bytes"],
|
||||||
"content-range": ["bytes 0-$cachedFileLength/$cachedFileLength"],
|
"content-range": ["bytes 0-$cachedFileLength/$cachedFileLength"],
|
||||||
}),
|
}),
|
||||||
requestOptions: RequestOptions(path: url),
|
requestOptions: RequestOptions(path: request.requestedUri.toString()),
|
||||||
),
|
),
|
||||||
bytes: bytes,
|
bytes: bytes,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final trackPartialCacheFile = File("${trackCacheFile.path}.part");
|
||||||
|
|
||||||
|
String url = track.url ??
|
||||||
|
await ref
|
||||||
|
.read(trackSourcesProvider(track.query).notifier)
|
||||||
|
.swapWithNextSibling()
|
||||||
|
.then((track) => track.url!);
|
||||||
|
|
||||||
|
var options = Options(
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
"user-agent": _randomUserAgent,
|
||||||
|
"Cache-Control": "max-age=3600",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"host": Uri.parse(url).host,
|
||||||
|
},
|
||||||
|
responseType: ResponseType.bytes,
|
||||||
|
validateStatus: (status) => status! < 400,
|
||||||
|
);
|
||||||
|
|
||||||
|
final contentLengthRes = await Future<dio_lib.Response?>.value(
|
||||||
|
dio.head(
|
||||||
|
url,
|
||||||
|
options: options,
|
||||||
|
),
|
||||||
|
).catchError((e, stack) async {
|
||||||
|
AppLogger.reportError(e, stack);
|
||||||
|
|
||||||
|
final sourcedTrack = await ref
|
||||||
|
.read(trackSourcesProvider(track.query).notifier)
|
||||||
|
.refreshStreamingUrl();
|
||||||
|
|
||||||
|
url = sourcedTrack.url!;
|
||||||
|
|
||||||
|
return dio.head(url, options: options);
|
||||||
|
});
|
||||||
|
|
||||||
|
final contentLength = contentLengthRes?.headers.value("content-length");
|
||||||
|
|
||||||
/// Forcing partial content range as mpv sometimes greedily wants
|
/// Forcing partial content range as mpv sometimes greedily wants
|
||||||
/// everything at one go. Slows down overall streaming.
|
/// everything at one go. Slows down overall streaming.
|
||||||
final range = RangeHeader.parse(headers["range"] ?? "");
|
final range = RangeHeader.parse(headers["range"] ?? "");
|
||||||
@ -123,33 +134,7 @@ class ServerPlaybackRoutes {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final res = await dio
|
final res = await dio.get<Uint8List>(url, options: options);
|
||||||
.get<Uint8List>(
|
|
||||||
url,
|
|
||||||
options: options.copyWith(headers: {
|
|
||||||
...?options.headers,
|
|
||||||
"user-agent": _randomUserAgent,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.catchError((e, stack) async {
|
|
||||||
AppLogger.reportError(e, stack);
|
|
||||||
final sourcedTrack = await ref
|
|
||||||
.read(trackSourcesProvider(track.query).notifier)
|
|
||||||
.refreshStreamingUrl();
|
|
||||||
|
|
||||||
// It gets updated by itself.
|
|
||||||
// if (playlist.activeTrack?.id == sourcedTrack.query.id) {
|
|
||||||
// ref.read(activeTrackSourcesProvider.notifier).update(sourcedTrack);
|
|
||||||
// }
|
|
||||||
|
|
||||||
return await dio.get<Uint8List>(
|
|
||||||
sourcedTrack.url!,
|
|
||||||
options: options.copyWith(headers: {
|
|
||||||
...?options.headers,
|
|
||||||
"user-agent": _randomUserAgent,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
final bytes = res.data;
|
final bytes = res.data;
|
||||||
|
|
||||||
@ -228,8 +213,11 @@ class ServerPlaybackRoutes {
|
|||||||
).future,
|
).future,
|
||||||
);
|
);
|
||||||
|
|
||||||
final (bytes: audioBytes, response: res) =
|
final (bytes: audioBytes, response: res) = await streamTrack(
|
||||||
await streamTrack(sourcedTrack!, request.headers);
|
request,
|
||||||
|
sourcedTrack!,
|
||||||
|
request.headers,
|
||||||
|
);
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
res.statusCode!,
|
res.statusCode!,
|
||||||
|
Loading…
Reference in New Issue
Block a user