✨ Bottom player
This commit is contained in:
parent
41e248f8cc
commit
e7ea852725
@ -28,6 +28,8 @@ android {
|
|||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
|
|
||||||
|
minSdkVersion 24
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
3
devtools_options.yaml
Normal file
3
devtools_options.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
@ -9,6 +9,7 @@ import 'package:rhythm_box/services/server/routes/playback.dart';
|
|||||||
import 'package:rhythm_box/services/server/server.dart';
|
import 'package:rhythm_box/services/server/server.dart';
|
||||||
import 'package:rhythm_box/services/server/sourced_track.dart';
|
import 'package:rhythm_box/services/server/sourced_track.dart';
|
||||||
import 'package:rhythm_box/translations.dart';
|
import 'package:rhythm_box/translations.dart';
|
||||||
|
import 'package:rhythm_box/widgets/tracks/querying_track_info.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
MediaKit.ensureInitialized();
|
MediaKit.ensureInitialized();
|
||||||
@ -51,9 +52,11 @@ class MyApp extends StatelessWidget {
|
|||||||
void _initializeProviders(BuildContext context) async {
|
void _initializeProviders(BuildContext context) async {
|
||||||
Get.lazyPut(() => SpotifyProvider());
|
Get.lazyPut(() => SpotifyProvider());
|
||||||
Get.lazyPut(() => ActiveSourcedTrackProvider());
|
Get.lazyPut(() => ActiveSourcedTrackProvider());
|
||||||
Get.lazyPut(() => SourcedTrackProvider());
|
|
||||||
|
|
||||||
Get.put(AudioPlayerProvider());
|
Get.put(AudioPlayerProvider());
|
||||||
|
Get.put(QueryingTrackInfoProvider());
|
||||||
|
Get.put(SourcedTrackProvider());
|
||||||
|
|
||||||
Get.put(ServerPlaybackRoutesProvider());
|
Get.put(ServerPlaybackRoutesProvider());
|
||||||
Get.put(PlaybackServerProvider());
|
Get.put(PlaybackServerProvider());
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@ -13,6 +14,8 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
class AudioPlayerProvider extends GetxController {
|
class AudioPlayerProvider extends GetxController {
|
||||||
late final SharedPreferences _prefs;
|
late final SharedPreferences _prefs;
|
||||||
|
|
||||||
|
RxBool isPlaying = false.obs;
|
||||||
|
|
||||||
Rx<AudioPlayerState> state = Rx(AudioPlayerState(
|
Rx<AudioPlayerState> state = Rx(AudioPlayerState(
|
||||||
playing: false,
|
playing: false,
|
||||||
shuffled: false,
|
shuffled: false,
|
||||||
@ -25,26 +28,12 @@ class AudioPlayerProvider extends GetxController {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
SharedPreferences.getInstance().then((ins) {
|
SharedPreferences.getInstance().then((ins) async {
|
||||||
_prefs = ins;
|
_prefs = ins;
|
||||||
_syncSavedState();
|
final res = await _readSavedState();
|
||||||
});
|
if (res != null) {
|
||||||
|
state.value = res;
|
||||||
_subscriptions = [
|
} else {
|
||||||
audioPlayer.playingStream.listen((playing) async {
|
|
||||||
state.value = state.value.copyWith(playing: playing);
|
|
||||||
}),
|
|
||||||
audioPlayer.loopModeStream.listen((loopMode) async {
|
|
||||||
state.value = state.value.copyWith(loopMode: loopMode);
|
|
||||||
}),
|
|
||||||
audioPlayer.shuffledStream.listen((shuffled) async {
|
|
||||||
state.value = state.value.copyWith(shuffled: shuffled);
|
|
||||||
}),
|
|
||||||
audioPlayer.playlistStream.listen((playlist) async {
|
|
||||||
state.value = state.value.copyWith(playlist: playlist);
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
state.value = AudioPlayerState(
|
state.value = AudioPlayerState(
|
||||||
loopMode: audioPlayer.loopMode,
|
loopMode: audioPlayer.loopMode,
|
||||||
playing: audioPlayer.isPlaying,
|
playing: audioPlayer.isPlaying,
|
||||||
@ -52,6 +41,31 @@ class AudioPlayerProvider extends GetxController {
|
|||||||
shuffled: audioPlayer.isShuffled,
|
shuffled: audioPlayer.isShuffled,
|
||||||
collections: [],
|
collections: [],
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_subscriptions = [
|
||||||
|
audioPlayer.playingStream.listen((playing) async {
|
||||||
|
state.value = state.value.copyWith(playing: playing);
|
||||||
|
await _updateSavedState();
|
||||||
|
}),
|
||||||
|
audioPlayer.loopModeStream.listen((loopMode) async {
|
||||||
|
state.value = state.value.copyWith(loopMode: loopMode);
|
||||||
|
await _updateSavedState();
|
||||||
|
}),
|
||||||
|
audioPlayer.shuffledStream.listen((shuffled) async {
|
||||||
|
state.value = state.value.copyWith(shuffled: shuffled);
|
||||||
|
await _updateSavedState();
|
||||||
|
}),
|
||||||
|
audioPlayer.playlistStream.listen((playlist) async {
|
||||||
|
state.value = state.value.copyWith(playlist: playlist);
|
||||||
|
await _updateSavedState();
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
audioPlayer.playingStream.listen((playing) {
|
||||||
|
isPlaying.value = playing;
|
||||||
|
});
|
||||||
|
|
||||||
super.onInit();
|
super.onInit();
|
||||||
}
|
}
|
||||||
@ -66,13 +80,16 @@ class AudioPlayerProvider extends GetxController {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _syncSavedState() async {
|
Future<AudioPlayerState?> _readSavedState() async {
|
||||||
final data = _prefs.getBool("player_state");
|
final data = _prefs.getString("player_state");
|
||||||
if (data == null) return;
|
if (data == null) return null;
|
||||||
|
|
||||||
// TODO Serilize and deserilize this state
|
return AudioPlayerState.fromJson(jsonDecode(data));
|
||||||
|
}
|
||||||
|
|
||||||
// TODO Sync saved playlist
|
Future<void> _updateSavedState() async {
|
||||||
|
final out = jsonEncode(state.value.toJson());
|
||||||
|
await _prefs.setString("player_state", out);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addCollections(List<String> collectionIds) async {
|
Future<void> addCollections(List<String> collectionIds) async {
|
||||||
@ -80,6 +97,8 @@ class AudioPlayerProvider extends GetxController {
|
|||||||
...state.value.collections,
|
...state.value.collections,
|
||||||
...collectionIds,
|
...collectionIds,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
await _updateSavedState();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addCollection(String collectionId) async {
|
Future<void> addCollection(String collectionId) async {
|
||||||
@ -92,6 +111,8 @@ class AudioPlayerProvider extends GetxController {
|
|||||||
.where((element) => !collectionIds.contains(element))
|
.where((element) => !collectionIds.contains(element))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await _updateSavedState();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeCollection(String collectionId) async {
|
Future<void> removeCollection(String collectionId) async {
|
||||||
|
@ -13,13 +13,6 @@ final router = GoRouter(routes: [
|
|||||||
name: "explore",
|
name: "explore",
|
||||||
builder: (context, state) => const ExploreScreen(),
|
builder: (context, state) => const ExploreScreen(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
path: "/settings",
|
|
||||||
name: "settings",
|
|
||||||
builder: (context, state) => const SettingsScreen(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/playlist/:id",
|
path: "/playlist/:id",
|
||||||
name: "playlistView",
|
name: "playlistView",
|
||||||
@ -27,4 +20,11 @@ final router = GoRouter(routes: [
|
|||||||
playlistId: state.pathParameters['id']!,
|
playlistId: state.pathParameters['id']!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "/settings",
|
||||||
|
name: "settings",
|
||||||
|
builder: (context, state) => const SettingsScreen(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
@ -22,7 +22,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
|
|
||||||
Future<void> _pullPlaylist() async {
|
Future<void> _pullPlaylist() async {
|
||||||
_featuredPlaylist =
|
_featuredPlaylist =
|
||||||
(await _spotify.api.playlists.featured.all(10)).toList();
|
(await _spotify.api.playlists.featured.getPage(20)).items!.toList();
|
||||||
setState(() => _isLoading = false);
|
setState(() => _isLoading = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
final item = _featuredPlaylist?[idx];
|
final item = _featuredPlaylist?[idx];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: ClipRRect(
|
leading: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: item != null
|
child: item != null
|
||||||
? AutoCacheImage(
|
? AutoCacheImage(
|
||||||
item.images!.first.url!,
|
item.images!.first.url!,
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
|
||||||
extension ArtistExtension on List<ArtistSimple> {
|
extension ArtistSimpleExtension on List<ArtistSimple> {
|
||||||
|
String asString() {
|
||||||
|
return map((e) => e.name?.replaceAll(",", " ")).join(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ArtistExtension on List<Artist> {
|
||||||
String asString() {
|
String asString() {
|
||||||
return map((e) => e.name?.replaceAll(",", " ")).join(", ");
|
return map((e) => e.name?.replaceAll(",", " ")).join(", ");
|
||||||
}
|
}
|
||||||
|
@ -48,11 +48,11 @@ class AudioServices with WidgetsBindingObserver {
|
|||||||
duration: track is SourcedTrack
|
duration: track is SourcedTrack
|
||||||
? track.sourceInfo.duration
|
? track.sourceInfo.duration
|
||||||
: Duration(milliseconds: track.durationMs ?? 0),
|
: Duration(milliseconds: track.durationMs ?? 0),
|
||||||
artUri: Uri.parse(
|
artUri: track.album?.images != null
|
||||||
(track.album?.images).asUrlString(
|
? Uri.parse(
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
(track.album?.images).asUrlString()!,
|
||||||
),
|
)
|
||||||
),
|
: null,
|
||||||
playable: true,
|
playable: true,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import 'package:rhythm_box/services/primitive.dart';
|
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:rhythm_box/collections/assets.gen.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
enum ImagePlaceholder {
|
enum ImagePlaceholder {
|
||||||
@ -11,24 +9,13 @@ enum ImagePlaceholder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension SpotifyImageExtensions on List<Image>? {
|
extension SpotifyImageExtensions on List<Image>? {
|
||||||
String asUrlString({
|
String? asUrlString({
|
||||||
int index = 1,
|
int index = 1,
|
||||||
required ImagePlaceholder placeholder,
|
|
||||||
}) {
|
}) {
|
||||||
final String placeholderUrl = {
|
|
||||||
ImagePlaceholder.albumArt: Assets.albumPlaceholder.path,
|
|
||||||
ImagePlaceholder.artist: Assets.userPlaceholder.path,
|
|
||||||
ImagePlaceholder.collection: Assets.placeholder.path,
|
|
||||||
ImagePlaceholder.online:
|
|
||||||
"https://avatars.dicebear.com/api/bottts/${PrimitiveUtils.uuid.v4()}.png",
|
|
||||||
}[placeholder]!;
|
|
||||||
|
|
||||||
final sortedImage = this?.sorted((a, b) => a.width!.compareTo(b.width!));
|
final sortedImage = this?.sorted((a, b) => a.width!.compareTo(b.width!));
|
||||||
|
|
||||||
return sortedImage != null && sortedImage.isNotEmpty
|
return sortedImage?[
|
||||||
? sortedImage[
|
|
||||||
index > sortedImage.length - 1 ? sortedImage.length - 1 : index]
|
index > sortedImage.length - 1 ? sortedImage.length - 1 : index]
|
||||||
.url!
|
.url;
|
||||||
: placeholderUrl;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,9 +84,7 @@ class WindowsAudioService {
|
|||||||
albumArtist: track.artists?.firstOrNull?.name ?? "Unknown",
|
albumArtist: track.artists?.firstOrNull?.name ?? "Unknown",
|
||||||
artist: track.artists?.asString() ?? "Unknown",
|
artist: track.artists?.asString() ?? "Unknown",
|
||||||
album: track.album?.name ?? "Unknown",
|
album: track.album?.name ?? "Unknown",
|
||||||
thumbnail: (track.album?.images).asUrlString(
|
thumbnail: (track.album?.images).asUrlString(),
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import 'package:rhythm_box/providers/audio_player.dart';
|
|||||||
import 'package:rhythm_box/services/audio_player/audio_player.dart';
|
import 'package:rhythm_box/services/audio_player/audio_player.dart';
|
||||||
import 'package:rhythm_box/services/local_track.dart';
|
import 'package:rhythm_box/services/local_track.dart';
|
||||||
import 'package:rhythm_box/services/sourced_track/sourced_track.dart';
|
import 'package:rhythm_box/services/sourced_track/sourced_track.dart';
|
||||||
|
import 'package:rhythm_box/widgets/tracks/querying_track_info.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
|
||||||
class SourcedTrackProvider extends GetxController {
|
class SourcedTrackProvider extends GetxController {
|
||||||
@ -17,6 +18,7 @@ class SourcedTrackProvider extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final AudioPlayerProvider playback = Get.find();
|
final AudioPlayerProvider playback = Get.find();
|
||||||
|
final QueryingTrackInfoProvider query = Get.find();
|
||||||
|
|
||||||
ever(playback.state.value.tracks.obs, (List<Track> tracks) {
|
ever(playback.state.value.tracks.obs, (List<Track> tracks) {
|
||||||
if (tracks.isEmpty || tracks.none((element) => element.id == track.id)) {
|
if (tracks.isEmpty || tracks.none((element) => element.id == track.id)) {
|
||||||
@ -24,7 +26,9 @@ class SourcedTrackProvider extends GetxController {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
query.isQueryingTrackInfo.value = true;
|
||||||
sourcedTrack.value = await SourcedTrack.fetchFromTrack(track: track);
|
sourcedTrack.value = await SourcedTrack.fetchFromTrack(track: track);
|
||||||
|
query.isQueryingTrackInfo.value = false;
|
||||||
|
|
||||||
return sourcedTrack.value;
|
return sourcedTrack.value;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:rhythm_box/widgets/player/bottom_player.dart';
|
||||||
|
|
||||||
class Destination {
|
class Destination {
|
||||||
const Destination(this.title, this.page, this.icon);
|
const Destination(this.title, this.page, this.icon);
|
||||||
@ -30,7 +31,15 @@ class _NavShellState extends State<NavShell> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: widget.child,
|
body: widget.child,
|
||||||
bottomNavigationBar: BottomNavigationBar(
|
bottomNavigationBar: Material(
|
||||||
|
elevation: 2,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const BottomPlayer(),
|
||||||
|
const Divider(height: 0.3, thickness: 0.3),
|
||||||
|
BottomNavigationBar(
|
||||||
|
elevation: 0,
|
||||||
showUnselectedLabels: false,
|
showUnselectedLabels: false,
|
||||||
currentIndex: _focusDestination,
|
currentIndex: _focusDestination,
|
||||||
items: _allDestinations
|
items: _allDestinations
|
||||||
@ -44,6 +53,9 @@ class _NavShellState extends State<NavShell> {
|
|||||||
setState(() => _focusDestination = value);
|
setState(() => _focusDestination = value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
172
lib/widgets/player/bottom_player.dart
Normal file
172
lib/widgets/player/bottom_player.dart
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:rhythm_box/providers/audio_player.dart';
|
||||||
|
import 'package:rhythm_box/services/audio_player/audio_player.dart';
|
||||||
|
import 'package:rhythm_box/services/audio_services/image.dart';
|
||||||
|
import 'package:rhythm_box/widgets/auto_cache_image.dart';
|
||||||
|
import 'package:rhythm_box/widgets/player/track_details.dart';
|
||||||
|
import 'package:rhythm_box/widgets/tracks/querying_track_info.dart';
|
||||||
|
|
||||||
|
class BottomPlayer extends StatefulWidget {
|
||||||
|
const BottomPlayer({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BottomPlayer> createState() => _BottomPlayerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BottomPlayerState extends State<BottomPlayer>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _animationController = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
late final Animation<double> _animation = CurvedAnimation(
|
||||||
|
parent: _animationController,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
|
||||||
|
late final AudioPlayerProvider _playback = Get.find();
|
||||||
|
late final QueryingTrackInfoProvider _query = Get.find();
|
||||||
|
|
||||||
|
String? get _albumArt =>
|
||||||
|
(_playback.state.value.activeTrack?.album?.images).asUrlString(
|
||||||
|
index:
|
||||||
|
(_playback.state.value.activeTrack?.album?.images?.length ?? 1) - 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
bool get _isPlaying => _playback.isPlaying.value;
|
||||||
|
bool get _isFetchingActiveTrack => _query.isQueryingTrackInfo.value;
|
||||||
|
|
||||||
|
Duration _durationCurrent = Duration.zero;
|
||||||
|
Duration _durationTotal = Duration.zero;
|
||||||
|
|
||||||
|
void _updateDurationCurrent(Duration dur) {
|
||||||
|
setState(() => _durationCurrent = dur);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateDurationTotal(Duration dur) {
|
||||||
|
setState(() => _durationTotal = dur);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<StreamSubscription>? _subscriptions;
|
||||||
|
|
||||||
|
Future<void> _togglePlayState() async {
|
||||||
|
if (!audioPlayer.isPlaying) {
|
||||||
|
await audioPlayer.resume();
|
||||||
|
} else {
|
||||||
|
await audioPlayer.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isLifted = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_subscriptions = [
|
||||||
|
audioPlayer.durationStream.listen(_updateDurationTotal),
|
||||||
|
audioPlayer.positionStream.listen(_updateDurationCurrent),
|
||||||
|
_playback.isPlaying.listen((value) {
|
||||||
|
if (value && !_isLifted) {
|
||||||
|
_animationController.animateTo(1);
|
||||||
|
_isLifted = true;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
_query.isQueryingTrackInfo.listen((value) {
|
||||||
|
if (value && !_isLifted) {
|
||||||
|
_animationController.animateTo(1);
|
||||||
|
_isLifted = true;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (_subscriptions != null) {
|
||||||
|
for (final subscription in _subscriptions!) {
|
||||||
|
subscription.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizeTransition(
|
||||||
|
sizeFactor: _animation,
|
||||||
|
axis: Axis.vertical,
|
||||||
|
axisAlignment: -1,
|
||||||
|
child: Obx(
|
||||||
|
() => Column(
|
||||||
|
children: [
|
||||||
|
if (_durationCurrent != Duration.zero)
|
||||||
|
TweenAnimationBuilder<double>(
|
||||||
|
tween: Tween(
|
||||||
|
begin: 0,
|
||||||
|
end: _durationCurrent.inMilliseconds /
|
||||||
|
max(_durationTotal.inMilliseconds, 1),
|
||||||
|
),
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
builder: (context, value, _) => LinearProgressIndicator(
|
||||||
|
minHeight: 3,
|
||||||
|
value: value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: _albumArt != null
|
||||||
|
? AutoCacheImage(_albumArt!, width: 64, height: 64)
|
||||||
|
: Container(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHigh,
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
child: const Center(child: Icon(Icons.image)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
Expanded(
|
||||||
|
child: PlayerTrackDetails(
|
||||||
|
track: _playback.state.value.activeTrack,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: _isFetchingActiveTrack
|
||||||
|
? const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 3,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
!_isPlaying ? Icons.play_arrow : Icons.pause,
|
||||||
|
),
|
||||||
|
onPressed:
|
||||||
|
_isFetchingActiveTrack ? null : _togglePlayState,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
],
|
||||||
|
).paddingSymmetric(horizontal: 12, vertical: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
49
lib/widgets/player/track_details.dart
Normal file
49
lib/widgets/player/track_details.dart
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:rhythm_box/providers/audio_player.dart';
|
||||||
|
import 'package:rhythm_box/services/artist.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
|
||||||
|
class PlayerTrackDetails extends StatelessWidget {
|
||||||
|
final Color? color;
|
||||||
|
final Track? track;
|
||||||
|
const PlayerTrackDetails({super.key, this.color, this.track});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final AudioPlayerProvider playback = Get.find();
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
InkWell(
|
||||||
|
child: Text(
|
||||||
|
playback.state.value.activeTrack?.name ?? "Not playing",
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: theme.textTheme.bodyMedium!.copyWith(
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
// TODO Push to track page
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
playback.state.value.activeTrack?.artists?.asString() ??
|
||||||
|
"No author",
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: theme.textTheme.bodySmall!.copyWith(color: color),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ import 'package:rhythm_box/providers/spotify.dart';
|
|||||||
import 'package:rhythm_box/widgets/auto_cache_image.dart';
|
import 'package:rhythm_box/widgets/auto_cache_image.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:rhythm_box/services/artist.dart';
|
||||||
|
|
||||||
class PlaylistTrackList extends StatefulWidget {
|
class PlaylistTrackList extends StatefulWidget {
|
||||||
final String playlistId;
|
final String playlistId;
|
||||||
@ -46,7 +47,7 @@ class _PlaylistTrackListState extends State<PlaylistTrackList> {
|
|||||||
final item = _tracks?[idx];
|
final item = _tracks?[idx];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: ClipRRect(
|
leading: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: item != null
|
child: item != null
|
||||||
? AutoCacheImage(
|
? AutoCacheImage(
|
||||||
item.album!.images!.first.url!,
|
item.album!.images!.first.url!,
|
||||||
@ -63,8 +64,7 @@ class _PlaylistTrackListState extends State<PlaylistTrackList> {
|
|||||||
),
|
),
|
||||||
title: Text(item?.name ?? 'Loading...'),
|
title: Text(item?.name ?? 'Loading...'),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
item?.artists!.map((x) => x.name!).join(', ') ??
|
item?.artists?.asString() ?? 'Please stand by...',
|
||||||
'Please stand by...',
|
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (item == null) return;
|
if (item == null) return;
|
||||||
|
5
lib/widgets/tracks/querying_track_info.dart
Normal file
5
lib/widgets/tracks/querying_track_info.dart
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
class QueryingTrackInfoProvider extends GetxController {
|
||||||
|
RxBool isQueryingTrackInfo = false.obs;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user