mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Merge pull request #1687 from KRTirtho/refactor/storage-n-providers
refactor: migrate to sqlite based storage from hive
This commit is contained in:
commit
243a843033
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -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)"
|
||||
|
@ -8,3 +8,10 @@ targets:
|
||||
options:
|
||||
any_map: true
|
||||
explicit_to_json: true
|
||||
drift_dev:
|
||||
options:
|
||||
sql:
|
||||
dialect: sqlite
|
||||
options:
|
||||
modules:
|
||||
- json1
|
||||
|
@ -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";
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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";
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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 = ({
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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,8 +84,7 @@ class TrackTile extends HookConsumerWidget {
|
||||
},
|
||||
child: HoverBuilder(
|
||||
permanentState: isSelected || constrains.smAndDown ? true : null,
|
||||
builder: (context, isHovering) {
|
||||
return ListTile(
|
||||
builder: (context, isHovering) => ListTile(
|
||||
selected: isSelected,
|
||||
onTap: () async {
|
||||
try {
|
||||
@ -103,8 +99,7 @@ class TrackTile extends HookConsumerWidget {
|
||||
onLongPress: onLongPress,
|
||||
enabled: !isBlackListed,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
tileColor:
|
||||
isBlackListed ? theme.colorScheme.errorContainer : null,
|
||||
tileColor: isBlackListed ? theme.colorScheme.errorContainer : null,
|
||||
horizontalTitleGap: 12,
|
||||
leadingAndTrailingTextStyle: theme.textTheme.bodyMedium,
|
||||
leading: Row(
|
||||
@ -162,9 +157,13 @@ class TrackTile extends HookConsumerWidget {
|
||||
data: theme.iconTheme
|
||||
.copyWith(size: 26, color: Colors.white),
|
||||
child: Skeleton.ignore(
|
||||
child: AnimatedSwitcher(
|
||||
child: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final isFetchingActiveTrack =
|
||||
ref.watch(queryingTrackInfoProvider);
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: (isPlaying && playlist.isFetching) ||
|
||||
child: (isPlaying && isFetchingActiveTrack) ||
|
||||
isLoading.value
|
||||
? const SizedBox(
|
||||
width: 26,
|
||||
@ -182,6 +181,8 @@ class TrackTile extends HookConsumerWidget {
|
||||
: !isHovering
|
||||
? const SizedBox.shrink()
|
||||
: const Icon(SpotubeIcons.play),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -266,8 +267,7 @@ class TrackTile extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
@ -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));
|
||||
|
||||
|
@ -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));
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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,9 +131,11 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (context.mounted) {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (compact) {
|
||||
return Row(
|
||||
|
@ -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';
|
||||
|
@ -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,
|
||||
],
|
||||
|
@ -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();
|
||||
|
@ -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';
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
85
lib/models/database/database.dart
Normal file
85
lib/models/database/database.dart
Normal 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);
|
||||
});
|
||||
}
|
5841
lib/models/database/database.g.dart
Normal file
5841
lib/models/database/database.g.dart
Normal file
File diff suppressed because it is too large
Load Diff
27
lib/models/database/tables/audio_player_state.dart
Normal file
27
lib/models/database/tables/audio_player_state.dart
Normal 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>())();
|
||||
}
|
8
lib/models/database/tables/authentication.dart
Normal file
8
lib/models/database/tables/authentication.dart
Normal 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()();
|
||||
}
|
18
lib/models/database/tables/blacklist.dart
Normal file
18
lib/models/database/tables/blacklist.dart
Normal 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()();
|
||||
}
|
25
lib/models/database/tables/history.dart
Normal file
25
lib/models/database/tables/history.dart
Normal 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;
|
||||
}
|
8
lib/models/database/tables/lyrics.dart
Normal file
8
lib/models/database/tables/lyrics.dart
Normal 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())();
|
||||
}
|
125
lib/models/database/tables/preferences.dart
Normal file
125
lib/models/database/tables/preferences.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
8
lib/models/database/tables/scrobbler.dart
Normal file
8
lib/models/database/tables/scrobbler.dart
Normal 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())();
|
||||
}
|
9
lib/models/database/tables/skip_segment.dart
Normal file
9
lib/models/database/tables/skip_segment.dart
Normal 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)();
|
||||
}
|
25
lib/models/database/tables/source_match.dart
Normal file
25
lib/models/database/tables/source_match.dart
Normal 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)();
|
||||
}
|
29
lib/models/database/typeconverters/color.dart
Normal file
29
lib/models/database/typeconverters/color.dart
Normal 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();
|
||||
}
|
||||
}
|
44
lib/models/database/typeconverters/encrypted_text.dart
Normal file
44
lib/models/database/typeconverters/encrypted_text.dart
Normal 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();
|
||||
}
|
||||
}
|
19
lib/models/database/typeconverters/locale.dart
Normal file
19
lib/models/database/typeconverters/locale.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
15
lib/models/database/typeconverters/map.dart
Normal file
15
lib/models/database/typeconverters/map.dart
Normal 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);
|
||||
}
|
||||
}
|
15
lib/models/database/typeconverters/string_list.dart
Normal file
15
lib/models/database/typeconverters/string_list.dart
Normal 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(",");
|
||||
}
|
||||
}
|
13
lib/models/database/typeconverters/subtitle.dart
Normal file
13
lib/models/database/typeconverters/subtitle.dart
Normal 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());
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
@ -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',
|
||||
};
|
@ -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() ?? ""}",
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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(),
|
||||
);
|
||||
|
@ -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();
|
||||
|
@ -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(
|
||||
return Skeletonizer(
|
||||
enabled: history.isLoading,
|
||||
child: HorizontalPlaybuttonCardView(
|
||||
title: const Text('Recently Played'),
|
||||
items: [
|
||||
for (final item in history)
|
||||
if (item is PlaybackHistoryPlaylist)
|
||||
for (final item in historyData)
|
||||
if (item.playlist != null)
|
||||
item.playlist
|
||||
else if (item is PlaybackHistoryAlbum)
|
||||
else if (item.album != null)
|
||||
item.album
|
||||
],
|
||||
hasNextPage: false,
|
||||
isLoadingNextPage: false,
|
||||
onFetchMore: () {},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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)),
|
||||
|
@ -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,40 +230,36 @@ 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;
|
||||
Consumer(builder: (context, ref, _) {
|
||||
final loopMode = ref
|
||||
.watch(audioPlayerProvider.select((s) => s.loopMode));
|
||||
|
||||
return 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.isFetching == true
|
||||
onPressed: isFetchingActiveTrack
|
||||
? null
|
||||
: () async {
|
||||
audioPlayer.setLoopMode(
|
||||
await audioPlayer.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,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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;
|
||||
|
@ -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: [
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
};
|
||||
}, []);
|
||||
|
@ -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: () {
|
||||
|
@ -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),
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
|
@ -33,7 +33,7 @@ class StatsAlbumItem extends StatelessWidget {
|
||||
Text("${album.albumType?.formatted} • "),
|
||||
Flexible(
|
||||
child: ArtistLink(
|
||||
artists: album.artists!,
|
||||
artists: album.artists ?? [],
|
||||
mainAxisAlignment: WrapAlignment.start,
|
||||
),
|
||||
),
|
||||
|
@ -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,8 +20,11 @@ class StatsPageSummarySection extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final summary = ref.watch(playbackHistorySummaryProvider);
|
||||
final summaryData = summary.asData?.value ?? FakeData.historySummary;
|
||||
|
||||
return SliverPadding(
|
||||
return Skeletonizer.sliver(
|
||||
enabled: summary.isLoading,
|
||||
child: SliverPadding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
sliver: SliverLayoutBuilder(builder: (context, constrains) {
|
||||
return SliverGrid(
|
||||
@ -39,7 +44,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
|
||||
),
|
||||
delegate: SliverChildListDelegate([
|
||||
SummaryCard(
|
||||
title: summary.duration.inMinutes.toDouble(),
|
||||
title: summaryData.duration.inMinutes.toDouble(),
|
||||
unit: "minutes",
|
||||
description: 'Listened to music',
|
||||
color: Colors.purple,
|
||||
@ -48,7 +53,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
SummaryCard(
|
||||
title: summary.tracks.toDouble(),
|
||||
title: summaryData.tracks.toDouble(),
|
||||
unit: "songs",
|
||||
description: 'Streamed overall',
|
||||
color: Colors.lightBlue,
|
||||
@ -57,7 +62,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
SummaryCard.unformatted(
|
||||
title: usdFormatter.format(summary.fees.toDouble()),
|
||||
title: usdFormatter.format(summaryData.fees.toDouble()),
|
||||
unit: "",
|
||||
description: 'Owed to artists\nthis month',
|
||||
color: Colors.green,
|
||||
@ -66,7 +71,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
SummaryCard(
|
||||
title: summary.artists.toDouble(),
|
||||
title: summaryData.artists.toDouble(),
|
||||
unit: "artist's",
|
||||
description: 'Music reached you',
|
||||
color: Colors.yellow,
|
||||
@ -75,7 +80,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
SummaryCard(
|
||||
title: summary.albums.toDouble(),
|
||||
title: summaryData.albums.toDouble(),
|
||||
unit: "full albums",
|
||||
description: 'Got your love',
|
||||
color: Colors.pink,
|
||||
@ -84,7 +89,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
SummaryCard(
|
||||
title: summary.playlists.toDouble(),
|
||||
title: summaryData.playlists.toDouble(),
|
||||
unit: "playlists",
|
||||
description: 'Were on repeat',
|
||||
color: Colors.teal,
|
||||
@ -95,6 +100,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
|
||||
]),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,13 +14,24 @@ 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,
|
||||
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 = albums[index];
|
||||
final album = albumsData[index];
|
||||
return StatsAlbumItem(
|
||||
album: album.album,
|
||||
info: Text(
|
||||
@ -24,6 +39,7 @@ class TopAlbums extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
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 = artists[index];
|
||||
final artist = artistsData[index];
|
||||
return StatsArtistItem(
|
||||
artist: artist.artist,
|
||||
info: Text("${compactNumberFormatter.format(artist.count)} plays"),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,15 +14,26 @@ 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,
|
||||
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 = tracks[index];
|
||||
final track = tracksData[index];
|
||||
return StatsTrackItem(
|
||||
track: track.track,
|
||||
info: Text(
|
||||
@ -26,6 +41,7 @@ class TopTracks extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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),
|
||||
|
@ -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";
|
||||
|
@ -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() ?? []);
|
||||
|
@ -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>>([]);
|
||||
|
@ -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,
|
||||
);
|
||||
},
|
||||
|
@ -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(),
|
||||
|
@ -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),
|
||||
);
|
||||
});
|
||||
},
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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("/");
|
||||
|
@ -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,
|
||||
|
@ -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: [
|
||||
|
@ -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(
|
||||
|
@ -24,19 +24,21 @@ class BlackListPage extends HookConsumerWidget {
|
||||
final filteredBlacklist = useMemoized(
|
||||
() {
|
||||
if (searchText.value.isEmpty) {
|
||||
return blacklist;
|
||||
return blacklist.asData?.value ?? [];
|
||||
}
|
||||
return blacklist
|
||||
return blacklist.asData?.value
|
||||
.map(
|
||||
(e) => (
|
||||
weightedRatio("${e.name} ${e.type.name}", searchText.value),
|
||||
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();
|
||||
.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);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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,16 +29,27 @@ class StatsAlbumsPage extends HookConsumerWidget {
|
||||
centerTitle: false,
|
||||
title: Text("Albums"),
|
||||
),
|
||||
body: ListView.builder(
|
||||
itemCount: albums.length,
|
||||
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 = albums[index];
|
||||
final album = albumsData[index];
|
||||
return StatsAlbumItem(
|
||||
album: album.album,
|
||||
info: Text("${compactNumberFormatter.format(album.count)} plays"),
|
||||
info: Text(
|
||||
"${compactNumberFormatter.format(album.count)} plays",
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,16 +32,26 @@ class StatsArtistsPage extends HookConsumerWidget {
|
||||
centerTitle: false,
|
||||
title: Text("Artists"),
|
||||
),
|
||||
body: ListView.builder(
|
||||
itemCount: artists.length,
|
||||
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 = artists[index];
|
||||
final artist = artistsData[index];
|
||||
return StatsArtistItem(
|
||||
artist: artist.artist,
|
||||
info: Text("${compactNumberFormatter.format(artist.count)} plays"),
|
||||
info:
|
||||
Text("${compactNumberFormatter.format(artist.count)} plays"),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,16 +66,74 @@ class StatsStreamFeesPage extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverList.builder(
|
||||
itemCount: artists.length,
|
||||
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 = artists[index];
|
||||
final artist = artistsData[index];
|
||||
return StatsArtistItem(
|
||||
artist: artist.artist,
|
||||
info: Text(usdFormatter.format(artist.count * 0.005)),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -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,20 +32,28 @@ class StatsMinutesPage extends HookConsumerWidget {
|
||||
centerTitle: false,
|
||||
automaticallyImplyLeading: true,
|
||||
),
|
||||
body: ListView.separated(
|
||||
body: Skeletonizer(
|
||||
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||
child: InfiniteList(
|
||||
separatorBuilder: (context, index) => const Gap(8),
|
||||
itemCount: topTracks.length,
|
||||
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, :count) = topTracks[index];
|
||||
|
||||
final track = tracksData[index];
|
||||
return StatsTrackItem(
|
||||
track: track,
|
||||
track: track.track,
|
||||
info: Text(
|
||||
"${compactNumberFormatter.format(count * track.duration!.inMinutes)} mins",
|
||||
"${compactNumberFormatter.format(track.count)} plays",
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,17 +30,26 @@ class StatsPlaylistsPage extends HookConsumerWidget {
|
||||
centerTitle: false,
|
||||
title: Text("Playlists"),
|
||||
),
|
||||
body: ListView.builder(
|
||||
itemCount: playlists.length,
|
||||
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 = playlists[index];
|
||||
final playlist = playlistsData[index];
|
||||
return StatsPlaylistItem(
|
||||
playlist: playlist.playlist.playlist,
|
||||
info:
|
||||
Text("${compactNumberFormatter.format(playlist.count)} plays"),
|
||||
playlist: playlist.playlist,
|
||||
info: Text(
|
||||
"${compactNumberFormatter.format(playlist.count)} plays"),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,20 +32,28 @@ class StatsStreamsPage extends HookConsumerWidget {
|
||||
centerTitle: false,
|
||||
automaticallyImplyLeading: true,
|
||||
),
|
||||
body: ListView.separated(
|
||||
body: Skeletonizer(
|
||||
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||
child: InfiniteList(
|
||||
separatorBuilder: (context, index) => const Gap(8),
|
||||
itemCount: topTracks.length,
|
||||
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, :count) = topTracks[index];
|
||||
|
||||
final track = tracksData[index];
|
||||
return StatsTrackItem(
|
||||
track: track,
|
||||
track: track.track,
|
||||
info: Text(
|
||||
"${compactNumberFormatter.format(count)} streams",
|
||||
"${compactNumberFormatter.format(track.count * track.track.duration!.inMinutes)} mins",
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user