✨ Full screen player
This commit is contained in:
		@@ -59,6 +59,7 @@ class MyApp extends StatelessWidget {
 | 
			
		||||
  void _initializeProviders(BuildContext context) async {
 | 
			
		||||
    Get.lazyPut(() => SpotifyProvider());
 | 
			
		||||
 | 
			
		||||
    Get.put(AudioPlayerProvider());
 | 
			
		||||
    Get.put(ActiveSourcedTrackProvider());
 | 
			
		||||
    Get.put(AudioPlayerStreamProvider());
 | 
			
		||||
 | 
			
		||||
@@ -69,7 +70,6 @@ class MyApp extends StatelessWidget {
 | 
			
		||||
    Get.put(ScrobblerProvider());
 | 
			
		||||
    Get.put(UserPreferencesProvider());
 | 
			
		||||
 | 
			
		||||
    Get.put(AudioPlayerProvider());
 | 
			
		||||
    Get.put(QueryingTrackInfoProvider());
 | 
			
		||||
    Get.put(SourcedTrackProvider());
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -128,11 +128,11 @@ class AudioPlayerProvider extends GetxController {
 | 
			
		||||
 | 
			
		||||
    // Giving the initial track a boost so MediaKit won't skip
 | 
			
		||||
    // because of timeout
 | 
			
		||||
    final intendedActiveTrack = medias.elementAt(initialIndex);
 | 
			
		||||
    if (intendedActiveTrack.track is! LocalTrack) {
 | 
			
		||||
      await Get.find<SourcedTrackProvider>()
 | 
			
		||||
          .fetch(RhythmMedia(intendedActiveTrack.track));
 | 
			
		||||
    }
 | 
			
		||||
    // final intendedActiveTrack = medias.elementAt(initialIndex);
 | 
			
		||||
    // if (intendedActiveTrack.track is! LocalTrack) {
 | 
			
		||||
    //   await Get.find<SourcedTrackProvider>()
 | 
			
		||||
    //       .fetch(RhythmMedia(intendedActiveTrack.track));
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    if (medias.isEmpty) return;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -20,12 +20,15 @@ class AudioPlayerStreamProvider extends GetxController {
 | 
			
		||||
 | 
			
		||||
  List<StreamSubscription>? _subscriptions;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void onInit() {
 | 
			
		||||
    super.onInit();
 | 
			
		||||
  AudioPlayerStreamProvider() {
 | 
			
		||||
    AudioServices.create().then(
 | 
			
		||||
      (value) => notificationService = value,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void onInit() {
 | 
			
		||||
    super.onInit();
 | 
			
		||||
 | 
			
		||||
    _subscriptions = [
 | 
			
		||||
      subscribeToPlaylist(),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:rhythm_box/screens/explore.dart';
 | 
			
		||||
import 'package:rhythm_box/screens/player/view.dart';
 | 
			
		||||
import 'package:rhythm_box/screens/playlist/view.dart';
 | 
			
		||||
import 'package:rhythm_box/screens/settings.dart';
 | 
			
		||||
import 'package:rhythm_box/shells/nav_shell.dart';
 | 
			
		||||
@@ -28,9 +27,4 @@ final router = GoRouter(routes: [
 | 
			
		||||
      ),
 | 
			
		||||
    ],
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/player',
 | 
			
		||||
    name: 'player',
 | 
			
		||||
    builder: (context, state) => const PlayerScreen(),
 | 
			
		||||
  ),
 | 
			
		||||
]);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,235 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
 | 
			
		||||
import 'package:dismissible_page/dismissible_page.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
import 'package:google_fonts/google_fonts.dart';
 | 
			
		||||
import 'package:rhythm_box/providers/audio_player.dart';
 | 
			
		||||
import 'package:rhythm_box/services/artist.dart';
 | 
			
		||||
import 'package:rhythm_box/services/audio_player/audio_player.dart';
 | 
			
		||||
import 'package:rhythm_box/widgets/auto_cache_image.dart';
 | 
			
		||||
import 'package:rhythm_box/services/audio_services/image.dart';
 | 
			
		||||
import 'package:rhythm_box/widgets/tracks/querying_track_info.dart';
 | 
			
		||||
 | 
			
		||||
class PlayerScreen extends StatefulWidget {
 | 
			
		||||
  const PlayerScreen({super.key});
 | 
			
		||||
  final Duration durationCurrent, durationTotal;
 | 
			
		||||
 | 
			
		||||
  const PlayerScreen({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.durationCurrent,
 | 
			
		||||
    required this.durationTotal,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<PlayerScreen> createState() => _PlayerScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _PlayerScreenState extends State<PlayerScreen> {
 | 
			
		||||
  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();
 | 
			
		||||
    }
 | 
			
		||||
    setState(() {});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String _formatDuration(Duration duration) {
 | 
			
		||||
    String negativeSign = duration.isNegative ? '-' : '';
 | 
			
		||||
    String twoDigits(int n) => n.toString().padLeft(2, '0');
 | 
			
		||||
    String twoDigitMinutes = twoDigits(duration.inMinutes.abs());
 | 
			
		||||
    String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60).abs());
 | 
			
		||||
    return '$negativeSign$twoDigitMinutes:$twoDigitSeconds';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  double? _draggingValue;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _durationCurrent = widget.durationCurrent;
 | 
			
		||||
    _durationTotal = widget.durationTotal;
 | 
			
		||||
    _subscriptions = [
 | 
			
		||||
      audioPlayer.durationStream.listen(_updateDurationTotal),
 | 
			
		||||
      audioPlayer.positionStream.listen(_updateDurationCurrent),
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    if (_subscriptions != null) {
 | 
			
		||||
      for (final subscription in _subscriptions!) {
 | 
			
		||||
        subscription.cancel();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    super.dispose();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return const Placeholder();
 | 
			
		||||
    final size = MediaQuery.of(context).size;
 | 
			
		||||
 | 
			
		||||
    return DismissiblePage(
 | 
			
		||||
      backgroundColor: Theme.of(context).colorScheme.surface,
 | 
			
		||||
      onDismissed: () {
 | 
			
		||||
        Navigator.of(context).pop();
 | 
			
		||||
      },
 | 
			
		||||
      direction: DismissiblePageDismissDirection.down,
 | 
			
		||||
      child: Material(
 | 
			
		||||
        color: Colors.transparent,
 | 
			
		||||
        child: SafeArea(
 | 
			
		||||
          child: Column(
 | 
			
		||||
            mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
            children: [
 | 
			
		||||
              Hero(
 | 
			
		||||
                tag: const Key('current-active-track-album-art'),
 | 
			
		||||
                child: ClipRRect(
 | 
			
		||||
                  borderRadius: const BorderRadius.all(Radius.circular(16)),
 | 
			
		||||
                  child: AspectRatio(
 | 
			
		||||
                    aspectRatio: 1,
 | 
			
		||||
                    child: _albumArt != null
 | 
			
		||||
                        ? AutoCacheImage(
 | 
			
		||||
                            _albumArt!,
 | 
			
		||||
                            width: size.width,
 | 
			
		||||
                            height: size.width,
 | 
			
		||||
                          )
 | 
			
		||||
                        : Container(
 | 
			
		||||
                            color: Theme.of(context)
 | 
			
		||||
                                .colorScheme
 | 
			
		||||
                                .surfaceContainerHigh,
 | 
			
		||||
                            width: 64,
 | 
			
		||||
                            height: 64,
 | 
			
		||||
                            child: const Center(child: Icon(Icons.image)),
 | 
			
		||||
                          ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ).marginSymmetric(horizontal: 24),
 | 
			
		||||
              ),
 | 
			
		||||
              const Gap(24),
 | 
			
		||||
              Text(
 | 
			
		||||
                _playback.state.value.activeTrack?.name ?? 'Not playing',
 | 
			
		||||
                style: Theme.of(context).textTheme.titleLarge,
 | 
			
		||||
              ),
 | 
			
		||||
              Text(
 | 
			
		||||
                _playback.state.value.activeTrack?.artists?.asString() ??
 | 
			
		||||
                    'No author',
 | 
			
		||||
                style: Theme.of(context).textTheme.bodyMedium,
 | 
			
		||||
                overflow: TextOverflow.ellipsis,
 | 
			
		||||
              ),
 | 
			
		||||
              const Gap(24),
 | 
			
		||||
              Column(
 | 
			
		||||
                children: [
 | 
			
		||||
                  SliderTheme(
 | 
			
		||||
                    data: SliderThemeData(
 | 
			
		||||
                      trackHeight: 2,
 | 
			
		||||
                      trackShape: _PlayerProgressTrackShape(),
 | 
			
		||||
                      thumbShape: const RoundSliderThumbShape(
 | 
			
		||||
                        enabledThumbRadius: 8,
 | 
			
		||||
                      ),
 | 
			
		||||
                      overlayShape: SliderComponentShape.noOverlay,
 | 
			
		||||
                    ),
 | 
			
		||||
                    child: Slider(
 | 
			
		||||
                      value: _draggingValue ??
 | 
			
		||||
                          _durationCurrent.inMilliseconds.toDouble(),
 | 
			
		||||
                      min: 0,
 | 
			
		||||
                      max: _durationTotal.inMilliseconds.toDouble(),
 | 
			
		||||
                      onChanged: (value) {
 | 
			
		||||
                        setState(() => _draggingValue = value);
 | 
			
		||||
                      },
 | 
			
		||||
                      onChangeEnd: (value) {
 | 
			
		||||
                        print('Seek to $value ms');
 | 
			
		||||
                        audioPlayer.seek(Duration(milliseconds: value.toInt()));
 | 
			
		||||
                      },
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                  Row(
 | 
			
		||||
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      Text(
 | 
			
		||||
                        _formatDuration(_durationCurrent),
 | 
			
		||||
                        style: GoogleFonts.robotoMono(fontSize: 12),
 | 
			
		||||
                      ),
 | 
			
		||||
                      Text(
 | 
			
		||||
                        _formatDuration(_durationTotal),
 | 
			
		||||
                        style: GoogleFonts.robotoMono(fontSize: 12),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ).paddingSymmetric(horizontal: 8, vertical: 4),
 | 
			
		||||
                ],
 | 
			
		||||
              ).paddingSymmetric(horizontal: 24),
 | 
			
		||||
              const Gap(24),
 | 
			
		||||
              Row(
 | 
			
		||||
                mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
                children: [
 | 
			
		||||
                  SizedBox(
 | 
			
		||||
                    width: 56,
 | 
			
		||||
                    height: 56,
 | 
			
		||||
                    child: IconButton.filled(
 | 
			
		||||
                      icon: _isFetchingActiveTrack
 | 
			
		||||
                          ? const SizedBox(
 | 
			
		||||
                              height: 20,
 | 
			
		||||
                              width: 20,
 | 
			
		||||
                              child: CircularProgressIndicator(
 | 
			
		||||
                                color: Colors.white,
 | 
			
		||||
                                strokeWidth: 2.5,
 | 
			
		||||
                              ),
 | 
			
		||||
                            )
 | 
			
		||||
                          : Icon(
 | 
			
		||||
                              !_isPlaying ? Icons.play_arrow : Icons.pause,
 | 
			
		||||
                              size: 28,
 | 
			
		||||
                            ),
 | 
			
		||||
                      onPressed:
 | 
			
		||||
                          _isFetchingActiveTrack ? null : _togglePlayState,
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
              )
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ).marginAll(24),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _PlayerProgressTrackShape extends RoundedRectSliderTrackShape {
 | 
			
		||||
  @override
 | 
			
		||||
  Rect getPreferredRect({
 | 
			
		||||
    required RenderBox parentBox,
 | 
			
		||||
    Offset offset = Offset.zero,
 | 
			
		||||
    required SliderThemeData sliderTheme,
 | 
			
		||||
    bool isEnabled = false,
 | 
			
		||||
    bool isDiscrete = false,
 | 
			
		||||
  }) {
 | 
			
		||||
    final trackHeight = sliderTheme.trackHeight;
 | 
			
		||||
    final trackLeft = offset.dx;
 | 
			
		||||
    final trackTop = offset.dy + (parentBox.size.height - trackHeight!) / 2;
 | 
			
		||||
    final trackWidth = parentBox.size.width;
 | 
			
		||||
    return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,12 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
import 'dart:math';
 | 
			
		||||
 | 
			
		||||
import 'package:dismissible_page/dismissible_page.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:rhythm_box/providers/audio_player.dart';
 | 
			
		||||
import 'package:rhythm_box/screens/player/view.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';
 | 
			
		||||
@@ -71,6 +72,12 @@ class _BottomPlayerState extends State<BottomPlayer>
 | 
			
		||||
    _subscriptions = [
 | 
			
		||||
      audioPlayer.durationStream.listen(_updateDurationTotal),
 | 
			
		||||
      audioPlayer.positionStream.listen(_updateDurationCurrent),
 | 
			
		||||
      _playback.state.listen((state) {
 | 
			
		||||
        if (state.playlist.medias.isNotEmpty && !_isLifted) {
 | 
			
		||||
          _animationController.animateTo(1);
 | 
			
		||||
          _isLifted = true;
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
      _playback.isPlaying.listen((value) {
 | 
			
		||||
        if (value && !_isLifted) {
 | 
			
		||||
          _animationController.animateTo(1);
 | 
			
		||||
@@ -104,6 +111,7 @@ class _BottomPlayerState extends State<BottomPlayer>
 | 
			
		||||
      axisAlignment: -1,
 | 
			
		||||
      child: Obx(
 | 
			
		||||
        () => GestureDetector(
 | 
			
		||||
          behavior: HitTestBehavior.translucent,
 | 
			
		||||
          child: Column(
 | 
			
		||||
            children: [
 | 
			
		||||
              if (_durationCurrent != Duration.zero)
 | 
			
		||||
@@ -122,10 +130,10 @@ class _BottomPlayerState extends State<BottomPlayer>
 | 
			
		||||
              Row(
 | 
			
		||||
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
			
		||||
                children: [
 | 
			
		||||
                  ClipRRect(
 | 
			
		||||
                    borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
                    child: Hero(
 | 
			
		||||
                  Hero(
 | 
			
		||||
                    tag: const Key('current-active-track-album-art'),
 | 
			
		||||
                    child: ClipRRect(
 | 
			
		||||
                      borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
                      child: _albumArt != null
 | 
			
		||||
                          ? AutoCacheImage(_albumArt!, width: 64, height: 64)
 | 
			
		||||
                          : Container(
 | 
			
		||||
@@ -172,7 +180,10 @@ class _BottomPlayerState extends State<BottomPlayer>
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            GoRouter.of(context).pushNamed('player');
 | 
			
		||||
            context.pushTransparentRoute(PlayerScreen(
 | 
			
		||||
              durationCurrent: _durationCurrent,
 | 
			
		||||
              durationTotal: _durationTotal,
 | 
			
		||||
            ));
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
 
 | 
			
		||||
@@ -65,6 +65,8 @@ class _PlaylistTrackListState extends State<PlaylistTrackList> {
 | 
			
		||||
            title: Text(item?.name ?? 'Loading...'),
 | 
			
		||||
            subtitle: Text(
 | 
			
		||||
              item?.artists?.asString() ?? 'Please stand by...',
 | 
			
		||||
              maxLines: 1,
 | 
			
		||||
              overflow: TextOverflow.ellipsis,
 | 
			
		||||
            ),
 | 
			
		||||
            onTap: () {
 | 
			
		||||
              if (item == null) return;
 | 
			
		||||
 
 | 
			
		||||
@@ -326,6 +326,14 @@ packages:
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.0.0"
 | 
			
		||||
  dismissible_page:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: dismissible_page
 | 
			
		||||
      sha256: "5b2316f770fe83583f770df1f6505cb19102081c5971979806e77f2e507a9958"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.0.2"
 | 
			
		||||
  drift:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
 
 | 
			
		||||
@@ -81,6 +81,7 @@ dependencies:
 | 
			
		||||
    git:
 | 
			
		||||
      url: https://github.com/KRTirtho/scrobblenaut.git
 | 
			
		||||
      ref: dart-3-support
 | 
			
		||||
  dismissible_page: ^1.0.2
 | 
			
		||||
 | 
			
		||||
dev_dependencies:
 | 
			
		||||
  flutter_test:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user