mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00

PlayerControls slider & duration are now vertical hotkey init moved to Home Player & YoutubeExplode are provided through riverpod Playback handles all things Player used to do GoRoutes are seperated from main to individual model file usePaletteColor bugfix occuring for before initilizing mount
177 lines
6.4 KiB
Dart
177 lines
6.4 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:just_audio/just_audio.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:spotify/spotify.dart' hide Image;
|
|
import 'package:spotube/components/Player/PlayerOverlay.dart';
|
|
import 'package:spotube/components/Player/PlayerTrackDetails.dart';
|
|
import 'package:spotube/components/Shared/DownloadTrackButton.dart';
|
|
import 'package:spotube/components/Player/PlayerControls.dart';
|
|
import 'package:spotube/helpers/image-to-url-string.dart';
|
|
import 'package:spotube/hooks/useBreakpoints.dart';
|
|
import 'package:spotube/models/LocalStorageKeys.dart';
|
|
import 'package:spotube/provider/Playback.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:spotube/provider/SpotifyDI.dart';
|
|
|
|
class Player extends HookConsumerWidget {
|
|
const Player({Key? key}) : super(key: key);
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
Playback playback = ref.watch(playbackProvider);
|
|
|
|
final _volume = useState(0.0);
|
|
|
|
final breakpoint = useBreakpoints();
|
|
|
|
final AudioPlayer player = playback.player;
|
|
|
|
final Future<SharedPreferences> future =
|
|
useMemoized(SharedPreferences.getInstance);
|
|
final AsyncSnapshot<SharedPreferences?> localStorage =
|
|
useFuture(future, initialData: null);
|
|
|
|
useEffect(() {
|
|
if (localStorage.hasData) {
|
|
_volume.value = localStorage.data?.getDouble(LocalStorageKeys.volume) ??
|
|
player.volume;
|
|
}
|
|
return null;
|
|
}, [localStorage.data]);
|
|
|
|
String albumArt = useMemoized(
|
|
() => imageToUrlString(
|
|
playback.currentTrack?.album?.images,
|
|
index: (playback.currentTrack?.album?.images?.length ?? 1) - 1,
|
|
),
|
|
[playback.currentTrack?.album?.images],
|
|
);
|
|
|
|
final entryRef = useRef<OverlayEntry?>(null);
|
|
|
|
disposeOverlay() {
|
|
try {
|
|
entryRef.value?.remove();
|
|
entryRef.value = null;
|
|
} catch (e, stack) {
|
|
if (e is! AssertionError) {
|
|
print("[Player.useEffect.cleanup] $e");
|
|
print(stack);
|
|
}
|
|
}
|
|
}
|
|
|
|
useEffect(() {
|
|
// clearing the overlay-entry as passing the already available
|
|
// entry will result in splashing while resizing the window
|
|
if (entryRef.value != null) disposeOverlay();
|
|
if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) {
|
|
entryRef.value = OverlayEntry(
|
|
opaque: false,
|
|
builder: (context) => PlayerOverlay(albumArt: albumArt),
|
|
);
|
|
// I can't believe useEffect doesn't run Post Frame aka
|
|
// after rendering/painting the UI
|
|
// `My disappointment is immeasurable and my day is ruined` XD
|
|
WidgetsBinding.instance?.addPostFrameCallback((time) {
|
|
Overlay.of(context)?.insert(entryRef.value!);
|
|
});
|
|
}
|
|
return () {
|
|
disposeOverlay();
|
|
};
|
|
}, [breakpoint]);
|
|
|
|
// returning an empty non spacious Container as the overlay will take
|
|
// place in the global overlay stack aka [_entries]
|
|
if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) {
|
|
return Container();
|
|
}
|
|
|
|
return Container(
|
|
color: Theme.of(context).backgroundColor,
|
|
child: Material(
|
|
type: MaterialType.transparency,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
Expanded(child: PlayerTrackDetails(albumArt: albumArt)),
|
|
// controls
|
|
const Expanded(
|
|
flex: 3,
|
|
child: PlayerControls(),
|
|
),
|
|
// add to saved tracks
|
|
Expanded(
|
|
flex: 1,
|
|
child: Wrap(
|
|
alignment: WrapAlignment.center,
|
|
runAlignment: WrapAlignment.center,
|
|
children: [
|
|
Container(
|
|
height: 20,
|
|
constraints: const BoxConstraints(maxWidth: 200),
|
|
child: Slider.adaptive(
|
|
value: _volume.value,
|
|
onChanged: (value) async {
|
|
try {
|
|
await player.setVolume(value).then((_) {
|
|
_volume.value = value;
|
|
localStorage.data?.setDouble(
|
|
LocalStorageKeys.volume,
|
|
value,
|
|
);
|
|
});
|
|
} catch (e, stack) {
|
|
print("[VolumeSlider.onChange()] $e");
|
|
print(stack);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
DownloadTrackButton(
|
|
track: playback.currentTrack,
|
|
),
|
|
Consumer(builder: (context, ref, widget) {
|
|
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
|
|
return FutureBuilder<bool>(
|
|
future: playback.currentTrack?.id != null
|
|
? spotifyApi.tracks.me
|
|
.containsOne(playback.currentTrack!.id!)
|
|
: Future.value(false),
|
|
initialData: false,
|
|
builder: (context, snapshot) {
|
|
bool isLiked = snapshot.data ?? false;
|
|
return IconButton(
|
|
icon: Icon(
|
|
!isLiked
|
|
? Icons.favorite_outline_rounded
|
|
: Icons.favorite_rounded,
|
|
color: isLiked ? Colors.green : null,
|
|
),
|
|
onPressed: () {
|
|
if (!isLiked &&
|
|
playback.currentTrack?.id != null) {
|
|
spotifyApi.tracks.me
|
|
.saveOne(playback.currentTrack!.id!);
|
|
}
|
|
});
|
|
});
|
|
}),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|