refactor: replace all instances of proxy playlist

This commit is contained in:
Kingkor Roy Tirtho 2024-06-24 20:52:40 +06:00
parent f79fedefd4
commit a83dd64476
51 changed files with 515 additions and 624 deletions

View File

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

View File

@ -24,7 +24,7 @@ import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart';
@ -96,8 +96,8 @@ class TrackOptions extends HookConsumerWidget {
WidgetRef ref,
Track track,
) async {
final playback = ref.read(proxyPlaylistProvider.notifier);
final playlist = ref.read(proxyPlaylistProvider);
final playback = ref.read(audioPlayerProvider.notifier);
final playlist = ref.read(audioPlayerProvider);
final spotify = ref.read(spotifyProvider);
final query = "${track.name} Radio";
final pages =
@ -160,8 +160,8 @@ class TrackOptions extends HookConsumerWidget {
final router = GoRouter.of(context);
final ThemeData(:colorScheme) = Theme.of(context);
final playlist = ref.watch(proxyPlaylistProvider);
final playback = ref.watch(proxyPlaylistProvider.notifier);
final playlist = ref.watch(audioPlayerProvider);
final playback = ref.watch(audioPlayerProvider.notifier);
final auth = ref.watch(authenticationProvider);
ref.watch(downloadManagerProvider);
final downloadManager = ref.watch(downloadManagerProvider.notifier);
@ -364,7 +364,7 @@ class TrackOptions extends HookConsumerWidget {
: context.l10n.save_as_favorite,
),
),
if (auth != null && !isLocalTrack) ...[
if (auth.asData?.value != null && !isLocalTrack) ...[
PopSheetEntry(
value: TrackOptionValue.startRadio,
leading: const Icon(SpotubeIcons.radio),
@ -376,7 +376,7 @@ class TrackOptions extends HookConsumerWidget {
title: Text(context.l10n.add_to_playlist),
),
],
if (userPlaylist && auth != null && !isLocalTrack)
if (userPlaylist && auth.asData?.value != null && !isLocalTrack)
PopSheetEntry(
value: TrackOptionValue.removeFromPlaylist,
leading: const Icon(SpotubeIcons.removeFilled),

View File

@ -17,8 +17,9 @@ import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/duration.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
class TrackTile extends HookConsumerWidget {
/// [index] will not be shown if null
@ -30,7 +31,7 @@ class TrackTile extends HookConsumerWidget {
final VoidCallback? onLongPress;
final bool userPlaylist;
final String? playlistId;
final ProxyPlaylist playlist;
final AudioPlayerState playlist;
final List<Widget>? leadingActions;
@ -160,7 +161,11 @@ class TrackTile extends HookConsumerWidget {
child: Skeleton.ignore(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: (isPlaying && playlist.isFetching) ||
child: (isPlaying &&
ref
.watch(audioPlayerProvider
.notifier)
.isFetching()) ||
isLoading.value
? const SizedBox(
width: 26,

View File

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

View File

@ -11,7 +11,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/history/history.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
class TrackViewBodyOptions extends HookConsumerWidget {
@ -24,7 +24,7 @@ class TrackViewBodyOptions extends HookConsumerWidget {
ref.watch(downloadManagerProvider);
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 audioSource =
ref.watch(userPreferencesProvider.select((s) => s.audioSource));

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@ import 'package:spotube/hooks/configurators/use_close_behavior.dart';
import 'package:spotube/hooks/configurators/use_deep_linking.dart';
import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.dart';
import 'package:spotube/hooks/configurators/use_get_storage_perms.dart';
import 'package:spotube/provider/audio_player/audio_player_streams.dart';
import 'package:spotube/provider/server/bonsoir.dart';
import 'package:spotube/provider/server/server.dart';
import 'package:spotube/provider/tray_manager/tray_manager.dart';
@ -113,9 +114,10 @@ class Spotube extends HookConsumerWidget {
ref.watch(paletteProvider.select((s) => s?.dominantColor?.color));
final router = ref.watch(routerProvider);
ref.listen(serverProvider, (_, __) {});
ref.listen(audioPlayerStreamListenersProvider, (_, __) {});
ref.listen(bonsoirProvider, (_, __) {});
ref.listen(connectClientsProvider, (_, __) {});
ref.listen(serverProvider, (_, __) {});
ref.listen(trayManagerProvider, (_, __) {});
useDisableBatteryOptimizations();

View File

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

View File

@ -325,12 +325,12 @@ class WebSocketErrorEvent extends WebSocketEvent<String> {
WebSocketErrorEvent(String data) : super(WsEvent.error, data);
}
class WebSocketQueueEvent extends WebSocketEvent<ProxyPlaylist> {
WebSocketQueueEvent(ProxyPlaylist data) : super(WsEvent.queue, data);
class WebSocketQueueEvent extends WebSocketEvent<AudioPlayerState> {
WebSocketQueueEvent(AudioPlayerState data) : super(WsEvent.queue, data);
factory WebSocketQueueEvent.fromJson(Map<String, dynamic> json) =>
WebSocketQueueEvent(
ProxyPlaylist.fromJsonRaw(json),
AudioPlayerState.fromJson(json),
);
}

View File

@ -2599,11 +2599,6 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable
requiredDuringInsert: true,
defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("playing" IN (0, 1))'));
static const VerificationMeta _volumeMeta = const VerificationMeta('volume');
@override
late final GeneratedColumn<double> volume = GeneratedColumn<double>(
'volume', aliasedName, false,
type: DriftSqlType.double, requiredDuringInsert: true);
static const VerificationMeta _loopModeMeta =
const VerificationMeta('loopMode');
@override
@ -2621,9 +2616,17 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable
requiredDuringInsert: true,
defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("shuffled" IN (0, 1))'));
static const VerificationMeta _collectionsMeta =
const VerificationMeta('collections');
@override
late final GeneratedColumnWithTypeConverter<List<String>, String>
collections = GeneratedColumn<String>('collections', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true)
.withConverter<List<String>>(
$AudioPlayerStateTableTable.$convertercollections);
@override
List<GeneratedColumn> get $columns =>
[id, playing, volume, loopMode, shuffled];
[id, playing, loopMode, shuffled, collections];
@override
String get aliasedName => _alias ?? actualTableName;
@override
@ -2644,12 +2647,6 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable
} else if (isInserting) {
context.missing(_playingMeta);
}
if (data.containsKey('volume')) {
context.handle(_volumeMeta,
volume.isAcceptableOrUnknown(data['volume']!, _volumeMeta));
} else if (isInserting) {
context.missing(_volumeMeta);
}
context.handle(_loopModeMeta, const VerificationResult.success());
if (data.containsKey('shuffled')) {
context.handle(_shuffledMeta,
@ -2657,6 +2654,7 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable
} else if (isInserting) {
context.missing(_shuffledMeta);
}
context.handle(_collectionsMeta, const VerificationResult.success());
return context;
}
@ -2671,13 +2669,14 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
playing: attachedDatabase.typeMapping
.read(DriftSqlType.bool, data['${effectivePrefix}playing'])!,
volume: attachedDatabase.typeMapping
.read(DriftSqlType.double, data['${effectivePrefix}volume'])!,
loopMode: $AudioPlayerStateTableTable.$converterloopMode.fromSql(
attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}loop_mode'])!),
shuffled: attachedDatabase.typeMapping
.read(DriftSqlType.bool, data['${effectivePrefix}shuffled'])!,
collections: $AudioPlayerStateTableTable.$convertercollections.fromSql(
attachedDatabase.typeMapping.read(
DriftSqlType.string, data['${effectivePrefix}collections'])!),
);
}
@ -2688,32 +2687,37 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable
static JsonTypeConverter2<PlaylistMode, String, String> $converterloopMode =
const EnumNameConverter<PlaylistMode>(PlaylistMode.values);
static TypeConverter<List<String>, String> $convertercollections =
const StringListConverter();
}
class AudioPlayerStateTableData extends DataClass
implements Insertable<AudioPlayerStateTableData> {
final int id;
final bool playing;
final double volume;
final PlaylistMode loopMode;
final bool shuffled;
final List<String> collections;
const AudioPlayerStateTableData(
{required this.id,
required this.playing,
required this.volume,
required this.loopMode,
required this.shuffled});
required this.shuffled,
required this.collections});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<int>(id);
map['playing'] = Variable<bool>(playing);
map['volume'] = Variable<double>(volume);
{
map['loop_mode'] = Variable<String>(
$AudioPlayerStateTableTable.$converterloopMode.toSql(loopMode));
}
map['shuffled'] = Variable<bool>(shuffled);
{
map['collections'] = Variable<String>(
$AudioPlayerStateTableTable.$convertercollections.toSql(collections));
}
return map;
}
@ -2721,9 +2725,9 @@ class AudioPlayerStateTableData extends DataClass
return AudioPlayerStateTableCompanion(
id: Value(id),
playing: Value(playing),
volume: Value(volume),
loopMode: Value(loopMode),
shuffled: Value(shuffled),
collections: Value(collections),
);
}
@ -2733,10 +2737,10 @@ class AudioPlayerStateTableData extends DataClass
return AudioPlayerStateTableData(
id: serializer.fromJson<int>(json['id']),
playing: serializer.fromJson<bool>(json['playing']),
volume: serializer.fromJson<double>(json['volume']),
loopMode: $AudioPlayerStateTableTable.$converterloopMode
.fromJson(serializer.fromJson<String>(json['loopMode'])),
shuffled: serializer.fromJson<bool>(json['shuffled']),
collections: serializer.fromJson<List<String>>(json['collections']),
);
}
@override
@ -2745,103 +2749,103 @@ class AudioPlayerStateTableData extends DataClass
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'playing': serializer.toJson<bool>(playing),
'volume': serializer.toJson<double>(volume),
'loopMode': serializer.toJson<String>(
$AudioPlayerStateTableTable.$converterloopMode.toJson(loopMode)),
'shuffled': serializer.toJson<bool>(shuffled),
'collections': serializer.toJson<List<String>>(collections),
};
}
AudioPlayerStateTableData copyWith(
{int? id,
bool? playing,
double? volume,
PlaylistMode? loopMode,
bool? shuffled}) =>
bool? shuffled,
List<String>? collections}) =>
AudioPlayerStateTableData(
id: id ?? this.id,
playing: playing ?? this.playing,
volume: volume ?? this.volume,
loopMode: loopMode ?? this.loopMode,
shuffled: shuffled ?? this.shuffled,
collections: collections ?? this.collections,
);
@override
String toString() {
return (StringBuffer('AudioPlayerStateTableData(')
..write('id: $id, ')
..write('playing: $playing, ')
..write('volume: $volume, ')
..write('loopMode: $loopMode, ')
..write('shuffled: $shuffled')
..write('shuffled: $shuffled, ')
..write('collections: $collections')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, playing, volume, loopMode, shuffled);
int get hashCode => Object.hash(id, playing, loopMode, shuffled, collections);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is AudioPlayerStateTableData &&
other.id == this.id &&
other.playing == this.playing &&
other.volume == this.volume &&
other.loopMode == this.loopMode &&
other.shuffled == this.shuffled);
other.shuffled == this.shuffled &&
other.collections == this.collections);
}
class AudioPlayerStateTableCompanion
extends UpdateCompanion<AudioPlayerStateTableData> {
final Value<int> id;
final Value<bool> playing;
final Value<double> volume;
final Value<PlaylistMode> loopMode;
final Value<bool> shuffled;
final Value<List<String>> collections;
const AudioPlayerStateTableCompanion({
this.id = const Value.absent(),
this.playing = const Value.absent(),
this.volume = const Value.absent(),
this.loopMode = const Value.absent(),
this.shuffled = const Value.absent(),
this.collections = const Value.absent(),
});
AudioPlayerStateTableCompanion.insert({
this.id = const Value.absent(),
required bool playing,
required double volume,
required PlaylistMode loopMode,
required bool shuffled,
required List<String> collections,
}) : playing = Value(playing),
volume = Value(volume),
loopMode = Value(loopMode),
shuffled = Value(shuffled);
shuffled = Value(shuffled),
collections = Value(collections);
static Insertable<AudioPlayerStateTableData> custom({
Expression<int>? id,
Expression<bool>? playing,
Expression<double>? volume,
Expression<String>? loopMode,
Expression<bool>? shuffled,
Expression<String>? collections,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (playing != null) 'playing': playing,
if (volume != null) 'volume': volume,
if (loopMode != null) 'loop_mode': loopMode,
if (shuffled != null) 'shuffled': shuffled,
if (collections != null) 'collections': collections,
});
}
AudioPlayerStateTableCompanion copyWith(
{Value<int>? id,
Value<bool>? playing,
Value<double>? volume,
Value<PlaylistMode>? loopMode,
Value<bool>? shuffled}) {
Value<bool>? shuffled,
Value<List<String>>? collections}) {
return AudioPlayerStateTableCompanion(
id: id ?? this.id,
playing: playing ?? this.playing,
volume: volume ?? this.volume,
loopMode: loopMode ?? this.loopMode,
shuffled: shuffled ?? this.shuffled,
collections: collections ?? this.collections,
);
}
@ -2854,9 +2858,6 @@ class AudioPlayerStateTableCompanion
if (playing.present) {
map['playing'] = Variable<bool>(playing.value);
}
if (volume.present) {
map['volume'] = Variable<double>(volume.value);
}
if (loopMode.present) {
map['loop_mode'] = Variable<String>(
$AudioPlayerStateTableTable.$converterloopMode.toSql(loopMode.value));
@ -2864,6 +2865,11 @@ class AudioPlayerStateTableCompanion
if (shuffled.present) {
map['shuffled'] = Variable<bool>(shuffled.value);
}
if (collections.present) {
map['collections'] = Variable<String>($AudioPlayerStateTableTable
.$convertercollections
.toSql(collections.value));
}
return map;
}
@ -2872,9 +2878,9 @@ class AudioPlayerStateTableCompanion
return (StringBuffer('AudioPlayerStateTableCompanion(')
..write('id: $id, ')
..write('playing: $playing, ')
..write('volume: $volume, ')
..write('loopMode: $loopMode, ')
..write('shuffled: $shuffled')
..write('shuffled: $shuffled, ')
..write('collections: $collections')
..write(')'))
.toString();
}
@ -4591,17 +4597,17 @@ typedef $$AudioPlayerStateTableTableInsertCompanionBuilder
= AudioPlayerStateTableCompanion Function({
Value<int> id,
required bool playing,
required double volume,
required PlaylistMode loopMode,
required bool shuffled,
required List<String> collections,
});
typedef $$AudioPlayerStateTableTableUpdateCompanionBuilder
= AudioPlayerStateTableCompanion Function({
Value<int> id,
Value<bool> playing,
Value<double> volume,
Value<PlaylistMode> loopMode,
Value<bool> shuffled,
Value<List<String>> collections,
});
class $$AudioPlayerStateTableTableTableManager extends RootTableManager<
@ -4627,30 +4633,30 @@ class $$AudioPlayerStateTableTableTableManager extends RootTableManager<
getUpdateCompanionBuilder: ({
Value<int> id = const Value.absent(),
Value<bool> playing = const Value.absent(),
Value<double> volume = const Value.absent(),
Value<PlaylistMode> loopMode = const Value.absent(),
Value<bool> shuffled = const Value.absent(),
Value<List<String>> collections = const Value.absent(),
}) =>
AudioPlayerStateTableCompanion(
id: id,
playing: playing,
volume: volume,
loopMode: loopMode,
shuffled: shuffled,
collections: collections,
),
getInsertCompanionBuilder: ({
Value<int> id = const Value.absent(),
required bool playing,
required double volume,
required PlaylistMode loopMode,
required bool shuffled,
required List<String> collections,
}) =>
AudioPlayerStateTableCompanion.insert(
id: id,
playing: playing,
volume: volume,
loopMode: loopMode,
shuffled: shuffled,
collections: collections,
),
));
}
@ -4681,11 +4687,6 @@ class $$AudioPlayerStateTableTableFilterComposer
builder: (column, joinBuilders) =>
ColumnFilters(column, joinBuilders: joinBuilders));
ColumnFilters<double> get volume => $state.composableBuilder(
column: $state.table.volume,
builder: (column, joinBuilders) =>
ColumnFilters(column, joinBuilders: joinBuilders));
ColumnWithTypeConverterFilters<PlaylistMode, PlaylistMode, String>
get loopMode => $state.composableBuilder(
column: $state.table.loopMode,
@ -4698,6 +4699,13 @@ class $$AudioPlayerStateTableTableFilterComposer
builder: (column, joinBuilders) =>
ColumnFilters(column, joinBuilders: joinBuilders));
ColumnWithTypeConverterFilters<List<String>, List<String>, String>
get collections => $state.composableBuilder(
column: $state.table.collections,
builder: (column, joinBuilders) => ColumnWithTypeConverterFilters(
column,
joinBuilders: joinBuilders));
ComposableFilter playlistTableRefs(
ComposableFilter Function($$PlaylistTableTableFilterComposer f) f) {
final $$PlaylistTableTableFilterComposer composer = $state.composerBuilder(
@ -4725,11 +4733,6 @@ class $$AudioPlayerStateTableTableOrderingComposer
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<double> get volume => $state.composableBuilder(
column: $state.table.volume,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<String> get loopMode => $state.composableBuilder(
column: $state.table.loopMode,
builder: (column, joinBuilders) =>
@ -4739,6 +4742,11 @@ class $$AudioPlayerStateTableTableOrderingComposer
column: $state.table.shuffled,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<String> get collections => $state.composableBuilder(
column: $state.table.collections,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
}
typedef $$PlaylistTableTableInsertCompanionBuilder = PlaylistTableCompanion

View File

@ -3,9 +3,9 @@ part of '../database.dart';
class AudioPlayerStateTable extends Table {
IntColumn get id => integer().autoIncrement()();
BoolColumn get playing => boolean()();
RealColumn get volume => real()();
TextColumn get loopMode => textEnum<PlaylistMode>()();
BoolColumn get shuffled => boolean()();
TextColumn get collections => text().map(const StringListConverter())();
}
class PlaylistTable extends Table {

View File

@ -12,7 +12,7 @@ import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/pages/album/album.dart';
import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/history/history.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/utils/service_utils.dart';
@ -30,10 +30,10 @@ class AlbumCard extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final playlist = ref.watch(proxyPlaylistProvider);
final playlist = ref.watch(audioPlayerProvider);
final playing =
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final historyNotifier = ref.read(playbackHistoryProvider.notifier);
bool isPlaylistPlaying = useMemoized(
@ -59,7 +59,7 @@ class AlbumCard extends HookConsumerWidget {
),
margin: const EdgeInsets.symmetric(horizontal: 10),
isPlaying: isPlaylistPlaying,
isLoading: (isPlaylistPlaying && playlist.isFetching == true) ||
isLoading: (isPlaylistPlaying && playlistNotifier.isFetching()) ||
updating.value,
title: album.name!,
description:

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart';
import 'package:spotube/modules/player/use_progress.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
class PlayerControls extends HookConsumerWidget {
@ -43,8 +43,8 @@ class PlayerControls extends HookConsumerWidget {
SeekIntent: SeekAction(),
},
[]);
final playlist = ref.watch(proxyPlaylistProvider);
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
ref.watch(audioPlayerProvider);
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final playing =
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
@ -132,7 +132,7 @@ class PlayerControls extends HookConsumerWidget {
// than total duration. Keeping it resolved
value: progress.value.toDouble(),
secondaryTrackValue: bufferProgress,
onChanged: playlist.isFetching == true
onChanged: playlistNotifier.isFetching()
? null
: (v) {
progress.value = v;
@ -183,7 +183,7 @@ class PlayerControls extends HookConsumerWidget {
: context.l10n.shuffle_playlist,
icon: const Icon(SpotubeIcons.shuffle),
style: shuffled ? activeButtonStyle : buttonStyle,
onPressed: playlist.isFetching == true
onPressed: playlistNotifier.isFetching()
? null
: () {
if (shuffled) {
@ -198,15 +198,15 @@ class PlayerControls extends HookConsumerWidget {
tooltip: context.l10n.previous_track,
icon: const Icon(SpotubeIcons.skipBack),
style: buttonStyle,
onPressed: playlist.isFetching == true
onPressed: playlistNotifier.isFetching()
? null
: playlistNotifier.previous,
: audioPlayer.skipToPrevious,
),
IconButton(
tooltip: playing
? context.l10n.pause_playback
: context.l10n.resume_playback,
icon: playlist.isFetching == true
icon: playlistNotifier.isFetching()
? SizedBox(
height: 20,
width: 20,
@ -219,7 +219,7 @@ class PlayerControls extends HookConsumerWidget {
playing ? SpotubeIcons.pause : SpotubeIcons.play,
),
style: resumePauseStyle,
onPressed: playlist.isFetching == true
onPressed: playlistNotifier.isFetching()
? null
: Actions.handler<PlayPauseIntent>(
context,
@ -230,9 +230,9 @@ class PlayerControls extends HookConsumerWidget {
tooltip: context.l10n.next_track,
icon: const Icon(SpotubeIcons.skipForward),
style: buttonStyle,
onPressed: playlist.isFetching == true
onPressed: playlistNotifier.isFetching()
? null
: playlistNotifier.next,
: audioPlayer.skipToNext,
),
StreamBuilder<PlaylistMode>(
stream: audioPlayer.loopModeStream,
@ -253,7 +253,7 @@ class PlayerControls extends HookConsumerWidget {
loopMode == PlaylistMode.loop
? activeButtonStyle
: buttonStyle,
onPressed: playlist.isFetching == true
onPressed: playlistNotifier.isFetching()
? null
: () async {
await audioPlayer.setLoopMode(loopMode);

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart';
import 'package:spotube/hooks/utils/use_debounce.dart';
import 'package:spotube/models/database/database.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/user_preferences/user_preferences_provider.dart';
@ -53,7 +53,8 @@ class SiblingTracksSheet extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final playlist = ref.watch(proxyPlaylistProvider);
final playlist = ref.watch(audioPlayerProvider);
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final preferences = ref.watch(userPreferencesProvider);
final isSearching = useState(false);
@ -129,13 +130,13 @@ class SiblingTracksSheet extends HookConsumerWidget {
]);
final siblings = useMemoized(
() => playlist.isFetching == false
() => playlistNotifier.isFetching()
? [
(activeTrack as SourcedTrack).sourceInfo,
...activeTrack.siblings,
]
: <SourceInfo>[],
[playlist.isFetching, activeTrack],
[activeTrack],
);
final borderRadius = floating
@ -175,12 +176,12 @@ class SiblingTracksSheet extends HookConsumerWidget {
Text("${sourceInfo.artist}"),
],
),
enabled: playlist.isFetching != true,
selected: playlist.isFetching != true &&
enabled: !playlistNotifier.isFetching(),
selected: !playlistNotifier.isFetching() &&
sourceInfo.id == (activeTrack as SourcedTrack).sourceInfo.id,
selectedTileColor: theme.popupMenuTheme.color,
onTap: () {
if (playlist.isFetching == false &&
if (!playlistNotifier.isFetching() &&
sourceInfo.id != (activeTrack as SourcedTrack).sourceInfo.id) {
activeTrackNotifier.swapSibling(sourceInfo);
Navigator.of(context).pop();
@ -188,7 +189,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
},
);
},
[playlist.isFetching, activeTrack, siblings],
[activeTrack, siblings],
);
final mediaQuery = MediaQuery.of(context);

View File

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

View File

@ -19,7 +19,7 @@ import 'package:spotube/hooks/utils/use_brightness_value.dart';
import 'package:spotube/models/logger.dart';
import 'package:flutter/material.dart';
import 'package:spotube/provider/authentication/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/volume_provider.dart';
@ -33,7 +33,7 @@ class BottomPlayer extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final auth = ref.watch(authenticationProvider);
final playlist = ref.watch(proxyPlaylistProvider);
final playlist = ref.watch(audioPlayerProvider);
final layoutMode =
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
@ -91,7 +91,7 @@ class BottomPlayer extends HookConsumerWidget {
children: [
PlayerActions(
extraActions: [
if (auth != null)
if (auth.asData?.value != null)
IconButton(
tooltip: context.l10n.mini_player,
icon: const Icon(SpotubeIcons.miniPlayer),

View File

@ -135,7 +135,7 @@ class ArtistPageHeader extends HookConsumerWidget {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (auth != null)
if (auth.asData?.value != null)
Consumer(
builder: (context, ref, _) {
final isFollowingQuery = ref

View File

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

View File

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

View File

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

View File

@ -18,7 +18,7 @@ import 'package:spotube/hooks/utils/use_palette_color.dart';
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
import 'package:spotube/provider/authentication/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/provider/spotify/spotify.dart';
@ -30,7 +30,7 @@ class LyricsPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final playlist = ref.watch(proxyPlaylistProvider);
final playlist = ref.watch(audioPlayerProvider);
String albumArt = useMemoized(
() => (playlist.activeTrack?.album?.images).asUrlString(
index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1,
@ -62,7 +62,7 @@ class LyricsPage extends HookConsumerWidget {
const Spacer(),
Consumer(
builder: (context, ref, child) {
final playback = ref.watch(proxyPlaylistProvider);
final playback = ref.watch(audioPlayerProvider);
final lyric =
ref.watch(syncedLyricsProvider(playback.activeTrack));
final providerName = lyric.asData?.value.provider;

View File

@ -15,7 +15,7 @@ import 'package:spotube/hooks/utils/use_force_update.dart';
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
import 'package:spotube/provider/authentication/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:window_manager/window_manager.dart';
@ -31,7 +31,7 @@ class MiniLyricsPage extends HookConsumerWidget {
final update = useForceUpdate();
final wasMaximized = useRef<bool>(false);
final playlistQueue = ref.watch(proxyPlaylistProvider);
final playlistQueue = ref.watch(audioPlayerProvider);
final areaActive = useState(false);
final hoverMode = useState(true);
@ -230,14 +230,13 @@ class MiniLyricsPage extends HookConsumerWidget {
builder: (context) {
return Consumer(builder: (context, ref, _) {
final playlist =
ref.watch(proxyPlaylistProvider);
ref.watch(audioPlayerProvider);
return PlayerQueue
.fromProxyPlaylistNotifier(
return PlayerQueue.fromAudioPlayerNotifier(
floating: true,
playlist: playlist,
notifier: ref
.read(proxyPlaylistProvider.notifier),
.read(audioPlayerProvider.notifier),
);
});
},

View File

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

View File

@ -12,7 +12,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart';
import 'package:spotube/modules/lyrics/use_synced_lyrics.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
@ -32,7 +32,7 @@ class SyncedLyrics extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final playlist = ref.watch(proxyPlaylistProvider);
final playlist = ref.watch(audioPlayerProvider);
final mediaQuery = MediaQuery.of(context);
final controller = useAutoScrollController();
@ -54,7 +54,7 @@ class SyncedLyrics extends HookConsumerWidget {
final textTheme = Theme.of(context).textTheme;
ref.listen(
proxyPlaylistProvider.select((s) => s.activeTrack),
audioPlayerProvider.select((s) => s.activeTrack),
(previous, next) {
controller.scrollToIndex(0);
ref.read(syncedLyricsDelayProvider.notifier).state = 0;

View File

@ -16,7 +16,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/configurators/use_endless_playback.dart';
import 'package:spotube/pages/home/home.dart';
import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/server/routes/connect.dart';
import 'package:spotube/services/connectivity_adapter.dart';
import 'package:spotube/utils/persisted_state_notifier.dart';
@ -201,11 +201,11 @@ class RootApp extends HookConsumerWidget {
),
child: Consumer(
builder: (context, ref, _) {
final playlist = ref.watch(proxyPlaylistProvider);
final playlist = ref.watch(audioPlayerProvider);
final playlistNotifier =
ref.read(proxyPlaylistProvider.notifier);
ref.read(audioPlayerProvider.notifier);
return PlayerQueue.fromProxyPlaylistNotifier(
return PlayerQueue.fromAudioPlayerNotifier(
floating: true,
playlist: playlist,
notifier: playlistNotifier,

View File

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

View File

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

View File

@ -1,13 +1,22 @@
import 'dart:math';
import 'package:drift/drift.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:media_kit/media_kit.dart' hide Track;
import 'package:spotify/spotify.dart' hide Playlist;
import 'package:spotube/extensions/track.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/database/database.dart';
import 'package:spotube/provider/discord_provider.dart';
import 'package:spotube/provider/server/sourced_track.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
BlackListNotifier get _blacklist => ref.read(blacklistProvider.notifier);
Future<void> _syncSavedState() async {
final database = ref.read(databaseProvider);
@ -18,9 +27,9 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
await database.into(database.audioPlayerStateTable).insert(
AudioPlayerStateTableCompanion.insert(
playing: audioPlayer.isPlaying,
volume: audioPlayer.volume,
loopMode: audioPlayer.loopMode,
shuffled: audioPlayer.isShuffled,
collections: <String>[],
id: const Value(0),
),
);
@ -28,7 +37,6 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
playerState =
await database.select(database.audioPlayerStateTable).getSingle();
} else {
await audioPlayer.setVolume(playerState.volume);
await audioPlayer.setLoopMode(playerState.loopMode);
await audioPlayer.setShuffle(playerState.shuffled);
}
@ -130,15 +138,6 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
),
);
}),
audioPlayer.volumeStream.listen((volume) async {
state = state.copyWith(volume: volume);
await _updatePlayerState(
AudioPlayerStateTableCompanion(
volume: Value(volume),
),
);
}),
audioPlayer.loopModeStream.listen((loopMode) async {
state = state.copyWith(loopMode: loopMode);
@ -177,49 +176,141 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
playing: audioPlayer.isPlaying,
playlist: audioPlayer.playlist,
shuffled: audioPlayer.isShuffled,
volume: audioPlayer.volume,
collections: [],
);
}
// Collection related methods
Future<void> addCollections(List<String> collectionIds) async {
state = state.copyWith(collections: [
...state.collections,
...collectionIds,
]);
await _updatePlayerState(
AudioPlayerStateTableCompanion(
collections: Value(state.collections),
),
);
}
Future<void> addCollection(String collectionId) async {
await addCollections([collectionId]);
}
Future<void> removeCollections(List<String> collectionIds) async {
state = state.copyWith(
collections: state.collections
.where((element) => !collectionIds.contains(element))
.toList(),
);
await _updatePlayerState(
AudioPlayerStateTableCompanion(
collections: Value(state.collections),
),
);
}
Future<void> removeCollection(String collectionId) async {
await removeCollections([collectionId]);
}
// Tracks related methods
Future<void> addTracksAtFirst(Iterable<Track> tracks) async {
if (state.tracks.length == 1) {
return addTracks(tracks);
}
tracks = _blacklist.filter(tracks).toList() as List<Track>;
for (int i = 0; i < tracks.length; i++) {
final track = tracks.elementAt(i);
await audioPlayer.addTrackAt(
SpotubeMedia(track),
max(state.playlist.index, 0) + i + 1,
);
}
}
Future<void> addTrack(Track track) async {
if (_blacklist.contains(track)) return;
await audioPlayer.addTrack(SpotubeMedia(track));
}
Future<void> addTracks(Iterable<Track> tracks) async {
tracks = _blacklist.filter(tracks).toList() as List<Track>;
for (final track in tracks) {
await addTrack(track);
await audioPlayer.addTrack(SpotubeMedia(track));
}
}
Future<void> removeTrack(Track track) async {
final index = state.tracks.indexWhere((element) => element == track);
Future<void> removeTrack(String trackId) async {
final index = state.tracks.indexWhere((element) => element.id == trackId);
if (index == -1) return;
await audioPlayer.removeTrack(index);
}
Future<void> removeTracks(Iterable<Track> tracks) async {
for (final track in tracks) {
await removeTrack(track);
Future<void> removeTracks(Iterable<String> trackIds) async {
for (final trackId in trackIds) {
await removeTrack(trackId);
}
}
Future<void> load(
List<Track> track, {
required int initialIndex,
List<Track> tracks, {
int initialIndex = 0,
bool autoPlay = false,
}) async {
tracks = _blacklist.filter(tracks).toList() as List<Track>;
// Giving the initial track a boost so MediaKit won't skip
// because of timeout
final intendedActiveTrack = tracks.elementAt(initialIndex);
if (intendedActiveTrack is! LocalTrack) {
await ref.read(sourcedTrackProvider(intendedActiveTrack).future);
}
await audioPlayer.openPlaylist(
track.map((t) => SpotubeMedia(t)).toList(),
tracks.asMediaList(),
initialIndex: initialIndex,
autoPlay: autoPlay,
);
}
Future<void> jumpToTrack(Track track) async {
final index =
state.tracks.toList().indexWhere((element) => element.id == track.id);
if (index == -1) return;
await audioPlayer.jumpTo(index);
}
final audioPlayerProvider = NotifierProvider<AudioPlayerNotifier, AudioPlayerState>(
Future<void> moveTrack(int oldIndex, int newIndex) async {
if (oldIndex == newIndex ||
newIndex < 0 ||
oldIndex < 0 ||
newIndex > state.tracks.length - 1 ||
oldIndex > state.tracks.length - 1) return;
await audioPlayer.moveTrack(oldIndex, newIndex);
}
bool isFetching() {
if (state.activeTrack == null) return false;
return ref.read(sourcedTrackProvider(state.activeTrack!)).isLoading;
}
Future<void> stop() async {
await audioPlayer.stop();
ref.read(discordProvider.notifier).clear();
}
}
final audioPlayerProvider =
NotifierProvider<AudioPlayerNotifier, AudioPlayerState>(
() => AudioPlayerNotifier(),
);

View File

@ -1,19 +1,53 @@
// ignore_for_file: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
import 'dart:async';
import 'package:spotube/services/logger/logger.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/provider/discord_provider.dart';
import 'package:spotube/provider/history/history.dart';
import 'package:spotube/provider/palette_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/proxy_playlist/skip_segments.dart';
import 'package:spotube/provider/skip_segments/skip_segments.dart';
import 'package:spotube/provider/scrobbler/scrobbler.dart';
import 'package:spotube/provider/server/sourced_track.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_services/audio_services.dart';
import 'package:spotube/services/logger/logger.dart';
class AudioPlayerStreamListeners {
final Ref ref;
late final AudioServices notificationService;
AudioPlayerStreamListeners(this.ref) {
AudioServices.create(ref, ref.read(audioPlayerProvider.notifier)).then(
(value) => notificationService = value,
);
final subscriptions = [
subscribeToPlaylist(),
subscribeToSkipSponsor(),
subscribeToScrobbleChanged(),
subscribeToPosition(),
subscribeToPlayerError(),
];
ref.onDispose(() {
for (final subscription in subscriptions) {
subscription.cancel();
}
});
}
ScrobblerNotifier get scrobbler => ref.read(scrobblerProvider.notifier);
UserPreferences get preferences => ref.read(userPreferencesProvider);
Discord get discord => ref.read(discordProvider);
AudioPlayerState get audioPlayerState => ref.read(audioPlayerProvider);
PlaybackHistoryNotifier get history =>
ref.read(playbackHistoryProvider.notifier);
extension ProxyPlaylistListeners on ProxyPlaylistNotifier {
Future<void> updatePalette() async {
final palette = ref.read(paletteProvider);
if (!preferences.albumColorSync) {
@ -21,11 +55,12 @@ extension ProxyPlaylistListeners on ProxyPlaylistNotifier {
return;
}
return Future.microtask(() async {
if (playlist.activeTrack == null) return;
final activeTrack = ref.read(audioPlayerProvider).activeTrack;
if (activeTrack == null) return;
final palette = await PaletteGenerator.fromImageProvider(
UniversalImage.imageProvider(
(playlist.activeTrack?.album?.images).asUrlString(
(activeTrack.album?.images).asUrlString(
placeholder: ImagePlaceholder.albumArt,
),
height: 50,
@ -38,15 +73,8 @@ extension ProxyPlaylistListeners on ProxyPlaylistNotifier {
StreamSubscription subscribeToPlaylist() {
return audioPlayer.playlistStream.listen((mpvPlaylist) {
state = playlist.copyWith(
tracks: mpvPlaylist.medias
.map((media) => SpotubeMedia.fromMedia(media).track)
.toSet(),
active: mpvPlaylist.index,
);
notificationService.addTrack(playlist.activeTrack!);
discord.updatePresence(playlist.activeTrack!);
notificationService.addTrack(audioPlayerState.activeTrack!);
discord.updatePresence(audioPlayerState.activeTrack!);
updatePalette();
});
}
@ -72,18 +100,18 @@ extension ProxyPlaylistListeners on ProxyPlaylistNotifier {
String? lastScrobbled;
return audioPlayer.positionStream.listen((position) {
try {
final uid = playlist.activeTrack is LocalTrack
? (playlist.activeTrack as LocalTrack).path
: playlist.activeTrack?.id;
final uid = audioPlayerState.activeTrack is LocalTrack
? (audioPlayerState.activeTrack as LocalTrack).path
: audioPlayerState.activeTrack?.id;
if (playlist.activeTrack == null ||
if (audioPlayerState.activeTrack == null ||
lastScrobbled == uid ||
position.inSeconds < 30) {
return;
}
scrobbler.scrobble(playlist.activeTrack!);
history.addTrack(playlist.activeTrack!);
scrobbler.scrobble(audioPlayerState.activeTrack!);
history.addTrack(audioPlayerState.activeTrack!);
lastScrobbled = uid;
} catch (e, stack) {
AppLogger.reportError(e, stack);
@ -95,9 +123,13 @@ extension ProxyPlaylistListeners on ProxyPlaylistNotifier {
String lastTrack = ""; // used to prevent multiple calls to the same track
return audioPlayer.positionStream.listen((event) async {
if (event < const Duration(seconds: 3) ||
playlist.active == null ||
playlist.active == playlist.tracks.length - 1) return;
final nextTrack = playlist.tracks.elementAt(playlist.active! + 1);
audioPlayerState.playlist.index == -1 ||
audioPlayerState.playlist.index ==
audioPlayerState.tracks.length - 1) {
return;
}
final nextTrack = audioPlayerState.tracks
.elementAt(audioPlayerState.playlist.index + 1);
if (lastTrack == nextTrack.id || nextTrack is LocalTrack) return;
@ -113,3 +145,6 @@ extension ProxyPlaylistListeners on ProxyPlaylistNotifier {
return audioPlayer.errorStream.listen((event) {});
}
}
final audioPlayerStreamListenersProvider =
Provider<AudioPlayerStreamListeners>(AudioPlayerStreamListeners.new);

View File

@ -4,39 +4,97 @@ import 'package:spotube/services/audio_player/audio_player.dart';
class AudioPlayerState {
final bool playing;
final double volume;
final PlaylistMode loopMode;
final bool shuffled;
final Playlist playlist;
final List<Track> tracks;
final List<String> collections;
AudioPlayerState({
required this.playing,
required this.volume,
required this.loopMode,
required this.shuffled,
required this.playlist,
required this.collections,
List<Track>? tracks,
}) : tracks = tracks ??
playlist.medias
.map((media) => SpotubeMedia.fromMedia(media).track)
.toList();
factory AudioPlayerState.fromJson(Map<String, dynamic> json) {
return AudioPlayerState(
playing: json['playing'],
loopMode: PlaylistMode.values.firstWhere(
(e) => e.name == json['loopMode'],
orElse: () => audioPlayer.loopMode,
),
shuffled: json['shuffled'],
playlist: Playlist(
json['playlist']['medias']
.map((media) => Media(
media['uri'],
extras: media['extras'],
httpHeaders: media['httpHeaders'],
))
.toList(),
index: json['playlist']['index'],
),
collections: List<String>.from(json['collections']),
);
}
Map<String, dynamic> toJson() {
return {
'playing': playing,
'loopMode': loopMode.name,
'shuffled': shuffled,
'playlist': {
'medias': playlist.medias
.map((media) => {
'uri': media.uri,
'extras': media.extras,
'httpHeaders': media.httpHeaders,
})
.toList(),
'index': playlist.index,
},
'collections': collections,
};
}
AudioPlayerState copyWith({
bool? playing,
double? volume,
PlaylistMode? loopMode,
bool? shuffled,
Playlist? playlist,
List<String>? collections,
}) {
return AudioPlayerState(
playing: playing ?? this.playing,
volume: volume ?? this.volume,
loopMode: loopMode ?? this.loopMode,
shuffled: shuffled ?? this.shuffled,
playlist: playlist ?? this.playlist,
collections: collections ?? this.collections,
tracks: playlist == null ? tracks : null,
);
}
Track? get activeTrack {
if (playlist.index == -1) return null;
return tracks.elementAtOrNull(playlist.index);
}
bool containsTrack(Track track) {
return tracks.any((t) => t.id == track.id);
}
bool containsTracks(List<Track> tracks) {
return tracks.every(containsTrack);
}
bool containsCollection(String collectionId) {
return collections.contains(collectionId);
}
}

View File

@ -1,13 +1,14 @@
import 'dart:convert';
import 'package:media_kit/media_kit.dart' hide Track;
import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart' hide Playlist;
import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/connect/clients.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:web_socket_channel/status.dart' as status;
@ -31,8 +32,14 @@ final loopModeProvider = StateProvider<PlaylistMode>(
(ref) => PlaylistMode.none,
);
final queueProvider = StateProvider<ProxyPlaylist>(
(ref) => ProxyPlaylist({}),
final queueProvider = StateProvider<AudioPlayerState>(
(ref) => AudioPlayerState(
playing: audioPlayer.isPlaying,
loopMode: audioPlayer.loopMode,
shuffled: audioPlayer.isShuffled,
playlist: audioPlayer.playlist,
collections: [],
),
);
final volumeProvider = StateProvider<double>(

View File

@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/env.dart';
import 'package:spotube/extensions/artist_simple.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/utils/platform.dart';
@ -55,7 +55,7 @@ final discordProvider = ChangeNotifierProvider(
(ref) {
final isEnabled =
ref.watch(userPreferencesProvider.select((s) => s.discordPresence));
final playback = ref.read(proxyPlaylistProvider);
final playback = ref.read(audioPlayerProvider);
final discord = Discord(isEnabled);
if (playback.activeTrack != null) {

View File

@ -1,101 +0,0 @@
import 'package:collection/collection.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
class ProxyPlaylist {
final Set<Track> tracks;
final Set<String> collections;
final int? active;
ProxyPlaylist(this.tracks, [this.active, this.collections = const {}]);
factory ProxyPlaylist.fromJson(
Map<String, dynamic> json,
) {
return ProxyPlaylist(
List.castFrom<dynamic, Map<String, dynamic>>(
json['tracks'] ?? <Map<String, dynamic>>[],
).map((t) => _makeAppropriateTrack(t)).toSet(),
json['active'] as int?,
json['collections'] == null
? {}
: (json['collections'] as List).toSet().cast<String>(),
);
}
factory ProxyPlaylist.fromJsonRaw(Map<String, dynamic> json) => ProxyPlaylist(
json['tracks'] == null
? <Track>{}
: (json['tracks'] as List).map((t) => Track.fromJson(t)).toSet(),
json['active'] as int?,
json['collections'] == null
? {}
: (json['collections'] as List).toSet().cast<String>(),
);
Track? get activeTrack =>
active == null || active == -1 ? null : tracks.elementAtOrNull(active!);
bool get isFetching => activeTrack == null && tracks.isNotEmpty;
bool containsCollection(String collection) {
return collections.contains(collection);
}
bool containsTrack(TrackSimple track) {
return tracks.firstWhereOrNull((element) {
if (element is LocalTrack && track is LocalTrack) {
return element.path == track.path;
}
return element.id == track.id;
}) !=
null;
}
bool containsTracks(Iterable<TrackSimple> tracks) {
if (tracks.isEmpty) return false;
return tracks.every(containsTrack);
}
static Track _makeAppropriateTrack(Map<String, dynamic> track) {
if (track.containsKey("path")) {
return LocalTrack.fromJson(track);
} else {
return Track.fromJson(track);
}
}
/// To make sure proper instance method is used for JSON serialization
/// Otherwise default super.toJson() is used
static Map<String, dynamic> _makeAppropriateTrackJson(Track track) {
return switch (track) {
// ignore: unnecessary_cast
LocalTrack() => (track as LocalTrack).toJson(),
// ignore: unnecessary_cast
SourcedTrack() => (track as SourcedTrack).toJson(),
_ => track.toJson(),
};
}
Map<String, dynamic> toJson() {
return {
'tracks': tracks.map(_makeAppropriateTrackJson).toList(),
'active': active,
'collections': collections.toList(),
};
}
ProxyPlaylist copyWith({
Set<Track>? tracks,
int? active,
Set<String>? collections,
}) {
return ProxyPlaylist(
tracks ?? this.tracks,
active ?? this.active,
collections ?? this.collections,
);
}
}

View File

@ -1,215 +0,0 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/extensions/track.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/history/history.dart';
import 'package:spotube/provider/palette_provider.dart';
import 'package:spotube/provider/proxy_playlist/player_listeners.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
import 'package:spotube/provider/scrobbler/scrobbler.dart';
import 'package:spotube/provider/server/sourced_track.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_services/audio_services.dart';
import 'package:spotube/provider/discord_provider.dart';
import 'package:spotube/utils/persisted_state_notifier.dart';
class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist> {
final Ref ref;
late final AudioServices notificationService;
ScrobblerNotifier get scrobbler => ref.read(scrobblerProvider.notifier);
UserPreferences get preferences => ref.read(userPreferencesProvider);
ProxyPlaylist get playlist => state;
BlackListNotifier get blacklist => ref.read(blacklistProvider.notifier);
Discord get discord => ref.read(discordProvider);
PlaybackHistoryNotifier get history =>
ref.read(playbackHistoryProvider.notifier);
List<StreamSubscription> _subscriptions = [];
ProxyPlaylistNotifier(this.ref) : super(ProxyPlaylist({}), "playlist") {
AudioServices.create(ref, this).then(
(value) => notificationService = value,
);
_subscriptions = [
// These are subscription methods from player_listeners.dart
subscribeToPlaylist(),
subscribeToSkipSponsor(),
subscribeToPosition(),
subscribeToScrobbleChanged(),
];
}
// Basic methods for adding or removing tracks to playlist
Future<void> addTrack(Track track) async {
if (blacklist.contains(track)) return;
await audioPlayer.addTrack(SpotubeMedia(track));
}
Future<void> addTracks(Iterable<Track> tracks) async {
tracks = blacklist.filter(tracks).toList() as List<Track>;
for (final track in tracks) {
await audioPlayer.addTrack(SpotubeMedia(track));
}
}
void addCollection(String collectionId) {
state = state.copyWith(collections: {
...state.collections,
collectionId,
});
}
void removeCollection(String collectionId) {
state = state.copyWith(collections: {
...state.collections..remove(collectionId),
});
}
Future<void> removeTrack(String trackId) async {
final trackIndex =
state.tracks.toList().indexWhere((element) => element.id == trackId);
if (trackIndex == -1) return;
await audioPlayer.removeTrack(trackIndex);
}
Future<void> removeTracks(Iterable<String> tracksIds) async {
final tracks = state.tracks.map((t) => t.id!).toList();
for (final track in tracks) {
final index = tracks.indexOf(track);
if (index == -1) continue;
await audioPlayer.removeTrack(index);
}
}
Future<void> load(
Iterable<Track> tracks, {
int initialIndex = 0,
bool autoPlay = false,
}) async {
tracks = blacklist.filter(tracks).toList() as List<Track>;
state = state.copyWith(collections: {});
// Giving the initial track a boost so MediaKit won't skip
// because of timeout
final intendedActiveTrack = tracks.elementAt(initialIndex);
if (intendedActiveTrack is! LocalTrack) {
await ref.read(sourcedTrackProvider(intendedActiveTrack).future);
}
await audioPlayer.openPlaylist(
tracks.asMediaList(),
initialIndex: initialIndex,
autoPlay: autoPlay,
);
}
Future<void> jumpTo(int index) async {
await audioPlayer.jumpTo(index);
}
Future<void> jumpToTrack(Track track) async {
final index =
state.tracks.toList().indexWhere((element) => element.id == track.id);
if (index == -1) return;
await jumpTo(index);
}
Future<void> moveTrack(int oldIndex, int newIndex) async {
if (oldIndex == newIndex ||
newIndex < 0 ||
oldIndex < 0 ||
newIndex > state.tracks.length - 1 ||
oldIndex > state.tracks.length - 1) return;
await audioPlayer.moveTrack(oldIndex, newIndex);
}
Future<void> addTracksAtFirst(Iterable<Track> tracks) async {
if (state.tracks.length == 1) {
return addTracks(tracks);
}
tracks = blacklist.filter(tracks).toList() as List<Track>;
for (int i = 0; i < tracks.length; i++) {
final track = tracks.elementAt(i);
await audioPlayer.addTrackAt(
SpotubeMedia(track),
(state.active ?? 0) + i + 1,
);
}
}
Future<void> next() async {
await audioPlayer.skipToNext();
}
Future<void> previous() async {
await audioPlayer.skipToPrevious();
}
Future<void> stop() async {
state = ProxyPlaylist({});
await audioPlayer.stop();
discord.clear();
}
@override
set state(state) {
super.state = state;
if (state.tracks.isEmpty && ref.read(paletteProvider) != null) {
ref.read(paletteProvider.notifier).state = null;
} else {
updatePalette();
}
}
@override
onInit() async {
if (state.tracks.isEmpty) return null;
final oldCollections = state.collections;
await load(
state.tracks,
initialIndex: max(state.active ?? 0, 0),
autoPlay: false,
);
state = state.copyWith(collections: oldCollections);
}
@override
FutureOr<ProxyPlaylist> fromJson(Map<String, dynamic> json) {
return ProxyPlaylist.fromJson(json);
}
@override
Map<String, dynamic> toJson() {
final json = state.toJson();
return json;
}
@override
void dispose() {
for (final subscription in _subscriptions) {
subscription.cancel();
}
super.dispose();
}
}
final proxyPlaylistProvider =
StateNotifierProvider<ProxyPlaylistNotifier, ProxyPlaylist>(
(ref) => ProxyPlaylistNotifier(ref),
);

View File

@ -1,5 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.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/sourced_track/models/source_info.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
@ -28,7 +28,7 @@ class ActiveSourcedTrackNotifier extends Notifier<SourcedTrack?> {
state = newTrack;
await audioPlayer.pause();
final playbackNotifier = ref.read(proxyPlaylistProvider.notifier);
final playbackNotifier = ref.read(audioPlayerProvider.notifier);
final oldActiveIndex = audioPlayer.currentIndex;
await playbackNotifier.addTracksAtFirst([newTrack]);

View File

@ -9,7 +9,7 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/models/logger.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/volume_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart';
@ -38,8 +38,8 @@ class ServerConnectRoutes {
});
}
ProxyPlaylistNotifier get playbackNotifier =>
ref.read(proxyPlaylistProvider.notifier);
AudioPlayerNotifier get audioPlayerNotifier =>
ref.read(audioPlayerProvider.notifier);
PlaybackHistoryNotifier get historyNotifier =>
ref.read(playbackHistoryProvider.notifier);
Stream<String> get connectClientStream =>
@ -57,7 +57,7 @@ class ServerConnectRoutes {
_connectClientStreamController.add(origin);
ref.listen(
proxyPlaylistProvider,
audioPlayerProvider,
(previous, next) {
channel.sink.addEvent(WebSocketQueueEvent(next));
},
@ -67,10 +67,10 @@ class ServerConnectRoutes {
// because audioPlayer events doesn't fireImmediately
channel.sink.addEvent(WebSocketPlayingEvent(audioPlayer.isPlaying));
channel.sink.addEvent(
WebSocketPositionEvent(await audioPlayer.position ?? Duration.zero),
WebSocketPositionEvent(audioPlayer.position),
);
channel.sink.addEvent(
WebSocketDurationEvent(await audioPlayer.duration ?? Duration.zero),
WebSocketDurationEvent(audioPlayer.duration),
);
channel.sink.addEvent(WebSocketShuffleEvent(audioPlayer.isShuffled));
channel.sink.addEvent(WebSocketLoopEvent(audioPlayer.loopMode));
@ -116,14 +116,14 @@ class ServerConnectRoutes {
);
event.onLoad((event) async {
await playbackNotifier.load(
await audioPlayerNotifier.load(
event.data.tracks,
autoPlay: true,
initialIndex: event.data.initialIndex ?? 0,
);
if (event.data.collectionId == null) return;
playbackNotifier.addCollection(event.data.collectionId!);
audioPlayerNotifier.addCollection(event.data.collectionId!);
if (event.data.collection is AlbumSimple) {
historyNotifier
.addAlbums([event.data.collection as AlbumSimple]);
@ -146,15 +146,15 @@ class ServerConnectRoutes {
});
event.onNext((event) async {
await playbackNotifier.next();
await audioPlayer.skipToNext();
});
event.onPrevious((event) async {
await playbackNotifier.previous();
await audioPlayer.skipToPrevious();
});
event.onJump((event) async {
await playbackNotifier.jumpTo(event.data);
await audioPlayer.jumpTo(event.data);
});
event.onSeek((event) async {
@ -170,15 +170,15 @@ class ServerConnectRoutes {
});
event.onAddTrack((event) async {
await playbackNotifier.addTrack(event.data);
await audioPlayerNotifier.addTrack(event.data);
});
event.onRemoveTrack((event) async {
await playbackNotifier.removeTrack(event.data);
await audioPlayerNotifier.removeTrack(event.data);
});
event.onReorder((event) async {
await playbackNotifier.moveTrack(
await audioPlayerNotifier.moveTrack(
event.data.oldIndex,
event.data.newIndex,
);

View File

@ -2,8 +2,8 @@ import 'package:dio/dio.dart' hide Response;
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shelf/shelf.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/provider/server/active_sourced_track.dart';
import 'package:spotube/provider/server/sourced_track.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
@ -12,7 +12,7 @@ import 'package:spotube/services/logger/logger.dart';
class ServerPlaybackRoutes {
final Ref ref;
UserPreferences get userPreferences => ref.read(userPreferencesProvider);
ProxyPlaylist get playlist => ref.read(proxyPlaylistProvider);
AudioPlayerState get playlist => ref.read(audioPlayerProvider);
final Dio dio;
ServerPlaybackRoutes(this.ref) : dio = Dio();

View File

@ -2,7 +2,7 @@ import 'package:collection/collection.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
final sourcedTrackProvider =
@ -12,10 +12,9 @@ final sourcedTrackProvider =
}
ref.listen(
proxyPlaylistProvider,
audioPlayerProvider.select((value) => value.tracks),
(old, next) {
if (next.tracks.isEmpty ||
next.tracks.none((element) => element.id == track.id)) {
if (next.isEmpty || next.none((element) => element.id == track.id)) {
ref.invalidateSelf();
}
},

View File

@ -1,7 +1,7 @@
import 'dart:io';
import 'package:hooks_riverpod/hooks_riverpod.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:media_kit/media_kit.dart' hide Track;
import 'package:tray_manager/tray_manager.dart';
@ -19,9 +19,9 @@ final audioPlayerPlaying = StreamProvider<bool>((ref) {
});
final trayMenuProvider = Provider((ref) {
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final isPlaybackPlaying =
ref.watch(proxyPlaylistProvider.select((s) => s.activeTrack != null));
ref.watch(audioPlayerProvider.select((s) => s.activeTrack != null));
final isLoopOne =
ref.watch(audioPlayerLoopMode).asData?.value == PlaylistMode.single;
final isShuffled = ref.watch(audioPlayerShuffleMode).asData?.value ?? false;
@ -56,14 +56,14 @@ final trayMenuProvider = Provider((ref) {
label: "Next",
disabled: !isPlaybackPlaying,
onClick: (menuItem) {
playlistNotifier.next();
audioPlayer.skipToNext();
},
),
MenuItem(
label: "Previous",
disabled: !isPlaybackPlaying,
onClick: (menuItem) {
playlistNotifier.previous();
audioPlayer.skipToPrevious();
},
),
MenuItem.submenu(

View File

@ -6,10 +6,9 @@ import 'package:path_provider/path_provider.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart';
import 'package:spotube/provider/audio_player/audio_player_streams.dart';
import 'package:spotube/provider/database/database.dart';
import 'package:spotube/provider/palette_provider.dart';
import 'package:spotube/provider/proxy_playlist/player_listeners.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/utils/platform.dart';
@ -115,7 +114,7 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
if (!sync) {
ref.read(paletteProvider.notifier).state = null;
} else {
ref.read(proxyPlaylistProvider.notifier).updatePalette();
ref.read(audioPlayerStreamListenersProvider).updatePalette();
}
}

View File

@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/extensions/artist_simple.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/services/audio_services/mobile_audio_service.dart';
import 'package:spotube/services/audio_services/windows_audio_service.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
@ -17,7 +17,7 @@ class AudioServices {
static Future<AudioServices> create(
Ref ref,
ProxyPlaylistNotifier playback,
AudioPlayerNotifier playback,
) async {
final mobile = kIsMobile || kIsMacOS || kIsLinux
? await AudioService.init(

View File

@ -2,19 +2,19 @@ import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:audio_session/audio_session.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:media_kit/media_kit.dart' hide Track;
class MobileAudioService extends BaseAudioHandler {
AudioSession? session;
final ProxyPlaylistNotifier playlistNotifier;
final AudioPlayerNotifier audioPlayerNotifier;
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
ProxyPlaylist get playlist => playlistNotifier.state;
AudioPlayerState get playlist => audioPlayerNotifier.state;
MobileAudioService(this.playlistNotifier) {
MobileAudioService(this.audioPlayerNotifier) {
AudioSession.instance.then((s) {
session = s;
session?.configure(const AudioSessionConfiguration.music());
@ -102,24 +102,24 @@ class MobileAudioService extends BaseAudioHandler {
@override
Future<void> stop() async {
await playlistNotifier.stop();
await audioPlayerNotifier.stop();
}
@override
Future<void> skipToNext() async {
await playlistNotifier.next();
await audioPlayer.skipToNext();
await super.skipToNext();
}
@override
Future<void> skipToPrevious() async {
await playlistNotifier.previous();
await audioPlayer.skipToPrevious();
await super.skipToPrevious();
}
@override
Future<void> onTaskRemoved() async {
await playlistNotifier.stop();
await audioPlayerNotifier.stop();
return super.onTaskRemoved();
}
@ -146,7 +146,7 @@ class MobileAudioService extends BaseAudioHandler {
PlaylistMode.single => AudioServiceRepeatMode.one,
_ => AudioServiceRepeatMode.none,
},
processingState: playlist.isFetching == true
processingState: audioPlayer.isBuffering
? AudioProcessingState.loading
: AudioProcessingState.ready,
);

View File

@ -5,18 +5,18 @@ import 'package:smtc_windows/smtc_windows.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/extensions/artist_simple.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/services/audio_player/audio_player.dart';
import 'package:spotube/services/audio_player/playback_state.dart';
class WindowsAudioService {
final SMTCWindows smtc;
final Ref ref;
final ProxyPlaylistNotifier playlistNotifier;
final AudioPlayerNotifier audioPlayerNotifier;
final subscriptions = <StreamSubscription>[];
WindowsAudioService(this.ref, this.playlistNotifier)
WindowsAudioService(this.ref, this.audioPlayerNotifier)
: smtc = SMTCWindows(enabled: false) {
smtc.setPlaybackStatus(PlaybackStatus.Stopped);
final buttonStream = smtc.buttonPressStream.listen((event) {
@ -28,13 +28,13 @@ class WindowsAudioService {
audioPlayer.pause();
break;
case PressedButton.next:
playlistNotifier.next();
audioPlayer.skipToNext();
break;
case PressedButton.previous:
playlistNotifier.previous();
audioPlayer.skipToPrevious();
break;
case PressedButton.stop:
playlistNotifier.stop();
audioPlayerNotifier.stop();
break;
default:
break;