Merge pull request #1687 from KRTirtho/refactor/storage-n-providers

refactor: migrate to sqlite based storage from hive
This commit is contained in:
Kingkor Roy Tirtho 2024-07-06 21:38:52 +06:00 committed by GitHub
commit 243a843033
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
180 changed files with 11075 additions and 3561 deletions

View File

@ -53,7 +53,7 @@ body:
description: Where did you install Spotube from? description: Where did you install Spotube from?
multiple: true multiple: true
options: options:
- "Website (spotube.netlify.app) or (spotube.krtirtho.dev)" - "Website (spotube.krtirtho.dev)"
- "GitHub Releases (Binary)" - "GitHub Releases (Binary)"
- "GitHub Actions (Nightly Binary)" - "GitHub Actions (Nightly Binary)"
- "Play Store (Android)" - "Play Store (Android)"

View File

@ -8,3 +8,10 @@ targets:
options: options:
any_map: true any_map: true
explicit_to_json: true explicit_to_json: true
drift_dev:
options:
sql:
dialect: sqlite
options:
modules:
- json1

View File

@ -1,21 +0,0 @@
abstract class LocalStorageKeys {
static String saveTrackLyrics = 'save_track_lyrics';
static String recommendationMarket = 'recommendation_market';
static String ytSearchFormate = 'youtube_search_format';
static String clientId = 'clientId';
static String clientSecret = 'clientSecret';
static String accessToken = 'accessToken';
static String refreshToken = 'refreshToken';
static String expiration = "expiration";
static String geniusAccessToken = "genius_access_token";
static String themeMode = "theme_mode";
static String nextTrackHotKey = "next_track_hot_key";
static String prevTrackHotKey = "prev_track_hot_key";
static String playPauseHotKey = "play_pause_hot_key";
static String volume = "volume";
static String windowSizeInfo = "window_size_info";
}

View File

@ -1,6 +1,8 @@
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/spotify/home_feed.dart'; import 'package:spotube/models/spotify/home_feed.dart';
import 'package:spotube/models/spotify_friends.dart'; import 'package:spotube/models/spotify_friends.dart';
import 'package:spotube/provider/history/summary.dart';
abstract class FakeData { abstract class FakeData {
static final Image image = Image() static final Image image = Image()
@ -222,4 +224,36 @@ abstract class FakeData {
) )
], ],
); );
static const historySummary = PlaybackHistorySummary(
albums: 1,
artists: 1,
duration: Duration(seconds: 1),
playlists: 1,
tracks: 1,
fees: 1,
);
static final historyRecentlyPlayedPlaylist = HistoryTableData(
id: 0,
type: HistoryEntryType.track,
createdAt: DateTime.now(),
itemId: "1",
data: playlist.toJson(),
);
static final historyRecentlyPlayedAlbum = HistoryTableData(
id: 0,
type: HistoryEntryType.track,
createdAt: DateTime.now(),
itemId: "1",
data: album.toJson(),
);
static final historyRecentlyPlayedItems = List.generate(
10,
(index) => index % 2 == 0
? historyRecentlyPlayedPlaylist
: historyRecentlyPlayedAlbum,
);
} }

View File

@ -11,7 +11,7 @@ import 'package:spotube/pages/home/home.dart';
import 'package:spotube/pages/library/library.dart'; import 'package:spotube/pages/library/library.dart';
import 'package:spotube/pages/lyrics/lyrics.dart'; import 'package:spotube/pages/lyrics/lyrics.dart';
import 'package:spotube/pages/search/search.dart'; import 'package:spotube/pages/search/search.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/querying_track_info.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
@ -96,8 +96,8 @@ class SeekIntent extends Intent {
class SeekAction extends Action<SeekIntent> { class SeekAction extends Action<SeekIntent> {
@override @override
invoke(intent) async { invoke(intent) async {
final playlist = intent.ref.read(proxyPlaylistProvider); final isFetchingActiveTrack = intent.ref.read(queryingTrackInfoProvider);
if (playlist.isFetching) { if (isFetchingActiveTrack) {
DirectionalFocusAction().invoke( DirectionalFocusAction().invoke(
DirectionalFocusIntent( DirectionalFocusIntent(
intent.forward ? TraversalDirection.right : TraversalDirection.left, intent.forward ? TraversalDirection.right : TraversalDirection.left,
@ -105,7 +105,7 @@ class SeekAction extends Action<SeekIntent> {
); );
return null; return null;
} }
final position = (await audioPlayer.position ?? Duration.zero).inSeconds; final position = audioPlayer.position.inSeconds;
await audioPlayer.seek( await audioPlayer.seek(
Duration( Duration(
seconds: intent.forward ? position + 5 : position - 5, seconds: intent.forward ? position + 5 : position - 5,

View File

@ -32,7 +32,7 @@ import 'package:spotube/pages/stats/playlists/playlists.dart';
import 'package:spotube/pages/stats/stats.dart'; import 'package:spotube/pages/stats/stats.dart';
import 'package:spotube/pages/stats/streams/streams.dart'; import 'package:spotube/pages/stats/streams/streams.dart';
import 'package:spotube/pages/track/track.dart'; import 'package:spotube/pages/track/track.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/components/spotube_page_route.dart'; import 'package:spotube/components/spotube_page_route.dart';
@ -59,11 +59,9 @@ final routerProvider = Provider((ref) {
path: "/", path: "/",
name: HomePage.name, name: HomePage.name,
redirect: (context, state) async { redirect: (context, state) async {
final authNotifier = ref.read(authenticationProvider.notifier); final auth = await ref.read(authenticationProvider.future);
final json = await authNotifier.box.get(authNotifier.cacheKey);
if (json?["cookie"] == null && if (auth == null && !KVStoreService.doneGettingStarted) {
!KVStoreService.doneGettingStarted) {
return "/getting-started"; return "/getting-started";
} }

View File

@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/settings/settings.dart'; import 'package:spotube/pages/settings/settings.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
class AnonymousFallback extends ConsumerWidget { class AnonymousFallback extends ConsumerWidget {

View File

@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/heart_button/use_track_toggle_like.dart'; import 'package:spotube/components/heart_button/use_track_toggle_like.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class HeartButton extends HookConsumerWidget { class HeartButton extends HookConsumerWidget {
@ -26,7 +26,7 @@ class HeartButton extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(authenticationProvider); final auth = ref.watch(authenticationProvider);
if (auth == null) return const SizedBox.shrink(); if (auth.asData?.value == null) return const SizedBox.shrink();
return IconButton( return IconButton(
tooltip: tooltip, tooltip: tooltip,

View File

@ -1,7 +1,7 @@
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/provider/scrobbler_provider.dart'; import 'package:spotube/provider/scrobbler/scrobbler.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
typedef UseTrackToggleLike = ({ typedef UseTrackToggleLike = ({

View File

@ -18,12 +18,13 @@ import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify_provider.dart';
@ -95,8 +96,8 @@ class TrackOptions extends HookConsumerWidget {
WidgetRef ref, WidgetRef ref,
Track track, Track track,
) async { ) async {
final playback = ref.read(proxyPlaylistProvider.notifier); final playback = ref.read(audioPlayerProvider.notifier);
final playlist = ref.read(proxyPlaylistProvider); final playlist = ref.read(audioPlayerProvider);
final spotify = ref.read(spotifyProvider); final spotify = ref.read(spotifyProvider);
final query = "${track.name} Radio"; final query = "${track.name} Radio";
final pages = final pages =
@ -159,8 +160,8 @@ class TrackOptions extends HookConsumerWidget {
final router = GoRouter.of(context); final router = GoRouter.of(context);
final ThemeData(:colorScheme) = Theme.of(context); final ThemeData(:colorScheme) = Theme.of(context);
final playlist = ref.watch(proxyPlaylistProvider); final playlist = ref.watch(audioPlayerProvider);
final playback = ref.watch(proxyPlaylistProvider.notifier); final playback = ref.watch(audioPlayerProvider.notifier);
final auth = ref.watch(authenticationProvider); final auth = ref.watch(authenticationProvider);
ref.watch(downloadManagerProvider); ref.watch(downloadManagerProvider);
final downloadManager = ref.watch(downloadManagerProvider.notifier); final downloadManager = ref.watch(downloadManagerProvider.notifier);
@ -170,11 +171,8 @@ class TrackOptions extends HookConsumerWidget {
final favorites = useTrackToggleLike(track, ref); final favorites = useTrackToggleLike(track, ref);
final isBlackListed = useMemoized( final isBlackListed = useMemoized(
() => blacklist.contains( () => blacklist.asData?.value.any(
BlacklistedElement.track( (element) => element.elementId == track.id,
track.id!,
track.name!,
),
), ),
[blacklist, track], [blacklist, track],
); );
@ -258,13 +256,16 @@ class TrackOptions extends HookConsumerWidget {
.removeTracks(playlistId ?? "", [track.id!]); .removeTracks(playlistId ?? "", [track.id!]);
break; break;
case TrackOptionValue.blacklist: case TrackOptionValue.blacklist:
if (isBlackListed) { if (isBlackListed == null) break;
ref.read(blacklistProvider.notifier).remove( if (isBlackListed == true) {
BlacklistedElement.track(track.id!, track.name!), await ref.read(blacklistProvider.notifier).remove(track.id!);
);
} else { } else {
ref.read(blacklistProvider.notifier).add( await ref.read(blacklistProvider.notifier).add(
BlacklistedElement.track(track.id!, track.name!), BlacklistTableCompanion.insert(
name: track.name!,
elementId: track.id!,
elementType: BlacklistedType.track,
),
); );
} }
break; break;
@ -363,7 +364,7 @@ class TrackOptions extends HookConsumerWidget {
: context.l10n.save_as_favorite, : context.l10n.save_as_favorite,
), ),
), ),
if (auth != null && !isLocalTrack) ...[ if (auth.asData?.value != null && !isLocalTrack) ...[
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.startRadio, value: TrackOptionValue.startRadio,
leading: const Icon(SpotubeIcons.radio), leading: const Icon(SpotubeIcons.radio),
@ -375,7 +376,7 @@ class TrackOptions extends HookConsumerWidget {
title: Text(context.l10n.add_to_playlist), title: Text(context.l10n.add_to_playlist),
), ),
], ],
if (userPlaylist && auth != null && !isLocalTrack) if (userPlaylist && auth.asData?.value != null && !isLocalTrack)
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.removeFromPlaylist, value: TrackOptionValue.removeFromPlaylist,
leading: const Icon(SpotubeIcons.removeFilled), leading: const Icon(SpotubeIcons.removeFilled),
@ -399,10 +400,10 @@ class TrackOptions extends HookConsumerWidget {
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.blacklist, value: TrackOptionValue.blacklist,
leading: const Icon(SpotubeIcons.playlistRemove), leading: const Icon(SpotubeIcons.playlistRemove),
iconColor: !isBlackListed ? Colors.red[400] : null, iconColor: isBlackListed != true ? Colors.red[400] : null,
textColor: !isBlackListed ? Colors.red[400] : null, textColor: isBlackListed != true ? Colors.red[400] : null,
title: Text( title: Text(
isBlackListed isBlackListed == true
? context.l10n.remove_from_blacklist ? context.l10n.remove_from_blacklist
: context.l10n.add_to_blacklist, : context.l10n.add_to_blacklist,
), ),

View File

@ -17,8 +17,9 @@ import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/duration.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/audio_player/querying_track_info.dart';
import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
class TrackTile extends HookConsumerWidget { class TrackTile extends HookConsumerWidget {
/// [index] will not be shown if null /// [index] will not be shown if null
@ -30,7 +31,7 @@ class TrackTile extends HookConsumerWidget {
final VoidCallback? onLongPress; final VoidCallback? onLongPress;
final bool userPlaylist; final bool userPlaylist;
final String? playlistId; final String? playlistId;
final ProxyPlaylist playlist; final AudioPlayerState playlist;
final List<Widget>? leadingActions; final List<Widget>? leadingActions;
@ -53,14 +54,10 @@ class TrackTile extends HookConsumerWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final blacklist = ref.watch(blacklistProvider); final blacklist = ref.watch(blacklistProvider);
final blacklistNotifier = ref.watch(blacklistProvider.notifier);
final isBlackListed = useMemoized( final isBlackListed = useMemoized(
() => blacklist.contains( () => blacklistNotifier.contains(track),
BlacklistedElement.track(
track.id!,
track.name!,
),
),
[blacklist, track], [blacklist, track],
); );
@ -87,187 +84,190 @@ class TrackTile extends HookConsumerWidget {
}, },
child: HoverBuilder( child: HoverBuilder(
permanentState: isSelected || constrains.smAndDown ? true : null, permanentState: isSelected || constrains.smAndDown ? true : null,
builder: (context, isHovering) { builder: (context, isHovering) => ListTile(
return ListTile( selected: isSelected,
selected: isSelected, onTap: () async {
onTap: () async { try {
try { isLoading.value = true;
isLoading.value = true; await onTap?.call();
await onTap?.call(); } finally {
} finally { if (context.mounted) {
if (context.mounted) { isLoading.value = false;
isLoading.value = false;
}
} }
}, }
onLongPress: onLongPress, },
enabled: !isBlackListed, onLongPress: onLongPress,
contentPadding: EdgeInsets.zero, enabled: !isBlackListed,
tileColor: contentPadding: EdgeInsets.zero,
isBlackListed ? theme.colorScheme.errorContainer : null, tileColor: isBlackListed ? theme.colorScheme.errorContainer : null,
horizontalTitleGap: 12, horizontalTitleGap: 12,
leadingAndTrailingTextStyle: theme.textTheme.bodyMedium, leadingAndTrailingTextStyle: theme.textTheme.bodyMedium,
leading: Row( leading: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
...?leadingActions, ...?leadingActions,
if (index != null && onChanged == null && constrains.mdAndUp) if (index != null && onChanged == null && constrains.mdAndUp)
SizedBox( SizedBox(
width: 50, width: 50,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6), padding: const EdgeInsets.symmetric(horizontal: 6),
child: Text( child: Text(
'${(index ?? 0) + 1}', '${(index ?? 0) + 1}',
maxLines: 1, maxLines: 1,
style: theme.textTheme.bodySmall, style: theme.textTheme.bodySmall,
textAlign: TextAlign.center, textAlign: TextAlign.center,
),
), ),
)
else if (constrains.smAndDown)
const SizedBox(width: 16),
if (onChanged != null)
Checkbox(
value: selected,
onChanged: onChanged,
), ),
Stack( )
children: [ else if (constrains.smAndDown)
ClipRRect( const SizedBox(width: 16),
borderRadius: BorderRadius.circular(4), if (onChanged != null)
child: AspectRatio( Checkbox(
aspectRatio: 1, value: selected,
child: UniversalImage( onChanged: onChanged,
path: (track.album?.images).asUrlString(
placeholder: ImagePlaceholder.albumArt,
),
fit: BoxFit.cover,
),
),
),
Positioned.fill(
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: isHovering
? Colors.black.withOpacity(0.4)
: Colors.transparent,
),
),
),
Positioned.fill(
child: Center(
child: IconTheme(
data: theme.iconTheme
.copyWith(size: 26, color: Colors.white),
child: Skeleton.ignore(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: (isPlaying && playlist.isFetching) ||
isLoading.value
? const SizedBox(
width: 26,
height: 26,
child: CircularProgressIndicator(
strokeWidth: 1.5,
color: Colors.white,
),
)
: isPlaying
? Icon(
SpotubeIcons.pause,
color: theme.colorScheme.primary,
)
: !isHovering
? const SizedBox.shrink()
: const Icon(SpotubeIcons.play),
),
),
),
),
),
],
), ),
], Stack(
), children: [
title: Row( ClipRRect(
children: [ borderRadius: BorderRadius.circular(4),
Expanded( child: AspectRatio(
flex: 6, aspectRatio: 1,
child: switch (track) { child: UniversalImage(
LocalTrack() => Text( path: (track.album?.images).asUrlString(
track.name!, placeholder: ImagePlaceholder.albumArt,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
_ => LinkText(
track.name!,
"/track/${track.id}",
push: true,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
},
),
if (constrains.mdAndUp) ...[
const SizedBox(width: 8),
Expanded(
flex: 4,
child: switch (track) {
LocalTrack() => Text(
track.album!.name!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
_ => Align( fit: BoxFit.cover,
alignment: Alignment.centerLeft, ),
child: LinkText( ),
track.album!.name!, ),
"/album/${track.album?.id}", Positioned.fill(
extra: track.album, child: AnimatedContainer(
push: true, duration: const Duration(milliseconds: 300),
overflow: TextOverflow.ellipsis, decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: isHovering
? Colors.black.withOpacity(0.4)
: Colors.transparent,
),
),
),
Positioned.fill(
child: Center(
child: IconTheme(
data: theme.iconTheme
.copyWith(size: 26, color: Colors.white),
child: Skeleton.ignore(
child: Consumer(
builder: (context, ref, _) {
final isFetchingActiveTrack =
ref.watch(queryingTrackInfoProvider);
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: (isPlaying && isFetchingActiveTrack) ||
isLoading.value
? const SizedBox(
width: 26,
height: 26,
child: CircularProgressIndicator(
strokeWidth: 1.5,
color: Colors.white,
),
)
: isPlaying
? Icon(
SpotubeIcons.pause,
color: theme.colorScheme.primary,
)
: !isHovering
? const SizedBox.shrink()
: const Icon(SpotubeIcons.play),
);
},
), ),
) ),
}, ),
),
), ),
], ],
], ),
), ],
subtitle: Align( ),
alignment: Alignment.centerLeft, title: Row(
child: track is LocalTrack children: [
? Text( Expanded(
track.artists?.asString() ?? '', flex: 6,
) child: switch (track) {
: ClipRect( LocalTrack() => Text(
child: ConstrainedBox( track.name!,
constraints: const BoxConstraints(maxHeight: 40), maxLines: 1,
child: ArtistLink(artists: track.artists ?? []), overflow: TextOverflow.ellipsis,
),
), ),
), _ => LinkText(
trailing: Row( track.name!,
mainAxisSize: MainAxisSize.min, "/track/${track.id}",
children: [ push: true,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
},
),
if (constrains.mdAndUp) ...[
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Expanded(
Duration(milliseconds: track.durationMs ?? 0) flex: 4,
.toHumanReadableString(padZero: false), child: switch (track) {
maxLines: 1, LocalTrack() => Text(
overflow: TextOverflow.ellipsis, track.album!.name!,
), maxLines: 1,
TrackOptions( overflow: TextOverflow.ellipsis,
track: track, ),
playlistId: playlistId, _ => Align(
userPlaylist: userPlaylist, alignment: Alignment.centerLeft,
showMenuCbRef: showOptionCbRef, child: LinkText(
track.album!.name!,
"/album/${track.album?.id}",
extra: track.album,
push: true,
overflow: TextOverflow.ellipsis,
),
)
},
), ),
], ],
), ],
); ),
}, subtitle: Align(
alignment: Alignment.centerLeft,
child: track is LocalTrack
? Text(
track.artists?.asString() ?? '',
)
: ClipRect(
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 40),
child: ArtistLink(artists: track.artists ?? []),
),
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(width: 8),
Text(
Duration(milliseconds: track.durationMs ?? 0)
.toHumanReadableString(padZero: false),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
TrackOptions(
track: track,
playlistId: playlistId,
userPlaylist: userPlaylist,
showMenuCbRef: showOptionCbRef,
),
],
),
),
), ),
); );
}); });

View File

@ -18,7 +18,7 @@ import 'package:spotube/components/tracks_view/track_view_provider.dart';
import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/history/history.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
@ -27,9 +27,9 @@ class TrackViewBodySection extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final playlist = ref.watch(proxyPlaylistProvider); final playlist = ref.watch(audioPlayerProvider);
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final historyNotifier = ref.watch(playbackHistoryProvider.notifier); final historyNotifier = ref.watch(playbackHistoryActionsProvider);
final props = InheritedTrackView.of(context); final props = InheritedTrackView.of(context);
final trackViewState = ref.watch(trackViewProvider(props.tracks)); final trackViewState = ref.watch(trackViewProvider(props.tracks));

View File

@ -8,11 +8,11 @@ import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart';
import 'package:spotube/components/tracks_view/track_view_props.dart'; import 'package:spotube/components/tracks_view/track_view_props.dart';
import 'package:spotube/components/tracks_view/track_view_provider.dart'; import 'package:spotube/components/tracks_view/track_view_provider.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/history/history.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
class TrackViewBodyOptions extends HookConsumerWidget { class TrackViewBodyOptions extends HookConsumerWidget {
const TrackViewBodyOptions({super.key}); const TrackViewBodyOptions({super.key});
@ -24,8 +24,8 @@ class TrackViewBodyOptions extends HookConsumerWidget {
ref.watch(downloadManagerProvider); ref.watch(downloadManagerProvider);
final downloader = ref.watch(downloadManagerProvider.notifier); final downloader = ref.watch(downloadManagerProvider.notifier);
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final historyNotifier = ref.watch(playbackHistoryProvider.notifier); final historyNotifier = ref.watch(playbackHistoryActionsProvider);
final audioSource = final audioSource =
ref.watch(userPreferencesProvider.select((s) => s.audioSource)); ref.watch(userPreferencesProvider.select((s) => s.audioSource));

View File

@ -9,9 +9,9 @@ import 'package:spotube/components/heart_button/heart_button.dart';
import 'package:spotube/components/tracks_view/sections/body/use_is_user_playlist.dart'; import 'package:spotube/components/tracks_view/sections/body/use_is_user_playlist.dart';
import 'package:spotube/components/tracks_view/track_view_props.dart'; import 'package:spotube/components/tracks_view/track_view_props.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/history/history.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
class TrackViewHeaderActions extends HookConsumerWidget { class TrackViewHeaderActions extends HookConsumerWidget {
const TrackViewHeaderActions({super.key}); const TrackViewHeaderActions({super.key});
@ -20,9 +20,9 @@ class TrackViewHeaderActions extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final props = InheritedTrackView.of(context); final props = InheritedTrackView.of(context);
final playlist = ref.watch(proxyPlaylistProvider); final playlist = ref.watch(audioPlayerProvider);
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final historyNotifier = ref.watch(playbackHistoryProvider.notifier); final historyNotifier = ref.watch(playbackHistoryActionsProvider);
final isActive = playlist.collections.contains(props.collectionId); final isActive = playlist.collections.contains(props.collectionId);
@ -73,7 +73,7 @@ class TrackViewHeaderActions extends HookConsumerWidget {
} }
}, },
), ),
if (props.onHeart != null && auth != null) if (props.onHeart != null && auth.asData?.value != null)
HeartButton( HeartButton(
isLiked: props.isLiked, isLiked: props.isLiked,
icon: isUserPlaylist ? SpotubeIcons.trash : null, icon: isUserPlaylist ? SpotubeIcons.trash : null,

View File

@ -13,7 +13,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/history/history.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
class TrackViewHeaderButtons extends HookConsumerWidget { class TrackViewHeaderButtons extends HookConsumerWidget {
@ -28,9 +28,9 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final props = InheritedTrackView.of(context); final props = InheritedTrackView.of(context);
final playlist = ref.watch(proxyPlaylistProvider); final playlist = ref.watch(audioPlayerProvider);
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final historyNotifier = ref.watch(playbackHistoryProvider.notifier); final historyNotifier = ref.watch(playbackHistoryActionsProvider);
final isActive = playlist.collections.contains(props.collectionId); final isActive = playlist.collections.contains(props.collectionId);
@ -131,7 +131,9 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
); );
} }
} finally { } finally {
isLoading.value = false; if (context.mounted) {
isLoading.value = false;
}
} }
} }

View File

@ -2,8 +2,9 @@ import 'dart:io';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/hooks/configurators/use_window_listener.dart'; import 'package:spotube/hooks/configurators/use_window_listener.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
import 'package:local_notifier/local_notifier.dart'; import 'package:local_notifier/local_notifier.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';

View File

@ -2,27 +2,27 @@ import 'package:spotube/services/logger/logger.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
void useEndlessPlayback(WidgetRef ref) { void useEndlessPlayback(WidgetRef ref) {
final auth = ref.watch(authenticationProvider); final auth = ref.watch(authenticationProvider);
final playback = ref.watch(proxyPlaylistProvider.notifier); final playback = ref.watch(audioPlayerProvider.notifier);
final playlist = ref.watch(proxyPlaylistProvider); final playlist = ref.watch(audioPlayerProvider.select((s) => s.playlist));
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
final endlessPlayback = final endlessPlayback =
ref.watch(userPreferencesProvider.select((s) => s.endlessPlayback)); ref.watch(userPreferencesProvider.select((s) => s.endlessPlayback));
useEffect( useEffect(
() { () {
if (!endlessPlayback || auth == null) return null; if (!endlessPlayback || auth.asData?.value == null) return null;
void listener(int index) async { void listener(int index) async {
try { try {
final playlist = ref.read(proxyPlaylistProvider); final playlist = ref.read(audioPlayerProvider);
if (index != playlist.tracks.length - 1) return; if (index != playlist.tracks.length - 1) return;
final track = playlist.tracks.last; final track = playlist.tracks.last;
@ -56,7 +56,7 @@ void useEndlessPlayback(WidgetRef ref) {
await playback.addTracks( await playback.addTracks(
tracks.toList() tracks.toList()
..removeWhere((e) { ..removeWhere((e) {
final playlist = ref.read(proxyPlaylistProvider); 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);
return e.id == track.id || isDuplicate; return e.id == track.id || isDuplicate;
}), }),
@ -69,9 +69,9 @@ void useEndlessPlayback(WidgetRef ref) {
// Sometimes user can change settings for which the currentIndexChanged // Sometimes user can change settings for which the currentIndexChanged
// might not be called. So we need to check if the current track is the // might not be called. So we need to check if the current track is the
// last track and if it is then we need to call the listener manually. // last track and if it is then we need to call the listener manually.
if (playlist.active == playlist.tracks.length - 1 && if (playlist.index == playlist.medias.length - 1 &&
audioPlayer.isPlaying) { audioPlayer.isPlaying) {
listener(playlist.active!); listener(playlist.index);
} }
final subscription = final subscription =
@ -82,7 +82,7 @@ void useEndlessPlayback(WidgetRef ref) {
[ [
spotify, spotify,
playback, playback,
playlist.tracks, playlist.medias,
endlessPlayback, endlessPlayback,
auth, auth,
], ],

View File

@ -18,22 +18,24 @@ import 'package:spotube/hooks/configurators/use_close_behavior.dart';
import 'package:spotube/hooks/configurators/use_deep_linking.dart'; import 'package:spotube/hooks/configurators/use_deep_linking.dart';
import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.dart'; import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.dart';
import 'package:spotube/hooks/configurators/use_get_storage_perms.dart'; import 'package:spotube/hooks/configurators/use_get_storage_perms.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/audio_player/audio_player_streams.dart';
import 'package:spotube/provider/database/database.dart';
import 'package:spotube/provider/server/bonsoir.dart'; import 'package:spotube/provider/server/bonsoir.dart';
import 'package:spotube/provider/server/server.dart'; import 'package:spotube/provider/server/server.dart';
import 'package:spotube/provider/tray_manager/tray_manager.dart'; import 'package:spotube/provider/tray_manager/tray_manager.dart';
import 'package:spotube/l10n/l10n.dart'; import 'package:spotube/l10n/l10n.dart';
import 'package:spotube/models/skip_segment.dart';
import 'package:spotube/models/source_match.dart';
import 'package:spotube/provider/connect/clients.dart'; import 'package:spotube/provider/connect/clients.dart';
import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/palette_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/cli/cli.dart'; import 'package:spotube/services/cli/cli.dart';
import 'package:spotube/services/kv_store/encrypted_kv_store.dart';
import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/wm_tools/wm_tools.dart'; import 'package:spotube/services/wm_tools/wm_tools.dart';
import 'package:spotube/themes/theme.dart'; import 'package:spotube/themes/theme.dart';
import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/migrations/hive.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:system_theme/system_theme.dart'; import 'package:system_theme/system_theme.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@ -78,39 +80,30 @@ Future<void> main(List<String> rawArgs) async {
} }
await KVStoreService.initialize(); await KVStoreService.initialize();
await EncryptedKvStoreService.initialize();
final hiveCacheDir = final hiveCacheDir =
kIsWeb ? null : (await getApplicationSupportDirectory()).path; kIsWeb ? null : (await getApplicationSupportDirectory()).path;
Hive.init(hiveCacheDir); Hive.init(hiveCacheDir);
Hive.registerAdapter(SkipSegmentAdapter()); final database = AppDatabase();
Hive.registerAdapter(SourceMatchAdapter()); await migrateFromHiveToDrift(database);
Hive.registerAdapter(SourceTypeAdapter());
// Cache versioning entities with Adapter
SourceMatch.version = 'v1';
SkipSegment.version = 'v1';
await Hive.openLazyBox<SourceMatch>(
SourceMatch.boxName,
path: hiveCacheDir,
);
await Hive.openLazyBox(
SkipSegment.boxName,
path: hiveCacheDir,
);
await PersistedStateNotifier.initializeBoxes(
path: hiveCacheDir,
);
if (kIsDesktop) { if (kIsDesktop) {
await localNotifier.setup(appName: "Spotube"); await localNotifier.setup(appName: "Spotube");
await WindowManagerTools.initialize(); await WindowManagerTools.initialize();
} }
runApp(const ProviderScope(child: Spotube())); runApp(
ProviderScope(
overrides: [
databaseProvider.overrideWith((ref) => database),
],
child: const Spotube(),
),
);
}); });
} }
@ -130,9 +123,10 @@ class Spotube extends HookConsumerWidget {
ref.watch(paletteProvider.select((s) => s?.dominantColor?.color)); ref.watch(paletteProvider.select((s) => s?.dominantColor?.color));
final router = ref.watch(routerProvider); final router = ref.watch(routerProvider);
ref.listen(serverProvider, (_, __) {}); ref.listen(audioPlayerStreamListenersProvider, (_, __) {});
ref.listen(bonsoirProvider, (_, __) {}); ref.listen(bonsoirProvider, (_, __) {});
ref.listen(connectClientsProvider, (_, __) {}); ref.listen(connectClientsProvider, (_, __) {});
ref.listen(serverProvider, (_, __) {});
ref.listen(trayManagerProvider, (_, __) {}); ref.listen(trayManagerProvider, (_, __) {});
useDisableBatteryOptimizations(); useDisableBatteryOptimizations();

View File

@ -4,9 +4,9 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:spotify/spotify.dart'; import 'package:media_kit/media_kit.dart' hide Track;
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; import 'package:spotify/spotify.dart' hide Playlist;
import 'package:spotube/services/audio_player/loop_mode.dart'; import 'package:spotube/provider/audio_player/state.dart';
part 'connect.freezed.dart'; part 'connect.freezed.dart';
part 'connect.g.dart'; part 'connect.g.dart';

View File

@ -183,7 +183,7 @@ class WebSocketEvent<T> {
if (type == WsEvent.loop) { if (type == WsEvent.loop) {
await callback( await callback(
WebSocketLoopEvent( WebSocketLoopEvent(
PlaybackLoopMode.fromString(data as String), PlaylistMode.values.firstWhere((e) => e.name == data as String),
), ),
); );
} }
@ -224,12 +224,16 @@ class WebSocketEvent<T> {
} }
} }
class WebSocketLoopEvent extends WebSocketEvent<PlaybackLoopMode> { class WebSocketLoopEvent extends WebSocketEvent<PlaylistMode> {
WebSocketLoopEvent(PlaybackLoopMode data) : super(WsEvent.loop, data); WebSocketLoopEvent(PlaylistMode data) : super(WsEvent.loop, data);
WebSocketLoopEvent.fromJson(Map<String, dynamic> json) WebSocketLoopEvent.fromJson(Map<String, dynamic> json)
: super( : super(
WsEvent.loop, PlaybackLoopMode.fromString(json["data"] as String)); WsEvent.loop,
PlaylistMode.values.firstWhere(
(e) => e.name == json["data"] as String,
),
);
@override @override
String toJson() { String toJson() {
@ -321,12 +325,12 @@ class WebSocketErrorEvent extends WebSocketEvent<String> {
WebSocketErrorEvent(String data) : super(WsEvent.error, data); WebSocketErrorEvent(String data) : super(WsEvent.error, data);
} }
class WebSocketQueueEvent extends WebSocketEvent<ProxyPlaylist> { class WebSocketQueueEvent extends WebSocketEvent<AudioPlayerState> {
WebSocketQueueEvent(ProxyPlaylist data) : super(WsEvent.queue, data); WebSocketQueueEvent(AudioPlayerState data) : super(WsEvent.queue, data);
factory WebSocketQueueEvent.fromJson(Map<String, dynamic> json) => factory WebSocketQueueEvent.fromJson(Map<String, dynamic> json) =>
WebSocketQueueEvent( WebSocketQueueEvent(
ProxyPlaylist.fromJsonRaw(json), AudioPlayerState.fromJson(json),
); );
} }

View File

@ -0,0 +1,85 @@
library database;
import 'dart:convert';
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:encrypt/encrypt.dart';
import 'package:media_kit/media_kit.dart' hide Track;
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:spotify/spotify.dart' hide Playlist;
import 'package:spotube/models/lyrics.dart';
import 'package:spotube/services/kv_store/encrypted_kv_store.dart';
import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:spotube/services/sourced_track/enums.dart';
import 'package:flutter/material.dart' hide Table, Key, View;
import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart';
import 'package:drift/native.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
part 'database.g.dart';
part 'tables/authentication.dart';
part 'tables/blacklist.dart';
part 'tables/preferences.dart';
part 'tables/scrobbler.dart';
part 'tables/skip_segment.dart';
part 'tables/source_match.dart';
part 'tables/audio_player_state.dart';
part 'tables/history.dart';
part 'tables/lyrics.dart';
part 'typeconverters/color.dart';
part 'typeconverters/locale.dart';
part 'typeconverters/string_list.dart';
part 'typeconverters/encrypted_text.dart';
part 'typeconverters/map.dart';
part 'typeconverters/subtitle.dart';
@DriftDatabase(
tables: [
AuthenticationTable,
BlacklistTable,
PreferencesTable,
ScrobblerTable,
SkipSegmentTable,
SourceMatchTable,
AudioPlayerStateTable,
PlaylistTable,
PlaylistMediaTable,
HistoryTable,
LyricsTable,
],
)
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
}
LazyDatabase _openConnection() {
// the LazyDatabase util lets us find the right location for the file async.
return LazyDatabase(() async {
// put the database file, called db.sqlite here, into the documents folder
// for your app.
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(join(dbFolder.path, 'db.sqlite'));
// Also work around limitations on old Android versions
if (Platform.isAndroid) {
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
}
// Make sqlite3 pick a more suitable location for temporary files - the
// one from the system may be inaccessible due to sandboxing.
final cacheBase = (await getTemporaryDirectory()).path;
// We can't access /tmp on Android, which sqlite3 would try by default.
// Explicitly tell it about the correct temporary directory.
sqlite3.tempDirectory = cacheBase;
return NativeDatabase.createInBackground(file);
});
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
part of '../database.dart';
class AudioPlayerStateTable extends Table {
IntColumn get id => integer().autoIncrement()();
BoolColumn get playing => boolean()();
TextColumn get loopMode => textEnum<PlaylistMode>()();
BoolColumn get shuffled => boolean()();
TextColumn get collections => text().map(const StringListConverter())();
}
class PlaylistTable extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get audioPlayerStateId =>
integer().references(AudioPlayerStateTable, #id)();
IntColumn get index => integer()();
}
class PlaylistMediaTable extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get playlistId => integer().references(PlaylistTable, #id)();
TextColumn get uri => text()();
TextColumn get extras =>
text().nullable().map(const MapTypeConverter<String, dynamic>())();
TextColumn get httpHeaders =>
text().nullable().map(const MapTypeConverter<String, String>())();
}

View File

@ -0,0 +1,8 @@
part of '../database.dart';
class AuthenticationTable extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get cookie => text().map(EncryptedTextConverter())();
TextColumn get accessToken => text().map(EncryptedTextConverter())();
DateTimeColumn get expiration => dateTime()();
}

View File

@ -0,0 +1,18 @@
part of '../database.dart';
enum BlacklistedType {
artist,
track;
}
@TableIndex(
name: "unique_blacklist",
unique: true,
columns: {#elementType, #elementId},
)
class BlacklistTable extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
TextColumn get elementType => textEnum<BlacklistedType>()();
TextColumn get elementId => text()();
}

View File

@ -0,0 +1,25 @@
part of '../database.dart';
enum HistoryEntryType {
playlist,
album,
track,
}
class HistoryTable extends Table {
IntColumn get id => integer().autoIncrement()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
TextColumn get type => textEnum<HistoryEntryType>()();
TextColumn get itemId => text()();
TextColumn get data =>
text().map(const MapTypeConverter<String, dynamic>())();
}
extension HistoryItemParseExtension on HistoryTableData {
PlaylistSimple? get playlist =>
type == HistoryEntryType.playlist ? PlaylistSimple.fromJson(data) : null;
AlbumSimple? get album =>
type == HistoryEntryType.album ? AlbumSimple.fromJson(data) : null;
Track? get track =>
type == HistoryEntryType.track ? Track.fromJson(data) : null;
}

View File

@ -0,0 +1,8 @@
part of '../database.dart';
class LyricsTable extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get trackId => text()();
TextColumn get data => text().map(SubtitleTypeConverter())();
}

View File

@ -0,0 +1,125 @@
part of '../database.dart';
enum LayoutMode {
compact,
extended,
adaptive,
}
enum CloseBehavior {
minimizeToTray,
close,
}
enum AudioSource {
youtube,
piped,
jiosaavn;
String get label => name[0].toUpperCase() + name.substring(1);
}
enum MusicCodec {
m4a._("M4a (Best for downloaded music)"),
weba._("WebA (Best for streamed music)\nDoesn't support audio metadata");
final String label;
const MusicCodec._(this.label);
}
enum SearchMode {
youtube._("YouTube"),
youtubeMusic._("YouTube Music");
final String label;
const SearchMode._(this.label);
factory SearchMode.fromString(String key) {
return SearchMode.values.firstWhere((e) => e.name == key);
}
}
class PreferencesTable extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get audioQuality => textEnum<SourceQualities>()
.withDefault(Constant(SourceQualities.high.name))();
BoolColumn get albumColorSync =>
boolean().withDefault(const Constant(true))();
BoolColumn get amoledDarkTheme =>
boolean().withDefault(const Constant(false))();
BoolColumn get checkUpdate => boolean().withDefault(const Constant(true))();
BoolColumn get normalizeAudio =>
boolean().withDefault(const Constant(false))();
BoolColumn get showSystemTrayIcon =>
boolean().withDefault(const Constant(false))();
BoolColumn get systemTitleBar =>
boolean().withDefault(const Constant(false))();
BoolColumn get skipNonMusic => boolean().withDefault(const Constant(false))();
TextColumn get closeBehavior => textEnum<CloseBehavior>()
.withDefault(Constant(CloseBehavior.close.name))();
TextColumn get accentColorScheme => text()
.withDefault(const Constant("Blue:0xFF2196F3"))
.map(const SpotubeColorConverter())();
TextColumn get layoutMode =>
textEnum<LayoutMode>().withDefault(Constant(LayoutMode.adaptive.name))();
TextColumn get locale => text()
.withDefault(
const Constant('{"languageCode":"system","countryCode":"system"}'),
)
.map(const LocaleConverter())();
TextColumn get market =>
textEnum<Market>().withDefault(Constant(Market.US.name))();
TextColumn get searchMode =>
textEnum<SearchMode>().withDefault(Constant(SearchMode.youtube.name))();
TextColumn get downloadLocation => text().withDefault(const Constant(""))();
TextColumn get localLibraryLocation =>
text().withDefault(const Constant("")).map(const StringListConverter())();
TextColumn get pipedInstance =>
text().withDefault(const Constant("https://pipedapi.kavin.rocks"))();
TextColumn get themeMode =>
textEnum<ThemeMode>().withDefault(Constant(ThemeMode.system.name))();
TextColumn get audioSource =>
textEnum<AudioSource>().withDefault(Constant(AudioSource.youtube.name))();
TextColumn get streamMusicCodec =>
textEnum<SourceCodecs>().withDefault(Constant(SourceCodecs.weba.name))();
TextColumn get downloadMusicCodec =>
textEnum<SourceCodecs>().withDefault(Constant(SourceCodecs.m4a.name))();
BoolColumn get discordPresence =>
boolean().withDefault(const Constant(true))();
BoolColumn get endlessPlayback =>
boolean().withDefault(const Constant(true))();
BoolColumn get enableConnect =>
boolean().withDefault(const Constant(false))();
// Default values as PreferencesTableData
static PreferencesTableData defaults() {
return PreferencesTableData(
id: 0,
audioQuality: SourceQualities.high,
albumColorSync: true,
amoledDarkTheme: false,
checkUpdate: true,
normalizeAudio: false,
showSystemTrayIcon: false,
systemTitleBar: false,
skipNonMusic: false,
closeBehavior: CloseBehavior.close,
accentColorScheme: SpotubeColor(Colors.blue.value, name: "Blue"),
layoutMode: LayoutMode.adaptive,
locale: const Locale("system", "system"),
market: Market.US,
searchMode: SearchMode.youtube,
downloadLocation: "",
localLibraryLocation: [],
pipedInstance: "https://pipedapi.kavin.rocks",
themeMode: ThemeMode.system,
audioSource: AudioSource.youtube,
streamMusicCodec: SourceCodecs.weba,
downloadMusicCodec: SourceCodecs.m4a,
discordPresence: true,
endlessPlayback: true,
enableConnect: false,
);
}
}

View File

@ -0,0 +1,8 @@
part of '../database.dart';
class ScrobblerTable extends Table {
IntColumn get id => integer().autoIncrement()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
TextColumn get username => text()();
TextColumn get passwordHash => text().map(EncryptedTextConverter())();
}

View File

@ -0,0 +1,9 @@
part of '../database.dart';
class SkipSegmentTable extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get start => integer()();
IntColumn get end => integer()();
TextColumn get trackId => text()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}

View File

@ -0,0 +1,25 @@
part of '../database.dart';
enum SourceType {
youtube._("YouTube"),
youtubeMusic._("YouTube Music"),
jiosaavn._("JioSaavn");
final String label;
const SourceType._(this.label);
}
@TableIndex(
name: "uniq_track_match",
columns: {#trackId, #sourceId, #sourceType},
unique: true,
)
class SourceMatchTable extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get trackId => text()();
TextColumn get sourceId => text()();
TextColumn get sourceType =>
textEnum<SourceType>().withDefault(Constant(SourceType.youtube.name))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}

View File

@ -0,0 +1,29 @@
part of '../database.dart';
class ColorConverter extends TypeConverter<Color, int> {
const ColorConverter();
@override
Color fromSql(int fromDb) {
return Color(fromDb);
}
@override
int toSql(Color value) {
return value.value;
}
}
class SpotubeColorConverter extends TypeConverter<SpotubeColor, String> {
const SpotubeColorConverter();
@override
SpotubeColor fromSql(String fromDb) {
return SpotubeColor.fromString(fromDb);
}
@override
String toSql(SpotubeColor value) {
return value.toString();
}
}

View File

@ -0,0 +1,44 @@
part of '../database.dart';
class DecryptedText {
final String value;
const DecryptedText(this.value);
static Encrypter? _encrypter;
factory DecryptedText.decrypted(String value) {
_encrypter ??= Encrypter(
Salsa20(
Key.fromUtf8(EncryptedKvStoreService.encryptionKeySync),
),
);
return DecryptedText(
_encrypter!.decrypt(
Encrypted.fromBase64(value),
iv: KVStoreService.ivKey,
),
);
}
String encrypt() {
_encrypter ??= Encrypter(
Salsa20(
Key.fromUtf8(EncryptedKvStoreService.encryptionKeySync),
),
);
return _encrypter!.encrypt(value, iv: KVStoreService.ivKey).base64;
}
}
class EncryptedTextConverter extends TypeConverter<DecryptedText, String> {
@override
DecryptedText fromSql(String fromDb) {
return DecryptedText.decrypted(fromDb);
}
@override
String toSql(DecryptedText value) {
return value.encrypt();
}
}

View File

@ -0,0 +1,19 @@
part of '../database.dart';
class LocaleConverter extends TypeConverter<Locale, String> {
const LocaleConverter();
@override
Locale fromSql(String fromDb) {
final rawMap = jsonDecode(fromDb) as Map<String, dynamic>;
return Locale(rawMap["languageCode"], rawMap["countryCode"]);
}
@override
String toSql(Locale value) {
return jsonEncode({
"languageCode": value.languageCode,
"countryCode": value.countryCode,
});
}
}

View File

@ -0,0 +1,15 @@
part of '../database.dart';
class MapTypeConverter<K, V> extends TypeConverter<Map<K, V>, String> {
const MapTypeConverter();
@override
fromSql(String fromDb) {
return json.decode(fromDb) as Map<K, V>;
}
@override
toSql(value) {
return json.encode(value);
}
}

View File

@ -0,0 +1,15 @@
part of '../database.dart';
class StringListConverter extends TypeConverter<List<String>, String> {
const StringListConverter();
@override
List<String> fromSql(String fromDb) {
return fromDb.split(",").where((e) => e.isNotEmpty).toList();
}
@override
String toSql(List<String> value) {
return value.join(",");
}
}

View File

@ -0,0 +1,13 @@
part of '../database.dart';
class SubtitleTypeConverter extends TypeConverter<SubtitleSimple, String> {
@override
SubtitleSimple fromSql(String fromDb) {
return SubtitleSimple.fromJson(jsonDecode(fromDb));
}
@override
String toSql(SubtitleSimple value) {
return jsonEncode(value.toJson());
}
}

View File

@ -1,25 +0,0 @@
import 'package:hive/hive.dart';
part 'skip_segment.g.dart';
@HiveType(typeId: 2)
class SkipSegment {
@HiveField(0)
final int start;
@HiveField(1)
final int end;
SkipSegment(this.start, this.end);
static String version = 'v1';
static final boxName = "oss.krtirtho.spotube.skip_segments.$version";
static LazyBox get box => Hive.lazyBox(boxName);
SkipSegment.fromJson(Map<String, dynamic> json)
: start = json['start'],
end = json['end'];
Map<String, dynamic> toJson() => {
'start': start,
'end': end,
};
}

View File

@ -1,44 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'skip_segment.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class SkipSegmentAdapter extends TypeAdapter<SkipSegment> {
@override
final int typeId = 2;
@override
SkipSegment read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return SkipSegment(
fields[0] as int,
fields[1] as int,
);
}
@override
void write(BinaryWriter writer, SkipSegment obj) {
writer
..writeByte(2)
..writeByte(0)
..write(obj.start)
..writeByte(1)
..write(obj.end);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SkipSegmentAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@ -1,54 +0,0 @@
import 'package:hive/hive.dart';
import 'package:json_annotation/json_annotation.dart';
part 'source_match.g.dart';
@JsonEnum()
@HiveType(typeId: 5)
enum SourceType {
@HiveField(0)
youtube._("YouTube"),
@HiveField(1)
youtubeMusic._("YouTube Music"),
@HiveField(2)
jiosaavn._("JioSaavn");
final String label;
const SourceType._(this.label);
}
@JsonSerializable()
@HiveType(typeId: 6)
class SourceMatch {
@HiveField(0)
String id;
@HiveField(1)
String sourceId;
@HiveField(2)
SourceType sourceType;
@HiveField(3)
DateTime createdAt;
SourceMatch({
required this.id,
required this.sourceId,
required this.sourceType,
required this.createdAt,
});
factory SourceMatch.fromJson(Map<String, dynamic> json) =>
_$SourceMatchFromJson(json);
Map<String, dynamic> toJson() => _$SourceMatchToJson(this);
static String version = 'v1';
static final boxName = "oss.krtirtho.spotube.source_matches.$version";
static LazyBox<SourceMatch> get box => Hive.lazyBox<SourceMatch>(boxName);
}

View File

@ -1,119 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'source_match.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class SourceMatchAdapter extends TypeAdapter<SourceMatch> {
@override
final int typeId = 6;
@override
SourceMatch read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return SourceMatch(
id: fields[0] as String,
sourceId: fields[1] as String,
sourceType: fields[2] as SourceType,
createdAt: fields[3] as DateTime,
);
}
@override
void write(BinaryWriter writer, SourceMatch obj) {
writer
..writeByte(4)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.sourceId)
..writeByte(2)
..write(obj.sourceType)
..writeByte(3)
..write(obj.createdAt);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SourceMatchAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class SourceTypeAdapter extends TypeAdapter<SourceType> {
@override
final int typeId = 5;
@override
SourceType read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return SourceType.youtube;
case 1:
return SourceType.youtubeMusic;
case 2:
return SourceType.jiosaavn;
default:
return SourceType.youtube;
}
}
@override
void write(BinaryWriter writer, SourceType obj) {
switch (obj) {
case SourceType.youtube:
writer.writeByte(0);
break;
case SourceType.youtubeMusic:
writer.writeByte(1);
break;
case SourceType.jiosaavn:
writer.writeByte(2);
break;
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SourceTypeAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
SourceMatch _$SourceMatchFromJson(Map json) => SourceMatch(
id: json['id'] as String,
sourceId: json['sourceId'] as String,
sourceType: $enumDecode(_$SourceTypeEnumMap, json['sourceType']),
createdAt: DateTime.parse(json['createdAt'] as String),
);
Map<String, dynamic> _$SourceMatchToJson(SourceMatch instance) =>
<String, dynamic>{
'id': instance.id,
'sourceId': instance.sourceId,
'sourceType': _$SourceTypeEnumMap[instance.sourceType]!,
'createdAt': instance.createdAt.toIso8601String(),
};
const _$SourceTypeEnumMap = {
SourceType.youtube: 'youtube',
SourceType.youtubeMusic: 'youtubeMusic',
SourceType.jiosaavn: 'jiosaavn',
};

View File

@ -10,9 +10,10 @@ import 'package:spotube/extensions/image.dart';
import 'package:spotube/extensions/track.dart'; import 'package:spotube/extensions/track.dart';
import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/pages/album/album.dart'; import 'package:spotube/pages/album/album.dart';
import 'package:spotube/provider/audio_player/querying_track_info.dart';
import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/history/history.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
@ -30,11 +31,12 @@ class AlbumCard extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final playlist = ref.watch(proxyPlaylistProvider); final playlist = ref.watch(audioPlayerProvider);
final playing = final playing =
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final historyNotifier = ref.read(playbackHistoryProvider.notifier); final historyNotifier = ref.read(playbackHistoryActionsProvider);
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
bool isPlaylistPlaying = useMemoized( bool isPlaylistPlaying = useMemoized(
() => playlist.containsCollection(album.id!), () => playlist.containsCollection(album.id!),
@ -59,8 +61,8 @@ class AlbumCard extends HookConsumerWidget {
), ),
margin: const EdgeInsets.symmetric(horizontal: 10), margin: const EdgeInsets.symmetric(horizontal: 10),
isPlaying: isPlaylistPlaying, isPlaying: isPlaylistPlaying,
isLoading: (isPlaylistPlaying && playlist.isFetching == true) || isLoading:
updating.value, (isPlaylistPlaying && isFetchingActiveTrack) || updating.value,
title: album.name!, title: album.name!,
description: description:
"${album.albumType?.formatted}${album.artists?.asString() ?? ""}", "${album.albumType?.formatted}${album.artists?.asString() ?? ""}",

View File

@ -27,8 +27,8 @@ class ArtistCard extends HookConsumerWidget {
); );
final isBlackListed = ref.watch( final isBlackListed = ref.watch(
blacklistProvider.select( blacklistProvider.select(
(blacklist) => blacklist.contains( (blacklist) => blacklist.asData?.value.any(
BlacklistedElement.artist(artist.id!, artist.name!), (element) => element.elementId == artist.id,
), ),
), ),
); );
@ -55,7 +55,7 @@ class ArtistCard extends HookConsumerWidget {
elevation: 3, elevation: 3,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: radius, borderRadius: radius,
side: isBlackListed side: isBlackListed == true
? const BorderSide( ? const BorderSide(
color: Colors.red, color: Colors.red,
width: 2, width: 2,

View File

@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
class TokenLoginForm extends HookConsumerWidget { class TokenLoginForm extends HookConsumerWidget {
final void Function()? onDone; final void Function()? onDone;
@ -52,10 +52,7 @@ class TokenLoginForm extends HookConsumerWidget {
final cookieHeader = final cookieHeader =
"sp_dc=${directCodeController.text.trim()}"; "sp_dc=${directCodeController.text.trim()}";
authenticationNotifier.setCredentials( await authenticationNotifier.login(cookieHeader);
await AuthenticationCredentials.fromCookie(
cookieHeader),
);
if (context.mounted) { if (context.mounted) {
onDone?.call(); onDone?.call();
} }

View File

@ -8,7 +8,7 @@ import 'package:spotube/collections/fake.dart';
import 'package:spotube/modules/home/sections/friends/friend_item.dart'; import 'package:spotube/modules/home/sections/friends/friend_item.dart';
import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
import 'package:spotube/models/spotify_friends.dart'; import 'package:spotube/models/spotify_friends.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class HomePageFriendsSection extends HookConsumerWidget { class HomePageFriendsSection extends HookConsumerWidget {
@ -59,7 +59,7 @@ class HomePageFriendsSection extends HookConsumerWidget {
if (friendsQuery.isLoading || if (friendsQuery.isLoading ||
friendsQuery.asData?.value.friends.isEmpty == true || friendsQuery.asData?.value.friends.isEmpty == true ||
auth == null) { auth.asData?.value == null) {
return const SliverToBoxAdapter( return const SliverToBoxAdapter(
child: SizedBox.shrink(), child: SizedBox.shrink(),
); );

View File

@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class HomeNewReleasesSection extends HookConsumerWidget { class HomeNewReleasesSection extends HookConsumerWidget {
@ -18,7 +18,7 @@ class HomeNewReleasesSection extends HookConsumerWidget {
final albums = ref.watch(userArtistAlbumReleasesProvider); final albums = ref.watch(userArtistAlbumReleasesProvider);
if (auth == null || if (auth.asData?.value == null ||
newReleases.isLoading || newReleases.isLoading ||
newReleases.asData?.value.items.isEmpty == true) { newReleases.asData?.value.items.isEmpty == true) {
return const SizedBox.shrink(); return const SizedBox.shrink();

View File

@ -1,8 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.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/models/database/database.dart';
import 'package:spotube/provider/history/recent.dart'; import 'package:spotube/provider/history/recent.dart';
import 'package:spotube/provider/history/state.dart';
class HomeRecentlyPlayedSection extends HookConsumerWidget { class HomeRecentlyPlayedSection extends HookConsumerWidget {
const HomeRecentlyPlayedSection({super.key}); const HomeRecentlyPlayedSection({super.key});
@ -10,23 +12,28 @@ class HomeRecentlyPlayedSection extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final history = ref.watch(recentlyPlayedItems); final history = ref.watch(recentlyPlayedItems);
final historyData =
history.asData?.value ?? FakeData.historyRecentlyPlayedItems;
if (history.isEmpty) { if (history.asData?.value.isEmpty == true) {
return const SizedBox(); return const SizedBox();
} }
return HorizontalPlaybuttonCardView( return Skeletonizer(
title: const Text('Recently Played'), enabled: history.isLoading,
items: [ child: HorizontalPlaybuttonCardView(
for (final item in history) title: const Text('Recently Played'),
if (item is PlaybackHistoryPlaylist) items: [
item.playlist for (final item in historyData)
else if (item is PlaybackHistoryAlbum) if (item.playlist != null)
item.album item.playlist
], else if (item.album != null)
hasNextPage: false, item.album
isLoadingNextPage: false, ],
onFetchMore: () {}, hasNextPage: false,
isLoadingNextPage: false,
onFetchMore: () {},
),
); );
} }
} }

View File

@ -46,7 +46,7 @@ class LocalFolderItem extends HookConsumerWidget {
...pathSegments.skip(pathSegments.length - 3).toList() ...pathSegments.skip(pathSegments.length - 3).toList()
..removeLast(), ..removeLast(),
] ]
: pathSegments.take(pathSegments.length - 1).toList(); : pathSegments.take(max(pathSegments.length - 1, 0)).toList();
final trackSnapshot = ref.watch( final trackSnapshot = ref.watch(
localTracksProvider.select( localTracksProvider.select(

View File

@ -14,7 +14,7 @@ import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/waypoint.dart'; import 'package:spotube/components/waypoint.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class UserAlbums extends HookConsumerWidget { class UserAlbums extends HookConsumerWidget {
@ -46,7 +46,7 @@ class UserAlbums extends HookConsumerWidget {
[]; [];
}, [albumsQuery.asData?.value, searchText.value]); }, [albumsQuery.asData?.value, searchText.value]);
if (auth == null) { if (auth.asData?.value == null) {
return const AnonymousFallback(); return const AnonymousFallback();
} }

View File

@ -14,7 +14,7 @@ import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/waypoint.dart'; import 'package:spotube/components/waypoint.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class UserArtists extends HookConsumerWidget { class UserArtists extends HookConsumerWidget {
@ -48,7 +48,7 @@ class UserArtists extends HookConsumerWidget {
final controller = useScrollController(); final controller = useScrollController();
if (auth == null) { if (auth.asData?.value == null) {
return const AnonymousFallback(); return const AnonymousFallback();
} }

View File

@ -17,7 +17,7 @@ import 'package:spotube/components/waypoint.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/pages/library/playlist_generate/playlist_generate.dart'; import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
@ -75,7 +75,7 @@ class UserPlaylists extends HookConsumerWidget {
final controller = useScrollController(); final controller = useScrollController();
if (auth == null) { if (auth.asData?.value == null) {
return const AnonymousFallback(); return const AnonymousFallback();
} }

View File

@ -24,8 +24,8 @@ import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart';
import 'package:spotube/hooks/utils/use_palette_color.dart'; import 'package:spotube/hooks/utils/use_palette_color.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
import 'package:spotube/pages/lyrics/lyrics.dart'; import 'package:spotube/pages/lyrics/lyrics.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/server/active_sourced_track.dart';
import 'package:spotube/provider/volume_provider.dart'; import 'package:spotube/provider/volume_provider.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart'; import 'package:spotube/services/sourced_track/sources/youtube.dart';
@ -47,7 +47,7 @@ class PlayerView extends HookConsumerWidget {
final auth = ref.watch(authenticationProvider); final auth = ref.watch(authenticationProvider);
final sourcedCurrentTrack = ref.watch(activeSourcedTrackProvider); final sourcedCurrentTrack = ref.watch(activeSourcedTrackProvider);
final currentActiveTrack = final currentActiveTrack =
ref.watch(proxyPlaylistProvider.select((s) => s.activeTrack)); ref.watch(audioPlayerProvider.select((s) => s.activeTrack));
final currentTrack = sourcedCurrentTrack ?? currentActiveTrack; final currentTrack = sourcedCurrentTrack ?? currentActiveTrack;
final isLocalTrack = currentTrack is LocalTrack; final isLocalTrack = currentTrack is LocalTrack;
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
@ -309,15 +309,13 @@ class PlayerView extends HookConsumerWidget {
builder: (context) => Consumer( builder: (context) => Consumer(
builder: (context, ref, _) { builder: (context, ref, _) {
final playlist = ref.watch( final playlist = ref.watch(
proxyPlaylistProvider, audioPlayerProvider,
);
final playlistNotifier =
ref.read(
proxyPlaylistProvider
.notifier,
); );
final playlistNotifier = ref
.read(audioPlayerProvider
.notifier);
return PlayerQueue return PlayerQueue
.fromProxyPlaylistNotifier( .fromAudioPlayerNotifier(
floating: false, floating: false,
playlist: playlist, playlist: playlist,
notifier: playlistNotifier, notifier: playlistNotifier,
@ -328,8 +326,9 @@ class PlayerView extends HookConsumerWidget {
} }
: null), : null),
), ),
if (auth != null) const SizedBox(width: 10), if (auth.asData?.value != null)
if (auth != null) const SizedBox(width: 10),
if (auth.asData?.value != null)
Expanded( Expanded(
child: OutlinedButton.icon( child: OutlinedButton.icon(
label: Text(context.l10n.lyrics), label: Text(context.l10n.lyrics),

View File

@ -13,8 +13,8 @@ import 'package:spotube/extensions/duration.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
import 'package:spotube/models/logger.dart'; import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/sleep_timer_provider.dart'; import 'package:spotube/provider/sleep_timer_provider.dart';
class PlayerActions extends HookConsumerWidget { class PlayerActions extends HookConsumerWidget {
@ -33,7 +33,7 @@ class PlayerActions extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final playlist = ref.watch(proxyPlaylistProvider); final playlist = ref.watch(audioPlayerProvider);
final isLocalTrack = playlist.activeTrack is LocalTrack; final isLocalTrack = playlist.activeTrack is LocalTrack;
ref.watch(downloadManagerProvider); ref.watch(downloadManagerProvider);
final downloader = ref.watch(downloadManagerProvider.notifier); final downloader = ref.watch(downloadManagerProvider.notifier);
@ -129,7 +129,9 @@ class PlayerActions extends HookConsumerWidget {
? () => downloader.addToQueue(playlist.activeTrack!) ? () => downloader.addToQueue(playlist.activeTrack!)
: null, : null,
), ),
if (playlist.activeTrack != null && !isLocalTrack && auth != null) if (playlist.activeTrack != null &&
!isLocalTrack &&
auth.asData?.value != null)
TrackHeartButton(track: playlist.activeTrack!), TrackHeartButton(track: playlist.activeTrack!),
AdaptivePopSheetList( AdaptivePopSheetList(
offset: Offset(0, -50 * (sleepTimerEntries.values.length + 2)), offset: Offset(0, -50 * (sleepTimerEntries.values.length + 2)),

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:media_kit/media_kit.dart';
import 'package:palette_generator/palette_generator.dart'; import 'package:palette_generator/palette_generator.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
@ -10,9 +11,9 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/duration.dart';
import 'package:spotube/modules/player/use_progress.dart'; import 'package:spotube/modules/player/use_progress.dart';
import 'package:spotube/models/logger.dart'; import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/audio_player/querying_track_info.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/audio_player/loop_mode.dart';
class PlayerControls extends HookConsumerWidget { class PlayerControls extends HookConsumerWidget {
final PaletteGenerator? palette; final PaletteGenerator? palette;
@ -43,8 +44,7 @@ class PlayerControls extends HookConsumerWidget {
SeekIntent: SeekAction(), SeekIntent: SeekAction(),
}, },
[]); []);
final playlist = ref.watch(proxyPlaylistProvider); final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
final playing = final playing =
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
@ -132,7 +132,7 @@ class PlayerControls extends HookConsumerWidget {
// than total duration. Keeping it resolved // than total duration. Keeping it resolved
value: progress.value.toDouble(), value: progress.value.toDouble(),
secondaryTrackValue: bufferProgress, secondaryTrackValue: bufferProgress,
onChanged: playlist.isFetching == true onChanged: isFetchingActiveTrack
? null ? null
: (v) { : (v) {
progress.value = v; progress.value = v;
@ -183,7 +183,7 @@ class PlayerControls extends HookConsumerWidget {
: context.l10n.shuffle_playlist, : context.l10n.shuffle_playlist,
icon: const Icon(SpotubeIcons.shuffle), icon: const Icon(SpotubeIcons.shuffle),
style: shuffled ? activeButtonStyle : buttonStyle, style: shuffled ? activeButtonStyle : buttonStyle,
onPressed: playlist.isFetching == true onPressed: isFetchingActiveTrack
? null ? null
: () { : () {
if (shuffled) { if (shuffled) {
@ -198,15 +198,15 @@ class PlayerControls extends HookConsumerWidget {
tooltip: context.l10n.previous_track, tooltip: context.l10n.previous_track,
icon: const Icon(SpotubeIcons.skipBack), icon: const Icon(SpotubeIcons.skipBack),
style: buttonStyle, style: buttonStyle,
onPressed: playlist.isFetching == true onPressed: isFetchingActiveTrack
? null ? null
: playlistNotifier.previous, : audioPlayer.skipToPrevious,
), ),
IconButton( IconButton(
tooltip: playing tooltip: playing
? context.l10n.pause_playback ? context.l10n.pause_playback
: context.l10n.resume_playback, : context.l10n.resume_playback,
icon: playlist.isFetching == true icon: isFetchingActiveTrack
? SizedBox( ? SizedBox(
height: 20, height: 20,
width: 20, width: 20,
@ -219,7 +219,7 @@ class PlayerControls extends HookConsumerWidget {
playing ? SpotubeIcons.pause : SpotubeIcons.play, playing ? SpotubeIcons.pause : SpotubeIcons.play,
), ),
style: resumePauseStyle, style: resumePauseStyle,
onPressed: playlist.isFetching == true onPressed: isFetchingActiveTrack
? null ? null
: Actions.handler<PlayPauseIntent>( : Actions.handler<PlayPauseIntent>(
context, context,
@ -230,45 +230,41 @@ class PlayerControls extends HookConsumerWidget {
tooltip: context.l10n.next_track, tooltip: context.l10n.next_track,
icon: const Icon(SpotubeIcons.skipForward), icon: const Icon(SpotubeIcons.skipForward),
style: buttonStyle, style: buttonStyle,
onPressed: playlist.isFetching == true onPressed:
? null isFetchingActiveTrack ? null : audioPlayer.skipToNext,
: playlistNotifier.next,
), ),
StreamBuilder<PlaybackLoopMode>( Consumer(builder: (context, ref, _) {
stream: audioPlayer.loopModeStream, final loopMode = ref
builder: (context, snapshot) { .watch(audioPlayerProvider.select((s) => s.loopMode));
final loopMode = snapshot.data ?? PlaybackLoopMode.none;
return IconButton( return IconButton(
tooltip: loopMode == PlaybackLoopMode.one tooltip: loopMode == PlaylistMode.single
? context.l10n.loop_track ? context.l10n.loop_track
: loopMode == PlaybackLoopMode.all : loopMode == PlaylistMode.loop
? context.l10n.repeat_playlist ? context.l10n.repeat_playlist
: null, : null,
icon: Icon( icon: Icon(
loopMode == PlaybackLoopMode.one loopMode == PlaylistMode.single
? SpotubeIcons.repeatOne ? SpotubeIcons.repeatOne
: SpotubeIcons.repeat, : SpotubeIcons.repeat,
), ),
style: loopMode == PlaybackLoopMode.one || style: loopMode == PlaylistMode.single ||
loopMode == PlaybackLoopMode.all loopMode == PlaylistMode.loop
? activeButtonStyle ? activeButtonStyle
: buttonStyle, : buttonStyle,
onPressed: playlist.isFetching == true onPressed: isFetchingActiveTrack
? null ? null
: () async { : () async {
audioPlayer.setLoopMode( await audioPlayer.setLoopMode(
switch (loopMode) { switch (loopMode) {
PlaybackLoopMode.all => PlaylistMode.loop => PlaylistMode.single,
PlaybackLoopMode.one, PlaylistMode.single => PlaylistMode.none,
PlaybackLoopMode.one => PlaylistMode.none => PlaylistMode.loop,
PlaybackLoopMode.none,
PlaybackLoopMode.none =>
PlaybackLoopMode.all,
},
);
}, },
); );
}), },
);
}),
], ],
), ),
const SizedBox(height: 5) const SizedBox(height: 5)

View File

@ -11,7 +11,8 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/collections/intents.dart'; import 'package:spotube/collections/intents.dart';
import 'package:spotube/modules/player/use_progress.dart'; import 'package:spotube/modules/player/use_progress.dart';
import 'package:spotube/modules/player/player.dart'; import 'package:spotube/modules/player/player.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/audio_player/querying_track_info.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
class PlayerOverlay extends HookConsumerWidget { class PlayerOverlay extends HookConsumerWidget {
@ -24,8 +25,8 @@ class PlayerOverlay extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
final playlist = ref.watch(proxyPlaylistProvider); final playlist = ref.watch(audioPlayerProvider);
final canShow = playlist.activeTrack != null; final canShow = playlist.activeTrack != null;
final playing = final playing =
@ -127,14 +128,14 @@ class PlayerOverlay extends HookConsumerWidget {
SpotubeIcons.skipBack, SpotubeIcons.skipBack,
color: textColor, color: textColor,
), ),
onPressed: playlist.isFetching onPressed: isFetchingActiveTrack
? null ? null
: playlistNotifier.previous, : audioPlayer.skipToPrevious,
), ),
Consumer( Consumer(
builder: (context, ref, _) { builder: (context, ref, _) {
return IconButton( return IconButton(
icon: playlist.isFetching icon: isFetchingActiveTrack
? const SizedBox( ? const SizedBox(
height: 20, height: 20,
width: 20, width: 20,
@ -158,9 +159,9 @@ class PlayerOverlay extends HookConsumerWidget {
SpotubeIcons.skipForward, SpotubeIcons.skipForward,
color: textColor, color: textColor,
), ),
onPressed: playlist.isFetching onPressed: isFetchingActiveTrack
? null ? null
: playlistNotifier.next, : audioPlayer.skipToNext,
), ),
], ],
), ),

View File

@ -18,12 +18,12 @@ import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart'; import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/state.dart';
class PlayerQueue extends HookConsumerWidget { class PlayerQueue extends HookConsumerWidget {
final bool floating; final bool floating;
final ProxyPlaylist playlist; final AudioPlayerState playlist;
final Future<void> Function(Track track) onJump; final Future<void> Function(Track track) onJump;
final Future<void> Function(String trackId) onRemove; final Future<void> Function(String trackId) onRemove;
@ -40,10 +40,10 @@ class PlayerQueue extends HookConsumerWidget {
super.key, super.key,
}); });
PlayerQueue.fromProxyPlaylistNotifier({ PlayerQueue.fromAudioPlayerNotifier({
this.floating = true, this.floating = true,
required this.playlist, required this.playlist,
required ProxyPlaylistNotifier notifier, required AudioPlayerNotifier notifier,
super.key, super.key,
}) : onJump = notifier.jumpToTrack, }) : onJump = notifier.jumpToTrack,
onRemove = notifier.removeTrack, onRemove = notifier.removeTrack,
@ -93,11 +93,10 @@ class PlayerQueue extends HookConsumerWidget {
); );
useEffect(() { useEffect(() {
if (playlist.active == null) return null; if (playlist.activeTrack == null) return null;
if (playlist.active! < 0) return;
controller.scrollToIndex( controller.scrollToIndex(
playlist.active!, playlist.playlist.index,
preferPosition: AutoScrollPosition.middle, preferPosition: AutoScrollPosition.middle,
); );
return null; return null;

View File

@ -9,7 +9,7 @@ import 'package:spotube/components/links/link_text.dart';
import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
class PlayerTrackDetails extends HookConsumerWidget { class PlayerTrackDetails extends HookConsumerWidget {
@ -21,7 +21,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final playback = ref.watch(proxyPlaylistProvider); final playback = ref.watch(audioPlayerProvider);
return Row( return Row(
children: [ children: [

View File

@ -14,10 +14,12 @@ import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/duration.dart';
import 'package:spotube/hooks/utils/use_debounce.dart'; import 'package:spotube/hooks/utils/use_debounce.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/audio_player/querying_track_info.dart';
import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/server/active_sourced_track.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
import 'package:spotube/services/sourced_track/models/source_info.dart'; import 'package:spotube/services/sourced_track/models/source_info.dart';
import 'package:spotube/services/sourced_track/models/video_info.dart'; import 'package:spotube/services/sourced_track/models/video_info.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart';
@ -52,7 +54,8 @@ class SiblingTracksSheet extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final playlist = ref.watch(proxyPlaylistProvider); final playlist = ref.watch(audioPlayerProvider);
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
final preferences = ref.watch(userPreferencesProvider); final preferences = ref.watch(userPreferencesProvider);
final isSearching = useState(false); final isSearching = useState(false);
@ -128,13 +131,13 @@ class SiblingTracksSheet extends HookConsumerWidget {
]); ]);
final siblings = useMemoized( final siblings = useMemoized(
() => playlist.isFetching == false () => !isFetchingActiveTrack
? [ ? [
(activeTrack as SourcedTrack).sourceInfo, (activeTrack as SourcedTrack).sourceInfo,
...activeTrack.siblings, ...activeTrack.siblings,
] ]
: <SourceInfo>[], : <SourceInfo>[],
[playlist.isFetching, activeTrack], [activeTrack, isFetchingActiveTrack],
); );
final borderRadius = floating final borderRadius = floating
@ -174,12 +177,12 @@ class SiblingTracksSheet extends HookConsumerWidget {
Text("${sourceInfo.artist}"), Text("${sourceInfo.artist}"),
], ],
), ),
enabled: playlist.isFetching != true, enabled: !isFetchingActiveTrack,
selected: playlist.isFetching != true && selected: !isFetchingActiveTrack &&
sourceInfo.id == (activeTrack as SourcedTrack).sourceInfo.id, sourceInfo.id == (activeTrack as SourcedTrack).sourceInfo.id,
selectedTileColor: theme.popupMenuTheme.color, selectedTileColor: theme.popupMenuTheme.color,
onTap: () { onTap: () {
if (playlist.isFetching == false && if (!isFetchingActiveTrack &&
sourceInfo.id != (activeTrack as SourcedTrack).sourceInfo.id) { sourceInfo.id != (activeTrack as SourcedTrack).sourceInfo.id) {
activeTrackNotifier.swapSibling(sourceInfo); activeTrackNotifier.swapSibling(sourceInfo);
Navigator.of(context).pop(); Navigator.of(context).pop();
@ -187,7 +190,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
}, },
); );
}, },
[playlist.isFetching, activeTrack, siblings], [activeTrack, siblings],
); );
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);

View File

@ -1,4 +1,3 @@
import 'package:async/async.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
@ -19,26 +18,13 @@ import 'package:spotube/services/audio_player/audio_player.dart';
final sliderValue = position.value.inSeconds; final sliderValue = position.value.inSeconds;
useEffect(() { useEffect(() {
final durationOperation = duration.value = audioPlayer.duration;
CancelableOperation.fromFuture(audioPlayer.duration);
durationOperation.then((value) {
if (value != null) {
duration.value = value;
}
});
final durationSubscription = audioPlayer.durationStream.listen((event) { final durationSubscription = audioPlayer.durationStream.listen((event) {
duration.value = event; duration.value = event;
}); });
final positionOperation = position.value = audioPlayer.position;
CancelableOperation.fromFuture(audioPlayer.position);
positionOperation.then((value) {
if (value != null) {
position.value = value;
}
});
var lastPosition = position.value; var lastPosition = position.value;
@ -54,9 +40,7 @@ import 'package:spotube/services/audio_player/audio_player.dart';
}); });
return () { return () {
positionOperation.cancel();
positionSubscription.cancel(); positionSubscription.cancel();
durationOperation.cancel();
durationSubscription.cancel(); durationSubscription.cancel();
}; };
}, []); }, []);

View File

@ -7,9 +7,10 @@ import 'package:spotube/components/playbutton_card.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/pages/playlist/playlist.dart'; import 'package:spotube/pages/playlist/playlist.dart';
import 'package:spotube/provider/audio_player/querying_track_info.dart';
import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/history/history.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
@ -22,9 +23,10 @@ class PlaylistCard extends HookConsumerWidget {
}); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final playlistQueue = ref.watch(proxyPlaylistProvider); final playlistQueue = ref.watch(audioPlayerProvider);
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final historyNotifier = ref.read(playbackHistoryProvider.notifier); final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
final historyNotifier = ref.read(playbackHistoryActionsProvider);
final playing = final playing =
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
@ -65,8 +67,7 @@ class PlaylistCard extends HookConsumerWidget {
placeholder: ImagePlaceholder.collection, placeholder: ImagePlaceholder.collection,
), ),
isPlaying: isPlaylistPlaying, isPlaying: isPlaylistPlaying,
isLoading: isLoading: (isPlaylistPlaying && isFetchingActiveTrack) || updating.value,
(isPlaylistPlaying && playlistQueue.isFetching) || updating.value,
isOwner: playlist.owner?.id == me.asData?.value.id && isOwner: playlist.owner?.id == me.asData?.value.id &&
me.asData?.value.id != null, me.asData?.value.id != null,
onTap: () { onTap: () {

View File

@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/player/player_actions.dart'; import 'package:spotube/modules/player/player_actions.dart';
import 'package:spotube/modules/player/player_overlay.dart'; import 'package:spotube/modules/player/player_overlay.dart';
import 'package:spotube/modules/player/player_track_details.dart'; import 'package:spotube/modules/player/player_track_details.dart';
@ -17,10 +18,10 @@ import 'package:spotube/extensions/image.dart';
import 'package:spotube/hooks/utils/use_brightness_value.dart'; import 'package:spotube/hooks/utils/use_brightness_value.dart';
import 'package:spotube/models/logger.dart'; import 'package:spotube/models/logger.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
import 'package:spotube/provider/volume_provider.dart'; import 'package:spotube/provider/volume_provider.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
@ -32,7 +33,7 @@ class BottomPlayer extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(authenticationProvider); final auth = ref.watch(authenticationProvider);
final playlist = ref.watch(proxyPlaylistProvider); final playlist = ref.watch(audioPlayerProvider);
final layoutMode = final layoutMode =
ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
@ -90,7 +91,7 @@ class BottomPlayer extends HookConsumerWidget {
children: [ children: [
PlayerActions( PlayerActions(
extraActions: [ extraActions: [
if (auth != null) if (auth.asData?.value != null)
IconButton( IconButton(
tooltip: context.l10n.mini_player, tooltip: context.l10n.mini_player,
icon: const Icon(SpotubeIcons.miniPlayer), icon: const Icon(SpotubeIcons.miniPlayer),

View File

@ -9,6 +9,7 @@ import 'package:sidebarx/sidebarx.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/side_bar_tiles.dart'; import 'package:spotube/collections/side_bar_tiles.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/connect/connect_device.dart'; import 'package:spotube/modules/connect/connect_device.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
@ -19,11 +20,11 @@ import 'package:spotube/hooks/controllers/use_sidebarx_controller.dart';
import 'package:spotube/pages/profile/profile.dart'; import 'package:spotube/pages/profile/profile.dart';
import 'package:spotube/pages/settings/settings.dart'; import 'package:spotube/pages/settings/settings.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
@ -268,7 +269,7 @@ class SidebarFooter extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
if (auth != null && data == null) if (auth.asData?.value != null && data == null)
const CircularProgressIndicator() const CircularProgressIndicator()
else if (data != null) else if (data != null)
Flexible( Flexible(

View File

@ -10,9 +10,10 @@ import 'package:spotube/collections/side_bar_tiles.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/utils/use_brightness_value.dart'; import 'package:spotube/hooks/utils/use_brightness_value.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
final navigationPanelHeight = StateProvider<double>((ref) => 50); final navigationPanelHeight = StateProvider<double>((ref) => 50);

View File

@ -33,7 +33,7 @@ class StatsAlbumItem extends StatelessWidget {
Text("${album.albumType?.formatted}"), Text("${album.albumType?.formatted}"),
Flexible( Flexible(
child: ArtistLink( child: ArtistLink(
artists: album.artists!, artists: album.artists ?? [],
mainAxisAlignment: WrapAlignment.start, mainAxisAlignment: WrapAlignment.start,
), ),
), ),

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/formatters.dart'; import 'package:spotube/collections/formatters.dart';
import 'package:spotube/modules/stats/summary/summary_card.dart'; import 'package:spotube/modules/stats/summary/summary_card.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
@ -18,83 +20,87 @@ class StatsPageSummarySection extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final summary = ref.watch(playbackHistorySummaryProvider); final summary = ref.watch(playbackHistorySummaryProvider);
final summaryData = summary.asData?.value ?? FakeData.historySummary;
return SliverPadding( return Skeletonizer.sliver(
padding: const EdgeInsets.all(10), enabled: summary.isLoading,
sliver: SliverLayoutBuilder(builder: (context, constrains) { child: SliverPadding(
return SliverGrid( padding: const EdgeInsets.all(10),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( sliver: SliverLayoutBuilder(builder: (context, constrains) {
crossAxisCount: constrains.isXs return SliverGrid(
? 2 gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
: constrains.smAndDown crossAxisCount: constrains.isXs
? 3 ? 2
: constrains.mdAndDown : constrains.smAndDown
? 4 ? 3
: constrains.lgAndDown : constrains.mdAndDown
? 5 ? 4
: 6, : constrains.lgAndDown
mainAxisSpacing: 10, ? 5
crossAxisSpacing: 10, : 6,
childAspectRatio: constrains.isXs ? 1.3 : 1.5, mainAxisSpacing: 10,
), crossAxisSpacing: 10,
delegate: SliverChildListDelegate([ childAspectRatio: constrains.isXs ? 1.3 : 1.5,
SummaryCard(
title: summary.duration.inMinutes.toDouble(),
unit: "minutes",
description: 'Listened to music',
color: Colors.purple,
onTap: () {
ServiceUtils.pushNamed(context, StatsMinutesPage.name);
},
), ),
SummaryCard( delegate: SliverChildListDelegate([
title: summary.tracks.toDouble(), SummaryCard(
unit: "songs", title: summaryData.duration.inMinutes.toDouble(),
description: 'Streamed overall', unit: "minutes",
color: Colors.lightBlue, description: 'Listened to music',
onTap: () { color: Colors.purple,
ServiceUtils.pushNamed(context, StatsStreamsPage.name); onTap: () {
}, ServiceUtils.pushNamed(context, StatsMinutesPage.name);
), },
SummaryCard.unformatted( ),
title: usdFormatter.format(summary.fees.toDouble()), SummaryCard(
unit: "", title: summaryData.tracks.toDouble(),
description: 'Owed to artists\nthis month', unit: "songs",
color: Colors.green, description: 'Streamed overall',
onTap: () { color: Colors.lightBlue,
ServiceUtils.pushNamed(context, StatsStreamFeesPage.name); onTap: () {
}, ServiceUtils.pushNamed(context, StatsStreamsPage.name);
), },
SummaryCard( ),
title: summary.artists.toDouble(), SummaryCard.unformatted(
unit: "artist's", title: usdFormatter.format(summaryData.fees.toDouble()),
description: 'Music reached you', unit: "",
color: Colors.yellow, description: 'Owed to artists\nthis month',
onTap: () { color: Colors.green,
ServiceUtils.pushNamed(context, StatsArtistsPage.name); onTap: () {
}, ServiceUtils.pushNamed(context, StatsStreamFeesPage.name);
), },
SummaryCard( ),
title: summary.albums.toDouble(), SummaryCard(
unit: "full albums", title: summaryData.artists.toDouble(),
description: 'Got your love', unit: "artist's",
color: Colors.pink, description: 'Music reached you',
onTap: () { color: Colors.yellow,
ServiceUtils.pushNamed(context, StatsAlbumsPage.name); onTap: () {
}, ServiceUtils.pushNamed(context, StatsArtistsPage.name);
), },
SummaryCard( ),
title: summary.playlists.toDouble(), SummaryCard(
unit: "playlists", title: summaryData.albums.toDouble(),
description: 'Were on repeat', unit: "full albums",
color: Colors.teal, description: 'Got your love',
onTap: () { color: Colors.pink,
ServiceUtils.pushNamed(context, StatsPlaylistsPage.name); onTap: () {
}, ServiceUtils.pushNamed(context, StatsAlbumsPage.name);
), },
]), ),
); SummaryCard(
}), title: summaryData.playlists.toDouble(),
unit: "playlists",
description: 'Were on repeat',
color: Colors.teal,
onTap: () {
ServiceUtils.pushNamed(context, StatsPlaylistsPage.name);
},
),
]),
);
}),
),
); );
} }
} }

View File

@ -1,8 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart'; import 'package:spotube/collections/formatters.dart';
import 'package:spotube/modules/stats/common/album_item.dart'; import 'package:spotube/modules/stats/common/album_item.dart';
import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top.dart';
import 'package:spotube/provider/history/top/albums.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class TopAlbums extends HookConsumerWidget { class TopAlbums extends HookConsumerWidget {
const TopAlbums({super.key}); const TopAlbums({super.key});
@ -10,20 +14,32 @@ class TopAlbums extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final historyDuration = ref.watch(playbackHistoryTopDurationProvider); final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
final albums = ref.watch(playbackHistoryTopProvider(historyDuration) final topAlbums = ref.watch(historyTopAlbumsProvider(historyDuration));
.select((value) => value.albums)); final topAlbumsNotifier =
ref.watch(historyTopAlbumsProvider(historyDuration).notifier);
return SliverList.builder( final albumsData = topAlbums.asData?.value.items ?? [];
itemCount: albums.length,
itemBuilder: (context, index) { return Skeletonizer.sliver(
final album = albums[index]; enabled: topAlbums.isLoading && !topAlbums.isLoadingNextPage,
return StatsAlbumItem( child: SliverInfiniteList(
album: album.album, onFetchData: () async {
info: Text( await topAlbumsNotifier.fetchMore();
"${compactNumberFormatter.format(album.count)} plays", },
), hasError: topAlbums.hasError,
); isLoading: topAlbums.isLoading && !topAlbums.isLoadingNextPage,
}, hasReachedMax: topAlbums.asData?.value.hasMore ?? true,
itemCount: albumsData.length,
itemBuilder: (context, index) {
final album = albumsData[index];
return StatsAlbumItem(
album: album.album,
info: Text(
"${compactNumberFormatter.format(album.count)} plays",
),
);
},
),
); );
} }
} }

View File

@ -1,8 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart'; import 'package:spotube/collections/formatters.dart';
import 'package:spotube/modules/stats/common/artist_item.dart'; import 'package:spotube/modules/stats/common/artist_item.dart';
import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top.dart';
import 'package:spotube/provider/history/top/tracks.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class TopArtists extends HookConsumerWidget { class TopArtists extends HookConsumerWidget {
const TopArtists({super.key}); const TopArtists({super.key});
@ -10,18 +15,33 @@ class TopArtists extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final historyDuration = ref.watch(playbackHistoryTopDurationProvider); final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
final artists = ref.watch(playbackHistoryTopProvider(historyDuration) final topTracks = ref.watch(
.select((value) => value.artists)); historyTopTracksProvider(historyDuration),
);
final topTracksNotifier =
ref.watch(historyTopTracksProvider(historyDuration).notifier);
return SliverList.builder( final artistsData = useMemoized(
itemCount: artists.length, () => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]);
itemBuilder: (context, index) {
final artist = artists[index]; return Skeletonizer.sliver(
return StatsArtistItem( enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
artist: artist.artist, child: SliverInfiniteList(
info: Text("${compactNumberFormatter.format(artist.count)} plays"), onFetchData: () async {
); await topTracksNotifier.fetchMore();
}, },
hasError: topTracks.hasError,
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
hasReachedMax: topTracks.asData?.value.hasMore ?? true,
itemCount: artistsData.length,
itemBuilder: (context, index) {
final artist = artistsData[index];
return StatsArtistItem(
artist: artist.artist,
info: Text("${compactNumberFormatter.format(artist.count)} plays"),
);
},
),
); );
} }
} }

View File

@ -5,7 +5,7 @@ import 'package:spotube/components/themed_button_tab_bar.dart';
import 'package:spotube/modules/stats/top/albums.dart'; import 'package:spotube/modules/stats/top/albums.dart';
import 'package:spotube/modules/stats/top/artists.dart'; import 'package:spotube/modules/stats/top/artists.dart';
import 'package:spotube/modules/stats/top/tracks.dart'; import 'package:spotube/modules/stats/top/tracks.dart';
import 'package:spotube/provider/history/state.dart';
import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top.dart';
class StatsPageTopSection extends HookConsumerWidget { class StatsPageTopSection extends HookConsumerWidget {

View File

@ -1,8 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart'; import 'package:spotube/collections/formatters.dart';
import 'package:spotube/modules/stats/common/track_item.dart'; import 'package:spotube/modules/stats/common/track_item.dart';
import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top.dart';
import 'package:spotube/provider/history/top/tracks.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class TopTracks extends HookConsumerWidget { class TopTracks extends HookConsumerWidget {
const TopTracks({super.key}); const TopTracks({super.key});
@ -10,22 +14,34 @@ class TopTracks extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final historyDuration = ref.watch(playbackHistoryTopDurationProvider); final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
final tracks = ref.watch( final topTracks = ref.watch(
playbackHistoryTopProvider(historyDuration) historyTopTracksProvider(historyDuration),
.select((value) => value.tracks),
); );
final topTracksNotifier =
ref.watch(historyTopTracksProvider(historyDuration).notifier);
return SliverList.builder( final tracksData = topTracks.asData?.value.items ?? [];
itemCount: tracks.length,
itemBuilder: (context, index) { return Skeletonizer.sliver(
final track = tracks[index]; enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
return StatsTrackItem( child: SliverInfiniteList(
track: track.track, onFetchData: () async {
info: Text( await topTracksNotifier.fetchMore();
"${compactNumberFormatter.format(track.count)} plays", },
), hasError: topTracks.hasError,
); isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
}, hasReachedMax: topTracks.asData?.value.hasMore ?? true,
itemCount: tracksData.length,
itemBuilder: (context, index) {
final track = tracksData[index];
return StatsTrackItem(
track: track.track,
info: Text(
"${compactNumberFormatter.format(track.count)} plays",
),
);
},
),
); );
} }
} }

View File

@ -10,7 +10,8 @@ import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';
@ -39,10 +40,9 @@ class ArtistPageHeader extends HookConsumerWidget {
); );
final auth = ref.watch(authenticationProvider); final auth = ref.watch(authenticationProvider);
final blacklist = ref.watch(blacklistProvider); ref.watch(blacklistProvider);
final isBlackListed = blacklist.contains( final blacklistNotifier = ref.watch(blacklistProvider.notifier);
BlacklistedElement.artist(artistId, artist.name!), final isBlackListed = blacklistNotifier.containsArtist(artist);
);
final image = artist.images.asUrlString( final image = artist.images.asUrlString(
placeholder: ImagePlaceholder.artist, placeholder: ImagePlaceholder.artist,
@ -135,7 +135,7 @@ class ArtistPageHeader extends HookConsumerWidget {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (auth != null) if (auth.asData?.value != null)
Consumer( Consumer(
builder: (context, ref, _) { builder: (context, ref, _) {
final isFollowingQuery = ref final isFollowingQuery = ref
@ -187,14 +187,16 @@ class ArtistPageHeader extends HookConsumerWidget {
), ),
onPressed: () async { onPressed: () async {
if (isBlackListed) { if (isBlackListed) {
ref.read(blacklistProvider.notifier).remove( await ref
BlacklistedElement.artist( .read(blacklistProvider.notifier)
artist.id!, artist.name!), .remove(artist.id!);
);
} else { } else {
ref.read(blacklistProvider.notifier).add( await ref.read(blacklistProvider.notifier).add(
BlacklistedElement.artist( BlacklistTableCompanion.insert(
artist.id!, artist.name!), name: artist.name!,
elementId: artist.id!,
elementType: BlacklistedType.artist,
),
); );
} }
}, },

View File

@ -9,7 +9,7 @@ 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/provider/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class ArtistPageTopTracks extends HookConsumerWidget { class ArtistPageTopTracks extends HookConsumerWidget {
@ -21,8 +21,8 @@ class ArtistPageTopTracks extends HookConsumerWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final scaffoldMessenger = ScaffoldMessenger.of(context); final scaffoldMessenger = ScaffoldMessenger.of(context);
final playlist = ref.watch(proxyPlaylistProvider); final playlist = ref.watch(audioPlayerProvider);
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final topTracksQuery = ref.watch(artistTopTracksProvider(artistId)); final topTracksQuery = ref.watch(artistTopTracksProvider(artistId));
final isPlaylistPlaying = playlist.containsTracks( final isPlaylistPlaying = playlist.containsTracks(

View File

@ -16,7 +16,7 @@ import 'package:spotube/extensions/image.dart';
import 'package:spotube/pages/track/track.dart'; import 'package:spotube/pages/track/track.dart';
import 'package:spotube/provider/connect/clients.dart'; import 'package:spotube/provider/connect/clients.dart';
import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/services/audio_player/loop_mode.dart'; import 'package:media_kit/media_kit.dart' hide Track;
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
class RemotePlayerQueue extends ConsumerWidget { class RemotePlayerQueue extends ConsumerWidget {
@ -244,18 +244,18 @@ class ConnectControlPage extends HookConsumerWidget {
: connectNotifier.next, : connectNotifier.next,
), ),
IconButton( IconButton(
tooltip: loopMode == PlaybackLoopMode.one tooltip: loopMode == PlaylistMode.single
? context.l10n.loop_track ? context.l10n.loop_track
: loopMode == PlaybackLoopMode.all : loopMode == PlaylistMode.loop
? context.l10n.repeat_playlist ? context.l10n.repeat_playlist
: null, : null,
icon: Icon( icon: Icon(
loopMode == PlaybackLoopMode.one loopMode == PlaylistMode.single
? SpotubeIcons.repeatOne ? SpotubeIcons.repeatOne
: SpotubeIcons.repeat, : SpotubeIcons.repeat,
), ),
style: loopMode == PlaybackLoopMode.one || style: loopMode == PlaylistMode.single ||
loopMode == PlaybackLoopMode.all loopMode == PlaylistMode.loop
? activeButtonStyle ? activeButtonStyle
: buttonStyle, : buttonStyle,
onPressed: playlist.activeTrack == null onPressed: playlist.activeTrack == null
@ -263,12 +263,11 @@ class ConnectControlPage extends HookConsumerWidget {
: () async { : () async {
connectNotifier.setLoopMode( connectNotifier.setLoopMode(
switch (loopMode) { switch (loopMode) {
PlaybackLoopMode.all => PlaylistMode.loop =>
PlaybackLoopMode.one, PlaylistMode.single,
PlaybackLoopMode.one => PlaylistMode.single =>
PlaybackLoopMode.none, PlaylistMode.none,
PlaybackLoopMode.none => PlaylistMode.none => PlaylistMode.loop,
PlaybackLoopMode.all,
}, },
); );
}, },

View File

@ -9,7 +9,7 @@ import 'package:spotube/components/links/hyper_link.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/home/home.dart'; import 'package:spotube/pages/home/home.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
class LoginTutorial extends ConsumerWidget { class LoginTutorial extends ConsumerWidget {
@ -18,8 +18,7 @@ class LoginTutorial extends ConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
ref.watch(authenticationProvider); final auth = ref.watch(authenticationProvider);
final authenticationNotifier = ref.watch(authenticationProvider.notifier);
final key = GlobalKey<State<IntroductionScreen>>(); final key = GlobalKey<State<IntroductionScreen>>();
final theme = Theme.of(context); final theme = Theme.of(context);
@ -53,7 +52,7 @@ class LoginTutorial extends ConsumerWidget {
), ),
showBackButton: true, showBackButton: true,
overrideDone: FilledButton( overrideDone: FilledButton(
onPressed: authenticationNotifier.isLoggedIn onPressed: auth.asData?.value != null
? () { ? () {
ServiceUtils.pushNamed(context, HomePage.name); ServiceUtils.pushNamed(context, HomePage.name);
} }
@ -91,7 +90,7 @@ class LoginTutorial extends ConsumerWidget {
bodyWidget: bodyWidget:
Text(context.l10n.step_3_steps, textAlign: TextAlign.left), Text(context.l10n.step_3_steps, textAlign: TextAlign.left),
), ),
if (authenticationNotifier.isLoggedIn) if (auth.asData?.value != null)
PageViewModel( PageViewModel(
decoration: pageDecoration.copyWith( decoration: pageDecoration.copyWith(
bodyAlignment: Alignment.center, bodyAlignment: Alignment.center,

View File

@ -4,11 +4,11 @@ import 'package:gap/gap.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/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/getting_started/blur_card.dart'; import 'package:spotube/modules/getting_started/blur_card.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/string.dart'; import 'package:spotube/extensions/string.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
final audioSourceToIconMap = { final audioSourceToIconMap = {
AudioSource.youtube: const Icon( AudioSource.youtube: const Icon(

View File

@ -55,14 +55,14 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget {
), ),
const Gap(16), const Gap(16),
DropdownMenu( DropdownMenu(
initialSelection: preferences.recommendationMarket, initialSelection: preferences.market,
onSelected: (value) { onSelected: (value) {
if (value == null) return; if (value == null) return;
ref ref
.read(userPreferencesProvider.notifier) .read(userPreferencesProvider.notifier)
.setRecommendationMarket(value); .setRecommendationMarket(value);
}, },
hintText: preferences.recommendationMarket.name, hintText: preferences.market.name,
label: Text(context.l10n.market_place_region), label: Text(context.l10n.market_place_region),
inputDecorationTheme: inputDecorationTheme:
const InputDecorationTheme(isDense: true), const InputDecorationTheme(isDense: true),

View File

@ -7,7 +7,7 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/dialogs/prompt_dialog.dart'; import 'package:spotube/components/dialogs/prompt_dialog.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/scrobbler_provider.dart'; import 'package:spotube/provider/scrobbler/scrobbler.dart';
class LastFMLoginPage extends HookConsumerWidget { class LastFMLoginPage extends HookConsumerWidget {
static const name = "lastfm_login"; static const name = "lastfm_login";

View File

@ -17,7 +17,7 @@ import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
class LocalLibraryPage extends HookConsumerWidget { class LocalLibraryPage extends HookConsumerWidget {
@ -32,8 +32,8 @@ class LocalLibraryPage extends HookConsumerWidget {
List<LocalTrack> tracks, { List<LocalTrack> tracks, {
LocalTrack? currentTrack, LocalTrack? currentTrack,
}) async { }) async {
final playlist = ref.read(proxyPlaylistProvider); final playlist = ref.read(audioPlayerProvider);
final playback = ref.read(proxyPlaylistProvider.notifier); final playback = ref.read(audioPlayerProvider.notifier);
currentTrack ??= tracks.first; currentTrack ??= tracks.first;
final isPlaylistPlaying = playlist.containsTracks(tracks); final isPlaylistPlaying = playlist.containsTracks(tracks);
if (!isPlaylistPlaying) { if (!isPlaylistPlaying) {
@ -52,7 +52,7 @@ class LocalLibraryPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final sortBy = useState<SortBy>(SortBy.none); final sortBy = useState<SortBy>(SortBy.none);
final playlist = ref.watch(proxyPlaylistProvider); final playlist = ref.watch(audioPlayerProvider);
final trackSnapshot = ref.watch(localTracksProvider); final trackSnapshot = ref.watch(localTracksProvider);
final isPlaylistPlaying = playlist.containsTracks( final isPlaylistPlaying = playlist.containsTracks(
trackSnapshot.asData?.value.values.flattened.toList() ?? []); trackSnapshot.asData?.value.values.flattened.toList() ?? []);

View File

@ -39,7 +39,7 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
final genresCollection = ref.watch(categoryGenresProvider); final genresCollection = ref.watch(categoryGenresProvider);
final limit = useValueNotifier<int>(10); final limit = useValueNotifier<int>(10);
final market = useValueNotifier<Market>(preferences.recommendationMarket); final market = useValueNotifier<Market>(preferences.market);
final genres = useState<List<String>>([]); final genres = useState<List<String>>([]);
final artists = useState<List<Artist>>([]); final artists = useState<List<Artist>>([]);

View File

@ -11,7 +11,7 @@ import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/spotify/recommendation_seeds.dart'; import 'package:spotube/models/spotify/recommendation_seeds.dart';
import 'package:spotube/pages/playlist/playlist.dart'; import 'package:spotube/pages/playlist/playlist.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class PlaylistGenerateResultPage extends HookConsumerWidget { class PlaylistGenerateResultPage extends HookConsumerWidget {
@ -28,7 +28,7 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final router = GoRouter.of(context); final router = GoRouter.of(context);
final scaffoldMessenger = ScaffoldMessenger.of(context); final scaffoldMessenger = ScaffoldMessenger.of(context);
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final generatedPlaylist = ref.watch(generatePlaylistProvider(state)); final generatedPlaylist = ref.watch(generatePlaylistProvider(state));
@ -81,9 +81,12 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
? null ? null
: () async { : () async {
await playlistNotifier.load( await playlistNotifier.load(
generatedPlaylist.asData!.value.where( generatedPlaylist.asData!.value
(e) => selectedTracks.value.contains(e.id!), .where(
), (e) => selectedTracks.value
.contains(e.id!),
)
.toList(),
autoPlay: true, autoPlay: true,
); );
}, },

View File

@ -17,8 +17,8 @@ import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart';
import 'package:spotube/hooks/utils/use_palette_color.dart'; import 'package:spotube/hooks/utils/use_palette_color.dart';
import 'package:spotube/pages/lyrics/plain_lyrics.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart';
import 'package:spotube/pages/lyrics/synced_lyrics.dart'; import 'package:spotube/pages/lyrics/synced_lyrics.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
@ -30,7 +30,7 @@ class LyricsPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final playlist = ref.watch(proxyPlaylistProvider); final playlist = ref.watch(audioPlayerProvider);
String albumArt = useMemoized( String albumArt = useMemoized(
() => (playlist.activeTrack?.album?.images).asUrlString( () => (playlist.activeTrack?.album?.images).asUrlString(
index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1, index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1,
@ -62,7 +62,7 @@ class LyricsPage extends HookConsumerWidget {
const Spacer(), const Spacer(),
Consumer( Consumer(
builder: (context, ref, child) { builder: (context, ref, child) {
final playback = ref.watch(proxyPlaylistProvider); final playback = ref.watch(audioPlayerProvider);
final lyric = final lyric =
ref.watch(syncedLyricsProvider(playback.activeTrack)); ref.watch(syncedLyricsProvider(playback.activeTrack));
final providerName = lyric.asData?.value.provider; final providerName = lyric.asData?.value.provider;
@ -84,7 +84,7 @@ class LyricsPage extends HookConsumerWidget {
final auth = ref.watch(authenticationProvider); final auth = ref.watch(authenticationProvider);
if (auth == null) { if (auth.asData?.value == null) {
return Scaffold( return Scaffold(
appBar: !kIsMacOS && !isModal ? const PageWindowTitleBar() : null, appBar: !kIsMacOS && !isModal ? const PageWindowTitleBar() : null,
body: const AnonymousFallback(), body: const AnonymousFallback(),

View File

@ -14,8 +14,8 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/utils/use_force_update.dart'; import 'package:spotube/hooks/utils/use_force_update.dart';
import 'package:spotube/pages/lyrics/plain_lyrics.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart';
import 'package:spotube/pages/lyrics/synced_lyrics.dart'; import 'package:spotube/pages/lyrics/synced_lyrics.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
@ -31,7 +31,7 @@ class MiniLyricsPage extends HookConsumerWidget {
final update = useForceUpdate(); final update = useForceUpdate();
final wasMaximized = useRef<bool>(false); final wasMaximized = useRef<bool>(false);
final playlistQueue = ref.watch(proxyPlaylistProvider); final playlistQueue = ref.watch(audioPlayerProvider);
final areaActive = useState(false); final areaActive = useState(false);
final hoverMode = useState(true); final hoverMode = useState(true);
@ -48,7 +48,7 @@ class MiniLyricsPage extends HookConsumerWidget {
final auth = ref.watch(authenticationProvider); final auth = ref.watch(authenticationProvider);
if (auth == null) { if (auth.asData?.value == null) {
return const Scaffold( return const Scaffold(
appBar: PageWindowTitleBar(), appBar: PageWindowTitleBar(),
body: AnonymousFallback(), body: AnonymousFallback(),
@ -230,14 +230,13 @@ class MiniLyricsPage extends HookConsumerWidget {
builder: (context) { builder: (context) {
return Consumer(builder: (context, ref, _) { return Consumer(builder: (context, ref, _) {
final playlist = final playlist =
ref.watch(proxyPlaylistProvider); ref.watch(audioPlayerProvider);
return PlayerQueue return PlayerQueue.fromAudioPlayerNotifier(
.fromProxyPlaylistNotifier(
floating: true, floating: true,
playlist: playlist, playlist: playlist,
notifier: ref notifier: ref
.read(proxyPlaylistProvider.notifier), .read(audioPlayerProvider.notifier),
); );
}); });
}, },

View File

@ -11,7 +11,7 @@ import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class PlainLyrics extends HookConsumerWidget { class PlainLyrics extends HookConsumerWidget {
@ -27,7 +27,7 @@ class PlainLyrics extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final playlist = ref.watch(proxyPlaylistProvider); final playlist = ref.watch(audioPlayerProvider);
final lyricsQuery = ref.watch(syncedLyricsProvider(playlist.activeTrack)); final lyricsQuery = ref.watch(syncedLyricsProvider(playlist.activeTrack));
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final textTheme = Theme.of(context).textTheme; final textTheme = Theme.of(context).textTheme;

View File

@ -12,7 +12,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart'; import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart';
import 'package:spotube/modules/lyrics/use_synced_lyrics.dart'; import 'package:spotube/modules/lyrics/use_synced_lyrics.dart';
import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
@ -32,7 +32,7 @@ class SyncedLyrics extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final playlist = ref.watch(proxyPlaylistProvider); final playlist = ref.watch(audioPlayerProvider);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final controller = useAutoScrollController(); final controller = useAutoScrollController();
@ -54,7 +54,7 @@ class SyncedLyrics extends HookConsumerWidget {
final textTheme = Theme.of(context).textTheme; final textTheme = Theme.of(context).textTheme;
ref.listen( ref.listen(
proxyPlaylistProvider.select((s) => s.activeTrack), audioPlayerProvider.select((s) => s.activeTrack),
(previous, next) { (previous, next) {
controller.scrollToIndex(0); controller.scrollToIndex(0);
ref.read(syncedLyricsDelayProvider.notifier).state = 0; ref.read(syncedLyricsDelayProvider.notifier).state = 0;
@ -139,14 +139,12 @@ class SyncedLyrics extends HookConsumerWidget {
textAlign: TextAlign.center, textAlign: TextAlign.center,
child: InkWell( child: InkWell(
onTap: () async { onTap: () async {
final duration =
await audioPlayer.duration ??
Duration.zero;
final time = Duration( final time = Duration(
seconds: seconds:
lyricSlice.time.inSeconds - delay, lyricSlice.time.inSeconds - delay,
); );
if (time > duration || time.isNegative) { if (time > audioPlayer.duration ||
time.isNegative) {
return; return;
} }
audioPlayer.seek(time); audioPlayer.seek(time);

View File

@ -3,7 +3,7 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
class WebViewLogin extends HookConsumerWidget { class WebViewLogin extends HookConsumerWidget {
@ -53,9 +53,7 @@ class WebViewLogin extends HookConsumerWidget {
final cookieHeader = final cookieHeader =
"sp_dc=${cookies.firstWhere((element) => element.name == "sp_dc").value}"; "sp_dc=${cookies.firstWhere((element) => element.name == "sp_dc").value}";
authenticationNotifier.setCredentials( await authenticationNotifier.login(cookieHeader);
await AuthenticationCredentials.fromCookie(cookieHeader),
);
if (context.mounted) { if (context.mounted) {
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
GoRouter.of(context).go("/"); GoRouter.of(context).go("/");

View File

@ -5,7 +5,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/modules/player/player_queue.dart'; import 'package:spotube/modules/player/player_queue.dart';
import 'package:spotube/components/dialogs/replace_downloaded_dialog.dart'; import 'package:spotube/components/dialogs/replace_downloaded_dialog.dart';
@ -16,10 +15,9 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/configurators/use_endless_playback.dart'; import 'package:spotube/hooks/configurators/use_endless_playback.dart';
import 'package:spotube/pages/home/home.dart'; import 'package:spotube/pages/home/home.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/server/routes/connect.dart'; import 'package:spotube/provider/server/routes/connect.dart';
import 'package:spotube/services/connectivity_adapter.dart'; import 'package:spotube/services/connectivity_adapter.dart';
import 'package:spotube/utils/persisted_state_notifier.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
@ -41,13 +39,6 @@ class RootApp extends HookConsumerWidget {
useEffect(() { useEffect(() {
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
ServiceUtils.checkForUpdates(context, ref); ServiceUtils.checkForUpdates(context, ref);
final sharedPreferences = await SharedPreferences.getInstance();
if (sharedPreferences.getBool(kIsUsingEncryption) == false &&
context.mounted) {
await PersistedStateNotifier.showNoEncryptionDialog(context);
}
}); });
final subscriptions = [ final subscriptions = [
@ -201,11 +192,11 @@ class RootApp extends HookConsumerWidget {
), ),
child: Consumer( child: Consumer(
builder: (context, ref, _) { builder: (context, ref, _) {
final playlist = ref.watch(proxyPlaylistProvider); final playlist = ref.watch(audioPlayerProvider);
final playlistNotifier = final playlistNotifier =
ref.read(proxyPlaylistProvider.notifier); ref.read(audioPlayerProvider.notifier);
return PlayerQueue.fromProxyPlaylistNotifier( return PlayerQueue.fromAudioPlayerNotifier(
floating: true, floating: true,
playlist: playlist, playlist: playlist,
notifier: playlistNotifier, notifier: playlistNotifier,

View File

@ -20,7 +20,7 @@ import 'package:spotube/pages/search/sections/albums.dart';
import 'package:spotube/pages/search/sections/artists.dart'; import 'package:spotube/pages/search/sections/artists.dart';
import 'package:spotube/pages/search/sections/playlists.dart'; import 'package:spotube/pages/search/sections/playlists.dart';
import 'package:spotube/pages/search/sections/tracks.dart'; import 'package:spotube/pages/search/sections/tracks.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart';
@ -37,8 +37,7 @@ class SearchPage extends HookConsumerWidget {
final searchTerm = ref.watch(searchTermStateProvider); final searchTerm = ref.watch(searchTermStateProvider);
final controller = useSearchController(); final controller = useSearchController();
ref.watch(authenticationProvider); final auth = ref.watch(authenticationProvider);
final authenticationNotifier = ref.watch(authenticationProvider.notifier);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final searchTrack = ref.watch(searchProvider(SearchType.track)); final searchTrack = ref.watch(searchProvider(SearchType.track));
@ -91,7 +90,7 @@ class SearchPage extends HookConsumerWidget {
appBar: kIsDesktop && !kIsMacOS appBar: kIsDesktop && !kIsMacOS
? const PageWindowTitleBar(automaticallyImplyLeading: true) ? const PageWindowTitleBar(automaticallyImplyLeading: true)
: null, : null,
body: !authenticationNotifier.isLoggedIn body: auth.asData?.value == null
? const AnonymousFallback() ? const AnonymousFallback()
: Column( : Column(
children: [ children: [

View File

@ -8,7 +8,7 @@ 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/provider/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class SearchTracksSection extends HookConsumerWidget { class SearchTracksSection extends HookConsumerWidget {
@ -24,8 +24,8 @@ class SearchTracksSection extends HookConsumerWidget {
ref.watch(searchProvider(SearchType.track).notifier); ref.watch(searchProvider(SearchType.track).notifier);
final tracks = searchTrack.asData?.value.items.cast<Track>() ?? []; final tracks = searchTrack.asData?.value.items.cast<Track>() ?? [];
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final playlist = ref.watch(proxyPlaylistProvider); final playlist = ref.watch(audioPlayerProvider);
final theme = Theme.of(context); final theme = Theme.of(context);
return Column( return Column(

View File

@ -24,19 +24,21 @@ class BlackListPage extends HookConsumerWidget {
final filteredBlacklist = useMemoized( final filteredBlacklist = useMemoized(
() { () {
if (searchText.value.isEmpty) { if (searchText.value.isEmpty) {
return blacklist; return blacklist.asData?.value ?? [];
} }
return blacklist return blacklist.asData?.value
.map( .map(
(e) => ( (e) => (
weightedRatio("${e.name} ${e.type.name}", searchText.value), weightedRatio(
e, "${e.name} ${e.elementType.name}", searchText.value),
), e,
) ),
.sorted((a, b) => b.$1.compareTo(a.$1)) )
.where((e) => e.$1 > 50) .sorted((a, b) => b.$1.compareTo(a.$1))
.map((e) => e.$2) .where((e) => e.$1 > 50)
.toList(); .map((e) => e.$2)
.toList() ??
[];
}, },
[blacklist, searchText.value], [blacklist, searchText.value],
); );
@ -70,14 +72,14 @@ class BlackListPage extends HookConsumerWidget {
final item = filteredBlacklist.elementAt(index); final item = filteredBlacklist.elementAt(index);
return ListTile( return ListTile(
leading: Text("${index + 1}."), leading: Text("${index + 1}."),
title: Text("${item.name} (${item.type.name})"), title: Text("${item.name} (${item.elementType.name})"),
subtitle: Text(item.id), subtitle: Text(item.elementId),
trailing: IconButton( trailing: IconButton(
icon: Icon(SpotubeIcons.trash, color: Colors.red[400]), icon: Icon(SpotubeIcons.trash, color: Colors.red[400]),
onPressed: () { onPressed: () {
ref ref
.read(blacklistProvider.notifier) .read(blacklistProvider.notifier)
.remove(filteredBlacklist.elementAt(index)); .remove(filteredBlacklist.elementAt(index).elementId);
}, },
), ),
); );

View File

@ -9,8 +9,8 @@ import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/pages/profile/profile.dart'; import 'package:spotube/pages/profile/profile.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/scrobbler_provider.dart'; import 'package:spotube/provider/scrobbler/scrobbler.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
@ -35,7 +35,7 @@ class SettingsAccountSection extends HookConsumerWidget {
return SectionCardWithHeading( return SectionCardWithHeading(
heading: context.l10n.account, heading: context.l10n.account,
children: [ children: [
if (auth != null) if (auth.asData?.value != null)
ListTile( ListTile(
leading: const Icon(SpotubeIcons.user), leading: const Icon(SpotubeIcons.user),
title: const Text("User Profile"), title: const Text("User Profile"),
@ -53,7 +53,7 @@ class SettingsAccountSection extends HookConsumerWidget {
ServiceUtils.pushNamed(context, ProfilePage.name); ServiceUtils.pushNamed(context, ProfilePage.name);
}, },
), ),
if (auth == null) if (auth.asData?.value == null)
LayoutBuilder(builder: (context, constrains) { LayoutBuilder(builder: (context, constrains) {
return ListTile( return ListTile(
leading: Icon( leading: Icon(
@ -119,7 +119,7 @@ class SettingsAccountSection extends HookConsumerWidget {
), ),
); );
}), }),
if (scrobbler == null) if (scrobbler.asData?.value == null)
ListTile( ListTile(
leading: const Icon(SpotubeIcons.lastFm), leading: const Icon(SpotubeIcons.lastFm),
title: Text(context.l10n.login_with_lastfm), title: Text(context.l10n.login_with_lastfm),

View File

@ -3,12 +3,12 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart';
import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart';
import 'package:spotube/components/adaptive/adaptive_select_tile.dart'; import 'package:spotube/components/adaptive/adaptive_select_tile.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
class SettingsAppearanceSection extends HookConsumerWidget { class SettingsAppearanceSection extends HookConsumerWidget {
final bool isGettingStarted; final bool isGettingStarted;

View File

@ -2,11 +2,12 @@ import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart';
import 'package:spotube/components/adaptive/adaptive_select_tile.dart'; import 'package:spotube/components/adaptive/adaptive_select_tile.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
class SettingsDesktopSection extends HookConsumerWidget { class SettingsDesktopSection extends HookConsumerWidget {

View File

@ -57,7 +57,7 @@ class SettingsLanguageRegionSection extends HookConsumerWidget {
secondary: const Icon(SpotubeIcons.shoppingBag), secondary: const Icon(SpotubeIcons.shoppingBag),
title: Text(context.l10n.market_place_region), title: Text(context.l10n.market_place_region),
subtitle: Text(context.l10n.recommendation_country), subtitle: Text(context.l10n.recommendation_country),
value: preferences.recommendationMarket, value: preferences.market,
onChanged: (value) { onChanged: (value) {
if (value == null) return; if (value == null) return;
preferencesNotifier.setRecommendationMarket(value); preferencesNotifier.setRecommendationMarket(value);

View File

@ -6,12 +6,13 @@ import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:piped_client/piped_client.dart'; import 'package:piped_client/piped_client.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart';
import 'package:spotube/components/adaptive/adaptive_select_tile.dart'; import 'package:spotube/components/adaptive/adaptive_select_tile.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/piped_instances_provider.dart'; import 'package:spotube/provider/piped_instances_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/enums.dart';
class SettingsPlaybackSection extends HookConsumerWidget { class SettingsPlaybackSection extends HookConsumerWidget {

View File

@ -1,10 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.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/modules/stats/common/album_item.dart'; import 'package:spotube/modules/stats/common/album_item.dart';
import 'package:spotube/provider/history/state.dart';
import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top.dart';
import 'package:spotube/provider/history/top/albums.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class StatsAlbumsPage extends HookConsumerWidget { class StatsAlbumsPage extends HookConsumerWidget {
static const name = "stats_albums"; static const name = "stats_albums";
@ -12,10 +16,12 @@ class StatsAlbumsPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final albums = ref.watch( final topAlbums =
playbackHistoryTopProvider(HistoryDuration.allTime) ref.watch(historyTopAlbumsProvider(HistoryDuration.allTime));
.select((s) => s.albums), final topAlbumsNotifier =
); ref.watch(historyTopAlbumsProvider(HistoryDuration.allTime).notifier);
final albumsData = topAlbums.asData?.value.items ?? [];
return Scaffold( return Scaffold(
appBar: const PageWindowTitleBar( appBar: const PageWindowTitleBar(
@ -23,15 +29,26 @@ class StatsAlbumsPage extends HookConsumerWidget {
centerTitle: false, centerTitle: false,
title: Text("Albums"), title: Text("Albums"),
), ),
body: ListView.builder( body: Skeletonizer(
itemCount: albums.length, enabled: topAlbums.isLoading && !topAlbums.isLoadingNextPage,
itemBuilder: (context, index) { child: InfiniteList(
final album = albums[index]; onFetchData: () async {
return StatsAlbumItem( await topAlbumsNotifier.fetchMore();
album: album.album, },
info: Text("${compactNumberFormatter.format(album.count)} plays"), hasError: topAlbums.hasError,
); isLoading: topAlbums.isLoading && !topAlbums.isLoadingNextPage,
}, hasReachedMax: topAlbums.asData?.value.hasMore ?? true,
itemCount: albumsData.length,
itemBuilder: (context, index) {
final album = albumsData[index];
return StatsAlbumItem(
album: album.album,
info: Text(
"${compactNumberFormatter.format(album.count)} plays",
),
);
},
),
), ),
); );
} }

View File

@ -1,10 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.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/modules/stats/common/artist_item.dart'; import 'package:spotube/modules/stats/common/artist_item.dart';
import 'package:spotube/provider/history/state.dart';
import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top.dart';
import 'package:spotube/provider/history/top/tracks.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class StatsArtistsPage extends HookConsumerWidget { class StatsArtistsPage extends HookConsumerWidget {
static const name = "stats_artists"; static const name = "stats_artists";
@ -12,10 +17,14 @@ class StatsArtistsPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final artists = ref.watch( final topTracks = ref.watch(
playbackHistoryTopProvider(HistoryDuration.allTime) historyTopTracksProvider(HistoryDuration.allTime),
.select((s) => s.artists),
); );
final topTracksNotifier =
ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier);
final artistsData = useMemoized(
() => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]);
return Scaffold( return Scaffold(
appBar: const PageWindowTitleBar( appBar: const PageWindowTitleBar(
@ -23,15 +32,25 @@ class StatsArtistsPage extends HookConsumerWidget {
centerTitle: false, centerTitle: false,
title: Text("Artists"), title: Text("Artists"),
), ),
body: ListView.builder( body: Skeletonizer(
itemCount: artists.length, enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
itemBuilder: (context, index) { child: InfiniteList(
final artist = artists[index]; onFetchData: () async {
return StatsArtistItem( await topTracksNotifier.fetchMore();
artist: artist.artist, },
info: Text("${compactNumberFormatter.format(artist.count)} plays"), hasError: topTracks.hasError,
); isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
}, hasReachedMax: topTracks.asData?.value.hasMore ?? true,
itemCount: artistsData.length,
itemBuilder: (context, index) {
final artist = artistsData[index];
return StatsArtistItem(
artist: artist.artist,
info:
Text("${compactNumberFormatter.format(artist.count)} plays"),
);
},
),
), ),
); );
} }

View File

@ -1,11 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:sliver_tools/sliver_tools.dart'; import 'package:sliver_tools/sliver_tools.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/modules/stats/common/artist_item.dart'; import 'package:spotube/modules/stats/common/artist_item.dart';
import 'package:spotube/provider/history/state.dart';
import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top.dart';
import 'package:spotube/provider/history/top/tracks.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class StatsStreamFeesPage extends HookConsumerWidget { class StatsStreamFeesPage extends HookConsumerWidget {
static const name = "stats_stream_fees"; static const name = "stats_stream_fees";
@ -15,10 +20,23 @@ class StatsStreamFeesPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final ThemeData(:textTheme, :hintColor) = Theme.of(context); final ThemeData(:textTheme, :hintColor) = Theme.of(context);
final duration = useState<HistoryDuration>(HistoryDuration.days30);
final artists = ref.watch( final topTracks = ref.watch(
playbackHistoryTopProvider(HistoryDuration.days30) historyTopTracksProvider(duration.value),
.select((value) => value.artists), );
final topTracksNotifier =
ref.watch(historyTopTracksProvider(duration.value).notifier);
final artistsData = useMemoized(
() => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]);
final total = useMemoized(
() => artistsData.fold<double>(
0,
(previousValue, element) => previousValue + element.count * 0.005,
),
[artistsData],
); );
return Scaffold( return Scaffold(
@ -48,15 +66,73 @@ class StatsStreamFeesPage extends HookConsumerWidget {
), ),
), ),
), ),
SliverList.builder( SliverToBoxAdapter(
itemCount: artists.length, child: Padding(
itemBuilder: (context, index) { padding: const EdgeInsets.symmetric(horizontal: 16),
final artist = artists[index]; child: Row(
return StatsArtistItem( mainAxisAlignment: MainAxisAlignment.spaceBetween,
artist: artist.artist, children: [
info: Text(usdFormatter.format(artist.count * 0.005)), Text(
); "Total ${usdFormatter.format(total)}",
}, style: textTheme.titleLarge,
),
DropdownButton<HistoryDuration>(
value: duration.value,
onChanged: (value) {
if (value == null) return;
duration.value = value;
},
items: const [
DropdownMenuItem(
value: HistoryDuration.days7,
child: Text("This week"),
),
DropdownMenuItem(
value: HistoryDuration.days30,
child: Text("This month"),
),
DropdownMenuItem(
value: HistoryDuration.months6,
child: Text("Last 6 months"),
),
DropdownMenuItem(
value: HistoryDuration.year,
child: Text("This year"),
),
DropdownMenuItem(
value: HistoryDuration.years2,
child: Text("Last 2 years"),
),
DropdownMenuItem(
value: HistoryDuration.allTime,
child: Text("All time"),
),
],
),
],
),
),
),
SliverSafeArea(
sliver: Skeletonizer.sliver(
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
child: SliverInfiniteList(
onFetchData: () async {
await topTracksNotifier.fetchMore();
},
hasError: topTracks.hasError,
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
hasReachedMax: topTracks.asData?.value.hasMore ?? true,
itemCount: artistsData.length,
itemBuilder: (context, index) {
final artist = artistsData[index];
return StatsArtistItem(
artist: artist.artist,
info: Text(usdFormatter.format(artist.count * 0.005)),
);
},
),
),
), ),
], ],
), ),

View File

@ -1,11 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.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/modules/stats/common/track_item.dart'; import 'package:spotube/modules/stats/common/track_item.dart';
import 'package:spotube/provider/history/state.dart';
import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top.dart';
import 'package:spotube/provider/history/top/tracks.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class StatsMinutesPage extends HookConsumerWidget { class StatsMinutesPage extends HookConsumerWidget {
static const name = "stats_minutes"; static const name = "stats_minutes";
@ -15,9 +19,12 @@ class StatsMinutesPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final topTracks = ref.watch( final topTracks = ref.watch(
playbackHistoryTopProvider(HistoryDuration.allTime) historyTopTracksProvider(HistoryDuration.allTime),
.select((s) => s.tracks),
); );
final topTracksNotifier =
ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier);
final tracksData = topTracks.asData?.value.items ?? [];
return Scaffold( return Scaffold(
appBar: const PageWindowTitleBar( appBar: const PageWindowTitleBar(
@ -25,19 +32,27 @@ class StatsMinutesPage extends HookConsumerWidget {
centerTitle: false, centerTitle: false,
automaticallyImplyLeading: true, automaticallyImplyLeading: true,
), ),
body: ListView.separated( body: Skeletonizer(
separatorBuilder: (context, index) => const Gap(8), enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
itemCount: topTracks.length, child: InfiniteList(
itemBuilder: (context, index) { separatorBuilder: (context, index) => const Gap(8),
final (:track, :count) = topTracks[index]; onFetchData: () async {
await topTracksNotifier.fetchMore();
return StatsTrackItem( },
track: track, hasError: topTracks.hasError,
info: Text( isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
"${compactNumberFormatter.format(count * track.duration!.inMinutes)} mins", hasReachedMax: topTracks.asData?.value.hasMore ?? true,
), itemCount: tracksData.length,
); itemBuilder: (context, index) {
}, final track = tracksData[index];
return StatsTrackItem(
track: track.track,
info: Text(
"${compactNumberFormatter.format(track.count)} plays",
),
);
},
),
), ),
); );
} }

View File

@ -1,10 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.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/modules/stats/common/playlist_item.dart'; import 'package:spotube/modules/stats/common/playlist_item.dart';
import 'package:spotube/provider/history/state.dart';
import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top.dart';
import 'package:spotube/provider/history/top/playlists.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class StatsPlaylistsPage extends HookConsumerWidget { class StatsPlaylistsPage extends HookConsumerWidget {
static const name = "stats_playlists"; static const name = "stats_playlists";
@ -12,10 +16,13 @@ class StatsPlaylistsPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final playlists = ref.watch( final topPlaylists =
playbackHistoryTopProvider(HistoryDuration.allTime) ref.watch(historyTopPlaylistsProvider(HistoryDuration.allTime));
.select((s) => s.playlists),
); final topPlaylistsNotifier = ref
.watch(historyTopPlaylistsProvider(HistoryDuration.allTime).notifier);
final playlistsData = topPlaylists.asData?.value.items ?? [];
return Scaffold( return Scaffold(
appBar: const PageWindowTitleBar( appBar: const PageWindowTitleBar(
@ -23,16 +30,25 @@ class StatsPlaylistsPage extends HookConsumerWidget {
centerTitle: false, centerTitle: false,
title: Text("Playlists"), title: Text("Playlists"),
), ),
body: ListView.builder( body: Skeletonizer(
itemCount: playlists.length, enabled: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage,
itemBuilder: (context, index) { child: InfiniteList(
final playlist = playlists[index]; onFetchData: () async {
return StatsPlaylistItem( await topPlaylistsNotifier.fetchMore();
playlist: playlist.playlist.playlist, },
info: hasError: topPlaylists.hasError,
Text("${compactNumberFormatter.format(playlist.count)} plays"), isLoading: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage,
); hasReachedMax: topPlaylists.asData?.value.hasMore ?? true,
}, itemCount: playlistsData.length,
itemBuilder: (context, index) {
final playlist = playlistsData[index];
return StatsPlaylistItem(
playlist: playlist.playlist,
info: Text(
"${compactNumberFormatter.format(playlist.count)} plays"),
);
},
),
), ),
); );
} }

View File

@ -1,11 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.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/modules/stats/common/track_item.dart'; import 'package:spotube/modules/stats/common/track_item.dart';
import 'package:spotube/provider/history/state.dart';
import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top.dart';
import 'package:spotube/provider/history/top/tracks.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class StatsStreamsPage extends HookConsumerWidget { class StatsStreamsPage extends HookConsumerWidget {
static const name = "stats_streams"; static const name = "stats_streams";
@ -15,9 +19,12 @@ class StatsStreamsPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final topTracks = ref.watch( final topTracks = ref.watch(
playbackHistoryTopProvider(HistoryDuration.allTime) historyTopTracksProvider(HistoryDuration.allTime),
.select((s) => s.tracks),
); );
final topTracksNotifier =
ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier);
final tracksData = topTracks.asData?.value.items ?? [];
return Scaffold( return Scaffold(
appBar: const PageWindowTitleBar( appBar: const PageWindowTitleBar(
@ -25,19 +32,27 @@ class StatsStreamsPage extends HookConsumerWidget {
centerTitle: false, centerTitle: false,
automaticallyImplyLeading: true, automaticallyImplyLeading: true,
), ),
body: ListView.separated( body: Skeletonizer(
separatorBuilder: (context, index) => const Gap(8), enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
itemCount: topTracks.length, child: InfiniteList(
itemBuilder: (context, index) { separatorBuilder: (context, index) => const Gap(8),
final (:track, :count) = topTracks[index]; onFetchData: () async {
await topTracksNotifier.fetchMore();
return StatsTrackItem( },
track: track, hasError: topTracks.hasError,
info: Text( isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
"${compactNumberFormatter.format(count)} streams", hasReachedMax: topTracks.asData?.value.hasMore ?? true,
), itemCount: tracksData.length,
); itemBuilder: (context, index) {
}, final track = tracksData[index];
return StatsTrackItem(
track: track.track,
info: Text(
"${compactNumberFormatter.format(track.count * track.track.duration!.inMinutes)} mins",
),
);
},
),
), ),
); );
} }

View File

@ -14,7 +14,7 @@ import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/components/track_tile/track_options.dart'; import 'package:spotube/components/track_tile/track_options.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
@ -34,8 +34,8 @@ class TrackPage extends HookConsumerWidget {
final ThemeData(:textTheme, :colorScheme) = Theme.of(context); final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final playlist = ref.watch(proxyPlaylistProvider); final playlist = ref.watch(audioPlayerProvider);
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final isActive = playlist.activeTrack?.id == trackId; final isActive = playlist.activeTrack?.id == trackId;

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