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?
|
description: Where did you install Spotube from?
|
||||||
multiple: true
|
multiple: true
|
||||||
options:
|
options:
|
||||||
- "Website (spotube.netlify.app) or (spotube.krtirtho.dev)"
|
- "Website (spotube.krtirtho.dev)"
|
||||||
- "GitHub Releases (Binary)"
|
- "GitHub Releases (Binary)"
|
||||||
- "GitHub Actions (Nightly Binary)"
|
- "GitHub Actions (Nightly Binary)"
|
||||||
- "Play Store (Android)"
|
- "Play Store (Android)"
|
||||||
|
@ -8,3 +8,10 @@ targets:
|
|||||||
options:
|
options:
|
||||||
any_map: true
|
any_map: true
|
||||||
explicit_to_json: true
|
explicit_to_json: true
|
||||||
|
drift_dev:
|
||||||
|
options:
|
||||||
|
sql:
|
||||||
|
dialect: sqlite
|
||||||
|
options:
|
||||||
|
modules:
|
||||||
|
- json1
|
||||||
|
@ -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:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/models/spotify/home_feed.dart';
|
import 'package:spotube/models/spotify/home_feed.dart';
|
||||||
import 'package:spotube/models/spotify_friends.dart';
|
import 'package:spotube/models/spotify_friends.dart';
|
||||||
|
import 'package:spotube/provider/history/summary.dart';
|
||||||
|
|
||||||
abstract class FakeData {
|
abstract class FakeData {
|
||||||
static final Image image = Image()
|
static final Image image = Image()
|
||||||
@ -222,4 +224,36 @@ abstract class FakeData {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static const historySummary = PlaybackHistorySummary(
|
||||||
|
albums: 1,
|
||||||
|
artists: 1,
|
||||||
|
duration: Duration(seconds: 1),
|
||||||
|
playlists: 1,
|
||||||
|
tracks: 1,
|
||||||
|
fees: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
static final historyRecentlyPlayedPlaylist = HistoryTableData(
|
||||||
|
id: 0,
|
||||||
|
type: HistoryEntryType.track,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
itemId: "1",
|
||||||
|
data: playlist.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
static final historyRecentlyPlayedAlbum = HistoryTableData(
|
||||||
|
id: 0,
|
||||||
|
type: HistoryEntryType.track,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
itemId: "1",
|
||||||
|
data: album.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
static final historyRecentlyPlayedItems = List.generate(
|
||||||
|
10,
|
||||||
|
(index) => index % 2 == 0
|
||||||
|
? historyRecentlyPlayedPlaylist
|
||||||
|
: historyRecentlyPlayedAlbum,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import 'package:spotube/pages/home/home.dart';
|
|||||||
import 'package:spotube/pages/library/library.dart';
|
import 'package:spotube/pages/library/library.dart';
|
||||||
import 'package:spotube/pages/lyrics/lyrics.dart';
|
import 'package:spotube/pages/lyrics/lyrics.dart';
|
||||||
import 'package:spotube/pages/search/search.dart';
|
import 'package:spotube/pages/search/search.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/querying_track_info.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
@ -96,8 +96,8 @@ class SeekIntent extends Intent {
|
|||||||
class SeekAction extends Action<SeekIntent> {
|
class SeekAction extends Action<SeekIntent> {
|
||||||
@override
|
@override
|
||||||
invoke(intent) async {
|
invoke(intent) async {
|
||||||
final playlist = intent.ref.read(proxyPlaylistProvider);
|
final isFetchingActiveTrack = intent.ref.read(queryingTrackInfoProvider);
|
||||||
if (playlist.isFetching) {
|
if (isFetchingActiveTrack) {
|
||||||
DirectionalFocusAction().invoke(
|
DirectionalFocusAction().invoke(
|
||||||
DirectionalFocusIntent(
|
DirectionalFocusIntent(
|
||||||
intent.forward ? TraversalDirection.right : TraversalDirection.left,
|
intent.forward ? TraversalDirection.right : TraversalDirection.left,
|
||||||
@ -105,7 +105,7 @@ class SeekAction extends Action<SeekIntent> {
|
|||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final position = (await audioPlayer.position ?? Duration.zero).inSeconds;
|
final position = audioPlayer.position.inSeconds;
|
||||||
await audioPlayer.seek(
|
await audioPlayer.seek(
|
||||||
Duration(
|
Duration(
|
||||||
seconds: intent.forward ? position + 5 : position - 5,
|
seconds: intent.forward ? position + 5 : position - 5,
|
||||||
|
@ -32,7 +32,7 @@ import 'package:spotube/pages/stats/playlists/playlists.dart';
|
|||||||
import 'package:spotube/pages/stats/stats.dart';
|
import 'package:spotube/pages/stats/stats.dart';
|
||||||
import 'package:spotube/pages/stats/streams/streams.dart';
|
import 'package:spotube/pages/stats/streams/streams.dart';
|
||||||
import 'package:spotube/pages/track/track.dart';
|
import 'package:spotube/pages/track/track.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:spotube/components/spotube_page_route.dart';
|
import 'package:spotube/components/spotube_page_route.dart';
|
||||||
@ -59,11 +59,9 @@ final routerProvider = Provider((ref) {
|
|||||||
path: "/",
|
path: "/",
|
||||||
name: HomePage.name,
|
name: HomePage.name,
|
||||||
redirect: (context, state) async {
|
redirect: (context, state) async {
|
||||||
final authNotifier = ref.read(authenticationProvider.notifier);
|
final auth = await ref.read(authenticationProvider.future);
|
||||||
final json = await authNotifier.box.get(authNotifier.cacheKey);
|
|
||||||
|
|
||||||
if (json?["cookie"] == null &&
|
if (auth == null && !KVStoreService.doneGettingStarted) {
|
||||||
!KVStoreService.doneGettingStarted) {
|
|
||||||
return "/getting-started";
|
return "/getting-started";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/pages/settings/settings.dart';
|
import 'package:spotube/pages/settings/settings.dart';
|
||||||
|
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class AnonymousFallback extends ConsumerWidget {
|
class AnonymousFallback extends ConsumerWidget {
|
||||||
|
@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/heart_button/use_track_toggle_like.dart';
|
import 'package:spotube/components/heart_button/use_track_toggle_like.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class HeartButton extends HookConsumerWidget {
|
class HeartButton extends HookConsumerWidget {
|
||||||
@ -26,7 +26,7 @@ class HeartButton extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final auth = ref.watch(authenticationProvider);
|
final auth = ref.watch(authenticationProvider);
|
||||||
|
|
||||||
if (auth == null) return const SizedBox.shrink();
|
if (auth.asData?.value == null) return const SizedBox.shrink();
|
||||||
|
|
||||||
return IconButton(
|
return IconButton(
|
||||||
tooltip: tooltip,
|
tooltip: tooltip,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/provider/scrobbler_provider.dart';
|
import 'package:spotube/provider/scrobbler/scrobbler.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
typedef UseTrackToggleLike = ({
|
typedef UseTrackToggleLike = ({
|
||||||
|
@ -18,12 +18,13 @@ import 'package:spotube/components/links/artist_link.dart';
|
|||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/provider/spotify_provider.dart';
|
||||||
|
|
||||||
@ -95,8 +96,8 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
Track track,
|
Track track,
|
||||||
) async {
|
) async {
|
||||||
final playback = ref.read(proxyPlaylistProvider.notifier);
|
final playback = ref.read(audioPlayerProvider.notifier);
|
||||||
final playlist = ref.read(proxyPlaylistProvider);
|
final playlist = ref.read(audioPlayerProvider);
|
||||||
final spotify = ref.read(spotifyProvider);
|
final spotify = ref.read(spotifyProvider);
|
||||||
final query = "${track.name} Radio";
|
final query = "${track.name} Radio";
|
||||||
final pages =
|
final pages =
|
||||||
@ -159,8 +160,8 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
final router = GoRouter.of(context);
|
final router = GoRouter.of(context);
|
||||||
final ThemeData(:colorScheme) = Theme.of(context);
|
final ThemeData(:colorScheme) = Theme.of(context);
|
||||||
|
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
final playback = ref.watch(proxyPlaylistProvider.notifier);
|
final playback = ref.watch(audioPlayerProvider.notifier);
|
||||||
final auth = ref.watch(authenticationProvider);
|
final auth = ref.watch(authenticationProvider);
|
||||||
ref.watch(downloadManagerProvider);
|
ref.watch(downloadManagerProvider);
|
||||||
final downloadManager = ref.watch(downloadManagerProvider.notifier);
|
final downloadManager = ref.watch(downloadManagerProvider.notifier);
|
||||||
@ -170,11 +171,8 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
final favorites = useTrackToggleLike(track, ref);
|
final favorites = useTrackToggleLike(track, ref);
|
||||||
|
|
||||||
final isBlackListed = useMemoized(
|
final isBlackListed = useMemoized(
|
||||||
() => blacklist.contains(
|
() => blacklist.asData?.value.any(
|
||||||
BlacklistedElement.track(
|
(element) => element.elementId == track.id,
|
||||||
track.id!,
|
|
||||||
track.name!,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
[blacklist, track],
|
[blacklist, track],
|
||||||
);
|
);
|
||||||
@ -258,13 +256,16 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
.removeTracks(playlistId ?? "", [track.id!]);
|
.removeTracks(playlistId ?? "", [track.id!]);
|
||||||
break;
|
break;
|
||||||
case TrackOptionValue.blacklist:
|
case TrackOptionValue.blacklist:
|
||||||
if (isBlackListed) {
|
if (isBlackListed == null) break;
|
||||||
ref.read(blacklistProvider.notifier).remove(
|
if (isBlackListed == true) {
|
||||||
BlacklistedElement.track(track.id!, track.name!),
|
await ref.read(blacklistProvider.notifier).remove(track.id!);
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
ref.read(blacklistProvider.notifier).add(
|
await ref.read(blacklistProvider.notifier).add(
|
||||||
BlacklistedElement.track(track.id!, track.name!),
|
BlacklistTableCompanion.insert(
|
||||||
|
name: track.name!,
|
||||||
|
elementId: track.id!,
|
||||||
|
elementType: BlacklistedType.track,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -363,7 +364,7 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
: context.l10n.save_as_favorite,
|
: context.l10n.save_as_favorite,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (auth != null && !isLocalTrack) ...[
|
if (auth.asData?.value != null && !isLocalTrack) ...[
|
||||||
PopSheetEntry(
|
PopSheetEntry(
|
||||||
value: TrackOptionValue.startRadio,
|
value: TrackOptionValue.startRadio,
|
||||||
leading: const Icon(SpotubeIcons.radio),
|
leading: const Icon(SpotubeIcons.radio),
|
||||||
@ -375,7 +376,7 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
title: Text(context.l10n.add_to_playlist),
|
title: Text(context.l10n.add_to_playlist),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (userPlaylist && auth != null && !isLocalTrack)
|
if (userPlaylist && auth.asData?.value != null && !isLocalTrack)
|
||||||
PopSheetEntry(
|
PopSheetEntry(
|
||||||
value: TrackOptionValue.removeFromPlaylist,
|
value: TrackOptionValue.removeFromPlaylist,
|
||||||
leading: const Icon(SpotubeIcons.removeFilled),
|
leading: const Icon(SpotubeIcons.removeFilled),
|
||||||
@ -399,10 +400,10 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
PopSheetEntry(
|
PopSheetEntry(
|
||||||
value: TrackOptionValue.blacklist,
|
value: TrackOptionValue.blacklist,
|
||||||
leading: const Icon(SpotubeIcons.playlistRemove),
|
leading: const Icon(SpotubeIcons.playlistRemove),
|
||||||
iconColor: !isBlackListed ? Colors.red[400] : null,
|
iconColor: isBlackListed != true ? Colors.red[400] : null,
|
||||||
textColor: !isBlackListed ? Colors.red[400] : null,
|
textColor: isBlackListed != true ? Colors.red[400] : null,
|
||||||
title: Text(
|
title: Text(
|
||||||
isBlackListed
|
isBlackListed == true
|
||||||
? context.l10n.remove_from_blacklist
|
? context.l10n.remove_from_blacklist
|
||||||
: context.l10n.add_to_blacklist,
|
: context.l10n.add_to_blacklist,
|
||||||
),
|
),
|
||||||
|
@ -17,8 +17,9 @@ import 'package:spotube/extensions/constrains.dart';
|
|||||||
import 'package:spotube/extensions/duration.dart';
|
import 'package:spotube/extensions/duration.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
|
import 'package:spotube/provider/audio_player/querying_track_info.dart';
|
||||||
|
import 'package:spotube/provider/audio_player/state.dart';
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
|
||||||
|
|
||||||
class TrackTile extends HookConsumerWidget {
|
class TrackTile extends HookConsumerWidget {
|
||||||
/// [index] will not be shown if null
|
/// [index] will not be shown if null
|
||||||
@ -30,7 +31,7 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
final VoidCallback? onLongPress;
|
final VoidCallback? onLongPress;
|
||||||
final bool userPlaylist;
|
final bool userPlaylist;
|
||||||
final String? playlistId;
|
final String? playlistId;
|
||||||
final ProxyPlaylist playlist;
|
final AudioPlayerState playlist;
|
||||||
|
|
||||||
final List<Widget>? leadingActions;
|
final List<Widget>? leadingActions;
|
||||||
|
|
||||||
@ -53,14 +54,10 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
final blacklist = ref.watch(blacklistProvider);
|
final blacklist = ref.watch(blacklistProvider);
|
||||||
|
final blacklistNotifier = ref.watch(blacklistProvider.notifier);
|
||||||
|
|
||||||
final isBlackListed = useMemoized(
|
final isBlackListed = useMemoized(
|
||||||
() => blacklist.contains(
|
() => blacklistNotifier.contains(track),
|
||||||
BlacklistedElement.track(
|
|
||||||
track.id!,
|
|
||||||
track.name!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
[blacklist, track],
|
[blacklist, track],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -87,8 +84,7 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
child: HoverBuilder(
|
child: HoverBuilder(
|
||||||
permanentState: isSelected || constrains.smAndDown ? true : null,
|
permanentState: isSelected || constrains.smAndDown ? true : null,
|
||||||
builder: (context, isHovering) {
|
builder: (context, isHovering) => ListTile(
|
||||||
return ListTile(
|
|
||||||
selected: isSelected,
|
selected: isSelected,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
try {
|
try {
|
||||||
@ -103,8 +99,7 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
onLongPress: onLongPress,
|
onLongPress: onLongPress,
|
||||||
enabled: !isBlackListed,
|
enabled: !isBlackListed,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
tileColor:
|
tileColor: isBlackListed ? theme.colorScheme.errorContainer : null,
|
||||||
isBlackListed ? theme.colorScheme.errorContainer : null,
|
|
||||||
horizontalTitleGap: 12,
|
horizontalTitleGap: 12,
|
||||||
leadingAndTrailingTextStyle: theme.textTheme.bodyMedium,
|
leadingAndTrailingTextStyle: theme.textTheme.bodyMedium,
|
||||||
leading: Row(
|
leading: Row(
|
||||||
@ -162,9 +157,13 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
data: theme.iconTheme
|
data: theme.iconTheme
|
||||||
.copyWith(size: 26, color: Colors.white),
|
.copyWith(size: 26, color: Colors.white),
|
||||||
child: Skeleton.ignore(
|
child: Skeleton.ignore(
|
||||||
child: AnimatedSwitcher(
|
child: Consumer(
|
||||||
|
builder: (context, ref, _) {
|
||||||
|
final isFetchingActiveTrack =
|
||||||
|
ref.watch(queryingTrackInfoProvider);
|
||||||
|
return AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: (isPlaying && playlist.isFetching) ||
|
child: (isPlaying && isFetchingActiveTrack) ||
|
||||||
isLoading.value
|
isLoading.value
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 26,
|
width: 26,
|
||||||
@ -182,6 +181,8 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
: !isHovering
|
: !isHovering
|
||||||
? const SizedBox.shrink()
|
? const SizedBox.shrink()
|
||||||
: const Icon(SpotubeIcons.play),
|
: 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/models/connect/connect.dart';
|
||||||
import 'package:spotube/provider/connect/connect.dart';
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
import 'package:spotube/provider/history/history.dart';
|
import 'package:spotube/provider/history/history.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
@ -27,9 +27,9 @@ class TrackViewBodySection extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
||||||
final historyNotifier = ref.watch(playbackHistoryProvider.notifier);
|
final historyNotifier = ref.watch(playbackHistoryActionsProvider);
|
||||||
final props = InheritedTrackView.of(context);
|
final props = InheritedTrackView.of(context);
|
||||||
final trackViewState = ref.watch(trackViewProvider(props.tracks));
|
final trackViewState = ref.watch(trackViewProvider(props.tracks));
|
||||||
|
|
||||||
|
@ -8,11 +8,11 @@ import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart';
|
|||||||
import 'package:spotube/components/tracks_view/track_view_props.dart';
|
import 'package:spotube/components/tracks_view/track_view_props.dart';
|
||||||
import 'package:spotube/components/tracks_view/track_view_provider.dart';
|
import 'package:spotube/components/tracks_view/track_view_provider.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/history/history.dart';
|
import 'package:spotube/provider/history/history.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
|
||||||
|
|
||||||
class TrackViewBodyOptions extends HookConsumerWidget {
|
class TrackViewBodyOptions extends HookConsumerWidget {
|
||||||
const TrackViewBodyOptions({super.key});
|
const TrackViewBodyOptions({super.key});
|
||||||
@ -24,8 +24,8 @@ class TrackViewBodyOptions extends HookConsumerWidget {
|
|||||||
|
|
||||||
ref.watch(downloadManagerProvider);
|
ref.watch(downloadManagerProvider);
|
||||||
final downloader = ref.watch(downloadManagerProvider.notifier);
|
final downloader = ref.watch(downloadManagerProvider.notifier);
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
||||||
final historyNotifier = ref.watch(playbackHistoryProvider.notifier);
|
final historyNotifier = ref.watch(playbackHistoryActionsProvider);
|
||||||
final audioSource =
|
final audioSource =
|
||||||
ref.watch(userPreferencesProvider.select((s) => s.audioSource));
|
ref.watch(userPreferencesProvider.select((s) => s.audioSource));
|
||||||
|
|
||||||
|
@ -9,9 +9,9 @@ import 'package:spotube/components/heart_button/heart_button.dart';
|
|||||||
import 'package:spotube/components/tracks_view/sections/body/use_is_user_playlist.dart';
|
import 'package:spotube/components/tracks_view/sections/body/use_is_user_playlist.dart';
|
||||||
import 'package:spotube/components/tracks_view/track_view_props.dart';
|
import 'package:spotube/components/tracks_view/track_view_props.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/history/history.dart';
|
import 'package:spotube/provider/history/history.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
|
|
||||||
class TrackViewHeaderActions extends HookConsumerWidget {
|
class TrackViewHeaderActions extends HookConsumerWidget {
|
||||||
const TrackViewHeaderActions({super.key});
|
const TrackViewHeaderActions({super.key});
|
||||||
@ -20,9 +20,9 @@ class TrackViewHeaderActions extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final props = InheritedTrackView.of(context);
|
final props = InheritedTrackView.of(context);
|
||||||
|
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
||||||
final historyNotifier = ref.watch(playbackHistoryProvider.notifier);
|
final historyNotifier = ref.watch(playbackHistoryActionsProvider);
|
||||||
|
|
||||||
final isActive = playlist.collections.contains(props.collectionId);
|
final isActive = playlist.collections.contains(props.collectionId);
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ class TrackViewHeaderActions extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (props.onHeart != null && auth != null)
|
if (props.onHeart != null && auth.asData?.value != null)
|
||||||
HeartButton(
|
HeartButton(
|
||||||
isLiked: props.isLiked,
|
isLiked: props.isLiked,
|
||||||
icon: isUserPlaylist ? SpotubeIcons.trash : null,
|
icon: isUserPlaylist ? SpotubeIcons.trash : null,
|
||||||
|
@ -13,7 +13,7 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
import 'package:spotube/provider/connect/connect.dart';
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
import 'package:spotube/provider/history/history.dart';
|
import 'package:spotube/provider/history/history.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
|
||||||
class TrackViewHeaderButtons extends HookConsumerWidget {
|
class TrackViewHeaderButtons extends HookConsumerWidget {
|
||||||
@ -28,9 +28,9 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final props = InheritedTrackView.of(context);
|
final props = InheritedTrackView.of(context);
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
||||||
final historyNotifier = ref.watch(playbackHistoryProvider.notifier);
|
final historyNotifier = ref.watch(playbackHistoryActionsProvider);
|
||||||
|
|
||||||
final isActive = playlist.collections.contains(props.collectionId);
|
final isActive = playlist.collections.contains(props.collectionId);
|
||||||
|
|
||||||
@ -131,9 +131,11 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
if (context.mounted) {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (compact) {
|
if (compact) {
|
||||||
return Row(
|
return Row(
|
||||||
|
@ -2,8 +2,9 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/hooks/configurators/use_window_listener.dart';
|
import 'package:spotube/hooks/configurators/use_window_listener.dart';
|
||||||
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
|
||||||
import 'package:local_notifier/local_notifier.dart';
|
import 'package:local_notifier/local_notifier.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
@ -2,27 +2,27 @@ import 'package:spotube/services/logger/logger.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/provider/spotify_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
|
||||||
void useEndlessPlayback(WidgetRef ref) {
|
void useEndlessPlayback(WidgetRef ref) {
|
||||||
final auth = ref.watch(authenticationProvider);
|
final auth = ref.watch(authenticationProvider);
|
||||||
final playback = ref.watch(proxyPlaylistProvider.notifier);
|
final playback = ref.watch(audioPlayerProvider.notifier);
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(audioPlayerProvider.select((s) => s.playlist));
|
||||||
final spotify = ref.watch(spotifyProvider);
|
final spotify = ref.watch(spotifyProvider);
|
||||||
final endlessPlayback =
|
final endlessPlayback =
|
||||||
ref.watch(userPreferencesProvider.select((s) => s.endlessPlayback));
|
ref.watch(userPreferencesProvider.select((s) => s.endlessPlayback));
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
if (!endlessPlayback || auth == null) return null;
|
if (!endlessPlayback || auth.asData?.value == null) return null;
|
||||||
|
|
||||||
void listener(int index) async {
|
void listener(int index) async {
|
||||||
try {
|
try {
|
||||||
final playlist = ref.read(proxyPlaylistProvider);
|
final playlist = ref.read(audioPlayerProvider);
|
||||||
if (index != playlist.tracks.length - 1) return;
|
if (index != playlist.tracks.length - 1) return;
|
||||||
|
|
||||||
final track = playlist.tracks.last;
|
final track = playlist.tracks.last;
|
||||||
@ -56,7 +56,7 @@ void useEndlessPlayback(WidgetRef ref) {
|
|||||||
await playback.addTracks(
|
await playback.addTracks(
|
||||||
tracks.toList()
|
tracks.toList()
|
||||||
..removeWhere((e) {
|
..removeWhere((e) {
|
||||||
final playlist = ref.read(proxyPlaylistProvider);
|
final playlist = ref.read(audioPlayerProvider);
|
||||||
final isDuplicate = playlist.tracks.any((t) => t.id == e.id);
|
final isDuplicate = playlist.tracks.any((t) => t.id == e.id);
|
||||||
return e.id == track.id || isDuplicate;
|
return e.id == track.id || isDuplicate;
|
||||||
}),
|
}),
|
||||||
@ -69,9 +69,9 @@ void useEndlessPlayback(WidgetRef ref) {
|
|||||||
// Sometimes user can change settings for which the currentIndexChanged
|
// Sometimes user can change settings for which the currentIndexChanged
|
||||||
// might not be called. So we need to check if the current track is the
|
// might not be called. So we need to check if the current track is the
|
||||||
// last track and if it is then we need to call the listener manually.
|
// last track and if it is then we need to call the listener manually.
|
||||||
if (playlist.active == playlist.tracks.length - 1 &&
|
if (playlist.index == playlist.medias.length - 1 &&
|
||||||
audioPlayer.isPlaying) {
|
audioPlayer.isPlaying) {
|
||||||
listener(playlist.active!);
|
listener(playlist.index);
|
||||||
}
|
}
|
||||||
|
|
||||||
final subscription =
|
final subscription =
|
||||||
@ -82,7 +82,7 @@ void useEndlessPlayback(WidgetRef ref) {
|
|||||||
[
|
[
|
||||||
spotify,
|
spotify,
|
||||||
playback,
|
playback,
|
||||||
playlist.tracks,
|
playlist.medias,
|
||||||
endlessPlayback,
|
endlessPlayback,
|
||||||
auth,
|
auth,
|
||||||
],
|
],
|
||||||
|
@ -18,22 +18,24 @@ import 'package:spotube/hooks/configurators/use_close_behavior.dart';
|
|||||||
import 'package:spotube/hooks/configurators/use_deep_linking.dart';
|
import 'package:spotube/hooks/configurators/use_deep_linking.dart';
|
||||||
import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.dart';
|
import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.dart';
|
||||||
import 'package:spotube/hooks/configurators/use_get_storage_perms.dart';
|
import 'package:spotube/hooks/configurators/use_get_storage_perms.dart';
|
||||||
|
import 'package:spotube/models/database/database.dart';
|
||||||
|
import 'package:spotube/provider/audio_player/audio_player_streams.dart';
|
||||||
|
import 'package:spotube/provider/database/database.dart';
|
||||||
import 'package:spotube/provider/server/bonsoir.dart';
|
import 'package:spotube/provider/server/bonsoir.dart';
|
||||||
import 'package:spotube/provider/server/server.dart';
|
import 'package:spotube/provider/server/server.dart';
|
||||||
import 'package:spotube/provider/tray_manager/tray_manager.dart';
|
import 'package:spotube/provider/tray_manager/tray_manager.dart';
|
||||||
import 'package:spotube/l10n/l10n.dart';
|
import 'package:spotube/l10n/l10n.dart';
|
||||||
import 'package:spotube/models/skip_segment.dart';
|
|
||||||
import 'package:spotube/models/source_match.dart';
|
|
||||||
import 'package:spotube/provider/connect/clients.dart';
|
import 'package:spotube/provider/connect/clients.dart';
|
||||||
import 'package:spotube/provider/palette_provider.dart';
|
import 'package:spotube/provider/palette_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/cli/cli.dart';
|
import 'package:spotube/services/cli/cli.dart';
|
||||||
|
import 'package:spotube/services/kv_store/encrypted_kv_store.dart';
|
||||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
import 'package:spotube/services/wm_tools/wm_tools.dart';
|
import 'package:spotube/services/wm_tools/wm_tools.dart';
|
||||||
import 'package:spotube/themes/theme.dart';
|
import 'package:spotube/themes/theme.dart';
|
||||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
import 'package:spotube/utils/migrations/hive.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:system_theme/system_theme.dart';
|
import 'package:system_theme/system_theme.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@ -78,39 +80,30 @@ Future<void> main(List<String> rawArgs) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await KVStoreService.initialize();
|
await KVStoreService.initialize();
|
||||||
|
await EncryptedKvStoreService.initialize();
|
||||||
|
|
||||||
final hiveCacheDir =
|
final hiveCacheDir =
|
||||||
kIsWeb ? null : (await getApplicationSupportDirectory()).path;
|
kIsWeb ? null : (await getApplicationSupportDirectory()).path;
|
||||||
|
|
||||||
Hive.init(hiveCacheDir);
|
Hive.init(hiveCacheDir);
|
||||||
|
|
||||||
Hive.registerAdapter(SkipSegmentAdapter());
|
final database = AppDatabase();
|
||||||
|
|
||||||
Hive.registerAdapter(SourceMatchAdapter());
|
await migrateFromHiveToDrift(database);
|
||||||
Hive.registerAdapter(SourceTypeAdapter());
|
|
||||||
|
|
||||||
// Cache versioning entities with Adapter
|
|
||||||
SourceMatch.version = 'v1';
|
|
||||||
SkipSegment.version = 'v1';
|
|
||||||
|
|
||||||
await Hive.openLazyBox<SourceMatch>(
|
|
||||||
SourceMatch.boxName,
|
|
||||||
path: hiveCacheDir,
|
|
||||||
);
|
|
||||||
await Hive.openLazyBox(
|
|
||||||
SkipSegment.boxName,
|
|
||||||
path: hiveCacheDir,
|
|
||||||
);
|
|
||||||
await PersistedStateNotifier.initializeBoxes(
|
|
||||||
path: hiveCacheDir,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (kIsDesktop) {
|
if (kIsDesktop) {
|
||||||
await localNotifier.setup(appName: "Spotube");
|
await localNotifier.setup(appName: "Spotube");
|
||||||
await WindowManagerTools.initialize();
|
await WindowManagerTools.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
runApp(const ProviderScope(child: Spotube()));
|
runApp(
|
||||||
|
ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
databaseProvider.overrideWith((ref) => database),
|
||||||
|
],
|
||||||
|
child: const Spotube(),
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,9 +123,10 @@ class Spotube extends HookConsumerWidget {
|
|||||||
ref.watch(paletteProvider.select((s) => s?.dominantColor?.color));
|
ref.watch(paletteProvider.select((s) => s?.dominantColor?.color));
|
||||||
final router = ref.watch(routerProvider);
|
final router = ref.watch(routerProvider);
|
||||||
|
|
||||||
ref.listen(serverProvider, (_, __) {});
|
ref.listen(audioPlayerStreamListenersProvider, (_, __) {});
|
||||||
ref.listen(bonsoirProvider, (_, __) {});
|
ref.listen(bonsoirProvider, (_, __) {});
|
||||||
ref.listen(connectClientsProvider, (_, __) {});
|
ref.listen(connectClientsProvider, (_, __) {});
|
||||||
|
ref.listen(serverProvider, (_, __) {});
|
||||||
ref.listen(trayManagerProvider, (_, __) {});
|
ref.listen(trayManagerProvider, (_, __) {});
|
||||||
|
|
||||||
useDisableBatteryOptimizations();
|
useDisableBatteryOptimizations();
|
||||||
|
@ -4,9 +4,9 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:media_kit/media_kit.dart' hide Track;
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
import 'package:spotify/spotify.dart' hide Playlist;
|
||||||
import 'package:spotube/services/audio_player/loop_mode.dart';
|
import 'package:spotube/provider/audio_player/state.dart';
|
||||||
|
|
||||||
part 'connect.freezed.dart';
|
part 'connect.freezed.dart';
|
||||||
part 'connect.g.dart';
|
part 'connect.g.dart';
|
||||||
|
@ -183,7 +183,7 @@ class WebSocketEvent<T> {
|
|||||||
if (type == WsEvent.loop) {
|
if (type == WsEvent.loop) {
|
||||||
await callback(
|
await callback(
|
||||||
WebSocketLoopEvent(
|
WebSocketLoopEvent(
|
||||||
PlaybackLoopMode.fromString(data as String),
|
PlaylistMode.values.firstWhere((e) => e.name == data as String),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -224,12 +224,16 @@ class WebSocketEvent<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebSocketLoopEvent extends WebSocketEvent<PlaybackLoopMode> {
|
class WebSocketLoopEvent extends WebSocketEvent<PlaylistMode> {
|
||||||
WebSocketLoopEvent(PlaybackLoopMode data) : super(WsEvent.loop, data);
|
WebSocketLoopEvent(PlaylistMode data) : super(WsEvent.loop, data);
|
||||||
|
|
||||||
WebSocketLoopEvent.fromJson(Map<String, dynamic> json)
|
WebSocketLoopEvent.fromJson(Map<String, dynamic> json)
|
||||||
: super(
|
: super(
|
||||||
WsEvent.loop, PlaybackLoopMode.fromString(json["data"] as String));
|
WsEvent.loop,
|
||||||
|
PlaylistMode.values.firstWhere(
|
||||||
|
(e) => e.name == json["data"] as String,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toJson() {
|
String toJson() {
|
||||||
@ -321,12 +325,12 @@ class WebSocketErrorEvent extends WebSocketEvent<String> {
|
|||||||
WebSocketErrorEvent(String data) : super(WsEvent.error, data);
|
WebSocketErrorEvent(String data) : super(WsEvent.error, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebSocketQueueEvent extends WebSocketEvent<ProxyPlaylist> {
|
class WebSocketQueueEvent extends WebSocketEvent<AudioPlayerState> {
|
||||||
WebSocketQueueEvent(ProxyPlaylist data) : super(WsEvent.queue, data);
|
WebSocketQueueEvent(AudioPlayerState data) : super(WsEvent.queue, data);
|
||||||
|
|
||||||
factory WebSocketQueueEvent.fromJson(Map<String, dynamic> json) =>
|
factory WebSocketQueueEvent.fromJson(Map<String, dynamic> json) =>
|
||||||
WebSocketQueueEvent(
|
WebSocketQueueEvent(
|
||||||
ProxyPlaylist.fromJsonRaw(json),
|
AudioPlayerState.fromJson(json),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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/extensions/track.dart';
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
import 'package:spotube/pages/album/album.dart';
|
import 'package:spotube/pages/album/album.dart';
|
||||||
|
import 'package:spotube/provider/audio_player/querying_track_info.dart';
|
||||||
import 'package:spotube/provider/connect/connect.dart';
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
import 'package:spotube/provider/history/history.dart';
|
import 'package:spotube/provider/history/history.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
@ -30,11 +31,12 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
final playing =
|
final playing =
|
||||||
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
||||||
final historyNotifier = ref.read(playbackHistoryProvider.notifier);
|
final historyNotifier = ref.read(playbackHistoryActionsProvider);
|
||||||
|
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
|
||||||
|
|
||||||
bool isPlaylistPlaying = useMemoized(
|
bool isPlaylistPlaying = useMemoized(
|
||||||
() => playlist.containsCollection(album.id!),
|
() => playlist.containsCollection(album.id!),
|
||||||
@ -59,8 +61,8 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 10),
|
margin: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
isPlaying: isPlaylistPlaying,
|
isPlaying: isPlaylistPlaying,
|
||||||
isLoading: (isPlaylistPlaying && playlist.isFetching == true) ||
|
isLoading:
|
||||||
updating.value,
|
(isPlaylistPlaying && isFetchingActiveTrack) || updating.value,
|
||||||
title: album.name!,
|
title: album.name!,
|
||||||
description:
|
description:
|
||||||
"${album.albumType?.formatted} • ${album.artists?.asString() ?? ""}",
|
"${album.albumType?.formatted} • ${album.artists?.asString() ?? ""}",
|
||||||
|
@ -27,8 +27,8 @@ class ArtistCard extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
final isBlackListed = ref.watch(
|
final isBlackListed = ref.watch(
|
||||||
blacklistProvider.select(
|
blacklistProvider.select(
|
||||||
(blacklist) => blacklist.contains(
|
(blacklist) => blacklist.asData?.value.any(
|
||||||
BlacklistedElement.artist(artist.id!, artist.name!),
|
(element) => element.elementId == artist.id,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -55,7 +55,7 @@ class ArtistCard extends HookConsumerWidget {
|
|||||||
elevation: 3,
|
elevation: 3,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: radius,
|
borderRadius: radius,
|
||||||
side: isBlackListed
|
side: isBlackListed == true
|
||||||
? const BorderSide(
|
? const BorderSide(
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
width: 2,
|
width: 2,
|
||||||
|
@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
|
|
||||||
class TokenLoginForm extends HookConsumerWidget {
|
class TokenLoginForm extends HookConsumerWidget {
|
||||||
final void Function()? onDone;
|
final void Function()? onDone;
|
||||||
@ -52,10 +52,7 @@ class TokenLoginForm extends HookConsumerWidget {
|
|||||||
final cookieHeader =
|
final cookieHeader =
|
||||||
"sp_dc=${directCodeController.text.trim()}";
|
"sp_dc=${directCodeController.text.trim()}";
|
||||||
|
|
||||||
authenticationNotifier.setCredentials(
|
await authenticationNotifier.login(cookieHeader);
|
||||||
await AuthenticationCredentials.fromCookie(
|
|
||||||
cookieHeader),
|
|
||||||
);
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
onDone?.call();
|
onDone?.call();
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import 'package:spotube/collections/fake.dart';
|
|||||||
import 'package:spotube/modules/home/sections/friends/friend_item.dart';
|
import 'package:spotube/modules/home/sections/friends/friend_item.dart';
|
||||||
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
||||||
import 'package:spotube/models/spotify_friends.dart';
|
import 'package:spotube/models/spotify_friends.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class HomePageFriendsSection extends HookConsumerWidget {
|
class HomePageFriendsSection extends HookConsumerWidget {
|
||||||
@ -59,7 +59,7 @@ class HomePageFriendsSection extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (friendsQuery.isLoading ||
|
if (friendsQuery.isLoading ||
|
||||||
friendsQuery.asData?.value.friends.isEmpty == true ||
|
friendsQuery.asData?.value.friends.isEmpty == true ||
|
||||||
auth == null) {
|
auth.asData?.value == null) {
|
||||||
return const SliverToBoxAdapter(
|
return const SliverToBoxAdapter(
|
||||||
child: SizedBox.shrink(),
|
child: SizedBox.shrink(),
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class HomeNewReleasesSection extends HookConsumerWidget {
|
class HomeNewReleasesSection extends HookConsumerWidget {
|
||||||
@ -18,7 +18,7 @@ class HomeNewReleasesSection extends HookConsumerWidget {
|
|||||||
|
|
||||||
final albums = ref.watch(userArtistAlbumReleasesProvider);
|
final albums = ref.watch(userArtistAlbumReleasesProvider);
|
||||||
|
|
||||||
if (auth == null ||
|
if (auth.asData?.value == null ||
|
||||||
newReleases.isLoading ||
|
newReleases.isLoading ||
|
||||||
newReleases.asData?.value.items.isEmpty == true) {
|
newReleases.asData?.value.items.isEmpty == true) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
import 'package:spotube/collections/fake.dart';
|
||||||
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||||
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/provider/history/recent.dart';
|
import 'package:spotube/provider/history/recent.dart';
|
||||||
import 'package:spotube/provider/history/state.dart';
|
|
||||||
|
|
||||||
class HomeRecentlyPlayedSection extends HookConsumerWidget {
|
class HomeRecentlyPlayedSection extends HookConsumerWidget {
|
||||||
const HomeRecentlyPlayedSection({super.key});
|
const HomeRecentlyPlayedSection({super.key});
|
||||||
@ -10,23 +12,28 @@ class HomeRecentlyPlayedSection extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final history = ref.watch(recentlyPlayedItems);
|
final history = ref.watch(recentlyPlayedItems);
|
||||||
|
final historyData =
|
||||||
|
history.asData?.value ?? FakeData.historyRecentlyPlayedItems;
|
||||||
|
|
||||||
if (history.isEmpty) {
|
if (history.asData?.value.isEmpty == true) {
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
return HorizontalPlaybuttonCardView(
|
return Skeletonizer(
|
||||||
|
enabled: history.isLoading,
|
||||||
|
child: HorizontalPlaybuttonCardView(
|
||||||
title: const Text('Recently Played'),
|
title: const Text('Recently Played'),
|
||||||
items: [
|
items: [
|
||||||
for (final item in history)
|
for (final item in historyData)
|
||||||
if (item is PlaybackHistoryPlaylist)
|
if (item.playlist != null)
|
||||||
item.playlist
|
item.playlist
|
||||||
else if (item is PlaybackHistoryAlbum)
|
else if (item.album != null)
|
||||||
item.album
|
item.album
|
||||||
],
|
],
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
isLoadingNextPage: false,
|
isLoadingNextPage: false,
|
||||||
onFetchMore: () {},
|
onFetchMore: () {},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ class LocalFolderItem extends HookConsumerWidget {
|
|||||||
...pathSegments.skip(pathSegments.length - 3).toList()
|
...pathSegments.skip(pathSegments.length - 3).toList()
|
||||||
..removeLast(),
|
..removeLast(),
|
||||||
]
|
]
|
||||||
: pathSegments.take(pathSegments.length - 1).toList();
|
: pathSegments.take(max(pathSegments.length - 1, 0)).toList();
|
||||||
|
|
||||||
final trackSnapshot = ref.watch(
|
final trackSnapshot = ref.watch(
|
||||||
localTracksProvider.select(
|
localTracksProvider.select(
|
||||||
|
@ -14,7 +14,7 @@ import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
|
|||||||
import 'package:spotube/components/waypoint.dart';
|
import 'package:spotube/components/waypoint.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class UserAlbums extends HookConsumerWidget {
|
class UserAlbums extends HookConsumerWidget {
|
||||||
@ -46,7 +46,7 @@ class UserAlbums extends HookConsumerWidget {
|
|||||||
[];
|
[];
|
||||||
}, [albumsQuery.asData?.value, searchText.value]);
|
}, [albumsQuery.asData?.value, searchText.value]);
|
||||||
|
|
||||||
if (auth == null) {
|
if (auth.asData?.value == null) {
|
||||||
return const AnonymousFallback();
|
return const AnonymousFallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
|||||||
import 'package:spotube/components/waypoint.dart';
|
import 'package:spotube/components/waypoint.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class UserArtists extends HookConsumerWidget {
|
class UserArtists extends HookConsumerWidget {
|
||||||
@ -48,7 +48,7 @@ class UserArtists extends HookConsumerWidget {
|
|||||||
|
|
||||||
final controller = useScrollController();
|
final controller = useScrollController();
|
||||||
|
|
||||||
if (auth == null) {
|
if (auth.asData?.value == null) {
|
||||||
return const AnonymousFallback();
|
return const AnonymousFallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ import 'package:spotube/components/waypoint.dart';
|
|||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
|
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
@ -75,7 +75,7 @@ class UserPlaylists extends HookConsumerWidget {
|
|||||||
|
|
||||||
final controller = useScrollController();
|
final controller = useScrollController();
|
||||||
|
|
||||||
if (auth == null) {
|
if (auth.asData?.value == null) {
|
||||||
return const AnonymousFallback();
|
return const AnonymousFallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,8 +24,8 @@ import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart';
|
|||||||
import 'package:spotube/hooks/utils/use_palette_color.dart';
|
import 'package:spotube/hooks/utils/use_palette_color.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
import 'package:spotube/pages/lyrics/lyrics.dart';
|
import 'package:spotube/pages/lyrics/lyrics.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/server/active_sourced_track.dart';
|
import 'package:spotube/provider/server/active_sourced_track.dart';
|
||||||
import 'package:spotube/provider/volume_provider.dart';
|
import 'package:spotube/provider/volume_provider.dart';
|
||||||
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
||||||
@ -47,7 +47,7 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
final auth = ref.watch(authenticationProvider);
|
final auth = ref.watch(authenticationProvider);
|
||||||
final sourcedCurrentTrack = ref.watch(activeSourcedTrackProvider);
|
final sourcedCurrentTrack = ref.watch(activeSourcedTrackProvider);
|
||||||
final currentActiveTrack =
|
final currentActiveTrack =
|
||||||
ref.watch(proxyPlaylistProvider.select((s) => s.activeTrack));
|
ref.watch(audioPlayerProvider.select((s) => s.activeTrack));
|
||||||
final currentTrack = sourcedCurrentTrack ?? currentActiveTrack;
|
final currentTrack = sourcedCurrentTrack ?? currentActiveTrack;
|
||||||
final isLocalTrack = currentTrack is LocalTrack;
|
final isLocalTrack = currentTrack is LocalTrack;
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
@ -309,15 +309,13 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
builder: (context) => Consumer(
|
builder: (context) => Consumer(
|
||||||
builder: (context, ref, _) {
|
builder: (context, ref, _) {
|
||||||
final playlist = ref.watch(
|
final playlist = ref.watch(
|
||||||
proxyPlaylistProvider,
|
audioPlayerProvider,
|
||||||
);
|
|
||||||
final playlistNotifier =
|
|
||||||
ref.read(
|
|
||||||
proxyPlaylistProvider
|
|
||||||
.notifier,
|
|
||||||
);
|
);
|
||||||
|
final playlistNotifier = ref
|
||||||
|
.read(audioPlayerProvider
|
||||||
|
.notifier);
|
||||||
return PlayerQueue
|
return PlayerQueue
|
||||||
.fromProxyPlaylistNotifier(
|
.fromAudioPlayerNotifier(
|
||||||
floating: false,
|
floating: false,
|
||||||
playlist: playlist,
|
playlist: playlist,
|
||||||
notifier: playlistNotifier,
|
notifier: playlistNotifier,
|
||||||
@ -328,8 +326,9 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
: null),
|
: null),
|
||||||
),
|
),
|
||||||
if (auth != null) const SizedBox(width: 10),
|
if (auth.asData?.value != null)
|
||||||
if (auth != null)
|
const SizedBox(width: 10),
|
||||||
|
if (auth.asData?.value != null)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
label: Text(context.l10n.lyrics),
|
label: Text(context.l10n.lyrics),
|
||||||
|
@ -13,8 +13,8 @@ import 'package:spotube/extensions/duration.dart';
|
|||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/sleep_timer_provider.dart';
|
import 'package:spotube/provider/sleep_timer_provider.dart';
|
||||||
|
|
||||||
class PlayerActions extends HookConsumerWidget {
|
class PlayerActions extends HookConsumerWidget {
|
||||||
@ -33,7 +33,7 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
final isLocalTrack = playlist.activeTrack is LocalTrack;
|
final isLocalTrack = playlist.activeTrack is LocalTrack;
|
||||||
ref.watch(downloadManagerProvider);
|
ref.watch(downloadManagerProvider);
|
||||||
final downloader = ref.watch(downloadManagerProvider.notifier);
|
final downloader = ref.watch(downloadManagerProvider.notifier);
|
||||||
@ -129,7 +129,9 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
? () => downloader.addToQueue(playlist.activeTrack!)
|
? () => downloader.addToQueue(playlist.activeTrack!)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
if (playlist.activeTrack != null && !isLocalTrack && auth != null)
|
if (playlist.activeTrack != null &&
|
||||||
|
!isLocalTrack &&
|
||||||
|
auth.asData?.value != null)
|
||||||
TrackHeartButton(track: playlist.activeTrack!),
|
TrackHeartButton(track: playlist.activeTrack!),
|
||||||
AdaptivePopSheetList(
|
AdaptivePopSheetList(
|
||||||
offset: Offset(0, -50 * (sleepTimerEntries.values.length + 2)),
|
offset: Offset(0, -50 * (sleepTimerEntries.values.length + 2)),
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:media_kit/media_kit.dart';
|
||||||
import 'package:palette_generator/palette_generator.dart';
|
import 'package:palette_generator/palette_generator.dart';
|
||||||
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
@ -10,9 +11,9 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/extensions/duration.dart';
|
import 'package:spotube/extensions/duration.dart';
|
||||||
import 'package:spotube/modules/player/use_progress.dart';
|
import 'package:spotube/modules/player/use_progress.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
|
import 'package:spotube/provider/audio_player/querying_track_info.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/audio_player/loop_mode.dart';
|
|
||||||
|
|
||||||
class PlayerControls extends HookConsumerWidget {
|
class PlayerControls extends HookConsumerWidget {
|
||||||
final PaletteGenerator? palette;
|
final PaletteGenerator? palette;
|
||||||
@ -43,8 +44,7 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
SeekIntent: SeekAction(),
|
SeekIntent: SeekAction(),
|
||||||
},
|
},
|
||||||
[]);
|
[]);
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
|
||||||
|
|
||||||
final playing =
|
final playing =
|
||||||
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
||||||
@ -132,7 +132,7 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
// than total duration. Keeping it resolved
|
// than total duration. Keeping it resolved
|
||||||
value: progress.value.toDouble(),
|
value: progress.value.toDouble(),
|
||||||
secondaryTrackValue: bufferProgress,
|
secondaryTrackValue: bufferProgress,
|
||||||
onChanged: playlist.isFetching == true
|
onChanged: isFetchingActiveTrack
|
||||||
? null
|
? null
|
||||||
: (v) {
|
: (v) {
|
||||||
progress.value = v;
|
progress.value = v;
|
||||||
@ -183,7 +183,7 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
: context.l10n.shuffle_playlist,
|
: context.l10n.shuffle_playlist,
|
||||||
icon: const Icon(SpotubeIcons.shuffle),
|
icon: const Icon(SpotubeIcons.shuffle),
|
||||||
style: shuffled ? activeButtonStyle : buttonStyle,
|
style: shuffled ? activeButtonStyle : buttonStyle,
|
||||||
onPressed: playlist.isFetching == true
|
onPressed: isFetchingActiveTrack
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
if (shuffled) {
|
if (shuffled) {
|
||||||
@ -198,15 +198,15 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
tooltip: context.l10n.previous_track,
|
tooltip: context.l10n.previous_track,
|
||||||
icon: const Icon(SpotubeIcons.skipBack),
|
icon: const Icon(SpotubeIcons.skipBack),
|
||||||
style: buttonStyle,
|
style: buttonStyle,
|
||||||
onPressed: playlist.isFetching == true
|
onPressed: isFetchingActiveTrack
|
||||||
? null
|
? null
|
||||||
: playlistNotifier.previous,
|
: audioPlayer.skipToPrevious,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: playing
|
tooltip: playing
|
||||||
? context.l10n.pause_playback
|
? context.l10n.pause_playback
|
||||||
: context.l10n.resume_playback,
|
: context.l10n.resume_playback,
|
||||||
icon: playlist.isFetching == true
|
icon: isFetchingActiveTrack
|
||||||
? SizedBox(
|
? SizedBox(
|
||||||
height: 20,
|
height: 20,
|
||||||
width: 20,
|
width: 20,
|
||||||
@ -219,7 +219,7 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
playing ? SpotubeIcons.pause : SpotubeIcons.play,
|
playing ? SpotubeIcons.pause : SpotubeIcons.play,
|
||||||
),
|
),
|
||||||
style: resumePauseStyle,
|
style: resumePauseStyle,
|
||||||
onPressed: playlist.isFetching == true
|
onPressed: isFetchingActiveTrack
|
||||||
? null
|
? null
|
||||||
: Actions.handler<PlayPauseIntent>(
|
: Actions.handler<PlayPauseIntent>(
|
||||||
context,
|
context,
|
||||||
@ -230,40 +230,36 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
tooltip: context.l10n.next_track,
|
tooltip: context.l10n.next_track,
|
||||||
icon: const Icon(SpotubeIcons.skipForward),
|
icon: const Icon(SpotubeIcons.skipForward),
|
||||||
style: buttonStyle,
|
style: buttonStyle,
|
||||||
onPressed: playlist.isFetching == true
|
onPressed:
|
||||||
? null
|
isFetchingActiveTrack ? null : audioPlayer.skipToNext,
|
||||||
: playlistNotifier.next,
|
|
||||||
),
|
),
|
||||||
StreamBuilder<PlaybackLoopMode>(
|
Consumer(builder: (context, ref, _) {
|
||||||
stream: audioPlayer.loopModeStream,
|
final loopMode = ref
|
||||||
builder: (context, snapshot) {
|
.watch(audioPlayerProvider.select((s) => s.loopMode));
|
||||||
final loopMode = snapshot.data ?? PlaybackLoopMode.none;
|
|
||||||
return IconButton(
|
return IconButton(
|
||||||
tooltip: loopMode == PlaybackLoopMode.one
|
tooltip: loopMode == PlaylistMode.single
|
||||||
? context.l10n.loop_track
|
? context.l10n.loop_track
|
||||||
: loopMode == PlaybackLoopMode.all
|
: loopMode == PlaylistMode.loop
|
||||||
? context.l10n.repeat_playlist
|
? context.l10n.repeat_playlist
|
||||||
: null,
|
: null,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
loopMode == PlaybackLoopMode.one
|
loopMode == PlaylistMode.single
|
||||||
? SpotubeIcons.repeatOne
|
? SpotubeIcons.repeatOne
|
||||||
: SpotubeIcons.repeat,
|
: SpotubeIcons.repeat,
|
||||||
),
|
),
|
||||||
style: loopMode == PlaybackLoopMode.one ||
|
style: loopMode == PlaylistMode.single ||
|
||||||
loopMode == PlaybackLoopMode.all
|
loopMode == PlaylistMode.loop
|
||||||
? activeButtonStyle
|
? activeButtonStyle
|
||||||
: buttonStyle,
|
: buttonStyle,
|
||||||
onPressed: playlist.isFetching == true
|
onPressed: isFetchingActiveTrack
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
audioPlayer.setLoopMode(
|
await audioPlayer.setLoopMode(
|
||||||
switch (loopMode) {
|
switch (loopMode) {
|
||||||
PlaybackLoopMode.all =>
|
PlaylistMode.loop => PlaylistMode.single,
|
||||||
PlaybackLoopMode.one,
|
PlaylistMode.single => PlaylistMode.none,
|
||||||
PlaybackLoopMode.one =>
|
PlaylistMode.none => PlaylistMode.loop,
|
||||||
PlaybackLoopMode.none,
|
|
||||||
PlaybackLoopMode.none =>
|
|
||||||
PlaybackLoopMode.all,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -11,7 +11,8 @@ import 'package:spotube/collections/spotube_icons.dart';
|
|||||||
import 'package:spotube/collections/intents.dart';
|
import 'package:spotube/collections/intents.dart';
|
||||||
import 'package:spotube/modules/player/use_progress.dart';
|
import 'package:spotube/modules/player/use_progress.dart';
|
||||||
import 'package:spotube/modules/player/player.dart';
|
import 'package:spotube/modules/player/player.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
|
import 'package:spotube/provider/audio_player/querying_track_info.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
|
||||||
class PlayerOverlay extends HookConsumerWidget {
|
class PlayerOverlay extends HookConsumerWidget {
|
||||||
@ -24,8 +25,8 @@ class PlayerOverlay extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
final canShow = playlist.activeTrack != null;
|
final canShow = playlist.activeTrack != null;
|
||||||
|
|
||||||
final playing =
|
final playing =
|
||||||
@ -127,14 +128,14 @@ class PlayerOverlay extends HookConsumerWidget {
|
|||||||
SpotubeIcons.skipBack,
|
SpotubeIcons.skipBack,
|
||||||
color: textColor,
|
color: textColor,
|
||||||
),
|
),
|
||||||
onPressed: playlist.isFetching
|
onPressed: isFetchingActiveTrack
|
||||||
? null
|
? null
|
||||||
: playlistNotifier.previous,
|
: audioPlayer.skipToPrevious,
|
||||||
),
|
),
|
||||||
Consumer(
|
Consumer(
|
||||||
builder: (context, ref, _) {
|
builder: (context, ref, _) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
icon: playlist.isFetching
|
icon: isFetchingActiveTrack
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
height: 20,
|
height: 20,
|
||||||
width: 20,
|
width: 20,
|
||||||
@ -158,9 +159,9 @@ class PlayerOverlay extends HookConsumerWidget {
|
|||||||
SpotubeIcons.skipForward,
|
SpotubeIcons.skipForward,
|
||||||
color: textColor,
|
color: textColor,
|
||||||
),
|
),
|
||||||
onPressed: playlist.isFetching
|
onPressed: isFetchingActiveTrack
|
||||||
? null
|
? null
|
||||||
: playlistNotifier.next,
|
: audioPlayer.skipToNext,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -18,12 +18,12 @@ import 'package:spotube/extensions/artist_simple.dart';
|
|||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart';
|
import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/state.dart';
|
||||||
|
|
||||||
class PlayerQueue extends HookConsumerWidget {
|
class PlayerQueue extends HookConsumerWidget {
|
||||||
final bool floating;
|
final bool floating;
|
||||||
final ProxyPlaylist playlist;
|
final AudioPlayerState playlist;
|
||||||
|
|
||||||
final Future<void> Function(Track track) onJump;
|
final Future<void> Function(Track track) onJump;
|
||||||
final Future<void> Function(String trackId) onRemove;
|
final Future<void> Function(String trackId) onRemove;
|
||||||
@ -40,10 +40,10 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
PlayerQueue.fromProxyPlaylistNotifier({
|
PlayerQueue.fromAudioPlayerNotifier({
|
||||||
this.floating = true,
|
this.floating = true,
|
||||||
required this.playlist,
|
required this.playlist,
|
||||||
required ProxyPlaylistNotifier notifier,
|
required AudioPlayerNotifier notifier,
|
||||||
super.key,
|
super.key,
|
||||||
}) : onJump = notifier.jumpToTrack,
|
}) : onJump = notifier.jumpToTrack,
|
||||||
onRemove = notifier.removeTrack,
|
onRemove = notifier.removeTrack,
|
||||||
@ -93,11 +93,10 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (playlist.active == null) return null;
|
if (playlist.activeTrack == null) return null;
|
||||||
|
|
||||||
if (playlist.active! < 0) return;
|
|
||||||
controller.scrollToIndex(
|
controller.scrollToIndex(
|
||||||
playlist.active!,
|
playlist.playlist.index,
|
||||||
preferPosition: AutoScrollPosition.middle,
|
preferPosition: AutoScrollPosition.middle,
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
|
@ -9,7 +9,7 @@ import 'package:spotube/components/links/link_text.dart';
|
|||||||
import 'package:spotube/extensions/artist_simple.dart';
|
import 'package:spotube/extensions/artist_simple.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class PlayerTrackDetails extends HookConsumerWidget {
|
class PlayerTrackDetails extends HookConsumerWidget {
|
||||||
@ -21,7 +21,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
final playback = ref.watch(proxyPlaylistProvider);
|
final playback = ref.watch(audioPlayerProvider);
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
|
@ -14,10 +14,12 @@ import 'package:spotube/extensions/constrains.dart';
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/duration.dart';
|
import 'package:spotube/extensions/duration.dart';
|
||||||
import 'package:spotube/hooks/utils/use_debounce.dart';
|
import 'package:spotube/hooks/utils/use_debounce.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
|
import 'package:spotube/provider/audio_player/querying_track_info.dart';
|
||||||
import 'package:spotube/provider/server/active_sourced_track.dart';
|
import 'package:spotube/provider/server/active_sourced_track.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
||||||
import 'package:spotube/services/sourced_track/models/video_info.dart';
|
import 'package:spotube/services/sourced_track/models/video_info.dart';
|
||||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||||
@ -52,7 +54,8 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
|
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
|
||||||
final preferences = ref.watch(userPreferencesProvider);
|
final preferences = ref.watch(userPreferencesProvider);
|
||||||
|
|
||||||
final isSearching = useState(false);
|
final isSearching = useState(false);
|
||||||
@ -128,13 +131,13 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
final siblings = useMemoized(
|
final siblings = useMemoized(
|
||||||
() => playlist.isFetching == false
|
() => !isFetchingActiveTrack
|
||||||
? [
|
? [
|
||||||
(activeTrack as SourcedTrack).sourceInfo,
|
(activeTrack as SourcedTrack).sourceInfo,
|
||||||
...activeTrack.siblings,
|
...activeTrack.siblings,
|
||||||
]
|
]
|
||||||
: <SourceInfo>[],
|
: <SourceInfo>[],
|
||||||
[playlist.isFetching, activeTrack],
|
[activeTrack, isFetchingActiveTrack],
|
||||||
);
|
);
|
||||||
|
|
||||||
final borderRadius = floating
|
final borderRadius = floating
|
||||||
@ -174,12 +177,12 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
Text(" • ${sourceInfo.artist}"),
|
Text(" • ${sourceInfo.artist}"),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
enabled: playlist.isFetching != true,
|
enabled: !isFetchingActiveTrack,
|
||||||
selected: playlist.isFetching != true &&
|
selected: !isFetchingActiveTrack &&
|
||||||
sourceInfo.id == (activeTrack as SourcedTrack).sourceInfo.id,
|
sourceInfo.id == (activeTrack as SourcedTrack).sourceInfo.id,
|
||||||
selectedTileColor: theme.popupMenuTheme.color,
|
selectedTileColor: theme.popupMenuTheme.color,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (playlist.isFetching == false &&
|
if (!isFetchingActiveTrack &&
|
||||||
sourceInfo.id != (activeTrack as SourcedTrack).sourceInfo.id) {
|
sourceInfo.id != (activeTrack as SourcedTrack).sourceInfo.id) {
|
||||||
activeTrackNotifier.swapSibling(sourceInfo);
|
activeTrackNotifier.swapSibling(sourceInfo);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
@ -187,7 +190,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[playlist.isFetching, activeTrack, siblings],
|
[activeTrack, siblings],
|
||||||
);
|
);
|
||||||
|
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'package:async/async.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
@ -19,26 +18,13 @@ import 'package:spotube/services/audio_player/audio_player.dart';
|
|||||||
final sliderValue = position.value.inSeconds;
|
final sliderValue = position.value.inSeconds;
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
final durationOperation =
|
duration.value = audioPlayer.duration;
|
||||||
CancelableOperation.fromFuture(audioPlayer.duration);
|
|
||||||
durationOperation.then((value) {
|
|
||||||
if (value != null) {
|
|
||||||
duration.value = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
final durationSubscription = audioPlayer.durationStream.listen((event) {
|
final durationSubscription = audioPlayer.durationStream.listen((event) {
|
||||||
duration.value = event;
|
duration.value = event;
|
||||||
});
|
});
|
||||||
|
|
||||||
final positionOperation =
|
position.value = audioPlayer.position;
|
||||||
CancelableOperation.fromFuture(audioPlayer.position);
|
|
||||||
|
|
||||||
positionOperation.then((value) {
|
|
||||||
if (value != null) {
|
|
||||||
position.value = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var lastPosition = position.value;
|
var lastPosition = position.value;
|
||||||
|
|
||||||
@ -54,9 +40,7 @@ import 'package:spotube/services/audio_player/audio_player.dart';
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () {
|
return () {
|
||||||
positionOperation.cancel();
|
|
||||||
positionSubscription.cancel();
|
positionSubscription.cancel();
|
||||||
durationOperation.cancel();
|
|
||||||
durationSubscription.cancel();
|
durationSubscription.cancel();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -7,9 +7,10 @@ import 'package:spotube/components/playbutton_card.dart';
|
|||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
import 'package:spotube/pages/playlist/playlist.dart';
|
import 'package:spotube/pages/playlist/playlist.dart';
|
||||||
|
import 'package:spotube/provider/audio_player/querying_track_info.dart';
|
||||||
import 'package:spotube/provider/connect/connect.dart';
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
import 'package:spotube/provider/history/history.dart';
|
import 'package:spotube/provider/history/history.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
@ -22,9 +23,10 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlistQueue = ref.watch(proxyPlaylistProvider);
|
final playlistQueue = ref.watch(audioPlayerProvider);
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
||||||
final historyNotifier = ref.read(playbackHistoryProvider.notifier);
|
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
|
||||||
|
final historyNotifier = ref.read(playbackHistoryActionsProvider);
|
||||||
|
|
||||||
final playing =
|
final playing =
|
||||||
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
||||||
@ -65,8 +67,7 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
placeholder: ImagePlaceholder.collection,
|
placeholder: ImagePlaceholder.collection,
|
||||||
),
|
),
|
||||||
isPlaying: isPlaylistPlaying,
|
isPlaying: isPlaylistPlaying,
|
||||||
isLoading:
|
isLoading: (isPlaylistPlaying && isFetchingActiveTrack) || updating.value,
|
||||||
(isPlaylistPlaying && playlistQueue.isFetching) || updating.value,
|
|
||||||
isOwner: playlist.owner?.id == me.asData?.value.id &&
|
isOwner: playlist.owner?.id == me.asData?.value.id &&
|
||||||
me.asData?.value.id != null,
|
me.asData?.value.id != null,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
|
|
||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/modules/player/player_actions.dart';
|
import 'package:spotube/modules/player/player_actions.dart';
|
||||||
import 'package:spotube/modules/player/player_overlay.dart';
|
import 'package:spotube/modules/player/player_overlay.dart';
|
||||||
import 'package:spotube/modules/player/player_track_details.dart';
|
import 'package:spotube/modules/player/player_track_details.dart';
|
||||||
@ -17,10 +18,10 @@ import 'package:spotube/extensions/image.dart';
|
|||||||
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
|
||||||
import 'package:spotube/provider/volume_provider.dart';
|
import 'package:spotube/provider/volume_provider.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
@ -32,7 +33,7 @@ class BottomPlayer extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final auth = ref.watch(authenticationProvider);
|
final auth = ref.watch(authenticationProvider);
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
final layoutMode =
|
final layoutMode =
|
||||||
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
|
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
|
||||||
|
|
||||||
@ -90,7 +91,7 @@ class BottomPlayer extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
PlayerActions(
|
PlayerActions(
|
||||||
extraActions: [
|
extraActions: [
|
||||||
if (auth != null)
|
if (auth.asData?.value != null)
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: context.l10n.mini_player,
|
tooltip: context.l10n.mini_player,
|
||||||
icon: const Icon(SpotubeIcons.miniPlayer),
|
icon: const Icon(SpotubeIcons.miniPlayer),
|
||||||
|
@ -9,6 +9,7 @@ import 'package:sidebarx/sidebarx.dart';
|
|||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
import 'package:spotube/collections/side_bar_tiles.dart';
|
import 'package:spotube/collections/side_bar_tiles.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/modules/connect/connect_device.dart';
|
import 'package:spotube/modules/connect/connect_device.dart';
|
||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
@ -19,11 +20,11 @@ import 'package:spotube/hooks/controllers/use_sidebarx_controller.dart';
|
|||||||
import 'package:spotube/pages/profile/profile.dart';
|
import 'package:spotube/pages/profile/profile.dart';
|
||||||
import 'package:spotube/pages/settings/settings.dart';
|
import 'package:spotube/pages/settings/settings.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
@ -268,7 +269,7 @@ class SidebarFooter extends HookConsumerWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
if (auth != null && data == null)
|
if (auth.asData?.value != null && data == null)
|
||||||
const CircularProgressIndicator()
|
const CircularProgressIndicator()
|
||||||
else if (data != null)
|
else if (data != null)
|
||||||
Flexible(
|
Flexible(
|
||||||
|
@ -10,9 +10,10 @@ import 'package:spotube/collections/side_bar_tiles.dart';
|
|||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
||||||
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
final navigationPanelHeight = StateProvider<double>((ref) => 50);
|
final navigationPanelHeight = StateProvider<double>((ref) => 50);
|
||||||
|
@ -33,7 +33,7 @@ class StatsAlbumItem extends StatelessWidget {
|
|||||||
Text("${album.albumType?.formatted} • "),
|
Text("${album.albumType?.formatted} • "),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: ArtistLink(
|
child: ArtistLink(
|
||||||
artists: album.artists!,
|
artists: album.artists ?? [],
|
||||||
mainAxisAlignment: WrapAlignment.start,
|
mainAxisAlignment: WrapAlignment.start,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
import 'package:spotube/collections/fake.dart';
|
||||||
import 'package:spotube/collections/formatters.dart';
|
import 'package:spotube/collections/formatters.dart';
|
||||||
import 'package:spotube/modules/stats/summary/summary_card.dart';
|
import 'package:spotube/modules/stats/summary/summary_card.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
@ -18,8 +20,11 @@ class StatsPageSummarySection extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final summary = ref.watch(playbackHistorySummaryProvider);
|
final summary = ref.watch(playbackHistorySummaryProvider);
|
||||||
|
final summaryData = summary.asData?.value ?? FakeData.historySummary;
|
||||||
|
|
||||||
return SliverPadding(
|
return Skeletonizer.sliver(
|
||||||
|
enabled: summary.isLoading,
|
||||||
|
child: SliverPadding(
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
sliver: SliverLayoutBuilder(builder: (context, constrains) {
|
sliver: SliverLayoutBuilder(builder: (context, constrains) {
|
||||||
return SliverGrid(
|
return SliverGrid(
|
||||||
@ -39,7 +44,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
delegate: SliverChildListDelegate([
|
delegate: SliverChildListDelegate([
|
||||||
SummaryCard(
|
SummaryCard(
|
||||||
title: summary.duration.inMinutes.toDouble(),
|
title: summaryData.duration.inMinutes.toDouble(),
|
||||||
unit: "minutes",
|
unit: "minutes",
|
||||||
description: 'Listened to music',
|
description: 'Listened to music',
|
||||||
color: Colors.purple,
|
color: Colors.purple,
|
||||||
@ -48,7 +53,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
SummaryCard(
|
SummaryCard(
|
||||||
title: summary.tracks.toDouble(),
|
title: summaryData.tracks.toDouble(),
|
||||||
unit: "songs",
|
unit: "songs",
|
||||||
description: 'Streamed overall',
|
description: 'Streamed overall',
|
||||||
color: Colors.lightBlue,
|
color: Colors.lightBlue,
|
||||||
@ -57,7 +62,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
SummaryCard.unformatted(
|
SummaryCard.unformatted(
|
||||||
title: usdFormatter.format(summary.fees.toDouble()),
|
title: usdFormatter.format(summaryData.fees.toDouble()),
|
||||||
unit: "",
|
unit: "",
|
||||||
description: 'Owed to artists\nthis month',
|
description: 'Owed to artists\nthis month',
|
||||||
color: Colors.green,
|
color: Colors.green,
|
||||||
@ -66,7 +71,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
SummaryCard(
|
SummaryCard(
|
||||||
title: summary.artists.toDouble(),
|
title: summaryData.artists.toDouble(),
|
||||||
unit: "artist's",
|
unit: "artist's",
|
||||||
description: 'Music reached you',
|
description: 'Music reached you',
|
||||||
color: Colors.yellow,
|
color: Colors.yellow,
|
||||||
@ -75,7 +80,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
SummaryCard(
|
SummaryCard(
|
||||||
title: summary.albums.toDouble(),
|
title: summaryData.albums.toDouble(),
|
||||||
unit: "full albums",
|
unit: "full albums",
|
||||||
description: 'Got your love',
|
description: 'Got your love',
|
||||||
color: Colors.pink,
|
color: Colors.pink,
|
||||||
@ -84,7 +89,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
SummaryCard(
|
SummaryCard(
|
||||||
title: summary.playlists.toDouble(),
|
title: summaryData.playlists.toDouble(),
|
||||||
unit: "playlists",
|
unit: "playlists",
|
||||||
description: 'Were on repeat',
|
description: 'Were on repeat',
|
||||||
color: Colors.teal,
|
color: Colors.teal,
|
||||||
@ -95,6 +100,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
|
|||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotube/collections/formatters.dart';
|
import 'package:spotube/collections/formatters.dart';
|
||||||
import 'package:spotube/modules/stats/common/album_item.dart';
|
import 'package:spotube/modules/stats/common/album_item.dart';
|
||||||
import 'package:spotube/provider/history/top.dart';
|
import 'package:spotube/provider/history/top.dart';
|
||||||
|
import 'package:spotube/provider/history/top/albums.dart';
|
||||||
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
class TopAlbums extends HookConsumerWidget {
|
class TopAlbums extends HookConsumerWidget {
|
||||||
const TopAlbums({super.key});
|
const TopAlbums({super.key});
|
||||||
@ -10,13 +14,24 @@ class TopAlbums extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
||||||
final albums = ref.watch(playbackHistoryTopProvider(historyDuration)
|
final topAlbums = ref.watch(historyTopAlbumsProvider(historyDuration));
|
||||||
.select((value) => value.albums));
|
final topAlbumsNotifier =
|
||||||
|
ref.watch(historyTopAlbumsProvider(historyDuration).notifier);
|
||||||
|
|
||||||
return SliverList.builder(
|
final albumsData = topAlbums.asData?.value.items ?? [];
|
||||||
itemCount: albums.length,
|
|
||||||
|
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) {
|
itemBuilder: (context, index) {
|
||||||
final album = albums[index];
|
final album = albumsData[index];
|
||||||
return StatsAlbumItem(
|
return StatsAlbumItem(
|
||||||
album: album.album,
|
album: album.album,
|
||||||
info: Text(
|
info: Text(
|
||||||
@ -24,6 +39,7 @@ class TopAlbums extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotube/collections/formatters.dart';
|
import 'package:spotube/collections/formatters.dart';
|
||||||
import 'package:spotube/modules/stats/common/artist_item.dart';
|
import 'package:spotube/modules/stats/common/artist_item.dart';
|
||||||
import 'package:spotube/provider/history/top.dart';
|
import 'package:spotube/provider/history/top.dart';
|
||||||
|
import 'package:spotube/provider/history/top/tracks.dart';
|
||||||
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
class TopArtists extends HookConsumerWidget {
|
class TopArtists extends HookConsumerWidget {
|
||||||
const TopArtists({super.key});
|
const TopArtists({super.key});
|
||||||
@ -10,18 +15,33 @@ class TopArtists extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
||||||
final artists = ref.watch(playbackHistoryTopProvider(historyDuration)
|
final topTracks = ref.watch(
|
||||||
.select((value) => value.artists));
|
historyTopTracksProvider(historyDuration),
|
||||||
|
);
|
||||||
|
final topTracksNotifier =
|
||||||
|
ref.watch(historyTopTracksProvider(historyDuration).notifier);
|
||||||
|
|
||||||
return SliverList.builder(
|
final artistsData = useMemoized(
|
||||||
itemCount: artists.length,
|
() => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]);
|
||||||
|
|
||||||
|
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) {
|
itemBuilder: (context, index) {
|
||||||
final artist = artists[index];
|
final artist = artistsData[index];
|
||||||
return StatsArtistItem(
|
return StatsArtistItem(
|
||||||
artist: artist.artist,
|
artist: artist.artist,
|
||||||
info: Text("${compactNumberFormatter.format(artist.count)} plays"),
|
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/albums.dart';
|
||||||
import 'package:spotube/modules/stats/top/artists.dart';
|
import 'package:spotube/modules/stats/top/artists.dart';
|
||||||
import 'package:spotube/modules/stats/top/tracks.dart';
|
import 'package:spotube/modules/stats/top/tracks.dart';
|
||||||
import 'package:spotube/provider/history/state.dart';
|
|
||||||
import 'package:spotube/provider/history/top.dart';
|
import 'package:spotube/provider/history/top.dart';
|
||||||
|
|
||||||
class StatsPageTopSection extends HookConsumerWidget {
|
class StatsPageTopSection extends HookConsumerWidget {
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotube/collections/formatters.dart';
|
import 'package:spotube/collections/formatters.dart';
|
||||||
import 'package:spotube/modules/stats/common/track_item.dart';
|
import 'package:spotube/modules/stats/common/track_item.dart';
|
||||||
import 'package:spotube/provider/history/top.dart';
|
import 'package:spotube/provider/history/top.dart';
|
||||||
|
import 'package:spotube/provider/history/top/tracks.dart';
|
||||||
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
class TopTracks extends HookConsumerWidget {
|
class TopTracks extends HookConsumerWidget {
|
||||||
const TopTracks({super.key});
|
const TopTracks({super.key});
|
||||||
@ -10,15 +14,26 @@ class TopTracks extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
||||||
final tracks = ref.watch(
|
final topTracks = ref.watch(
|
||||||
playbackHistoryTopProvider(historyDuration)
|
historyTopTracksProvider(historyDuration),
|
||||||
.select((value) => value.tracks),
|
|
||||||
);
|
);
|
||||||
|
final topTracksNotifier =
|
||||||
|
ref.watch(historyTopTracksProvider(historyDuration).notifier);
|
||||||
|
|
||||||
return SliverList.builder(
|
final tracksData = topTracks.asData?.value.items ?? [];
|
||||||
itemCount: tracks.length,
|
|
||||||
|
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) {
|
itemBuilder: (context, index) {
|
||||||
final track = tracks[index];
|
final track = tracksData[index];
|
||||||
return StatsTrackItem(
|
return StatsTrackItem(
|
||||||
track: track.track,
|
track: track.track,
|
||||||
info: Text(
|
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/context.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/utils/primitive_utils.dart';
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
@ -39,10 +40,9 @@ class ArtistPageHeader extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final auth = ref.watch(authenticationProvider);
|
final auth = ref.watch(authenticationProvider);
|
||||||
final blacklist = ref.watch(blacklistProvider);
|
ref.watch(blacklistProvider);
|
||||||
final isBlackListed = blacklist.contains(
|
final blacklistNotifier = ref.watch(blacklistProvider.notifier);
|
||||||
BlacklistedElement.artist(artistId, artist.name!),
|
final isBlackListed = blacklistNotifier.containsArtist(artist);
|
||||||
);
|
|
||||||
|
|
||||||
final image = artist.images.asUrlString(
|
final image = artist.images.asUrlString(
|
||||||
placeholder: ImagePlaceholder.artist,
|
placeholder: ImagePlaceholder.artist,
|
||||||
@ -135,7 +135,7 @@ class ArtistPageHeader extends HookConsumerWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (auth != null)
|
if (auth.asData?.value != null)
|
||||||
Consumer(
|
Consumer(
|
||||||
builder: (context, ref, _) {
|
builder: (context, ref, _) {
|
||||||
final isFollowingQuery = ref
|
final isFollowingQuery = ref
|
||||||
@ -187,14 +187,16 @@ class ArtistPageHeader extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (isBlackListed) {
|
if (isBlackListed) {
|
||||||
ref.read(blacklistProvider.notifier).remove(
|
await ref
|
||||||
BlacklistedElement.artist(
|
.read(blacklistProvider.notifier)
|
||||||
artist.id!, artist.name!),
|
.remove(artist.id!);
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
ref.read(blacklistProvider.notifier).add(
|
await ref.read(blacklistProvider.notifier).add(
|
||||||
BlacklistedElement.artist(
|
BlacklistTableCompanion.insert(
|
||||||
artist.id!, artist.name!),
|
name: artist.name!,
|
||||||
|
elementId: artist.id!,
|
||||||
|
elementType: BlacklistedType.artist,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -9,7 +9,7 @@ import 'package:spotube/components/track_tile/track_tile.dart';
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
import 'package:spotube/provider/connect/connect.dart';
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class ArtistPageTopTracks extends HookConsumerWidget {
|
class ArtistPageTopTracks extends HookConsumerWidget {
|
||||||
@ -21,8 +21,8 @@ class ArtistPageTopTracks extends HookConsumerWidget {
|
|||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||||
|
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
||||||
final topTracksQuery = ref.watch(artistTopTracksProvider(artistId));
|
final topTracksQuery = ref.watch(artistTopTracksProvider(artistId));
|
||||||
|
|
||||||
final isPlaylistPlaying = playlist.containsTracks(
|
final isPlaylistPlaying = playlist.containsTracks(
|
||||||
|
@ -16,7 +16,7 @@ import 'package:spotube/extensions/image.dart';
|
|||||||
import 'package:spotube/pages/track/track.dart';
|
import 'package:spotube/pages/track/track.dart';
|
||||||
import 'package:spotube/provider/connect/clients.dart';
|
import 'package:spotube/provider/connect/clients.dart';
|
||||||
import 'package:spotube/provider/connect/connect.dart';
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
import 'package:spotube/services/audio_player/loop_mode.dart';
|
import 'package:media_kit/media_kit.dart' hide Track;
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class RemotePlayerQueue extends ConsumerWidget {
|
class RemotePlayerQueue extends ConsumerWidget {
|
||||||
@ -244,18 +244,18 @@ class ConnectControlPage extends HookConsumerWidget {
|
|||||||
: connectNotifier.next,
|
: connectNotifier.next,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: loopMode == PlaybackLoopMode.one
|
tooltip: loopMode == PlaylistMode.single
|
||||||
? context.l10n.loop_track
|
? context.l10n.loop_track
|
||||||
: loopMode == PlaybackLoopMode.all
|
: loopMode == PlaylistMode.loop
|
||||||
? context.l10n.repeat_playlist
|
? context.l10n.repeat_playlist
|
||||||
: null,
|
: null,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
loopMode == PlaybackLoopMode.one
|
loopMode == PlaylistMode.single
|
||||||
? SpotubeIcons.repeatOne
|
? SpotubeIcons.repeatOne
|
||||||
: SpotubeIcons.repeat,
|
: SpotubeIcons.repeat,
|
||||||
),
|
),
|
||||||
style: loopMode == PlaybackLoopMode.one ||
|
style: loopMode == PlaylistMode.single ||
|
||||||
loopMode == PlaybackLoopMode.all
|
loopMode == PlaylistMode.loop
|
||||||
? activeButtonStyle
|
? activeButtonStyle
|
||||||
: buttonStyle,
|
: buttonStyle,
|
||||||
onPressed: playlist.activeTrack == null
|
onPressed: playlist.activeTrack == null
|
||||||
@ -263,12 +263,11 @@ class ConnectControlPage extends HookConsumerWidget {
|
|||||||
: () async {
|
: () async {
|
||||||
connectNotifier.setLoopMode(
|
connectNotifier.setLoopMode(
|
||||||
switch (loopMode) {
|
switch (loopMode) {
|
||||||
PlaybackLoopMode.all =>
|
PlaylistMode.loop =>
|
||||||
PlaybackLoopMode.one,
|
PlaylistMode.single,
|
||||||
PlaybackLoopMode.one =>
|
PlaylistMode.single =>
|
||||||
PlaybackLoopMode.none,
|
PlaylistMode.none,
|
||||||
PlaybackLoopMode.none =>
|
PlaylistMode.none => PlaylistMode.loop,
|
||||||
PlaybackLoopMode.all,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -9,7 +9,7 @@ import 'package:spotube/components/links/hyper_link.dart';
|
|||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/pages/home/home.dart';
|
import 'package:spotube/pages/home/home.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class LoginTutorial extends ConsumerWidget {
|
class LoginTutorial extends ConsumerWidget {
|
||||||
@ -18,8 +18,7 @@ class LoginTutorial extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
ref.watch(authenticationProvider);
|
final auth = ref.watch(authenticationProvider);
|
||||||
final authenticationNotifier = ref.watch(authenticationProvider.notifier);
|
|
||||||
final key = GlobalKey<State<IntroductionScreen>>();
|
final key = GlobalKey<State<IntroductionScreen>>();
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
@ -53,7 +52,7 @@ class LoginTutorial extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
overrideDone: FilledButton(
|
overrideDone: FilledButton(
|
||||||
onPressed: authenticationNotifier.isLoggedIn
|
onPressed: auth.asData?.value != null
|
||||||
? () {
|
? () {
|
||||||
ServiceUtils.pushNamed(context, HomePage.name);
|
ServiceUtils.pushNamed(context, HomePage.name);
|
||||||
}
|
}
|
||||||
@ -91,7 +90,7 @@ class LoginTutorial extends ConsumerWidget {
|
|||||||
bodyWidget:
|
bodyWidget:
|
||||||
Text(context.l10n.step_3_steps, textAlign: TextAlign.left),
|
Text(context.l10n.step_3_steps, textAlign: TextAlign.left),
|
||||||
),
|
),
|
||||||
if (authenticationNotifier.isLoggedIn)
|
if (auth.asData?.value != null)
|
||||||
PageViewModel(
|
PageViewModel(
|
||||||
decoration: pageDecoration.copyWith(
|
decoration: pageDecoration.copyWith(
|
||||||
bodyAlignment: Alignment.center,
|
bodyAlignment: Alignment.center,
|
||||||
|
@ -4,11 +4,11 @@ import 'package:gap/gap.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/modules/getting_started/blur_card.dart';
|
import 'package:spotube/modules/getting_started/blur_card.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/string.dart';
|
import 'package:spotube/extensions/string.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
|
||||||
|
|
||||||
final audioSourceToIconMap = {
|
final audioSourceToIconMap = {
|
||||||
AudioSource.youtube: const Icon(
|
AudioSource.youtube: const Icon(
|
||||||
|
@ -55,14 +55,14 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
initialSelection: preferences.recommendationMarket,
|
initialSelection: preferences.market,
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
ref
|
ref
|
||||||
.read(userPreferencesProvider.notifier)
|
.read(userPreferencesProvider.notifier)
|
||||||
.setRecommendationMarket(value);
|
.setRecommendationMarket(value);
|
||||||
},
|
},
|
||||||
hintText: preferences.recommendationMarket.name,
|
hintText: preferences.market.name,
|
||||||
label: Text(context.l10n.market_place_region),
|
label: Text(context.l10n.market_place_region),
|
||||||
inputDecorationTheme:
|
inputDecorationTheme:
|
||||||
const InputDecorationTheme(isDense: true),
|
const InputDecorationTheme(isDense: true),
|
||||||
|
@ -7,7 +7,7 @@ import 'package:spotube/collections/spotube_icons.dart';
|
|||||||
import 'package:spotube/components/dialogs/prompt_dialog.dart';
|
import 'package:spotube/components/dialogs/prompt_dialog.dart';
|
||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/scrobbler_provider.dart';
|
import 'package:spotube/provider/scrobbler/scrobbler.dart';
|
||||||
|
|
||||||
class LastFMLoginPage extends HookConsumerWidget {
|
class LastFMLoginPage extends HookConsumerWidget {
|
||||||
static const name = "lastfm_login";
|
static const name = "lastfm_login";
|
||||||
|
@ -17,7 +17,7 @@ import 'package:spotube/extensions/artist_simple.dart';
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class LocalLibraryPage extends HookConsumerWidget {
|
class LocalLibraryPage extends HookConsumerWidget {
|
||||||
@ -32,8 +32,8 @@ class LocalLibraryPage extends HookConsumerWidget {
|
|||||||
List<LocalTrack> tracks, {
|
List<LocalTrack> tracks, {
|
||||||
LocalTrack? currentTrack,
|
LocalTrack? currentTrack,
|
||||||
}) async {
|
}) async {
|
||||||
final playlist = ref.read(proxyPlaylistProvider);
|
final playlist = ref.read(audioPlayerProvider);
|
||||||
final playback = ref.read(proxyPlaylistProvider.notifier);
|
final playback = ref.read(audioPlayerProvider.notifier);
|
||||||
currentTrack ??= tracks.first;
|
currentTrack ??= tracks.first;
|
||||||
final isPlaylistPlaying = playlist.containsTracks(tracks);
|
final isPlaylistPlaying = playlist.containsTracks(tracks);
|
||||||
if (!isPlaylistPlaying) {
|
if (!isPlaylistPlaying) {
|
||||||
@ -52,7 +52,7 @@ class LocalLibraryPage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final sortBy = useState<SortBy>(SortBy.none);
|
final sortBy = useState<SortBy>(SortBy.none);
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
final trackSnapshot = ref.watch(localTracksProvider);
|
final trackSnapshot = ref.watch(localTracksProvider);
|
||||||
final isPlaylistPlaying = playlist.containsTracks(
|
final isPlaylistPlaying = playlist.containsTracks(
|
||||||
trackSnapshot.asData?.value.values.flattened.toList() ?? []);
|
trackSnapshot.asData?.value.values.flattened.toList() ?? []);
|
||||||
|
@ -39,7 +39,7 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
final genresCollection = ref.watch(categoryGenresProvider);
|
final genresCollection = ref.watch(categoryGenresProvider);
|
||||||
|
|
||||||
final limit = useValueNotifier<int>(10);
|
final limit = useValueNotifier<int>(10);
|
||||||
final market = useValueNotifier<Market>(preferences.recommendationMarket);
|
final market = useValueNotifier<Market>(preferences.market);
|
||||||
|
|
||||||
final genres = useState<List<String>>([]);
|
final genres = useState<List<String>>([]);
|
||||||
final artists = useState<List<Artist>>([]);
|
final artists = useState<List<Artist>>([]);
|
||||||
|
@ -11,7 +11,7 @@ import 'package:spotube/components/titlebar/titlebar.dart';
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/spotify/recommendation_seeds.dart';
|
import 'package:spotube/models/spotify/recommendation_seeds.dart';
|
||||||
import 'package:spotube/pages/playlist/playlist.dart';
|
import 'package:spotube/pages/playlist/playlist.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class PlaylistGenerateResultPage extends HookConsumerWidget {
|
class PlaylistGenerateResultPage extends HookConsumerWidget {
|
||||||
@ -28,7 +28,7 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final router = GoRouter.of(context);
|
final router = GoRouter.of(context);
|
||||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
||||||
|
|
||||||
final generatedPlaylist = ref.watch(generatePlaylistProvider(state));
|
final generatedPlaylist = ref.watch(generatePlaylistProvider(state));
|
||||||
|
|
||||||
@ -81,9 +81,12 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
|
|||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
await playlistNotifier.load(
|
await playlistNotifier.load(
|
||||||
generatedPlaylist.asData!.value.where(
|
generatedPlaylist.asData!.value
|
||||||
(e) => selectedTracks.value.contains(e.id!),
|
.where(
|
||||||
),
|
(e) => selectedTracks.value
|
||||||
|
.contains(e.id!),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
autoPlay: true,
|
autoPlay: true,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -17,8 +17,8 @@ import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart';
|
|||||||
import 'package:spotube/hooks/utils/use_palette_color.dart';
|
import 'package:spotube/hooks/utils/use_palette_color.dart';
|
||||||
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
|
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
|
||||||
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
|
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ class LyricsPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
String albumArt = useMemoized(
|
String albumArt = useMemoized(
|
||||||
() => (playlist.activeTrack?.album?.images).asUrlString(
|
() => (playlist.activeTrack?.album?.images).asUrlString(
|
||||||
index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1,
|
index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1,
|
||||||
@ -62,7 +62,7 @@ class LyricsPage extends HookConsumerWidget {
|
|||||||
const Spacer(),
|
const Spacer(),
|
||||||
Consumer(
|
Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final playback = ref.watch(proxyPlaylistProvider);
|
final playback = ref.watch(audioPlayerProvider);
|
||||||
final lyric =
|
final lyric =
|
||||||
ref.watch(syncedLyricsProvider(playback.activeTrack));
|
ref.watch(syncedLyricsProvider(playback.activeTrack));
|
||||||
final providerName = lyric.asData?.value.provider;
|
final providerName = lyric.asData?.value.provider;
|
||||||
@ -84,7 +84,7 @@ class LyricsPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
final auth = ref.watch(authenticationProvider);
|
final auth = ref.watch(authenticationProvider);
|
||||||
|
|
||||||
if (auth == null) {
|
if (auth.asData?.value == null) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: !kIsMacOS && !isModal ? const PageWindowTitleBar() : null,
|
appBar: !kIsMacOS && !isModal ? const PageWindowTitleBar() : null,
|
||||||
body: const AnonymousFallback(),
|
body: const AnonymousFallback(),
|
||||||
|
@ -14,8 +14,8 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/hooks/utils/use_force_update.dart';
|
import 'package:spotube/hooks/utils/use_force_update.dart';
|
||||||
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
|
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
|
||||||
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
|
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ class MiniLyricsPage extends HookConsumerWidget {
|
|||||||
final update = useForceUpdate();
|
final update = useForceUpdate();
|
||||||
final wasMaximized = useRef<bool>(false);
|
final wasMaximized = useRef<bool>(false);
|
||||||
|
|
||||||
final playlistQueue = ref.watch(proxyPlaylistProvider);
|
final playlistQueue = ref.watch(audioPlayerProvider);
|
||||||
|
|
||||||
final areaActive = useState(false);
|
final areaActive = useState(false);
|
||||||
final hoverMode = useState(true);
|
final hoverMode = useState(true);
|
||||||
@ -48,7 +48,7 @@ class MiniLyricsPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
final auth = ref.watch(authenticationProvider);
|
final auth = ref.watch(authenticationProvider);
|
||||||
|
|
||||||
if (auth == null) {
|
if (auth.asData?.value == null) {
|
||||||
return const Scaffold(
|
return const Scaffold(
|
||||||
appBar: PageWindowTitleBar(),
|
appBar: PageWindowTitleBar(),
|
||||||
body: AnonymousFallback(),
|
body: AnonymousFallback(),
|
||||||
@ -230,14 +230,13 @@ class MiniLyricsPage extends HookConsumerWidget {
|
|||||||
builder: (context) {
|
builder: (context) {
|
||||||
return Consumer(builder: (context, ref, _) {
|
return Consumer(builder: (context, ref, _) {
|
||||||
final playlist =
|
final playlist =
|
||||||
ref.watch(proxyPlaylistProvider);
|
ref.watch(audioPlayerProvider);
|
||||||
|
|
||||||
return PlayerQueue
|
return PlayerQueue.fromAudioPlayerNotifier(
|
||||||
.fromProxyPlaylistNotifier(
|
|
||||||
floating: true,
|
floating: true,
|
||||||
playlist: playlist,
|
playlist: playlist,
|
||||||
notifier: ref
|
notifier: ref
|
||||||
.read(proxyPlaylistProvider.notifier),
|
.read(audioPlayerProvider.notifier),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -11,7 +11,7 @@ import 'package:spotube/extensions/artist_simple.dart';
|
|||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class PlainLyrics extends HookConsumerWidget {
|
class PlainLyrics extends HookConsumerWidget {
|
||||||
@ -27,7 +27,7 @@ class PlainLyrics extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
final lyricsQuery = ref.watch(syncedLyricsProvider(playlist.activeTrack));
|
final lyricsQuery = ref.watch(syncedLyricsProvider(playlist.activeTrack));
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
final textTheme = Theme.of(context).textTheme;
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
@ -12,7 +12,7 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart';
|
import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart';
|
||||||
import 'package:spotube/modules/lyrics/use_synced_lyrics.dart';
|
import 'package:spotube/modules/lyrics/use_synced_lyrics.dart';
|
||||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ class SyncedLyrics extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
|
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
final controller = useAutoScrollController();
|
final controller = useAutoScrollController();
|
||||||
@ -54,7 +54,7 @@ class SyncedLyrics extends HookConsumerWidget {
|
|||||||
final textTheme = Theme.of(context).textTheme;
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
|
||||||
ref.listen(
|
ref.listen(
|
||||||
proxyPlaylistProvider.select((s) => s.activeTrack),
|
audioPlayerProvider.select((s) => s.activeTrack),
|
||||||
(previous, next) {
|
(previous, next) {
|
||||||
controller.scrollToIndex(0);
|
controller.scrollToIndex(0);
|
||||||
ref.read(syncedLyricsDelayProvider.notifier).state = 0;
|
ref.read(syncedLyricsDelayProvider.notifier).state = 0;
|
||||||
@ -139,14 +139,12 @@ class SyncedLyrics extends HookConsumerWidget {
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final duration =
|
|
||||||
await audioPlayer.duration ??
|
|
||||||
Duration.zero;
|
|
||||||
final time = Duration(
|
final time = Duration(
|
||||||
seconds:
|
seconds:
|
||||||
lyricSlice.time.inSeconds - delay,
|
lyricSlice.time.inSeconds - delay,
|
||||||
);
|
);
|
||||||
if (time > duration || time.isNegative) {
|
if (time > audioPlayer.duration ||
|
||||||
|
time.isNegative) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
audioPlayer.seek(time);
|
audioPlayer.seek(time);
|
||||||
|
@ -3,7 +3,7 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
class WebViewLogin extends HookConsumerWidget {
|
class WebViewLogin extends HookConsumerWidget {
|
||||||
@ -53,9 +53,7 @@ class WebViewLogin extends HookConsumerWidget {
|
|||||||
final cookieHeader =
|
final cookieHeader =
|
||||||
"sp_dc=${cookies.firstWhere((element) => element.name == "sp_dc").value}";
|
"sp_dc=${cookies.firstWhere((element) => element.name == "sp_dc").value}";
|
||||||
|
|
||||||
authenticationNotifier.setCredentials(
|
await authenticationNotifier.login(cookieHeader);
|
||||||
await AuthenticationCredentials.fromCookie(cookieHeader),
|
|
||||||
);
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
GoRouter.of(context).go("/");
|
GoRouter.of(context).go("/");
|
||||||
|
@ -5,7 +5,6 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/modules/player/player_queue.dart';
|
import 'package:spotube/modules/player/player_queue.dart';
|
||||||
import 'package:spotube/components/dialogs/replace_downloaded_dialog.dart';
|
import 'package:spotube/components/dialogs/replace_downloaded_dialog.dart';
|
||||||
@ -16,10 +15,9 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/hooks/configurators/use_endless_playback.dart';
|
import 'package:spotube/hooks/configurators/use_endless_playback.dart';
|
||||||
import 'package:spotube/pages/home/home.dart';
|
import 'package:spotube/pages/home/home.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/server/routes/connect.dart';
|
import 'package:spotube/provider/server/routes/connect.dart';
|
||||||
import 'package:spotube/services/connectivity_adapter.dart';
|
import 'package:spotube/services/connectivity_adapter.dart';
|
||||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
@ -41,13 +39,6 @@ class RootApp extends HookConsumerWidget {
|
|||||||
useEffect(() {
|
useEffect(() {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
ServiceUtils.checkForUpdates(context, ref);
|
ServiceUtils.checkForUpdates(context, ref);
|
||||||
|
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
|
||||||
|
|
||||||
if (sharedPreferences.getBool(kIsUsingEncryption) == false &&
|
|
||||||
context.mounted) {
|
|
||||||
await PersistedStateNotifier.showNoEncryptionDialog(context);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final subscriptions = [
|
final subscriptions = [
|
||||||
@ -201,11 +192,11 @@ class RootApp extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
child: Consumer(
|
child: Consumer(
|
||||||
builder: (context, ref, _) {
|
builder: (context, ref, _) {
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
final playlistNotifier =
|
final playlistNotifier =
|
||||||
ref.read(proxyPlaylistProvider.notifier);
|
ref.read(audioPlayerProvider.notifier);
|
||||||
|
|
||||||
return PlayerQueue.fromProxyPlaylistNotifier(
|
return PlayerQueue.fromAudioPlayerNotifier(
|
||||||
floating: true,
|
floating: true,
|
||||||
playlist: playlist,
|
playlist: playlist,
|
||||||
notifier: playlistNotifier,
|
notifier: playlistNotifier,
|
||||||
|
@ -20,7 +20,7 @@ import 'package:spotube/pages/search/sections/albums.dart';
|
|||||||
import 'package:spotube/pages/search/sections/artists.dart';
|
import 'package:spotube/pages/search/sections/artists.dart';
|
||||||
import 'package:spotube/pages/search/sections/playlists.dart';
|
import 'package:spotube/pages/search/sections/playlists.dart';
|
||||||
import 'package:spotube/pages/search/sections/tracks.dart';
|
import 'package:spotube/pages/search/sections/tracks.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||||
|
|
||||||
@ -37,8 +37,7 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
final searchTerm = ref.watch(searchTermStateProvider);
|
final searchTerm = ref.watch(searchTermStateProvider);
|
||||||
final controller = useSearchController();
|
final controller = useSearchController();
|
||||||
|
|
||||||
ref.watch(authenticationProvider);
|
final auth = ref.watch(authenticationProvider);
|
||||||
final authenticationNotifier = ref.watch(authenticationProvider.notifier);
|
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
|
||||||
final searchTrack = ref.watch(searchProvider(SearchType.track));
|
final searchTrack = ref.watch(searchProvider(SearchType.track));
|
||||||
@ -91,7 +90,7 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
appBar: kIsDesktop && !kIsMacOS
|
appBar: kIsDesktop && !kIsMacOS
|
||||||
? const PageWindowTitleBar(automaticallyImplyLeading: true)
|
? const PageWindowTitleBar(automaticallyImplyLeading: true)
|
||||||
: null,
|
: null,
|
||||||
body: !authenticationNotifier.isLoggedIn
|
body: auth.asData?.value == null
|
||||||
? const AnonymousFallback()
|
? const AnonymousFallback()
|
||||||
: Column(
|
: Column(
|
||||||
children: [
|
children: [
|
||||||
|
@ -8,7 +8,7 @@ import 'package:spotube/components/track_tile/track_tile.dart';
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
import 'package:spotube/provider/connect/connect.dart';
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class SearchTracksSection extends HookConsumerWidget {
|
class SearchTracksSection extends HookConsumerWidget {
|
||||||
@ -24,8 +24,8 @@ class SearchTracksSection extends HookConsumerWidget {
|
|||||||
ref.watch(searchProvider(SearchType.track).notifier);
|
ref.watch(searchProvider(SearchType.track).notifier);
|
||||||
|
|
||||||
final tracks = searchTrack.asData?.value.items.cast<Track>() ?? [];
|
final tracks = searchTrack.asData?.value.items.cast<Track>() ?? [];
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
|
@ -24,19 +24,21 @@ class BlackListPage extends HookConsumerWidget {
|
|||||||
final filteredBlacklist = useMemoized(
|
final filteredBlacklist = useMemoized(
|
||||||
() {
|
() {
|
||||||
if (searchText.value.isEmpty) {
|
if (searchText.value.isEmpty) {
|
||||||
return blacklist;
|
return blacklist.asData?.value ?? [];
|
||||||
}
|
}
|
||||||
return blacklist
|
return blacklist.asData?.value
|
||||||
.map(
|
.map(
|
||||||
(e) => (
|
(e) => (
|
||||||
weightedRatio("${e.name} ${e.type.name}", searchText.value),
|
weightedRatio(
|
||||||
|
"${e.name} ${e.elementType.name}", searchText.value),
|
||||||
e,
|
e,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.sorted((a, b) => b.$1.compareTo(a.$1))
|
.sorted((a, b) => b.$1.compareTo(a.$1))
|
||||||
.where((e) => e.$1 > 50)
|
.where((e) => e.$1 > 50)
|
||||||
.map((e) => e.$2)
|
.map((e) => e.$2)
|
||||||
.toList();
|
.toList() ??
|
||||||
|
[];
|
||||||
},
|
},
|
||||||
[blacklist, searchText.value],
|
[blacklist, searchText.value],
|
||||||
);
|
);
|
||||||
@ -70,14 +72,14 @@ class BlackListPage extends HookConsumerWidget {
|
|||||||
final item = filteredBlacklist.elementAt(index);
|
final item = filteredBlacklist.elementAt(index);
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: Text("${index + 1}."),
|
leading: Text("${index + 1}."),
|
||||||
title: Text("${item.name} (${item.type.name})"),
|
title: Text("${item.name} (${item.elementType.name})"),
|
||||||
subtitle: Text(item.id),
|
subtitle: Text(item.elementId),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: Icon(SpotubeIcons.trash, color: Colors.red[400]),
|
icon: Icon(SpotubeIcons.trash, color: Colors.red[400]),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref
|
ref
|
||||||
.read(blacklistProvider.notifier)
|
.read(blacklistProvider.notifier)
|
||||||
.remove(filteredBlacklist.elementAt(index));
|
.remove(filteredBlacklist.elementAt(index).elementId);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -9,8 +9,8 @@ import 'package:spotube/extensions/constrains.dart';
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/pages/profile/profile.dart';
|
import 'package:spotube/pages/profile/profile.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/scrobbler_provider.dart';
|
import 'package:spotube/provider/scrobbler/scrobbler.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ class SettingsAccountSection extends HookConsumerWidget {
|
|||||||
return SectionCardWithHeading(
|
return SectionCardWithHeading(
|
||||||
heading: context.l10n.account,
|
heading: context.l10n.account,
|
||||||
children: [
|
children: [
|
||||||
if (auth != null)
|
if (auth.asData?.value != null)
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(SpotubeIcons.user),
|
leading: const Icon(SpotubeIcons.user),
|
||||||
title: const Text("User Profile"),
|
title: const Text("User Profile"),
|
||||||
@ -53,7 +53,7 @@ class SettingsAccountSection extends HookConsumerWidget {
|
|||||||
ServiceUtils.pushNamed(context, ProfilePage.name);
|
ServiceUtils.pushNamed(context, ProfilePage.name);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (auth == null)
|
if (auth.asData?.value == null)
|
||||||
LayoutBuilder(builder: (context, constrains) {
|
LayoutBuilder(builder: (context, constrains) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
@ -119,7 +119,7 @@ class SettingsAccountSection extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
if (scrobbler == null)
|
if (scrobbler.asData?.value == null)
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(SpotubeIcons.lastFm),
|
leading: const Icon(SpotubeIcons.lastFm),
|
||||||
title: Text(context.l10n.login_with_lastfm),
|
title: Text(context.l10n.login_with_lastfm),
|
||||||
|
@ -3,12 +3,12 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart';
|
import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart';
|
||||||
import 'package:spotube/modules/settings/section_card_with_heading.dart';
|
import 'package:spotube/modules/settings/section_card_with_heading.dart';
|
||||||
import 'package:spotube/components/adaptive/adaptive_select_tile.dart';
|
import 'package:spotube/components/adaptive/adaptive_select_tile.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
|
||||||
|
|
||||||
class SettingsAppearanceSection extends HookConsumerWidget {
|
class SettingsAppearanceSection extends HookConsumerWidget {
|
||||||
final bool isGettingStarted;
|
final bool isGettingStarted;
|
||||||
|
@ -2,11 +2,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/modules/settings/section_card_with_heading.dart';
|
import 'package:spotube/modules/settings/section_card_with_heading.dart';
|
||||||
import 'package:spotube/components/adaptive/adaptive_select_tile.dart';
|
import 'package:spotube/components/adaptive/adaptive_select_tile.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
class SettingsDesktopSection extends HookConsumerWidget {
|
class SettingsDesktopSection extends HookConsumerWidget {
|
||||||
|
@ -57,7 +57,7 @@ class SettingsLanguageRegionSection extends HookConsumerWidget {
|
|||||||
secondary: const Icon(SpotubeIcons.shoppingBag),
|
secondary: const Icon(SpotubeIcons.shoppingBag),
|
||||||
title: Text(context.l10n.market_place_region),
|
title: Text(context.l10n.market_place_region),
|
||||||
subtitle: Text(context.l10n.recommendation_country),
|
subtitle: Text(context.l10n.recommendation_country),
|
||||||
value: preferences.recommendationMarket,
|
value: preferences.market,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
preferencesNotifier.setRecommendationMarket(value);
|
preferencesNotifier.setRecommendationMarket(value);
|
||||||
|
@ -6,12 +6,13 @@ import 'package:google_fonts/google_fonts.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:piped_client/piped_client.dart';
|
import 'package:piped_client/piped_client.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/modules/settings/section_card_with_heading.dart';
|
import 'package:spotube/modules/settings/section_card_with_heading.dart';
|
||||||
import 'package:spotube/components/adaptive/adaptive_select_tile.dart';
|
import 'package:spotube/components/adaptive/adaptive_select_tile.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/piped_instances_provider.dart';
|
import 'package:spotube/provider/piped_instances_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/enums.dart';
|
import 'package:spotube/services/sourced_track/enums.dart';
|
||||||
|
|
||||||
class SettingsPlaybackSection extends HookConsumerWidget {
|
class SettingsPlaybackSection extends HookConsumerWidget {
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotube/collections/formatters.dart';
|
import 'package:spotube/collections/formatters.dart';
|
||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
import 'package:spotube/modules/stats/common/album_item.dart';
|
import 'package:spotube/modules/stats/common/album_item.dart';
|
||||||
import 'package:spotube/provider/history/state.dart';
|
|
||||||
import 'package:spotube/provider/history/top.dart';
|
import 'package:spotube/provider/history/top.dart';
|
||||||
|
import 'package:spotube/provider/history/top/albums.dart';
|
||||||
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
class StatsAlbumsPage extends HookConsumerWidget {
|
class StatsAlbumsPage extends HookConsumerWidget {
|
||||||
static const name = "stats_albums";
|
static const name = "stats_albums";
|
||||||
@ -12,10 +16,12 @@ class StatsAlbumsPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final albums = ref.watch(
|
final topAlbums =
|
||||||
playbackHistoryTopProvider(HistoryDuration.allTime)
|
ref.watch(historyTopAlbumsProvider(HistoryDuration.allTime));
|
||||||
.select((s) => s.albums),
|
final topAlbumsNotifier =
|
||||||
);
|
ref.watch(historyTopAlbumsProvider(HistoryDuration.allTime).notifier);
|
||||||
|
|
||||||
|
final albumsData = topAlbums.asData?.value.items ?? [];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const PageWindowTitleBar(
|
appBar: const PageWindowTitleBar(
|
||||||
@ -23,16 +29,27 @@ class StatsAlbumsPage extends HookConsumerWidget {
|
|||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
title: Text("Albums"),
|
title: Text("Albums"),
|
||||||
),
|
),
|
||||||
body: ListView.builder(
|
body: Skeletonizer(
|
||||||
itemCount: albums.length,
|
enabled: topAlbums.isLoading && !topAlbums.isLoadingNextPage,
|
||||||
|
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) {
|
itemBuilder: (context, index) {
|
||||||
final album = albums[index];
|
final album = albumsData[index];
|
||||||
return StatsAlbumItem(
|
return StatsAlbumItem(
|
||||||
album: album.album,
|
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/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotube/collections/formatters.dart';
|
import 'package:spotube/collections/formatters.dart';
|
||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
import 'package:spotube/modules/stats/common/artist_item.dart';
|
import 'package:spotube/modules/stats/common/artist_item.dart';
|
||||||
import 'package:spotube/provider/history/state.dart';
|
|
||||||
import 'package:spotube/provider/history/top.dart';
|
import 'package:spotube/provider/history/top.dart';
|
||||||
|
import 'package:spotube/provider/history/top/tracks.dart';
|
||||||
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
class StatsArtistsPage extends HookConsumerWidget {
|
class StatsArtistsPage extends HookConsumerWidget {
|
||||||
static const name = "stats_artists";
|
static const name = "stats_artists";
|
||||||
@ -12,10 +17,14 @@ class StatsArtistsPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final artists = ref.watch(
|
final topTracks = ref.watch(
|
||||||
playbackHistoryTopProvider(HistoryDuration.allTime)
|
historyTopTracksProvider(HistoryDuration.allTime),
|
||||||
.select((s) => s.artists),
|
|
||||||
);
|
);
|
||||||
|
final topTracksNotifier =
|
||||||
|
ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier);
|
||||||
|
|
||||||
|
final artistsData = useMemoized(
|
||||||
|
() => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const PageWindowTitleBar(
|
appBar: const PageWindowTitleBar(
|
||||||
@ -23,16 +32,26 @@ class StatsArtistsPage extends HookConsumerWidget {
|
|||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
title: Text("Artists"),
|
title: Text("Artists"),
|
||||||
),
|
),
|
||||||
body: ListView.builder(
|
body: Skeletonizer(
|
||||||
itemCount: artists.length,
|
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||||
|
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) {
|
itemBuilder: (context, index) {
|
||||||
final artist = artists[index];
|
final artist = artistsData[index];
|
||||||
return StatsArtistItem(
|
return StatsArtistItem(
|
||||||
artist: artist.artist,
|
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/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:sliver_tools/sliver_tools.dart';
|
import 'package:sliver_tools/sliver_tools.dart';
|
||||||
import 'package:spotube/collections/formatters.dart';
|
import 'package:spotube/collections/formatters.dart';
|
||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
import 'package:spotube/modules/stats/common/artist_item.dart';
|
import 'package:spotube/modules/stats/common/artist_item.dart';
|
||||||
import 'package:spotube/provider/history/state.dart';
|
|
||||||
import 'package:spotube/provider/history/top.dart';
|
import 'package:spotube/provider/history/top.dart';
|
||||||
|
import 'package:spotube/provider/history/top/tracks.dart';
|
||||||
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
class StatsStreamFeesPage extends HookConsumerWidget {
|
class StatsStreamFeesPage extends HookConsumerWidget {
|
||||||
static const name = "stats_stream_fees";
|
static const name = "stats_stream_fees";
|
||||||
@ -15,10 +20,23 @@ class StatsStreamFeesPage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final ThemeData(:textTheme, :hintColor) = Theme.of(context);
|
final ThemeData(:textTheme, :hintColor) = Theme.of(context);
|
||||||
|
final duration = useState<HistoryDuration>(HistoryDuration.days30);
|
||||||
|
|
||||||
final artists = ref.watch(
|
final topTracks = ref.watch(
|
||||||
playbackHistoryTopProvider(HistoryDuration.days30)
|
historyTopTracksProvider(duration.value),
|
||||||
.select((value) => value.artists),
|
);
|
||||||
|
final topTracksNotifier =
|
||||||
|
ref.watch(historyTopTracksProvider(duration.value).notifier);
|
||||||
|
|
||||||
|
final artistsData = useMemoized(
|
||||||
|
() => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]);
|
||||||
|
|
||||||
|
final total = useMemoized(
|
||||||
|
() => artistsData.fold<double>(
|
||||||
|
0,
|
||||||
|
(previousValue, element) => previousValue + element.count * 0.005,
|
||||||
|
),
|
||||||
|
[artistsData],
|
||||||
);
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -48,16 +66,74 @@ class StatsStreamFeesPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverList.builder(
|
SliverToBoxAdapter(
|
||||||
itemCount: artists.length,
|
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) {
|
itemBuilder: (context, index) {
|
||||||
final artist = artists[index];
|
final artist = artistsData[index];
|
||||||
return StatsArtistItem(
|
return StatsArtistItem(
|
||||||
artist: artist.artist,
|
artist: artist.artist,
|
||||||
info: Text(usdFormatter.format(artist.count * 0.005)),
|
info: Text(usdFormatter.format(artist.count * 0.005)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotube/collections/formatters.dart';
|
import 'package:spotube/collections/formatters.dart';
|
||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
import 'package:spotube/modules/stats/common/track_item.dart';
|
import 'package:spotube/modules/stats/common/track_item.dart';
|
||||||
import 'package:spotube/provider/history/state.dart';
|
|
||||||
import 'package:spotube/provider/history/top.dart';
|
import 'package:spotube/provider/history/top.dart';
|
||||||
|
import 'package:spotube/provider/history/top/tracks.dart';
|
||||||
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
class StatsMinutesPage extends HookConsumerWidget {
|
class StatsMinutesPage extends HookConsumerWidget {
|
||||||
static const name = "stats_minutes";
|
static const name = "stats_minutes";
|
||||||
@ -15,9 +19,12 @@ class StatsMinutesPage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final topTracks = ref.watch(
|
final topTracks = ref.watch(
|
||||||
playbackHistoryTopProvider(HistoryDuration.allTime)
|
historyTopTracksProvider(HistoryDuration.allTime),
|
||||||
.select((s) => s.tracks),
|
|
||||||
);
|
);
|
||||||
|
final topTracksNotifier =
|
||||||
|
ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier);
|
||||||
|
|
||||||
|
final tracksData = topTracks.asData?.value.items ?? [];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const PageWindowTitleBar(
|
appBar: const PageWindowTitleBar(
|
||||||
@ -25,20 +32,28 @@ class StatsMinutesPage extends HookConsumerWidget {
|
|||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
automaticallyImplyLeading: true,
|
automaticallyImplyLeading: true,
|
||||||
),
|
),
|
||||||
body: ListView.separated(
|
body: Skeletonizer(
|
||||||
|
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||||
|
child: InfiniteList(
|
||||||
separatorBuilder: (context, index) => const Gap(8),
|
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) {
|
itemBuilder: (context, index) {
|
||||||
final (:track, :count) = topTracks[index];
|
final track = tracksData[index];
|
||||||
|
|
||||||
return StatsTrackItem(
|
return StatsTrackItem(
|
||||||
track: track,
|
track: track.track,
|
||||||
info: Text(
|
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:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotube/collections/formatters.dart';
|
import 'package:spotube/collections/formatters.dart';
|
||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
import 'package:spotube/modules/stats/common/playlist_item.dart';
|
import 'package:spotube/modules/stats/common/playlist_item.dart';
|
||||||
import 'package:spotube/provider/history/state.dart';
|
|
||||||
import 'package:spotube/provider/history/top.dart';
|
import 'package:spotube/provider/history/top.dart';
|
||||||
|
import 'package:spotube/provider/history/top/playlists.dart';
|
||||||
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
class StatsPlaylistsPage extends HookConsumerWidget {
|
class StatsPlaylistsPage extends HookConsumerWidget {
|
||||||
static const name = "stats_playlists";
|
static const name = "stats_playlists";
|
||||||
@ -12,10 +16,13 @@ class StatsPlaylistsPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlists = ref.watch(
|
final topPlaylists =
|
||||||
playbackHistoryTopProvider(HistoryDuration.allTime)
|
ref.watch(historyTopPlaylistsProvider(HistoryDuration.allTime));
|
||||||
.select((s) => s.playlists),
|
|
||||||
);
|
final topPlaylistsNotifier = ref
|
||||||
|
.watch(historyTopPlaylistsProvider(HistoryDuration.allTime).notifier);
|
||||||
|
|
||||||
|
final playlistsData = topPlaylists.asData?.value.items ?? [];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const PageWindowTitleBar(
|
appBar: const PageWindowTitleBar(
|
||||||
@ -23,17 +30,26 @@ class StatsPlaylistsPage extends HookConsumerWidget {
|
|||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
title: Text("Playlists"),
|
title: Text("Playlists"),
|
||||||
),
|
),
|
||||||
body: ListView.builder(
|
body: Skeletonizer(
|
||||||
itemCount: playlists.length,
|
enabled: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage,
|
||||||
|
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) {
|
itemBuilder: (context, index) {
|
||||||
final playlist = playlists[index];
|
final playlist = playlistsData[index];
|
||||||
return StatsPlaylistItem(
|
return StatsPlaylistItem(
|
||||||
playlist: playlist.playlist.playlist,
|
playlist: playlist.playlist,
|
||||||
info:
|
info: Text(
|
||||||
Text("${compactNumberFormatter.format(playlist.count)} plays"),
|
"${compactNumberFormatter.format(playlist.count)} plays"),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotube/collections/formatters.dart';
|
import 'package:spotube/collections/formatters.dart';
|
||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
import 'package:spotube/modules/stats/common/track_item.dart';
|
import 'package:spotube/modules/stats/common/track_item.dart';
|
||||||
import 'package:spotube/provider/history/state.dart';
|
|
||||||
import 'package:spotube/provider/history/top.dart';
|
import 'package:spotube/provider/history/top.dart';
|
||||||
|
import 'package:spotube/provider/history/top/tracks.dart';
|
||||||
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
class StatsStreamsPage extends HookConsumerWidget {
|
class StatsStreamsPage extends HookConsumerWidget {
|
||||||
static const name = "stats_streams";
|
static const name = "stats_streams";
|
||||||
@ -15,9 +19,12 @@ class StatsStreamsPage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final topTracks = ref.watch(
|
final topTracks = ref.watch(
|
||||||
playbackHistoryTopProvider(HistoryDuration.allTime)
|
historyTopTracksProvider(HistoryDuration.allTime),
|
||||||
.select((s) => s.tracks),
|
|
||||||
);
|
);
|
||||||
|
final topTracksNotifier =
|
||||||
|
ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier);
|
||||||
|
|
||||||
|
final tracksData = topTracks.asData?.value.items ?? [];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const PageWindowTitleBar(
|
appBar: const PageWindowTitleBar(
|
||||||
@ -25,20 +32,28 @@ class StatsStreamsPage extends HookConsumerWidget {
|
|||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
automaticallyImplyLeading: true,
|
automaticallyImplyLeading: true,
|
||||||
),
|
),
|
||||||
body: ListView.separated(
|
body: Skeletonizer(
|
||||||
|
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||||
|
child: InfiniteList(
|
||||||
separatorBuilder: (context, index) => const Gap(8),
|
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) {
|
itemBuilder: (context, index) {
|
||||||
final (:track, :count) = topTracks[index];
|
final track = tracksData[index];
|
||||||
|
|
||||||
return StatsTrackItem(
|
return StatsTrackItem(
|
||||||
track: track,
|
track: track.track,
|
||||||
info: Text(
|
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/components/track_tile/track_options.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
|
||||||
@ -34,8 +34,8 @@ class TrackPage extends HookConsumerWidget {
|
|||||||
final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
|
final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
||||||
|
|
||||||
final isActive = playlist.activeTrack?.id == trackId;
|
final isActive = playlist.activeTrack?.id == trackId;
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user