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?
multiple: true
options:
- "Website (spotube.netlify.app) or (spotube.krtirtho.dev)"
- "Website (spotube.krtirtho.dev)"
- "GitHub Releases (Binary)"
- "GitHub Actions (Nightly Binary)"
- "Play Store (Android)"
@ -77,4 +77,4 @@ body:
description: If you are a developer and want to work on this issue yourself, you can check this box and wait for maintainer response. We welcome contributions!
options:
- label: I'm ready to work on this issue!
required: false
required: false

View File

@ -8,3 +8,10 @@ targets:
options:
any_map: 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:spotube/models/database/database.dart';
import 'package:spotube/models/spotify/home_feed.dart';
import 'package:spotube/models/spotify_friends.dart';
import 'package:spotube/provider/history/summary.dart';
abstract class FakeData {
static final Image image = Image()
@ -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/lyrics/lyrics.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/utils/platform.dart';
@ -96,8 +96,8 @@ class SeekIntent extends Intent {
class SeekAction extends Action<SeekIntent> {
@override
invoke(intent) async {
final playlist = intent.ref.read(proxyPlaylistProvider);
if (playlist.isFetching) {
final isFetchingActiveTrack = intent.ref.read(queryingTrackInfoProvider);
if (isFetchingActiveTrack) {
DirectionalFocusAction().invoke(
DirectionalFocusIntent(
intent.forward ? TraversalDirection.right : TraversalDirection.left,
@ -105,7 +105,7 @@ class SeekAction extends Action<SeekIntent> {
);
return null;
}
final position = (await audioPlayer.position ?? Duration.zero).inSeconds;
final position = audioPlayer.position.inSeconds;
await audioPlayer.seek(
Duration(
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/streams/streams.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/utils/platform.dart';
import 'package:spotube/components/spotube_page_route.dart';
@ -59,11 +59,9 @@ final routerProvider = Provider((ref) {
path: "/",
name: HomePage.name,
redirect: (context, state) async {
final authNotifier = ref.read(authenticationProvider.notifier);
final json = await authNotifier.box.get(authNotifier.cacheKey);
final auth = await ref.read(authenticationProvider.future);
if (json?["cookie"] == null &&
!KVStoreService.doneGettingStarted) {
if (auth == null && !KVStoreService.doneGettingStarted) {
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/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';
class AnonymousFallback extends ConsumerWidget {

View File

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

View File

@ -1,7 +1,7 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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';
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/context.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
import 'package:spotube/provider/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_provider.dart';
@ -95,8 +96,8 @@ class TrackOptions extends HookConsumerWidget {
WidgetRef ref,
Track track,
) async {
final playback = ref.read(proxyPlaylistProvider.notifier);
final playlist = ref.read(proxyPlaylistProvider);
final playback = ref.read(audioPlayerProvider.notifier);
final playlist = ref.read(audioPlayerProvider);
final spotify = ref.read(spotifyProvider);
final query = "${track.name} Radio";
final pages =
@ -159,8 +160,8 @@ class TrackOptions extends HookConsumerWidget {
final router = GoRouter.of(context);
final ThemeData(:colorScheme) = Theme.of(context);
final playlist = ref.watch(proxyPlaylistProvider);
final playback = ref.watch(proxyPlaylistProvider.notifier);
final playlist = ref.watch(audioPlayerProvider);
final playback = ref.watch(audioPlayerProvider.notifier);
final auth = ref.watch(authenticationProvider);
ref.watch(downloadManagerProvider);
final downloadManager = ref.watch(downloadManagerProvider.notifier);
@ -170,11 +171,8 @@ class TrackOptions extends HookConsumerWidget {
final favorites = useTrackToggleLike(track, ref);
final isBlackListed = useMemoized(
() => blacklist.contains(
BlacklistedElement.track(
track.id!,
track.name!,
),
() => blacklist.asData?.value.any(
(element) => element.elementId == track.id,
),
[blacklist, track],
);
@ -258,13 +256,16 @@ class TrackOptions extends HookConsumerWidget {
.removeTracks(playlistId ?? "", [track.id!]);
break;
case TrackOptionValue.blacklist:
if (isBlackListed) {
ref.read(blacklistProvider.notifier).remove(
BlacklistedElement.track(track.id!, track.name!),
);
if (isBlackListed == null) break;
if (isBlackListed == true) {
await ref.read(blacklistProvider.notifier).remove(track.id!);
} else {
ref.read(blacklistProvider.notifier).add(
BlacklistedElement.track(track.id!, track.name!),
await ref.read(blacklistProvider.notifier).add(
BlacklistTableCompanion.insert(
name: track.name!,
elementId: track.id!,
elementType: BlacklistedType.track,
),
);
}
break;
@ -363,7 +364,7 @@ class TrackOptions extends HookConsumerWidget {
: context.l10n.save_as_favorite,
),
),
if (auth != null && !isLocalTrack) ...[
if (auth.asData?.value != null && !isLocalTrack) ...[
PopSheetEntry(
value: TrackOptionValue.startRadio,
leading: const Icon(SpotubeIcons.radio),
@ -375,7 +376,7 @@ class TrackOptions extends HookConsumerWidget {
title: Text(context.l10n.add_to_playlist),
),
],
if (userPlaylist && auth != null && !isLocalTrack)
if (userPlaylist && auth.asData?.value != null && !isLocalTrack)
PopSheetEntry(
value: TrackOptionValue.removeFromPlaylist,
leading: const Icon(SpotubeIcons.removeFilled),
@ -399,10 +400,10 @@ class TrackOptions extends HookConsumerWidget {
PopSheetEntry(
value: TrackOptionValue.blacklist,
leading: const Icon(SpotubeIcons.playlistRemove),
iconColor: !isBlackListed ? Colors.red[400] : null,
textColor: !isBlackListed ? Colors.red[400] : null,
iconColor: isBlackListed != true ? Colors.red[400] : null,
textColor: isBlackListed != true ? Colors.red[400] : null,
title: Text(
isBlackListed
isBlackListed == true
? context.l10n.remove_from_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/image.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/proxy_playlist/proxy_playlist.dart';
class TrackTile extends HookConsumerWidget {
/// [index] will not be shown if null
@ -30,7 +31,7 @@ class TrackTile extends HookConsumerWidget {
final VoidCallback? onLongPress;
final bool userPlaylist;
final String? playlistId;
final ProxyPlaylist playlist;
final AudioPlayerState playlist;
final List<Widget>? leadingActions;
@ -53,14 +54,10 @@ class TrackTile extends HookConsumerWidget {
final theme = Theme.of(context);
final blacklist = ref.watch(blacklistProvider);
final blacklistNotifier = ref.watch(blacklistProvider.notifier);
final isBlackListed = useMemoized(
() => blacklist.contains(
BlacklistedElement.track(
track.id!,
track.name!,
),
),
() => blacklistNotifier.contains(track),
[blacklist, track],
);
@ -87,187 +84,190 @@ class TrackTile extends HookConsumerWidget {
},
child: HoverBuilder(
permanentState: isSelected || constrains.smAndDown ? true : null,
builder: (context, isHovering) {
return ListTile(
selected: isSelected,
onTap: () async {
try {
isLoading.value = true;
await onTap?.call();
} finally {
if (context.mounted) {
isLoading.value = false;
}
builder: (context, isHovering) => ListTile(
selected: isSelected,
onTap: () async {
try {
isLoading.value = true;
await onTap?.call();
} finally {
if (context.mounted) {
isLoading.value = false;
}
},
onLongPress: onLongPress,
enabled: !isBlackListed,
contentPadding: EdgeInsets.zero,
tileColor:
isBlackListed ? theme.colorScheme.errorContainer : null,
horizontalTitleGap: 12,
leadingAndTrailingTextStyle: theme.textTheme.bodyMedium,
leading: Row(
mainAxisSize: MainAxisSize.min,
children: [
...?leadingActions,
if (index != null && onChanged == null && constrains.mdAndUp)
SizedBox(
width: 50,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: Text(
'${(index ?? 0) + 1}',
maxLines: 1,
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center,
),
}
},
onLongPress: onLongPress,
enabled: !isBlackListed,
contentPadding: EdgeInsets.zero,
tileColor: isBlackListed ? theme.colorScheme.errorContainer : null,
horizontalTitleGap: 12,
leadingAndTrailingTextStyle: theme.textTheme.bodyMedium,
leading: Row(
mainAxisSize: MainAxisSize.min,
children: [
...?leadingActions,
if (index != null && onChanged == null && constrains.mdAndUp)
SizedBox(
width: 50,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: Text(
'${(index ?? 0) + 1}',
maxLines: 1,
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center,
),
)
else if (constrains.smAndDown)
const SizedBox(width: 16),
if (onChanged != null)
Checkbox(
value: selected,
onChanged: onChanged,
),
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: AspectRatio(
aspectRatio: 1,
child: UniversalImage(
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),
),
),
),
),
),
],
)
else if (constrains.smAndDown)
const SizedBox(width: 16),
if (onChanged != null)
Checkbox(
value: selected,
onChanged: onChanged,
),
],
),
title: Row(
children: [
Expanded(
flex: 6,
child: switch (track) {
LocalTrack() => Text(
track.name!,
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,
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: AspectRatio(
aspectRatio: 1,
child: UniversalImage(
path: (track.album?.images).asUrlString(
placeholder: ImagePlaceholder.albumArt,
),
_ => Align(
alignment: Alignment.centerLeft,
child: LinkText(
track.album!.name!,
"/album/${track.album?.id}",
extra: track.album,
push: true,
overflow: TextOverflow.ellipsis,
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: 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,
child: track is LocalTrack
? Text(
track.artists?.asString() ?? '',
)
: ClipRect(
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 40),
child: ArtistLink(artists: track.artists ?? []),
),
),
],
),
title: Row(
children: [
Expanded(
flex: 6,
child: switch (track) {
LocalTrack() => Text(
track.name!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
_ => LinkText(
track.name!,
"/track/${track.id}",
push: true,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
},
),
if (constrains.mdAndUp) ...[
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,
Expanded(
flex: 4,
child: switch (track) {
LocalTrack() => Text(
track.album!.name!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
_ => Align(
alignment: Alignment.centerLeft,
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/provider/connect/connect.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:very_good_infinite_list/very_good_infinite_list.dart';
@ -27,9 +27,9 @@ class TrackViewBodySection extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final playlist = ref.watch(proxyPlaylistProvider);
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
final historyNotifier = ref.watch(playbackHistoryProvider.notifier);
final playlist = ref.watch(audioPlayerProvider);
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final historyNotifier = ref.watch(playbackHistoryActionsProvider);
final props = InheritedTrackView.of(context);
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_provider.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/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_state.dart';
class TrackViewBodyOptions extends HookConsumerWidget {
const TrackViewBodyOptions({super.key});
@ -24,8 +24,8 @@ class TrackViewBodyOptions extends HookConsumerWidget {
ref.watch(downloadManagerProvider);
final downloader = ref.watch(downloadManagerProvider.notifier);
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
final historyNotifier = ref.watch(playbackHistoryProvider.notifier);
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final historyNotifier = ref.watch(playbackHistoryActionsProvider);
final 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/track_view_props.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/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
class TrackViewHeaderActions extends HookConsumerWidget {
const TrackViewHeaderActions({super.key});
@ -20,9 +20,9 @@ class TrackViewHeaderActions extends HookConsumerWidget {
Widget build(BuildContext context, ref) {
final props = InheritedTrackView.of(context);
final playlist = ref.watch(proxyPlaylistProvider);
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
final historyNotifier = ref.watch(playbackHistoryProvider.notifier);
final playlist = ref.watch(audioPlayerProvider);
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final historyNotifier = ref.watch(playbackHistoryActionsProvider);
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(
isLiked: props.isLiked,
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/provider/connect/connect.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';
class TrackViewHeaderButtons extends HookConsumerWidget {
@ -28,9 +28,9 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final props = InheritedTrackView.of(context);
final playlist = ref.watch(proxyPlaylistProvider);
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
final historyNotifier = ref.watch(playbackHistoryProvider.notifier);
final playlist = ref.watch(audioPlayerProvider);
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final historyNotifier = ref.watch(playbackHistoryActionsProvider);
final isActive = playlist.collections.contains(props.collectionId);
@ -131,7 +131,9 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
);
}
} 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: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_state.dart';
import 'package:local_notifier/local_notifier.dart';
import 'package:spotube/utils/platform.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_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
void useEndlessPlayback(WidgetRef ref) {
final auth = ref.watch(authenticationProvider);
final playback = ref.watch(proxyPlaylistProvider.notifier);
final playlist = ref.watch(proxyPlaylistProvider);
final playback = ref.watch(audioPlayerProvider.notifier);
final playlist = ref.watch(audioPlayerProvider.select((s) => s.playlist));
final spotify = ref.watch(spotifyProvider);
final endlessPlayback =
ref.watch(userPreferencesProvider.select((s) => s.endlessPlayback));
useEffect(
() {
if (!endlessPlayback || auth == null) return null;
if (!endlessPlayback || auth.asData?.value == null) return null;
void listener(int index) async {
try {
final playlist = ref.read(proxyPlaylistProvider);
final playlist = ref.read(audioPlayerProvider);
if (index != playlist.tracks.length - 1) return;
final track = playlist.tracks.last;
@ -56,7 +56,7 @@ void useEndlessPlayback(WidgetRef ref) {
await playback.addTracks(
tracks.toList()
..removeWhere((e) {
final playlist = ref.read(proxyPlaylistProvider);
final playlist = ref.read(audioPlayerProvider);
final isDuplicate = playlist.tracks.any((t) => t.id == e.id);
return e.id == track.id || isDuplicate;
}),
@ -69,9 +69,9 @@ void useEndlessPlayback(WidgetRef ref) {
// Sometimes user can change settings for which the currentIndexChanged
// 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.
if (playlist.active == playlist.tracks.length - 1 &&
if (playlist.index == playlist.medias.length - 1 &&
audioPlayer.isPlaying) {
listener(playlist.active!);
listener(playlist.index);
}
final subscription =
@ -82,7 +82,7 @@ void useEndlessPlayback(WidgetRef ref) {
[
spotify,
playback,
playlist.tracks,
playlist.medias,
endlessPlayback,
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_disable_battery_optimizations.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/server.dart';
import 'package:spotube/provider/tray_manager/tray_manager.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/palette_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/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/logger/logger.dart';
import 'package:spotube/services/wm_tools/wm_tools.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:system_theme/system_theme.dart';
import 'package:path_provider/path_provider.dart';
@ -78,39 +80,30 @@ Future<void> main(List<String> rawArgs) async {
}
await KVStoreService.initialize();
await EncryptedKvStoreService.initialize();
final hiveCacheDir =
kIsWeb ? null : (await getApplicationSupportDirectory()).path;
Hive.init(hiveCacheDir);
Hive.registerAdapter(SkipSegmentAdapter());
final database = AppDatabase();
Hive.registerAdapter(SourceMatchAdapter());
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,
);
await migrateFromHiveToDrift(database);
if (kIsDesktop) {
await localNotifier.setup(appName: "Spotube");
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));
final router = ref.watch(routerProvider);
ref.listen(serverProvider, (_, __) {});
ref.listen(audioPlayerStreamListenersProvider, (_, __) {});
ref.listen(bonsoirProvider, (_, __) {});
ref.listen(connectClientsProvider, (_, __) {});
ref.listen(serverProvider, (_, __) {});
ref.listen(trayManagerProvider, (_, __) {});
useDisableBatteryOptimizations();

View File

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

View File

@ -183,7 +183,7 @@ class WebSocketEvent<T> {
if (type == WsEvent.loop) {
await callback(
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> {
WebSocketLoopEvent(PlaybackLoopMode data) : super(WsEvent.loop, data);
class WebSocketLoopEvent extends WebSocketEvent<PlaylistMode> {
WebSocketLoopEvent(PlaylistMode data) : super(WsEvent.loop, data);
WebSocketLoopEvent.fromJson(Map<String, dynamic> json)
: super(
WsEvent.loop, PlaybackLoopMode.fromString(json["data"] as String));
WsEvent.loop,
PlaylistMode.values.firstWhere(
(e) => e.name == json["data"] as String,
),
);
@override
String toJson() {
@ -321,12 +325,12 @@ class WebSocketErrorEvent extends WebSocketEvent<String> {
WebSocketErrorEvent(String data) : super(WsEvent.error, data);
}
class WebSocketQueueEvent extends WebSocketEvent<ProxyPlaylist> {
WebSocketQueueEvent(ProxyPlaylist data) : super(WsEvent.queue, data);
class WebSocketQueueEvent extends WebSocketEvent<AudioPlayerState> {
WebSocketQueueEvent(AudioPlayerState data) : super(WsEvent.queue, data);
factory WebSocketQueueEvent.fromJson(Map<String, dynamic> json) =>
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/models/connect/connect.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/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/services/audio_player/audio_player.dart';
import 'package:spotube/utils/service_utils.dart';
@ -30,11 +31,12 @@ class AlbumCard extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final playlist = ref.watch(proxyPlaylistProvider);
final playlist = ref.watch(audioPlayerProvider);
final playing =
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
final historyNotifier = ref.read(playbackHistoryProvider.notifier);
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final historyNotifier = ref.read(playbackHistoryActionsProvider);
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
bool isPlaylistPlaying = useMemoized(
() => playlist.containsCollection(album.id!),
@ -59,8 +61,8 @@ class AlbumCard extends HookConsumerWidget {
),
margin: const EdgeInsets.symmetric(horizontal: 10),
isPlaying: isPlaylistPlaying,
isLoading: (isPlaylistPlaying && playlist.isFetching == true) ||
updating.value,
isLoading:
(isPlaylistPlaying && isFetchingActiveTrack) || updating.value,
title: album.name!,
description:
"${album.albumType?.formatted}${album.artists?.asString() ?? ""}",

View File

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

View File

@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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 {
final void Function()? onDone;
@ -52,10 +52,7 @@ class TokenLoginForm extends HookConsumerWidget {
final cookieHeader =
"sp_dc=${directCodeController.text.trim()}";
authenticationNotifier.setCredentials(
await AuthenticationCredentials.fromCookie(
cookieHeader),
);
await authenticationNotifier.login(cookieHeader);
if (context.mounted) {
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/hooks/utils/use_breakpoint_value.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';
class HomePageFriendsSection extends HookConsumerWidget {
@ -59,7 +59,7 @@ class HomePageFriendsSection extends HookConsumerWidget {
if (friendsQuery.isLoading ||
friendsQuery.asData?.value.friends.isEmpty == true ||
auth == null) {
auth.asData?.value == null) {
return const SliverToBoxAdapter(
child: SizedBox.shrink(),
);

View File

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

View File

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

View File

@ -46,7 +46,7 @@ class LocalFolderItem extends HookConsumerWidget {
...pathSegments.skip(pathSegments.length - 3).toList()
..removeLast(),
]
: pathSegments.take(pathSegments.length - 1).toList();
: pathSegments.take(max(pathSegments.length - 1, 0)).toList();
final trackSnapshot = ref.watch(
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/extensions/constrains.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';
class UserAlbums extends HookConsumerWidget {
@ -46,7 +46,7 @@ class UserAlbums extends HookConsumerWidget {
[];
}, [albumsQuery.asData?.value, searchText.value]);
if (auth == null) {
if (auth.asData?.value == null) {
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/extensions/constrains.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';
class UserArtists extends HookConsumerWidget {
@ -48,7 +48,7 @@ class UserArtists extends HookConsumerWidget {
final controller = useScrollController();
if (auth == null) {
if (auth.asData?.value == null) {
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/context.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/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart';
@ -75,7 +75,7 @@ class UserPlaylists extends HookConsumerWidget {
final controller = useScrollController();
if (auth == null) {
if (auth.asData?.value == null) {
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/models/local_track.dart';
import 'package:spotube/pages/lyrics/lyrics.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/server/active_sourced_track.dart';
import 'package:spotube/provider/volume_provider.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart';
@ -47,7 +47,7 @@ class PlayerView extends HookConsumerWidget {
final auth = ref.watch(authenticationProvider);
final sourcedCurrentTrack = ref.watch(activeSourcedTrackProvider);
final currentActiveTrack =
ref.watch(proxyPlaylistProvider.select((s) => s.activeTrack));
ref.watch(audioPlayerProvider.select((s) => s.activeTrack));
final currentTrack = sourcedCurrentTrack ?? currentActiveTrack;
final isLocalTrack = currentTrack is LocalTrack;
final mediaQuery = MediaQuery.of(context);
@ -309,15 +309,13 @@ class PlayerView extends HookConsumerWidget {
builder: (context) => Consumer(
builder: (context, ref, _) {
final playlist = ref.watch(
proxyPlaylistProvider,
);
final playlistNotifier =
ref.read(
proxyPlaylistProvider
.notifier,
audioPlayerProvider,
);
final playlistNotifier = ref
.read(audioPlayerProvider
.notifier);
return PlayerQueue
.fromProxyPlaylistNotifier(
.fromAudioPlayerNotifier(
floating: false,
playlist: playlist,
notifier: playlistNotifier,
@ -328,8 +326,9 @@ class PlayerView extends HookConsumerWidget {
}
: null),
),
if (auth != null) const SizedBox(width: 10),
if (auth != null)
if (auth.asData?.value != null)
const SizedBox(width: 10),
if (auth.asData?.value != null)
Expanded(
child: OutlinedButton.icon(
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/logger.dart';
import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/sleep_timer_provider.dart';
class PlayerActions extends HookConsumerWidget {
@ -33,7 +33,7 @@ class PlayerActions extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final playlist = ref.watch(proxyPlaylistProvider);
final playlist = ref.watch(audioPlayerProvider);
final isLocalTrack = playlist.activeTrack is LocalTrack;
ref.watch(downloadManagerProvider);
final downloader = ref.watch(downloadManagerProvider.notifier);
@ -129,7 +129,9 @@ class PlayerActions extends HookConsumerWidget {
? () => downloader.addToQueue(playlist.activeTrack!)
: null,
),
if (playlist.activeTrack != null && !isLocalTrack && auth != null)
if (playlist.activeTrack != null &&
!isLocalTrack &&
auth.asData?.value != null)
TrackHeartButton(track: playlist.activeTrack!),
AdaptivePopSheetList(
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_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:media_kit/media_kit.dart';
import 'package:palette_generator/palette_generator.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/modules/player/use_progress.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/loop_mode.dart';
class PlayerControls extends HookConsumerWidget {
final PaletteGenerator? palette;
@ -43,8 +44,7 @@ class PlayerControls extends HookConsumerWidget {
SeekIntent: SeekAction(),
},
[]);
final playlist = ref.watch(proxyPlaylistProvider);
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
final playing =
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
@ -132,7 +132,7 @@ class PlayerControls extends HookConsumerWidget {
// than total duration. Keeping it resolved
value: progress.value.toDouble(),
secondaryTrackValue: bufferProgress,
onChanged: playlist.isFetching == true
onChanged: isFetchingActiveTrack
? null
: (v) {
progress.value = v;
@ -183,7 +183,7 @@ class PlayerControls extends HookConsumerWidget {
: context.l10n.shuffle_playlist,
icon: const Icon(SpotubeIcons.shuffle),
style: shuffled ? activeButtonStyle : buttonStyle,
onPressed: playlist.isFetching == true
onPressed: isFetchingActiveTrack
? null
: () {
if (shuffled) {
@ -198,15 +198,15 @@ class PlayerControls extends HookConsumerWidget {
tooltip: context.l10n.previous_track,
icon: const Icon(SpotubeIcons.skipBack),
style: buttonStyle,
onPressed: playlist.isFetching == true
onPressed: isFetchingActiveTrack
? null
: playlistNotifier.previous,
: audioPlayer.skipToPrevious,
),
IconButton(
tooltip: playing
? context.l10n.pause_playback
: context.l10n.resume_playback,
icon: playlist.isFetching == true
icon: isFetchingActiveTrack
? SizedBox(
height: 20,
width: 20,
@ -219,7 +219,7 @@ class PlayerControls extends HookConsumerWidget {
playing ? SpotubeIcons.pause : SpotubeIcons.play,
),
style: resumePauseStyle,
onPressed: playlist.isFetching == true
onPressed: isFetchingActiveTrack
? null
: Actions.handler<PlayPauseIntent>(
context,
@ -230,45 +230,41 @@ class PlayerControls extends HookConsumerWidget {
tooltip: context.l10n.next_track,
icon: const Icon(SpotubeIcons.skipForward),
style: buttonStyle,
onPressed: playlist.isFetching == true
? null
: playlistNotifier.next,
onPressed:
isFetchingActiveTrack ? null : audioPlayer.skipToNext,
),
StreamBuilder<PlaybackLoopMode>(
stream: audioPlayer.loopModeStream,
builder: (context, snapshot) {
final loopMode = snapshot.data ?? PlaybackLoopMode.none;
return IconButton(
tooltip: loopMode == PlaybackLoopMode.one
? context.l10n.loop_track
: loopMode == PlaybackLoopMode.all
? context.l10n.repeat_playlist
: null,
icon: Icon(
loopMode == PlaybackLoopMode.one
? SpotubeIcons.repeatOne
: SpotubeIcons.repeat,
),
style: loopMode == PlaybackLoopMode.one ||
loopMode == PlaybackLoopMode.all
? activeButtonStyle
: buttonStyle,
onPressed: playlist.isFetching == true
? null
: () async {
audioPlayer.setLoopMode(
switch (loopMode) {
PlaybackLoopMode.all =>
PlaybackLoopMode.one,
PlaybackLoopMode.one =>
PlaybackLoopMode.none,
PlaybackLoopMode.none =>
PlaybackLoopMode.all,
},
);
Consumer(builder: (context, ref, _) {
final loopMode = ref
.watch(audioPlayerProvider.select((s) => s.loopMode));
return IconButton(
tooltip: loopMode == PlaylistMode.single
? context.l10n.loop_track
: loopMode == PlaylistMode.loop
? context.l10n.repeat_playlist
: null,
icon: Icon(
loopMode == PlaylistMode.single
? SpotubeIcons.repeatOne
: SpotubeIcons.repeat,
),
style: loopMode == PlaylistMode.single ||
loopMode == PlaylistMode.loop
? activeButtonStyle
: buttonStyle,
onPressed: isFetchingActiveTrack
? null
: () async {
await audioPlayer.setLoopMode(
switch (loopMode) {
PlaylistMode.loop => PlaylistMode.single,
PlaylistMode.single => PlaylistMode.none,
PlaylistMode.none => PlaylistMode.loop,
},
);
}),
);
},
);
}),
],
),
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/modules/player/use_progress.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';
class PlayerOverlay extends HookConsumerWidget {
@ -24,8 +25,8 @@ class PlayerOverlay extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
final playlist = ref.watch(proxyPlaylistProvider);
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
final playlist = ref.watch(audioPlayerProvider);
final canShow = playlist.activeTrack != null;
final playing =
@ -127,14 +128,14 @@ class PlayerOverlay extends HookConsumerWidget {
SpotubeIcons.skipBack,
color: textColor,
),
onPressed: playlist.isFetching
onPressed: isFetchingActiveTrack
? null
: playlistNotifier.previous,
: audioPlayer.skipToPrevious,
),
Consumer(
builder: (context, ref, _) {
return IconButton(
icon: playlist.isFetching
icon: isFetchingActiveTrack
? const SizedBox(
height: 20,
width: 20,
@ -158,9 +159,9 @@ class PlayerOverlay extends HookConsumerWidget {
SpotubeIcons.skipForward,
color: textColor,
),
onPressed: playlist.isFetching
onPressed: isFetchingActiveTrack
? 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/context.dart';
import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart';
import 'package:spotube/provider/proxy_playlist/proxy_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/audio_player/state.dart';
class PlayerQueue extends HookConsumerWidget {
final bool floating;
final ProxyPlaylist playlist;
final AudioPlayerState playlist;
final Future<void> Function(Track track) onJump;
final Future<void> Function(String trackId) onRemove;
@ -40,10 +40,10 @@ class PlayerQueue extends HookConsumerWidget {
super.key,
});
PlayerQueue.fromProxyPlaylistNotifier({
PlayerQueue.fromAudioPlayerNotifier({
this.floating = true,
required this.playlist,
required ProxyPlaylistNotifier notifier,
required AudioPlayerNotifier notifier,
super.key,
}) : onJump = notifier.jumpToTrack,
onRemove = notifier.removeTrack,
@ -93,11 +93,10 @@ class PlayerQueue extends HookConsumerWidget {
);
useEffect(() {
if (playlist.active == null) return null;
if (playlist.activeTrack == null) return null;
if (playlist.active! < 0) return;
controller.scrollToIndex(
playlist.active!,
playlist.playlist.index,
preferPosition: AutoScrollPosition.middle,
);
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/constrains.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';
class PlayerTrackDetails extends HookConsumerWidget {
@ -21,7 +21,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
final playback = ref.watch(proxyPlaylistProvider);
final playback = ref.watch(audioPlayerProvider);
return Row(
children: [

View File

@ -14,10 +14,12 @@ import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.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/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/video_info.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
@ -52,7 +54,8 @@ class SiblingTracksSheet extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
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 isSearching = useState(false);
@ -128,13 +131,13 @@ class SiblingTracksSheet extends HookConsumerWidget {
]);
final siblings = useMemoized(
() => playlist.isFetching == false
() => !isFetchingActiveTrack
? [
(activeTrack as SourcedTrack).sourceInfo,
...activeTrack.siblings,
]
: <SourceInfo>[],
[playlist.isFetching, activeTrack],
[activeTrack, isFetchingActiveTrack],
);
final borderRadius = floating
@ -174,12 +177,12 @@ class SiblingTracksSheet extends HookConsumerWidget {
Text("${sourceInfo.artist}"),
],
),
enabled: playlist.isFetching != true,
selected: playlist.isFetching != true &&
enabled: !isFetchingActiveTrack,
selected: !isFetchingActiveTrack &&
sourceInfo.id == (activeTrack as SourcedTrack).sourceInfo.id,
selectedTileColor: theme.popupMenuTheme.color,
onTap: () {
if (playlist.isFetching == false &&
if (!isFetchingActiveTrack &&
sourceInfo.id != (activeTrack as SourcedTrack).sourceInfo.id) {
activeTrackNotifier.swapSibling(sourceInfo);
Navigator.of(context).pop();
@ -187,7 +190,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
},
);
},
[playlist.isFetching, activeTrack, siblings],
[activeTrack, siblings],
);
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:hooks_riverpod/hooks_riverpod.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;
useEffect(() {
final durationOperation =
CancelableOperation.fromFuture(audioPlayer.duration);
durationOperation.then((value) {
if (value != null) {
duration.value = value;
}
});
duration.value = audioPlayer.duration;
final durationSubscription = audioPlayer.durationStream.listen((event) {
duration.value = event;
});
final positionOperation =
CancelableOperation.fromFuture(audioPlayer.position);
positionOperation.then((value) {
if (value != null) {
position.value = value;
}
});
position.value = audioPlayer.position;
var lastPosition = position.value;
@ -54,9 +40,7 @@ import 'package:spotube/services/audio_player/audio_player.dart';
});
return () {
positionOperation.cancel();
positionSubscription.cancel();
durationOperation.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/models/connect/connect.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/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/services/audio_player/audio_player.dart';
import 'package:spotube/utils/service_utils.dart';
@ -22,9 +23,10 @@ class PlaylistCard extends HookConsumerWidget {
});
@override
Widget build(BuildContext context, ref) {
final playlistQueue = ref.watch(proxyPlaylistProvider);
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
final historyNotifier = ref.read(playbackHistoryProvider.notifier);
final playlistQueue = ref.watch(audioPlayerProvider);
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
final historyNotifier = ref.read(playbackHistoryActionsProvider);
final playing =
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
@ -65,8 +67,7 @@ class PlaylistCard extends HookConsumerWidget {
placeholder: ImagePlaceholder.collection,
),
isPlaying: isPlaylistPlaying,
isLoading:
(isPlaylistPlaying && playlistQueue.isFetching) || updating.value,
isLoading: (isPlaylistPlaying && isFetchingActiveTrack) || updating.value,
isOwner: playlist.owner?.id == me.asData?.value.id &&
me.asData?.value.id != null,
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/spotube_icons.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/player/player_actions.dart';
import 'package:spotube/modules/player/player_overlay.dart';
import 'package:spotube/modules/player/player_track_details.dart';
@ -17,10 +18,10 @@ import 'package:spotube/extensions/image.dart';
import 'package:spotube/hooks/utils/use_brightness_value.dart';
import 'package:spotube/models/logger.dart';
import 'package:flutter/material.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/authentication/authentication.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_state.dart';
import 'package:spotube/provider/volume_provider.dart';
import 'package:spotube/utils/platform.dart';
import 'package:window_manager/window_manager.dart';
@ -32,7 +33,7 @@ class BottomPlayer extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final auth = ref.watch(authenticationProvider);
final playlist = ref.watch(proxyPlaylistProvider);
final playlist = ref.watch(audioPlayerProvider);
final layoutMode =
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
@ -90,7 +91,7 @@ class BottomPlayer extends HookConsumerWidget {
children: [
PlayerActions(
extraActions: [
if (auth != null)
if (auth.asData?.value != null)
IconButton(
tooltip: context.l10n.mini_player,
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/side_bar_tiles.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/components/image/universal_image.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/settings/settings.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/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/service_utils.dart';
import 'package:window_manager/window_manager.dart';
@ -268,7 +269,7 @@ class SidebarFooter extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (auth != null && data == null)
if (auth.asData?.value != null && data == null)
const CircularProgressIndicator()
else if (data != null)
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/context.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/user_preferences/user_preferences_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
import 'package:spotube/utils/service_utils.dart';
final navigationPanelHeight = StateProvider<double>((ref) => 50);

View File

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

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.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/modules/stats/summary/summary_card.dart';
import 'package:spotube/extensions/constrains.dart';
@ -18,83 +20,87 @@ class StatsPageSummarySection extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final summary = ref.watch(playbackHistorySummaryProvider);
final summaryData = summary.asData?.value ?? FakeData.historySummary;
return SliverPadding(
padding: const EdgeInsets.all(10),
sliver: SliverLayoutBuilder(builder: (context, constrains) {
return SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: constrains.isXs
? 2
: constrains.smAndDown
? 3
: constrains.mdAndDown
? 4
: constrains.lgAndDown
? 5
: 6,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: constrains.isXs ? 1.3 : 1.5,
),
delegate: SliverChildListDelegate([
SummaryCard(
title: summary.duration.inMinutes.toDouble(),
unit: "minutes",
description: 'Listened to music',
color: Colors.purple,
onTap: () {
ServiceUtils.pushNamed(context, StatsMinutesPage.name);
},
return Skeletonizer.sliver(
enabled: summary.isLoading,
child: SliverPadding(
padding: const EdgeInsets.all(10),
sliver: SliverLayoutBuilder(builder: (context, constrains) {
return SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: constrains.isXs
? 2
: constrains.smAndDown
? 3
: constrains.mdAndDown
? 4
: constrains.lgAndDown
? 5
: 6,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: constrains.isXs ? 1.3 : 1.5,
),
SummaryCard(
title: summary.tracks.toDouble(),
unit: "songs",
description: 'Streamed overall',
color: Colors.lightBlue,
onTap: () {
ServiceUtils.pushNamed(context, StatsStreamsPage.name);
},
),
SummaryCard.unformatted(
title: usdFormatter.format(summary.fees.toDouble()),
unit: "",
description: 'Owed to artists\nthis month',
color: Colors.green,
onTap: () {
ServiceUtils.pushNamed(context, StatsStreamFeesPage.name);
},
),
SummaryCard(
title: summary.artists.toDouble(),
unit: "artist's",
description: 'Music reached you',
color: Colors.yellow,
onTap: () {
ServiceUtils.pushNamed(context, StatsArtistsPage.name);
},
),
SummaryCard(
title: summary.albums.toDouble(),
unit: "full albums",
description: 'Got your love',
color: Colors.pink,
onTap: () {
ServiceUtils.pushNamed(context, StatsAlbumsPage.name);
},
),
SummaryCard(
title: summary.playlists.toDouble(),
unit: "playlists",
description: 'Were on repeat',
color: Colors.teal,
onTap: () {
ServiceUtils.pushNamed(context, StatsPlaylistsPage.name);
},
),
]),
);
}),
delegate: SliverChildListDelegate([
SummaryCard(
title: summaryData.duration.inMinutes.toDouble(),
unit: "minutes",
description: 'Listened to music',
color: Colors.purple,
onTap: () {
ServiceUtils.pushNamed(context, StatsMinutesPage.name);
},
),
SummaryCard(
title: summaryData.tracks.toDouble(),
unit: "songs",
description: 'Streamed overall',
color: Colors.lightBlue,
onTap: () {
ServiceUtils.pushNamed(context, StatsStreamsPage.name);
},
),
SummaryCard.unformatted(
title: usdFormatter.format(summaryData.fees.toDouble()),
unit: "",
description: 'Owed to artists\nthis month',
color: Colors.green,
onTap: () {
ServiceUtils.pushNamed(context, StatsStreamFeesPage.name);
},
),
SummaryCard(
title: summaryData.artists.toDouble(),
unit: "artist's",
description: 'Music reached you',
color: Colors.yellow,
onTap: () {
ServiceUtils.pushNamed(context, StatsArtistsPage.name);
},
),
SummaryCard(
title: summaryData.albums.toDouble(),
unit: "full albums",
description: 'Got your love',
color: Colors.pink,
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:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart';
import 'package:spotube/modules/stats/common/album_item.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 {
const TopAlbums({super.key});
@ -10,20 +14,32 @@ class TopAlbums extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
final albums = ref.watch(playbackHistoryTopProvider(historyDuration)
.select((value) => value.albums));
final topAlbums = ref.watch(historyTopAlbumsProvider(historyDuration));
final topAlbumsNotifier =
ref.watch(historyTopAlbumsProvider(historyDuration).notifier);
return SliverList.builder(
itemCount: albums.length,
itemBuilder: (context, index) {
final album = albums[index];
return StatsAlbumItem(
album: album.album,
info: Text(
"${compactNumberFormatter.format(album.count)} plays",
),
);
},
final albumsData = topAlbums.asData?.value.items ?? [];
return Skeletonizer.sliver(
enabled: topAlbums.isLoading && !topAlbums.isLoadingNextPage,
child: SliverInfiniteList(
onFetchData: () async {
await topAlbumsNotifier.fetchMore();
},
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_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart';
import 'package:spotube/modules/stats/common/artist_item.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 {
const TopArtists({super.key});
@ -10,18 +15,33 @@ class TopArtists extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
final artists = ref.watch(playbackHistoryTopProvider(historyDuration)
.select((value) => value.artists));
final topTracks = ref.watch(
historyTopTracksProvider(historyDuration),
);
final topTracksNotifier =
ref.watch(historyTopTracksProvider(historyDuration).notifier);
return SliverList.builder(
itemCount: artists.length,
itemBuilder: (context, index) {
final artist = artists[index];
return StatsArtistItem(
artist: artist.artist,
info: Text("${compactNumberFormatter.format(artist.count)} plays"),
);
},
final artistsData = useMemoized(
() => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]);
return 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("${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/artists.dart';
import 'package:spotube/modules/stats/top/tracks.dart';
import 'package:spotube/provider/history/state.dart';
import 'package:spotube/provider/history/top.dart';
class StatsPageTopSection extends HookConsumerWidget {

View File

@ -1,8 +1,12 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart';
import 'package:spotube/modules/stats/common/track_item.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 {
const TopTracks({super.key});
@ -10,22 +14,34 @@ class TopTracks extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
final tracks = ref.watch(
playbackHistoryTopProvider(historyDuration)
.select((value) => value.tracks),
final topTracks = ref.watch(
historyTopTracksProvider(historyDuration),
);
final topTracksNotifier =
ref.watch(historyTopTracksProvider(historyDuration).notifier);
return SliverList.builder(
itemCount: tracks.length,
itemBuilder: (context, index) {
final track = tracks[index];
return StatsTrackItem(
track: track.track,
info: Text(
"${compactNumberFormatter.format(track.count)} plays",
),
);
},
final tracksData = topTracks.asData?.value.items ?? [];
return 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: 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/image.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/spotify/spotify.dart';
import 'package:spotube/utils/primitive_utils.dart';
@ -39,10 +40,9 @@ class ArtistPageHeader extends HookConsumerWidget {
);
final auth = ref.watch(authenticationProvider);
final blacklist = ref.watch(blacklistProvider);
final isBlackListed = blacklist.contains(
BlacklistedElement.artist(artistId, artist.name!),
);
ref.watch(blacklistProvider);
final blacklistNotifier = ref.watch(blacklistProvider.notifier);
final isBlackListed = blacklistNotifier.containsArtist(artist);
final image = artist.images.asUrlString(
placeholder: ImagePlaceholder.artist,
@ -135,7 +135,7 @@ class ArtistPageHeader extends HookConsumerWidget {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (auth != null)
if (auth.asData?.value != null)
Consumer(
builder: (context, ref, _) {
final isFollowingQuery = ref
@ -187,14 +187,16 @@ class ArtistPageHeader extends HookConsumerWidget {
),
onPressed: () async {
if (isBlackListed) {
ref.read(blacklistProvider.notifier).remove(
BlacklistedElement.artist(
artist.id!, artist.name!),
);
await ref
.read(blacklistProvider.notifier)
.remove(artist.id!);
} else {
ref.read(blacklistProvider.notifier).add(
BlacklistedElement.artist(
artist.id!, artist.name!),
await ref.read(blacklistProvider.notifier).add(
BlacklistTableCompanion.insert(
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/models/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';
class ArtistPageTopTracks extends HookConsumerWidget {
@ -21,8 +21,8 @@ class ArtistPageTopTracks extends HookConsumerWidget {
final theme = Theme.of(context);
final scaffoldMessenger = ScaffoldMessenger.of(context);
final playlist = ref.watch(proxyPlaylistProvider);
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
final playlist = ref.watch(audioPlayerProvider);
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final topTracksQuery = ref.watch(artistTopTracksProvider(artistId));
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/provider/connect/clients.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';
class RemotePlayerQueue extends ConsumerWidget {
@ -244,18 +244,18 @@ class ConnectControlPage extends HookConsumerWidget {
: connectNotifier.next,
),
IconButton(
tooltip: loopMode == PlaybackLoopMode.one
tooltip: loopMode == PlaylistMode.single
? context.l10n.loop_track
: loopMode == PlaybackLoopMode.all
: loopMode == PlaylistMode.loop
? context.l10n.repeat_playlist
: null,
icon: Icon(
loopMode == PlaybackLoopMode.one
loopMode == PlaylistMode.single
? SpotubeIcons.repeatOne
: SpotubeIcons.repeat,
),
style: loopMode == PlaybackLoopMode.one ||
loopMode == PlaybackLoopMode.all
style: loopMode == PlaylistMode.single ||
loopMode == PlaylistMode.loop
? activeButtonStyle
: buttonStyle,
onPressed: playlist.activeTrack == null
@ -263,12 +263,11 @@ class ConnectControlPage extends HookConsumerWidget {
: () async {
connectNotifier.setLoopMode(
switch (loopMode) {
PlaybackLoopMode.all =>
PlaybackLoopMode.one,
PlaybackLoopMode.one =>
PlaybackLoopMode.none,
PlaybackLoopMode.none =>
PlaybackLoopMode.all,
PlaylistMode.loop =>
PlaylistMode.single,
PlaylistMode.single =>
PlaylistMode.none,
PlaylistMode.none => PlaylistMode.loop,
},
);
},

View File

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

View File

@ -4,11 +4,11 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/assets.gen.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/extensions/context.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_state.dart';
final audioSourceToIconMap = {
AudioSource.youtube: const Icon(

View File

@ -55,14 +55,14 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget {
),
const Gap(16),
DropdownMenu(
initialSelection: preferences.recommendationMarket,
initialSelection: preferences.market,
onSelected: (value) {
if (value == null) return;
ref
.read(userPreferencesProvider.notifier)
.setRecommendationMarket(value);
},
hintText: preferences.recommendationMarket.name,
hintText: preferences.market.name,
label: Text(context.l10n.market_place_region),
inputDecorationTheme:
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/titlebar/titlebar.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 {
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/models/local_track.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';
class LocalLibraryPage extends HookConsumerWidget {
@ -32,8 +32,8 @@ class LocalLibraryPage extends HookConsumerWidget {
List<LocalTrack> tracks, {
LocalTrack? currentTrack,
}) async {
final playlist = ref.read(proxyPlaylistProvider);
final playback = ref.read(proxyPlaylistProvider.notifier);
final playlist = ref.read(audioPlayerProvider);
final playback = ref.read(audioPlayerProvider.notifier);
currentTrack ??= tracks.first;
final isPlaylistPlaying = playlist.containsTracks(tracks);
if (!isPlaylistPlaying) {
@ -52,7 +52,7 @@ class LocalLibraryPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final sortBy = useState<SortBy>(SortBy.none);
final playlist = ref.watch(proxyPlaylistProvider);
final playlist = ref.watch(audioPlayerProvider);
final trackSnapshot = ref.watch(localTracksProvider);
final isPlaylistPlaying = playlist.containsTracks(
trackSnapshot.asData?.value.values.flattened.toList() ?? []);

View File

@ -39,7 +39,7 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
final genresCollection = ref.watch(categoryGenresProvider);
final limit = useValueNotifier<int>(10);
final market = useValueNotifier<Market>(preferences.recommendationMarket);
final market = useValueNotifier<Market>(preferences.market);
final genres = useState<List<String>>([]);
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/models/spotify/recommendation_seeds.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';
class PlaylistGenerateResultPage extends HookConsumerWidget {
@ -28,7 +28,7 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
Widget build(BuildContext context, ref) {
final router = GoRouter.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));
@ -81,9 +81,12 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
? null
: () async {
await playlistNotifier.load(
generatedPlaylist.asData!.value.where(
(e) => selectedTracks.value.contains(e.id!),
),
generatedPlaylist.asData!.value
.where(
(e) => selectedTracks.value
.contains(e.id!),
)
.toList(),
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/pages/lyrics/plain_lyrics.dart';
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/provider/spotify/spotify.dart';
@ -30,7 +30,7 @@ class LyricsPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final playlist = ref.watch(proxyPlaylistProvider);
final playlist = ref.watch(audioPlayerProvider);
String albumArt = useMemoized(
() => (playlist.activeTrack?.album?.images).asUrlString(
index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1,
@ -62,7 +62,7 @@ class LyricsPage extends HookConsumerWidget {
const Spacer(),
Consumer(
builder: (context, ref, child) {
final playback = ref.watch(proxyPlaylistProvider);
final playback = ref.watch(audioPlayerProvider);
final lyric =
ref.watch(syncedLyricsProvider(playback.activeTrack));
final providerName = lyric.asData?.value.provider;
@ -84,7 +84,7 @@ class LyricsPage extends HookConsumerWidget {
final auth = ref.watch(authenticationProvider);
if (auth == null) {
if (auth.asData?.value == null) {
return Scaffold(
appBar: !kIsMacOS && !isModal ? const PageWindowTitleBar() : null,
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/pages/lyrics/plain_lyrics.dart';
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/utils/platform.dart';
import 'package:window_manager/window_manager.dart';
@ -31,7 +31,7 @@ class MiniLyricsPage extends HookConsumerWidget {
final update = useForceUpdate();
final wasMaximized = useRef<bool>(false);
final playlistQueue = ref.watch(proxyPlaylistProvider);
final playlistQueue = ref.watch(audioPlayerProvider);
final areaActive = useState(false);
final hoverMode = useState(true);
@ -48,7 +48,7 @@ class MiniLyricsPage extends HookConsumerWidget {
final auth = ref.watch(authenticationProvider);
if (auth == null) {
if (auth.asData?.value == null) {
return const Scaffold(
appBar: PageWindowTitleBar(),
body: AnonymousFallback(),
@ -230,14 +230,13 @@ class MiniLyricsPage extends HookConsumerWidget {
builder: (context) {
return Consumer(builder: (context, ref, _) {
final playlist =
ref.watch(proxyPlaylistProvider);
ref.watch(audioPlayerProvider);
return PlayerQueue
.fromProxyPlaylistNotifier(
return PlayerQueue.fromAudioPlayerNotifier(
floating: true,
playlist: playlist,
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/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';
class PlainLyrics extends HookConsumerWidget {
@ -27,7 +27,7 @@ class PlainLyrics extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final playlist = ref.watch(proxyPlaylistProvider);
final playlist = ref.watch(audioPlayerProvider);
final lyricsQuery = ref.watch(syncedLyricsProvider(playlist.activeTrack));
final mediaQuery = MediaQuery.of(context);
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/modules/lyrics/use_synced_lyrics.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/services/audio_player/audio_player.dart';
@ -32,7 +32,7 @@ class SyncedLyrics extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final playlist = ref.watch(proxyPlaylistProvider);
final playlist = ref.watch(audioPlayerProvider);
final mediaQuery = MediaQuery.of(context);
final controller = useAutoScrollController();
@ -54,7 +54,7 @@ class SyncedLyrics extends HookConsumerWidget {
final textTheme = Theme.of(context).textTheme;
ref.listen(
proxyPlaylistProvider.select((s) => s.activeTrack),
audioPlayerProvider.select((s) => s.activeTrack),
(previous, next) {
controller.scrollToIndex(0);
ref.read(syncedLyricsDelayProvider.notifier).state = 0;
@ -139,14 +139,12 @@ class SyncedLyrics extends HookConsumerWidget {
textAlign: TextAlign.center,
child: InkWell(
onTap: () async {
final duration =
await audioPlayer.duration ??
Duration.zero;
final time = Duration(
seconds:
lyricSlice.time.inSeconds - delay,
);
if (time > duration || time.isNegative) {
if (time > audioPlayer.duration ||
time.isNegative) {
return;
}
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: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';
class WebViewLogin extends HookConsumerWidget {
@ -53,9 +53,7 @@ class WebViewLogin extends HookConsumerWidget {
final cookieHeader =
"sp_dc=${cookies.firstWhere((element) => element.name == "sp_dc").value}";
authenticationNotifier.setCredentials(
await AuthenticationCredentials.fromCookie(cookieHeader),
);
await authenticationNotifier.login(cookieHeader);
if (context.mounted) {
// ignore: use_build_context_synchronously
GoRouter.of(context).go("/");

View File

@ -5,7 +5,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.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/modules/player/player_queue.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/pages/home/home.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/services/connectivity_adapter.dart';
import 'package:spotube/utils/persisted_state_notifier.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart';
@ -41,13 +39,6 @@ class RootApp extends HookConsumerWidget {
useEffect(() {
WidgetsBinding.instance.addPostFrameCallback((_) async {
ServiceUtils.checkForUpdates(context, ref);
final sharedPreferences = await SharedPreferences.getInstance();
if (sharedPreferences.getBool(kIsUsingEncryption) == false &&
context.mounted) {
await PersistedStateNotifier.showNoEncryptionDialog(context);
}
});
final subscriptions = [
@ -201,11 +192,11 @@ class RootApp extends HookConsumerWidget {
),
child: Consumer(
builder: (context, ref, _) {
final playlist = ref.watch(proxyPlaylistProvider);
final playlist = ref.watch(audioPlayerProvider);
final playlistNotifier =
ref.read(proxyPlaylistProvider.notifier);
ref.read(audioPlayerProvider.notifier);
return PlayerQueue.fromProxyPlaylistNotifier(
return PlayerQueue.fromAudioPlayerNotifier(
floating: true,
playlist: playlist,
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/playlists.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/services/kv_store/kv_store.dart';
@ -37,8 +37,7 @@ class SearchPage extends HookConsumerWidget {
final searchTerm = ref.watch(searchTermStateProvider);
final controller = useSearchController();
ref.watch(authenticationProvider);
final authenticationNotifier = ref.watch(authenticationProvider.notifier);
final auth = ref.watch(authenticationProvider);
final mediaQuery = MediaQuery.of(context);
final searchTrack = ref.watch(searchProvider(SearchType.track));
@ -91,7 +90,7 @@ class SearchPage extends HookConsumerWidget {
appBar: kIsDesktop && !kIsMacOS
? const PageWindowTitleBar(automaticallyImplyLeading: true)
: null,
body: !authenticationNotifier.isLoggedIn
body: auth.asData?.value == null
? const AnonymousFallback()
: Column(
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/models/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';
class SearchTracksSection extends HookConsumerWidget {
@ -24,8 +24,8 @@ class SearchTracksSection extends HookConsumerWidget {
ref.watch(searchProvider(SearchType.track).notifier);
final tracks = searchTrack.asData?.value.items.cast<Track>() ?? [];
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
final playlist = ref.watch(proxyPlaylistProvider);
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final playlist = ref.watch(audioPlayerProvider);
final theme = Theme.of(context);
return Column(

View File

@ -24,19 +24,21 @@ class BlackListPage extends HookConsumerWidget {
final filteredBlacklist = useMemoized(
() {
if (searchText.value.isEmpty) {
return blacklist;
return blacklist.asData?.value ?? [];
}
return blacklist
.map(
(e) => (
weightedRatio("${e.name} ${e.type.name}", searchText.value),
e,
),
)
.sorted((a, b) => b.$1.compareTo(a.$1))
.where((e) => e.$1 > 50)
.map((e) => e.$2)
.toList();
return blacklist.asData?.value
.map(
(e) => (
weightedRatio(
"${e.name} ${e.elementType.name}", searchText.value),
e,
),
)
.sorted((a, b) => b.$1.compareTo(a.$1))
.where((e) => e.$1 > 50)
.map((e) => e.$2)
.toList() ??
[];
},
[blacklist, searchText.value],
);
@ -70,14 +72,14 @@ class BlackListPage extends HookConsumerWidget {
final item = filteredBlacklist.elementAt(index);
return ListTile(
leading: Text("${index + 1}."),
title: Text("${item.name} (${item.type.name})"),
subtitle: Text(item.id),
title: Text("${item.name} (${item.elementType.name})"),
subtitle: Text(item.elementId),
trailing: IconButton(
icon: Icon(SpotubeIcons.trash, color: Colors.red[400]),
onPressed: () {
ref
.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/image.dart';
import 'package:spotube/pages/profile/profile.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/scrobbler_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/scrobbler/scrobbler.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/utils/service_utils.dart';
@ -35,7 +35,7 @@ class SettingsAccountSection extends HookConsumerWidget {
return SectionCardWithHeading(
heading: context.l10n.account,
children: [
if (auth != null)
if (auth.asData?.value != null)
ListTile(
leading: const Icon(SpotubeIcons.user),
title: const Text("User Profile"),
@ -53,7 +53,7 @@ class SettingsAccountSection extends HookConsumerWidget {
ServiceUtils.pushNamed(context, ProfilePage.name);
},
),
if (auth == null)
if (auth.asData?.value == null)
LayoutBuilder(builder: (context, constrains) {
return ListTile(
leading: Icon(
@ -119,7 +119,7 @@ class SettingsAccountSection extends HookConsumerWidget {
),
);
}),
if (scrobbler == null)
if (scrobbler.asData?.value == null)
ListTile(
leading: const Icon(SpotubeIcons.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:hooks_riverpod/hooks_riverpod.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/section_card_with_heading.dart';
import 'package:spotube/components/adaptive/adaptive_select_tile.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_state.dart';
class SettingsAppearanceSection extends HookConsumerWidget {
final bool isGettingStarted;

View File

@ -2,11 +2,12 @@ import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/components/adaptive/adaptive_select_tile.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_state.dart';
import 'package:spotube/utils/platform.dart';
class SettingsDesktopSection extends HookConsumerWidget {

View File

@ -57,7 +57,7 @@ class SettingsLanguageRegionSection extends HookConsumerWidget {
secondary: const Icon(SpotubeIcons.shoppingBag),
title: Text(context.l10n.market_place_region),
subtitle: Text(context.l10n.recommendation_country),
value: preferences.recommendationMarket,
value: preferences.market,
onChanged: (value) {
if (value == null) return;
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:piped_client/piped_client.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/components/adaptive/adaptive_select_tile.dart';
import 'package:spotube/extensions/context.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_state.dart';
import 'package:spotube/services/sourced_track/enums.dart';
class SettingsPlaybackSection extends HookConsumerWidget {

View File

@ -1,10 +1,14 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart';
import 'package:spotube/components/titlebar/titlebar.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/albums.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class StatsAlbumsPage extends HookConsumerWidget {
static const name = "stats_albums";
@ -12,10 +16,12 @@ class StatsAlbumsPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final albums = ref.watch(
playbackHistoryTopProvider(HistoryDuration.allTime)
.select((s) => s.albums),
);
final topAlbums =
ref.watch(historyTopAlbumsProvider(HistoryDuration.allTime));
final topAlbumsNotifier =
ref.watch(historyTopAlbumsProvider(HistoryDuration.allTime).notifier);
final albumsData = topAlbums.asData?.value.items ?? [];
return Scaffold(
appBar: const PageWindowTitleBar(
@ -23,15 +29,26 @@ class StatsAlbumsPage extends HookConsumerWidget {
centerTitle: false,
title: Text("Albums"),
),
body: ListView.builder(
itemCount: albums.length,
itemBuilder: (context, index) {
final album = albums[index];
return StatsAlbumItem(
album: album.album,
info: Text("${compactNumberFormatter.format(album.count)} plays"),
);
},
body: Skeletonizer(
enabled: topAlbums.isLoading && !topAlbums.isLoadingNextPage,
child: InfiniteList(
onFetchData: () async {
await topAlbumsNotifier.fetchMore();
},
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_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart';
import 'package:spotube/components/titlebar/titlebar.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/tracks.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class StatsArtistsPage extends HookConsumerWidget {
static const name = "stats_artists";
@ -12,10 +17,14 @@ class StatsArtistsPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final artists = ref.watch(
playbackHistoryTopProvider(HistoryDuration.allTime)
.select((s) => s.artists),
final topTracks = ref.watch(
historyTopTracksProvider(HistoryDuration.allTime),
);
final topTracksNotifier =
ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier);
final artistsData = useMemoized(
() => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]);
return Scaffold(
appBar: const PageWindowTitleBar(
@ -23,15 +32,25 @@ class StatsArtistsPage extends HookConsumerWidget {
centerTitle: false,
title: Text("Artists"),
),
body: ListView.builder(
itemCount: artists.length,
itemBuilder: (context, index) {
final artist = artists[index];
return StatsArtistItem(
artist: artist.artist,
info: Text("${compactNumberFormatter.format(artist.count)} plays"),
);
},
body: Skeletonizer(
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
child: InfiniteList(
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

@ -1,11 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:sliver_tools/sliver_tools.dart';
import 'package:spotube/collections/formatters.dart';
import 'package:spotube/components/titlebar/titlebar.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/tracks.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class StatsStreamFeesPage extends HookConsumerWidget {
static const name = "stats_stream_fees";
@ -15,10 +20,23 @@ class StatsStreamFeesPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final ThemeData(:textTheme, :hintColor) = Theme.of(context);
final duration = useState<HistoryDuration>(HistoryDuration.days30);
final artists = ref.watch(
playbackHistoryTopProvider(HistoryDuration.days30)
.select((value) => value.artists),
final topTracks = ref.watch(
historyTopTracksProvider(duration.value),
);
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(
@ -48,15 +66,73 @@ class StatsStreamFeesPage extends HookConsumerWidget {
),
),
),
SliverList.builder(
itemCount: artists.length,
itemBuilder: (context, index) {
final artist = artists[index];
return StatsArtistItem(
artist: artist.artist,
info: Text(usdFormatter.format(artist.count * 0.005)),
);
},
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
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:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart';
import 'package:spotube/components/titlebar/titlebar.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/tracks.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class StatsMinutesPage extends HookConsumerWidget {
static const name = "stats_minutes";
@ -15,9 +19,12 @@ class StatsMinutesPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final topTracks = ref.watch(
playbackHistoryTopProvider(HistoryDuration.allTime)
.select((s) => s.tracks),
historyTopTracksProvider(HistoryDuration.allTime),
);
final topTracksNotifier =
ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier);
final tracksData = topTracks.asData?.value.items ?? [];
return Scaffold(
appBar: const PageWindowTitleBar(
@ -25,19 +32,27 @@ class StatsMinutesPage extends HookConsumerWidget {
centerTitle: false,
automaticallyImplyLeading: true,
),
body: ListView.separated(
separatorBuilder: (context, index) => const Gap(8),
itemCount: topTracks.length,
itemBuilder: (context, index) {
final (:track, :count) = topTracks[index];
return StatsTrackItem(
track: track,
info: Text(
"${compactNumberFormatter.format(count * track.duration!.inMinutes)} mins",
),
);
},
body: Skeletonizer(
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
child: InfiniteList(
separatorBuilder: (context, index) => const Gap(8),
onFetchData: () async {
await topTracksNotifier.fetchMore();
},
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

@ -1,10 +1,14 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart';
import 'package:spotube/components/titlebar/titlebar.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/playlists.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class StatsPlaylistsPage extends HookConsumerWidget {
static const name = "stats_playlists";
@ -12,10 +16,13 @@ class StatsPlaylistsPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final playlists = ref.watch(
playbackHistoryTopProvider(HistoryDuration.allTime)
.select((s) => s.playlists),
);
final topPlaylists =
ref.watch(historyTopPlaylistsProvider(HistoryDuration.allTime));
final topPlaylistsNotifier = ref
.watch(historyTopPlaylistsProvider(HistoryDuration.allTime).notifier);
final playlistsData = topPlaylists.asData?.value.items ?? [];
return Scaffold(
appBar: const PageWindowTitleBar(
@ -23,16 +30,25 @@ class StatsPlaylistsPage extends HookConsumerWidget {
centerTitle: false,
title: Text("Playlists"),
),
body: ListView.builder(
itemCount: playlists.length,
itemBuilder: (context, index) {
final playlist = playlists[index];
return StatsPlaylistItem(
playlist: playlist.playlist.playlist,
info:
Text("${compactNumberFormatter.format(playlist.count)} plays"),
);
},
body: Skeletonizer(
enabled: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage,
child: InfiniteList(
onFetchData: () async {
await topPlaylistsNotifier.fetchMore();
},
hasError: topPlaylists.hasError,
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:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart';
import 'package:spotube/components/titlebar/titlebar.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/tracks.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class StatsStreamsPage extends HookConsumerWidget {
static const name = "stats_streams";
@ -15,9 +19,12 @@ class StatsStreamsPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final topTracks = ref.watch(
playbackHistoryTopProvider(HistoryDuration.allTime)
.select((s) => s.tracks),
historyTopTracksProvider(HistoryDuration.allTime),
);
final topTracksNotifier =
ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier);
final tracksData = topTracks.asData?.value.items ?? [];
return Scaffold(
appBar: const PageWindowTitleBar(
@ -25,19 +32,27 @@ class StatsStreamsPage extends HookConsumerWidget {
centerTitle: false,
automaticallyImplyLeading: true,
),
body: ListView.separated(
separatorBuilder: (context, index) => const Gap(8),
itemCount: topTracks.length,
itemBuilder: (context, index) {
final (:track, :count) = topTracks[index];
return StatsTrackItem(
track: track,
info: Text(
"${compactNumberFormatter.format(count)} streams",
),
);
},
body: Skeletonizer(
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
child: InfiniteList(
separatorBuilder: (context, index) => const Gap(8),
onFetchData: () async {
await topTracksNotifier.fetchMore();
},
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 * 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/extensions/context.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/services/audio_player/audio_player.dart';
@ -34,8 +34,8 @@ class TrackPage extends HookConsumerWidget {
final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
final playlist = ref.watch(proxyPlaylistProvider);
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
final playlist = ref.watch(audioPlayerProvider);
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final isActive = playlist.activeTrack?.id == trackId;

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