mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00

Downloaded tracks are saved with metadata. Only MP3 file metadata support is available in local track player for now
147 lines
5.0 KiB
Dart
147 lines
5.0 KiB
Dart
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:spotube/components/Player/PlayerActions.dart';
|
|
import 'package:spotube/components/Player/PlayerOverlay.dart';
|
|
import 'package:spotube/components/Player/PlayerTrackDetails.dart';
|
|
import 'package:spotube/components/Player/PlayerControls.dart';
|
|
import 'package:spotube/hooks/useBreakpoints.dart';
|
|
import 'package:spotube/models/Logger.dart';
|
|
import 'package:spotube/provider/Playback.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
|
|
class Player extends HookConsumerWidget {
|
|
Player({Key? key}) : super(key: key);
|
|
|
|
final logger = getLogger(Player);
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
Playback playback = ref.watch(playbackProvider);
|
|
|
|
final breakpoint = useBreakpoints();
|
|
|
|
String albumArt = useMemoized(
|
|
() => playback.track?.album?.images?.isNotEmpty == true
|
|
? TypeConversionUtils.image_X_UrlString(
|
|
playback.track?.album?.images,
|
|
index: (playback.track?.album?.images?.length ?? 1) - 1,
|
|
)
|
|
: "assets/album-placeholder.png",
|
|
[playback.track?.album?.images],
|
|
);
|
|
|
|
final entryRef = useRef<OverlayEntry?>(null);
|
|
|
|
void disposeOverlay() {
|
|
try {
|
|
entryRef.value?.remove();
|
|
entryRef.value = null;
|
|
} catch (e, stack) {
|
|
if (e is! AssertionError) {
|
|
logger.e("useEffect.cleanup", e, stack);
|
|
}
|
|
}
|
|
}
|
|
|
|
useEffect(() {
|
|
// 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) {
|
|
// clearing the overlay-entry as passing the already available
|
|
// entry will result in splashing while resizing the window
|
|
if (breakpoint.isLessThanOrEqualTo(Breakpoints.md) &&
|
|
entryRef.value == null &&
|
|
playback.track != null) {
|
|
entryRef.value = OverlayEntry(
|
|
opaque: false,
|
|
builder: (context) => PlayerOverlay(albumArt: albumArt),
|
|
);
|
|
try {
|
|
Overlay.of(context)?.insert(entryRef.value!);
|
|
} catch (e) {
|
|
if (e is AssertionError &&
|
|
e.message ==
|
|
'The specified entry is already present in the Overlay.') {
|
|
disposeOverlay();
|
|
Overlay.of(context)?.insert(entryRef.value!);
|
|
}
|
|
}
|
|
} else {
|
|
disposeOverlay();
|
|
}
|
|
});
|
|
return () {
|
|
disposeOverlay();
|
|
};
|
|
}, [breakpoint, playback.track]);
|
|
|
|
// 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
|
|
Flexible(
|
|
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: HookBuilder(builder: (context) {
|
|
final volume = useState(playback.volume);
|
|
|
|
useEffect(() {
|
|
if (volume.value != playback.volume) {
|
|
volume.value = playback.volume;
|
|
}
|
|
return null;
|
|
}, [playback.volume]);
|
|
return Slider.adaptive(
|
|
min: 0,
|
|
max: 1,
|
|
value: volume.value,
|
|
onChanged: (v) {
|
|
volume.value = v;
|
|
},
|
|
onChangeEnd: (value) async {
|
|
try {
|
|
// You don't really need to know why but this
|
|
// way it works only
|
|
await playback.setVolume(value);
|
|
await playback.setVolume(value);
|
|
} catch (e, stack) {
|
|
logger.e("onChange", e, stack);
|
|
}
|
|
},
|
|
);
|
|
}),
|
|
),
|
|
PlayerActions()
|
|
],
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|