From 6fe7a363571a78415a3189a80bfcdd1a714159b8 Mon Sep 17 00:00:00 2001 From: devaryakumar Date: Sat, 23 Sep 2023 04:03:21 +0530 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=92=84=20Improve=20UI=20for=20Mini=20?= =?UTF-8?q?audio=20player?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app.dart | 29 ++- lib/cubits/config/config_cubit.dart | 30 +++ lib/features/library/cubit/library_cubit.dart | 10 +- .../library/ui/library_search_page.dart | 1 - lib/utils/generate_pallette.dart | 12 -- lib/utils/router.dart | 16 +- lib/widgets/animated_overflow_text.dart | 1 + lib/widgets/page_with_navbar.dart | 42 ++++- lib/widgets/play_pause_button.dart | 5 + lib/widgets/player/mini_player.dart | 171 ++++++++++++------ pubspec.yaml | 2 +- 11 files changed, 217 insertions(+), 102 deletions(-) delete mode 100644 lib/utils/generate_pallette.dart diff --git a/lib/app.dart b/lib/app.dart index 1d76287..3e20802 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:varanasi_mobile_app/utils/constants/constants.dart'; import 'package:varanasi_mobile_app/utils/router.dart'; import 'package:varanasi_mobile_app/widgets/responsive_sizer.dart'; +import 'cubits/config/config_cubit.dart'; +import 'cubits/player/player_cubit.dart'; import 'utils/theme.dart'; class Varanasi extends StatelessWidget { @@ -12,13 +15,25 @@ class Varanasi extends StatelessWidget { Widget build(BuildContext context) { return ResponsiveSizer( builder: (context, orientation, screenType) { - return MaterialApp.router( - title: AppStrings.appName, - theme: lightTheme, - darkTheme: darkTheme, - themeMode: ThemeMode.dark, - routerConfig: routerConfig, - debugShowCheckedModeBanner: false, + return MultiBlocProvider( + providers: [ + BlocProvider( + lazy: false, + create: (context) => ConfigCubit()..initialise(), + ), + BlocProvider( + lazy: false, + create: (context) => MediaPlayerCubit()..init(), + ), + ], + child: MaterialApp.router( + title: AppStrings.appName, + theme: lightTheme, + darkTheme: darkTheme, + themeMode: ThemeMode.dark, + routerConfig: routerConfig, + debugShowCheckedModeBanner: false, + ), ); }, ); diff --git a/lib/cubits/config/config_cubit.dart b/lib/cubits/config/config_cubit.dart index 31f6e75..ec25a21 100644 --- a/lib/cubits/config/config_cubit.dart +++ b/lib/cubits/config/config_cubit.dart @@ -1,6 +1,8 @@ import 'package:bloc/bloc.dart'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:equatable/equatable.dart'; import 'package:hive/hive.dart'; +import 'package:palette_generator/palette_generator.dart'; import 'package:varanasi_mobile_app/models/app_config.dart'; import 'package:varanasi_mobile_app/models/sort_type.dart'; import 'package:varanasi_mobile_app/utils/logger.dart'; @@ -9,11 +11,14 @@ part 'config_state.dart'; class ConfigCubit extends Cubit { late final Box _configBox; + final Map _imageProviderCache = {}; + late final Expando _paletteGeneratorExpando; Logger logger = Logger.instance; ConfigCubit() : super(ConfigInitial()); void initialise() async { + _paletteGeneratorExpando = Expando(); _configBox = await Hive.openBox('config'); if (_configBox.isEmpty) { logger.i('Config box is empty'); @@ -38,4 +43,29 @@ class ConfigCubit extends Cubit { emit(ConfigLoaded(config: config)); } } + + PaletteGenerator? maybeGetPaletteGenerator(String mediaUrl) { + final provider = _imageProviderCache[mediaUrl]; + if (provider == null) return null; + return _paletteGeneratorExpando[provider]; + } + + CachedNetworkImageProvider getProvider(String mediaUrl) { + return _imageProviderCache[mediaUrl] ??= + CachedNetworkImageProvider(mediaUrl); + } + + Future generatePalleteGenerator(String mediaUrl) async { + try { + final exists = maybeGetPaletteGenerator(mediaUrl); + if (exists != null) return exists; + final provider = getProvider(mediaUrl); + final paletteGenerator = + await PaletteGenerator.fromImageProvider(provider); + _paletteGeneratorExpando[provider] = paletteGenerator; + return paletteGenerator; + } catch (e) { + return null; + } + } } diff --git a/lib/features/library/cubit/library_cubit.dart b/lib/features/library/cubit/library_cubit.dart index 9b24d87..20597b0 100644 --- a/lib/features/library/cubit/library_cubit.dart +++ b/lib/features/library/cubit/library_cubit.dart @@ -1,15 +1,15 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:palette_generator/palette_generator.dart'; +import 'package:varanasi_mobile_app/cubits/config/config_cubit.dart'; import 'package:varanasi_mobile_app/features/library/data/library_repository.dart'; import 'package:varanasi_mobile_app/models/media_playlist.dart'; import 'package:varanasi_mobile_app/models/playable_item.dart'; import 'package:varanasi_mobile_app/models/sort_type.dart'; import 'package:varanasi_mobile_app/utils/configs.dart'; import 'package:varanasi_mobile_app/utils/extensions/extensions.dart'; -import 'package:varanasi_mobile_app/utils/generate_pallette.dart'; +import 'package:varanasi_mobile_app/utils/helpers/get_app_context.dart'; import 'package:varanasi_mobile_app/utils/logger.dart'; part 'library_state.dart'; @@ -27,8 +27,10 @@ class LibraryCubit extends Cubit { if (link == appConfig.placeholderImageLink) { link = media.artworkUrl!; } - final image = CachedNetworkImageProvider(link); - final colorPalette = await generateColorPalette(imageProvider: image); + if (!appContext.mounted) return; + final configCubit = appContext.read(); + final colorPalette = await configCubit.generatePalleteGenerator(link); + final image = configCubit.getProvider(link); emit(LibraryLoaded( playlist, colorPalette!, diff --git a/lib/features/library/ui/library_search_page.dart b/lib/features/library/ui/library_search_page.dart index baf51ca..40b236d 100644 --- a/lib/features/library/ui/library_search_page.dart +++ b/lib/features/library/ui/library_search_page.dart @@ -73,7 +73,6 @@ class LibrarySearchPage extends HookWidget { } else { context.readMediaPlayerCubit.playFromMediaPlaylist(playlist); } - // context.go('/player'); }, ), ); diff --git a/lib/utils/generate_pallette.dart b/lib/utils/generate_pallette.dart deleted file mode 100644 index 1d5f77f..0000000 --- a/lib/utils/generate_pallette.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:palette_generator/palette_generator.dart'; - -Future generateColorPalette( - {String? imageUrl, Size? size, ImageProvider? imageProvider}) async { - assert(imageUrl != null || imageProvider != null, 'Image is required'); - return await PaletteGenerator.fromImageProvider( - imageProvider ?? NetworkImage(imageUrl ?? ''), - size: const Size(110, 110), - maximumColorCount: 20, - ); -} diff --git a/lib/utils/router.dart b/lib/utils/router.dart index 32c5fbf..b8e0f65 100644 --- a/lib/utils/router.dart +++ b/lib/utils/router.dart @@ -1,7 +1,5 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:varanasi_mobile_app/cubits/config/config_cubit.dart'; -import 'package:varanasi_mobile_app/cubits/player/player_cubit.dart'; import 'package:varanasi_mobile_app/features/home/bloc/home_bloc.dart'; import 'package:varanasi_mobile_app/features/home/ui/home_screen.dart'; import 'package:varanasi_mobile_app/features/library/cubit/library_cubit.dart'; @@ -67,19 +65,7 @@ final routerConfig = GoRouter( ), ], builder: (context, state, navigationShell) { - return MultiBlocProvider( - providers: [ - BlocProvider( - lazy: false, - create: (context) => ConfigCubit()..initialise(), - ), - BlocProvider( - lazy: false, - create: (context) => MediaPlayerCubit()..init(), - ), - ], - child: PageWithNavbar(child: navigationShell), - ); + return PageWithNavbar(child: navigationShell); }, ), ], diff --git a/lib/widgets/animated_overflow_text.dart b/lib/widgets/animated_overflow_text.dart index f67cebd..4e1385a 100644 --- a/lib/widgets/animated_overflow_text.dart +++ b/lib/widgets/animated_overflow_text.dart @@ -223,6 +223,7 @@ class AnimatedText extends StatelessWidget { showFadingOnlyWhenScrolling: true, fadingEdgeStartFraction: 0.1, fadingEdgeEndFraction: 0.1, + style: style?.copyWith(fontSize: maxFontSize), ); } diff --git a/lib/widgets/page_with_navbar.dart b/lib/widgets/page_with_navbar.dart index bd4a75a..3c5cdb3 100644 --- a/lib/widgets/page_with_navbar.dart +++ b/lib/widgets/page_with_navbar.dart @@ -1,6 +1,8 @@ +import 'package:another_flushbar/flushbar_helper.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:varanasi_mobile_app/widgets/player/mini_player.dart'; + +import 'player/mini_player.dart'; class PageWithNavbar extends StatelessWidget { final StatefulNavigationShell child; @@ -9,8 +11,42 @@ class PageWithNavbar extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - body: child, - bottomNavigationBar: const MiniPlayer(), + body: Stack( + children: [ + Positioned.fill(child: child), + const Positioned( + bottom: 0, + right: 8, + left: 8, + height: 56, + child: MiniPlayer(), + ), + ], + ), + bottomNavigationBar: NavigationBar( + surfaceTintColor: Colors.transparent, + selectedIndex: child.currentIndex, + onDestinationSelected: (value) { + FlushbarHelper.createError( + message: 'Coming soon!', + duration: const Duration(seconds: 1), + ).show(context); + }, + destinations: const [ + NavigationDestination( + icon: Icon(Icons.home_filled), + label: 'Home', + ), + NavigationDestination( + icon: Icon(Icons.search_rounded), + label: 'Search', + ), + NavigationDestination( + icon: Icon(Icons.library_music_outlined), + label: 'Library', + ), + ], + ), ); } } diff --git a/lib/widgets/play_pause_button.dart b/lib/widgets/play_pause_button.dart index 05398cd..d4317a6 100644 --- a/lib/widgets/play_pause_button.dart +++ b/lib/widgets/play_pause_button.dart @@ -11,11 +11,13 @@ class PlayPauseMediaButton extends StatefulHookWidget { this.backgroundColor, this.foregroundColor, this.variant = ButtonVariant.floatingactionbutton, + this.size, }); final ButtonVariant variant; final VoidCallback onPressed; final bool isPlaying; final Color? backgroundColor, foregroundColor; + final double? size; @override State createState() => _PlayPauseMediaButtonState(); @@ -54,6 +56,7 @@ class _PlayPauseMediaButtonState extends State onPressed: widget.onPressed, shape: const CircleBorder(), child: AnimatedIcon( + size: widget.size, icon: AnimatedIcons.play_pause, progress: _controller, ), @@ -63,6 +66,8 @@ class _PlayPauseMediaButtonState extends State Widget _buildIconButton() { return IconButton( onPressed: widget.onPressed, + color: widget.foregroundColor, + iconSize: widget.size, icon: AnimatedIcon( icon: AnimatedIcons.play_pause, progress: _controller, diff --git a/lib/widgets/player/mini_player.dart b/lib/widgets/player/mini_player.dart index 0372078..ae6013e 100644 --- a/lib/widgets/player/mini_player.dart +++ b/lib/widgets/player/mini_player.dart @@ -1,6 +1,9 @@ +import 'package:audio_service/audio_service.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:palette_generator/palette_generator.dart'; +import 'package:varanasi_mobile_app/cubits/config/config_cubit.dart'; import 'package:varanasi_mobile_app/cubits/player/player_cubit.dart'; import 'package:varanasi_mobile_app/utils/extensions/theme.dart'; import 'package:varanasi_mobile_app/widgets/animated_overflow_text.dart'; @@ -15,9 +18,29 @@ class MiniPlayer extends StatefulWidget { class _MiniPlayerState extends State { late final PageController _pageController; + PaletteGenerator? _paletteGenerator; + void _generatePallete(MediaItem mediaItem) { + context + .read() + .generatePalleteGenerator(mediaItem.artUri.toString()) + .then( + (value) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _paletteGenerator = value; + }); + }); + }, + ); + } + @override void initState() { super.initState(); + final state = context.readMediaPlayerCubit.state; + if (state.currentMediaItem != null) { + _generatePallete(state.currentMediaItem!); + } _pageController = PageController( initialPage: context.readMediaPlayerCubit.state.queueState.queueIndex ?? 0, @@ -26,6 +49,11 @@ class _MiniPlayerState extends State { @override Widget build(BuildContext context) { + final PaletteColor? selectedColor = _paletteGenerator?.darkVibrantColor ?? + _paletteGenerator?.darkMutedColor ?? + _paletteGenerator?.dominantColor; + final Color? backgroundColor = selectedColor?.color.withOpacity(1); + final Color? foregroundColor = selectedColor?.bodyTextColor.withOpacity(1); final (media, isPlaying, queueState) = context.select( (MediaPlayerCubit cubit) { final queueState = cubit.state.queueState; @@ -41,19 +69,30 @@ class _MiniPlayerState extends State { } Widget buildLeading() { - return CachedNetworkImage( - imageUrl: media.artUri.toString(), - height: 56, - width: 56, + return Card( + elevation: 24, + borderOnForeground: false, + margin: EdgeInsets.zero, + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + child: CachedNetworkImage( + imageUrl: media.artUri.toString(), + fit: BoxFit.cover, + ), ); } Widget buildSubtitle() { - return Text( + return AnimatedText( media.displayDescription ?? '', + minFontSize: 12, + maxFontSize: 12, maxLines: 1, - overflow: TextOverflow.ellipsis, - style: context.textTheme.labelMedium, + style: context.textTheme.labelMedium?.copyWith( + color: foregroundColor, + ), ); } @@ -66,16 +105,16 @@ class _MiniPlayerState extends State { _pageController.page!.round() != queueIndex) { _pageController.jumpToPage(queueIndex); } + _generatePallete(state.currentMediaItem!); }, - listenWhen: (previous, current) => - previous.queueState.queueIndex != current.queueState.queueIndex, buildWhen: (previous, current) { return previous.queueState.queueIndex != - current.queueState.queueIndex; + current.queueState.queueIndex || + previous.currentMediaItem != current.currentMediaItem; }, builder: (context, state) { return SizedBox( - height: 56, + height: 40, child: NotificationListener( onNotification: (ScrollNotification notification) { if (notification.depth == 0 && @@ -101,17 +140,21 @@ class _MiniPlayerState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( - height: 24, + height: 20, child: AnimatedText( title, key: ValueKey(media.id), - minFontSize: 16, - maxFontSize: 16, + minFontSize: 12, + maxFontSize: 12, maxLines: 1, - style: context.textTheme.headlineSmall, + style: context.textTheme.bodyLarge?.copyWith( + color: foregroundColor, + fontWeight: FontWeight.bold, + letterSpacing: 0.24, + ), ), ), - buildSubtitle(), + Expanded(child: buildSubtitle()), ], ); }, @@ -123,53 +166,63 @@ class _MiniPlayerState extends State { } Widget buildTrailing() { - return Row( - mainAxisSize: MainAxisSize.min, + return PlayPauseMediaButton( + foregroundColor: foregroundColor, + variant: ButtonVariant.iconbutton, + onPressed: () { + if (isPlaying) { + context.read().pause(); + return; + } + context.read().play(); + }, + isPlaying: isPlaying, + ); + } + + return AnimatedContainer( + key: ValueKey('${media.id}miniplayer'), + duration: const Duration(milliseconds: 300), + decoration: BoxDecoration( + color: backgroundColor ?? context.theme.primaryColor, + borderRadius: BorderRadius.circular(8), + ), + child: Stack( children: [ - IconButton( - icon: const Icon(Icons.skip_previous), - onPressed: !queueState.hasPrevious - ? null - : () { - _pageController.previousPage( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - context.read().skipToPrevious(); - }, - ), - PlayPauseMediaButton( - variant: ButtonVariant.iconbutton, - onPressed: () { - if (isPlaying) { - context.read().pause(); - return; - } - context.read().play(); - }, - isPlaying: isPlaying, + Padding( + padding: const EdgeInsets.symmetric(vertical: 8).copyWith(left: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + buildLeading(), + const SizedBox(width: 8), + Expanded(child: buildTitle()), + buildTrailing(), + ], + ), ), - IconButton( - icon: const Icon(Icons.skip_next), - onPressed: !queueState.hasNext - ? null - : () { - _pageController.nextPage( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - context.read().skipToNext(); - }, + Positioned( + left: 8, + right: 8, + bottom: 0.5, + child: StreamBuilder( + stream: AudioService.position, + builder: (context, snapshot) { + return LinearProgressIndicator( + value: snapshot.hasData + ? snapshot.data!.inMilliseconds / + media.duration!.inMilliseconds + : 0, + minHeight: 1.5, + color: foregroundColor, + backgroundColor: foregroundColor?.withOpacity(0.3), + borderRadius: BorderRadius.circular(8), + ); + }, + ), ), ], - ); - } - - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 4), - leading: buildLeading(), - title: buildTitle(), - trailing: buildTrailing(), + ), ); } diff --git a/pubspec.yaml b/pubspec.yaml index c6755e4..775335a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.0.6+1 +version: 0.0.6+3 environment: sdk: ">=3.0.6 <4.0.0" From 6039aada517ff691b605f63009725c039bc7c3bb Mon Sep 17 00:00:00 2001 From: devaryakumar Date: Sat, 23 Sep 2023 17:29:54 +0530 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20weird=20jump=20in=20m?= =?UTF-8?q?ini=20player=20slider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app.dart | 3 +- lib/cubits/config/config_cubit.dart | 18 +++ lib/cubits/config/config_state.dart | 22 ++- lib/cubits/player/player_cubit.dart | 53 ++++++- lib/cubits/player/player_state.dart | 13 +- lib/utils/theme.dart | 2 + lib/widgets/player/current_duration.dart | 38 +++++ lib/widgets/player/mini_player.dart | 173 ++++------------------- lib/widgets/player/title.dart | 82 +++++++++++ pubspec.lock | 8 ++ pubspec.yaml | 1 + 11 files changed, 255 insertions(+), 158 deletions(-) create mode 100644 lib/widgets/player/current_duration.dart create mode 100644 lib/widgets/player/title.dart diff --git a/lib/app.dart b/lib/app.dart index 3e20802..33fa1bd 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -23,7 +23,8 @@ class Varanasi extends StatelessWidget { ), BlocProvider( lazy: false, - create: (context) => MediaPlayerCubit()..init(), + create: (context) => + MediaPlayerCubit(() => context.read())..init(), ), ], child: MaterialApp.router( diff --git a/lib/cubits/config/config_cubit.dart b/lib/cubits/config/config_cubit.dart index ec25a21..280e477 100644 --- a/lib/cubits/config/config_cubit.dart +++ b/lib/cubits/config/config_cubit.dart @@ -1,5 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:carousel_slider/carousel_controller.dart'; import 'package:equatable/equatable.dart'; import 'package:hive/hive.dart'; import 'package:palette_generator/palette_generator.dart'; @@ -27,6 +28,7 @@ class ConfigCubit extends Cubit { logger.i('Config box is not empty'); } emit(ConfigLoaded(config: _configBox.values.first)); + recreateMiniPlayerController(); } Stream get sortTypeStream => stream.map( @@ -68,4 +70,20 @@ class ConfigCubit extends Cubit { return null; } } + + CarouselController? get miniPlayerPageController => (state is ConfigLoaded) + ? (state as ConfigLoaded).miniPlayerPageController + : null; + + void recreateMiniPlayerController( + [int? initialPage, double? viewportFraction]) { + if (state is ConfigLoaded) { + final config = (state as ConfigLoaded); + final controller = CarouselController(); + final newConfig = config.copyWith( + miniPlayerPageController: controller, + ); + emit(newConfig); + } + } } diff --git a/lib/cubits/config/config_state.dart b/lib/cubits/config/config_state.dart index a3d2343..df0632c 100644 --- a/lib/cubits/config/config_state.dart +++ b/lib/cubits/config/config_state.dart @@ -1,19 +1,35 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first part of 'config_cubit.dart'; sealed class ConfigState extends Equatable { const ConfigState(); @override - List get props => []; + List get props => []; } final class ConfigInitial extends ConfigState {} final class ConfigLoaded extends ConfigState { final AppConfig config; + final CarouselController? miniPlayerPageController; - const ConfigLoaded({required this.config}); + const ConfigLoaded({ + required this.config, + this.miniPlayerPageController, + }); @override - List get props => [config]; + List get props => [config, miniPlayerPageController]; + + ConfigLoaded copyWith({ + AppConfig? config, + CarouselController? miniPlayerPageController, + }) { + return ConfigLoaded( + config: config ?? this.config, + miniPlayerPageController: + miniPlayerPageController ?? this.miniPlayerPageController, + ); + } } diff --git a/lib/cubits/player/player_cubit.dart b/lib/cubits/player/player_cubit.dart index e7f814f..ca6c49a 100644 --- a/lib/cubits/player/player_cubit.dart +++ b/lib/cubits/player/player_cubit.dart @@ -5,7 +5,9 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hive/hive.dart'; +import 'package:palette_generator/palette_generator.dart'; import 'package:rxdart/rxdart.dart'; +import 'package:varanasi_mobile_app/cubits/config/config_cubit.dart'; import 'package:varanasi_mobile_app/models/media_playlist.dart'; import 'package:varanasi_mobile_app/models/playable_item.dart'; import 'package:varanasi_mobile_app/models/song.dart'; @@ -24,12 +26,35 @@ extension MediaPlayerCubitExtension on BuildContext { MediaPlayerCubit get selectMediaPlayerCubit => watch(); } +class MediaColorPalette { + final Color? backgroundColor; + final Color? foregroundColor; + + const MediaColorPalette({ + this.backgroundColor, + this.foregroundColor, + }); + + factory MediaColorPalette.fromPaletteGenerator(PaletteGenerator palette) { + final PaletteColor? selectedColor = palette.darkVibrantColor ?? + palette.darkMutedColor ?? + palette.dominantColor; + final Color? backgroundColor = selectedColor?.color.withOpacity(1); + final Color? foregroundColor = selectedColor?.bodyTextColor.withOpacity(1); + return MediaColorPalette( + backgroundColor: backgroundColor, + foregroundColor: foregroundColor, + ); + } +} + class MediaPlayerCubit extends AppCubit with DataProviderProtocol, CacheableService { + final ConfigCubit Function() configCubitGetter; late final AudioHandlerImpl audioHandler; late final Box _box; - MediaPlayerCubit() : super(const MediaPlayerState()); + MediaPlayerCubit(this.configCubitGetter) : super(const MediaPlayerState()); Future playFromMediaPlaylist( MediaPlaylist playlist, [ @@ -96,6 +121,7 @@ class MediaPlayerCubit extends AppCubit Future skipToNext() => audioHandler.skipToNext(); Future skipToIndex(int index) async { + if (index == state.queueState.queueIndex) return; await audioHandler.skipToQueueItem(index); if (!audioHandler.player.playing) { await play(); @@ -105,7 +131,10 @@ class MediaPlayerCubit extends AppCubit Future skipToMediaItem(PlayableMedia mediaItem) async { final index = state.queueState.queue.indexOf(mediaItem.toMediaItem()); if (index == -1) return; - return skipToIndex(index); + final configCubit = configCubitGetter(); + final controller = configCubit.miniPlayerPageController; + await skipToIndex(index); + controller?.animateToPage(index); } @override @@ -126,11 +155,19 @@ class MediaPlayerCubit extends AppCubit audioHandler.mediaItem.stream, audioHandler.queueState, (playing, mediaItem, queueState) => (playing, mediaItem, queueState), - ).distinct().listen((value) { + ).distinct().listen((value) async { + PaletteGenerator? palette; + if (value.$2 != null) { + palette = await configCubitGetter() + .generatePalleteGenerator(value.$2?.artUri.toString() ?? ''); + } else { + palette = null; + } emit(state.copyWith( isPlaying: value.$1, currentMediaItem: value.$2, queueState: value.$3, + paletteGenerator: palette, )); }); } @@ -140,4 +177,14 @@ class MediaPlayerCubit extends AppCubit @override String get cacheBoxName => AppStrings.commonCacheBoxName; + + MediaColorPalette? get mediaColorPalette { + if (state.paletteGenerator == null) return null; + return MediaColorPalette.fromPaletteGenerator(state.paletteGenerator!); + } + + void recreateMiniPlayerController() { + final currentIndex = state.queueState.queueIndex; + configCubitGetter().recreateMiniPlayerController(currentIndex); + } } diff --git a/lib/cubits/player/player_state.dart b/lib/cubits/player/player_state.dart index d5e2cd2..f38c169 100644 --- a/lib/cubits/player/player_state.dart +++ b/lib/cubits/player/player_state.dart @@ -5,6 +5,7 @@ class MediaPlayerState extends Equatable { final String? currentPlaylist; final bool isPlaying; final MediaItem? currentMediaItem; + final PaletteGenerator? paletteGenerator; final QueueState queueState; const MediaPlayerState({ @@ -12,23 +13,31 @@ class MediaPlayerState extends Equatable { this.isPlaying = false, this.currentMediaItem, this.queueState = QueueState.empty, + this.paletteGenerator, }); MediaPlayerState copyWith({ String? currentPlaylist, bool? isPlaying, MediaItem? currentMediaItem, + PaletteGenerator? paletteGenerator, QueueState? queueState, }) { return MediaPlayerState( currentPlaylist: currentPlaylist ?? this.currentPlaylist, isPlaying: isPlaying ?? this.isPlaying, currentMediaItem: currentMediaItem ?? this.currentMediaItem, + paletteGenerator: paletteGenerator ?? this.paletteGenerator, queueState: queueState ?? this.queueState, ); } @override - List get props => - [currentPlaylist, isPlaying, currentMediaItem, queueState]; + List get props => [ + currentPlaylist, + isPlaying, + currentMediaItem, + queueState, + paletteGenerator + ]; } diff --git a/lib/utils/theme.dart b/lib/utils/theme.dart index e24a036..4d8887f 100644 --- a/lib/utils/theme.dart +++ b/lib/utils/theme.dart @@ -12,6 +12,7 @@ final lightTheme = FlexThemeData.light( useMaterial3: true, textTheme: GoogleFonts.openSansTextTheme(), surfaceTint: Colors.transparent, + platform: TargetPlatform.iOS, ); final darkTheme = FlexThemeData.dark( @@ -23,4 +24,5 @@ final darkTheme = FlexThemeData.dark( useMaterial3: true, textTheme: GoogleFonts.openSansTextTheme(), surfaceTint: Colors.transparent, + platform: TargetPlatform.iOS, ); diff --git a/lib/widgets/player/current_duration.dart b/lib/widgets/player/current_duration.dart new file mode 100644 index 0000000..45b66cb --- /dev/null +++ b/lib/widgets/player/current_duration.dart @@ -0,0 +1,38 @@ +import 'package:audio_service/audio_service.dart'; +import 'package:flutter/material.dart'; +import 'package:varanasi_mobile_app/cubits/player/player_cubit.dart'; + +class CurrentDuration extends StatelessWidget { + const CurrentDuration({ + super.key, + required this.media, + required this.colorPalette, + }); + + final MediaItem? media; + final MediaColorPalette? colorPalette; + + @override + Widget build(BuildContext context) { + return Positioned( + left: 8, + right: 8, + bottom: 0.5, + child: StreamBuilder( + stream: AudioService.position, + builder: (context, snapshot) { + return LinearProgressIndicator( + value: snapshot.hasData + ? snapshot.data!.inMilliseconds / + media!.duration!.inMilliseconds + : 0, + minHeight: 1.5, + color: colorPalette?.foregroundColor, + backgroundColor: colorPalette?.foregroundColor?.withOpacity(0.3), + borderRadius: BorderRadius.circular(8), + ); + }, + ), + ); + } +} diff --git a/lib/widgets/player/mini_player.dart b/lib/widgets/player/mini_player.dart index ae6013e..665a0b6 100644 --- a/lib/widgets/player/mini_player.dart +++ b/lib/widgets/player/mini_player.dart @@ -1,13 +1,12 @@ -import 'package:audio_service/audio_service.dart'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide Title; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:palette_generator/palette_generator.dart'; -import 'package:varanasi_mobile_app/cubits/config/config_cubit.dart'; import 'package:varanasi_mobile_app/cubits/player/player_cubit.dart'; import 'package:varanasi_mobile_app/utils/extensions/theme.dart'; -import 'package:varanasi_mobile_app/widgets/animated_overflow_text.dart'; import 'package:varanasi_mobile_app/widgets/play_pause_button.dart'; +import 'package:varanasi_mobile_app/widgets/player/current_duration.dart'; + +import 'title.dart'; class MiniPlayer extends StatefulWidget { const MiniPlayer({super.key}); @@ -17,50 +16,27 @@ class MiniPlayer extends StatefulWidget { } class _MiniPlayerState extends State { - late final PageController _pageController; - PaletteGenerator? _paletteGenerator; - void _generatePallete(MediaItem mediaItem) { - context - .read() - .generatePalleteGenerator(mediaItem.artUri.toString()) - .then( - (value) { - WidgetsBinding.instance.addPostFrameCallback((_) { - setState(() { - _paletteGenerator = value; - }); - }); - }, - ); - } - @override void initState() { super.initState(); - final state = context.readMediaPlayerCubit.state; - if (state.currentMediaItem != null) { - _generatePallete(state.currentMediaItem!); - } - _pageController = PageController( - initialPage: - context.readMediaPlayerCubit.state.queueState.queueIndex ?? 0, - ); + context.readMediaPlayerCubit.recreateMiniPlayerController(); } @override Widget build(BuildContext context) { - final PaletteColor? selectedColor = _paletteGenerator?.darkVibrantColor ?? - _paletteGenerator?.darkMutedColor ?? - _paletteGenerator?.dominantColor; - final Color? backgroundColor = selectedColor?.color.withOpacity(1); - final Color? foregroundColor = selectedColor?.bodyTextColor.withOpacity(1); - final (media, isPlaying, queueState) = context.select( + final (media, isPlaying, queueState, colorPalette) = context.select( (MediaPlayerCubit cubit) { - final queueState = cubit.state.queueState; + final state = cubit.state; + final queueState = state.queueState; final media = queueState.queue.isNotEmpty ? queueState.queue[queueState.queueIndex ?? 0] : null; - return (media, cubit.state.isPlaying, cubit.state.queueState); + return ( + media, + state.isPlaying, + state.queueState, + cubit.mediaColorPalette + ); }, ); @@ -84,90 +60,9 @@ class _MiniPlayerState extends State { ); } - Widget buildSubtitle() { - return AnimatedText( - media.displayDescription ?? '', - minFontSize: 12, - maxFontSize: 12, - maxLines: 1, - style: context.textTheme.labelMedium?.copyWith( - color: foregroundColor, - ), - ); - } - - Widget buildTitle() { - return BlocConsumer( - listener: (context, state) { - final queueIndex = state.queueState.queueIndex; - if (queueIndex != null && - _pageController.hasClients && - _pageController.page!.round() != queueIndex) { - _pageController.jumpToPage(queueIndex); - } - _generatePallete(state.currentMediaItem!); - }, - buildWhen: (previous, current) { - return previous.queueState.queueIndex != - current.queueState.queueIndex || - previous.currentMediaItem != current.currentMediaItem; - }, - builder: (context, state) { - return SizedBox( - height: 40, - child: NotificationListener( - onNotification: (ScrollNotification notification) { - if (notification.depth == 0 && - notification is ScrollUpdateNotification) { - final PageMetrics metrics = - notification.metrics as PageMetrics; - final int currentPage = metrics.page!.round(); - if (currentPage != state.queueState.queueIndex) { - context.readMediaPlayerCubit.skipToIndex(currentPage); - } - } - return false; - }, - child: PageView.builder( - controller: _pageController, - itemCount: queueState.queue.length, - itemBuilder: (context, index) { - final media = queueState.queue[index]; - final title = media.displayTitle ?? ''; - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - height: 20, - child: AnimatedText( - title, - key: ValueKey(media.id), - minFontSize: 12, - maxFontSize: 12, - maxLines: 1, - style: context.textTheme.bodyLarge?.copyWith( - color: foregroundColor, - fontWeight: FontWeight.bold, - letterSpacing: 0.24, - ), - ), - ), - Expanded(child: buildSubtitle()), - ], - ); - }, - ), - ), - ); - }, - ); - } - Widget buildTrailing() { return PlayPauseMediaButton( - foregroundColor: foregroundColor, + foregroundColor: colorPalette?.foregroundColor, variant: ButtonVariant.iconbutton, onPressed: () { if (isPlaying) { @@ -181,10 +76,9 @@ class _MiniPlayerState extends State { } return AnimatedContainer( - key: ValueKey('${media.id}miniplayer'), duration: const Duration(milliseconds: 300), decoration: BoxDecoration( - color: backgroundColor ?? context.theme.primaryColor, + color: colorPalette?.backgroundColor ?? context.theme.primaryColor, borderRadius: BorderRadius.circular(8), ), child: Stack( @@ -196,39 +90,20 @@ class _MiniPlayerState extends State { children: [ buildLeading(), const SizedBox(width: 8), - Expanded(child: buildTitle()), + Expanded( + child: Title( + key: ValueKey('${media.id}pageview'), + queueState: queueState, + colorPalette: colorPalette, + ), + ), buildTrailing(), ], ), ), - Positioned( - left: 8, - right: 8, - bottom: 0.5, - child: StreamBuilder( - stream: AudioService.position, - builder: (context, snapshot) { - return LinearProgressIndicator( - value: snapshot.hasData - ? snapshot.data!.inMilliseconds / - media.duration!.inMilliseconds - : 0, - minHeight: 1.5, - color: foregroundColor, - backgroundColor: foregroundColor?.withOpacity(0.3), - borderRadius: BorderRadius.circular(8), - ); - }, - ), - ), + CurrentDuration(media: media, colorPalette: colorPalette), ], ), ); } - - @override - void dispose() { - super.dispose(); - _pageController.dispose(); - } } diff --git a/lib/widgets/player/title.dart b/lib/widgets/player/title.dart new file mode 100644 index 0000000..56c7bae --- /dev/null +++ b/lib/widgets/player/title.dart @@ -0,0 +1,82 @@ +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:varanasi_mobile_app/cubits/config/config_cubit.dart'; +import 'package:varanasi_mobile_app/cubits/player/player_cubit.dart'; +import 'package:varanasi_mobile_app/utils/extensions/theme.dart'; +import 'package:varanasi_mobile_app/utils/player/typings.dart'; +import 'package:varanasi_mobile_app/widgets/animated_overflow_text.dart'; + +class Title extends StatelessWidget { + const Title({ + super.key, + required this.queueState, + required this.colorPalette, + }); + + final QueueState queueState; + final MediaColorPalette? colorPalette; + + @override + Widget build(BuildContext context) { + final controller = context.select((ConfigCubit cubit) => + cubit.state is ConfigLoaded + ? (cubit.state as ConfigLoaded).miniPlayerPageController + : null); + return SizedBox( + height: 40, + child: CarouselSlider.builder( + itemCount: queueState.queue.length, + itemBuilder: (context, index, _) { + final media = queueState.queue[index]; + final title = media.displayTitle ?? ''; + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + height: 20, + child: AnimatedText( + title, + key: ValueKey(media.id), + minFontSize: 12, + maxFontSize: 12, + maxLines: 1, + style: context.textTheme.bodyLarge?.copyWith( + color: colorPalette?.foregroundColor, + fontWeight: FontWeight.bold, + letterSpacing: 0.24, + ), + ), + ), + Expanded( + child: AnimatedText( + media.displayDescription ?? '', + minFontSize: 12, + maxFontSize: 12, + maxLines: 1, + style: context.textTheme.labelMedium?.copyWith( + color: colorPalette?.foregroundColor, + ), + ), + ), + ], + ); + }, + carouselController: controller, + options: CarouselOptions( + disableCenter: true, + initialPage: queueState.queueIndex ?? 0, + height: 40, + viewportFraction: 1.0, + onPageChanged: (index, reason) { + if (reason == CarouselPageChangedReason.manual) { + context.read().skipToIndex(index); + } + }, + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 7eea808..bf35da1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -193,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + carousel_slider: + dependency: "direct main" + description: + name: carousel_slider + sha256: "9c695cc963bf1d04a47bd6021f68befce8970bcd61d24938e1fb0918cf5d9c42" + url: "https://pub.dev" + source: hosted + version: "4.2.1" characters: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 775335a..70afbca 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -66,6 +66,7 @@ dependencies: auto_size_text: ^3.0.0 audio_session: ^0.1.16 marquee: ^2.2.3 + carousel_slider: ^4.2.1 dev_dependencies: flutter_test: From c4d4ba392ce202cffde319890146c6c64982b415 Mon Sep 17 00:00:00 2001 From: devaryakumar Date: Sat, 23 Sep 2023 21:21:29 +0530 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=9A=A7=20WIP:=20full=20screen=20playe?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/cubits/config/config_cubit.dart | 25 ++--- lib/cubits/config/config_state.dart | 16 ++- lib/cubits/player/player_cubit.dart | 19 ++-- lib/cubits/player/player_state.dart | 19 ++++ lib/widgets/page_with_navbar.dart | 98 ++++++++++++----- .../flutter_screen_player.dart | 104 ++++++++++++++++++ lib/widgets/player/mini_player.dart | 67 +++++------ lib/widgets/player/title.dart | 16 ++- lib/widgets/seek_bar.dart | 89 +++++++++++++++ 9 files changed, 361 insertions(+), 92 deletions(-) create mode 100644 lib/widgets/player/full_screen_player/flutter_screen_player.dart create mode 100644 lib/widgets/seek_bar.dart diff --git a/lib/cubits/config/config_cubit.dart b/lib/cubits/config/config_cubit.dart index 280e477..4695011 100644 --- a/lib/cubits/config/config_cubit.dart +++ b/lib/cubits/config/config_cubit.dart @@ -4,6 +4,7 @@ import 'package:carousel_slider/carousel_controller.dart'; import 'package:equatable/equatable.dart'; import 'package:hive/hive.dart'; import 'package:palette_generator/palette_generator.dart'; +import 'package:sliding_up_panel/sliding_up_panel.dart'; import 'package:varanasi_mobile_app/models/app_config.dart'; import 'package:varanasi_mobile_app/models/sort_type.dart'; import 'package:varanasi_mobile_app/utils/logger.dart'; @@ -27,8 +28,12 @@ class ConfigCubit extends Cubit { } else { logger.i('Config box is not empty'); } - emit(ConfigLoaded(config: _configBox.values.first)); - recreateMiniPlayerController(); + emit(ConfigLoaded( + config: _configBox.values.first, + panelController: PanelController(), + playerPageController: CarouselController(), + miniPlayerPageController: CarouselController(), + )); } Stream get sortTypeStream => stream.map( @@ -42,7 +47,7 @@ class ConfigCubit extends Cubit { if (state is ConfigLoaded) { final config = (state as ConfigLoaded).config.copyWith(sortBy: sortBy); _configBox.putAt(0, config); - emit(ConfigLoaded(config: config)); + emit(ConfigLoaded(config: config, panelController: PanelController())); } } @@ -75,15 +80,7 @@ class ConfigCubit extends Cubit { ? (state as ConfigLoaded).miniPlayerPageController : null; - void recreateMiniPlayerController( - [int? initialPage, double? viewportFraction]) { - if (state is ConfigLoaded) { - final config = (state as ConfigLoaded); - final controller = CarouselController(); - final newConfig = config.copyWith( - miniPlayerPageController: controller, - ); - emit(newConfig); - } - } + CarouselController? get playerPageController => (state is ConfigLoaded) + ? (state as ConfigLoaded).playerPageController + : null; } diff --git a/lib/cubits/config/config_state.dart b/lib/cubits/config/config_state.dart index df0632c..f089dc3 100644 --- a/lib/cubits/config/config_state.dart +++ b/lib/cubits/config/config_state.dart @@ -12,22 +12,34 @@ final class ConfigInitial extends ConfigState {} final class ConfigLoaded extends ConfigState { final AppConfig config; - final CarouselController? miniPlayerPageController; + final CarouselController? miniPlayerPageController, playerPageController; + final PanelController panelController; const ConfigLoaded({ required this.config, this.miniPlayerPageController, + this.playerPageController, + required this.panelController, }); @override - List get props => [config, miniPlayerPageController]; + List get props => [ + config, + miniPlayerPageController, + panelController, + playerPageController, + ]; ConfigLoaded copyWith({ AppConfig? config, CarouselController? miniPlayerPageController, + CarouselController? playerPageController, + PanelController? panelController, }) { return ConfigLoaded( config: config ?? this.config, + playerPageController: playerPageController ?? this.playerPageController, + panelController: panelController ?? this.panelController, miniPlayerPageController: miniPlayerPageController ?? this.miniPlayerPageController, ); diff --git a/lib/cubits/player/player_cubit.dart b/lib/cubits/player/player_cubit.dart index ca6c49a..a8fc8af 100644 --- a/lib/cubits/player/player_cubit.dart +++ b/lib/cubits/player/player_cubit.dart @@ -121,7 +121,6 @@ class MediaPlayerCubit extends AppCubit Future skipToNext() => audioHandler.skipToNext(); Future skipToIndex(int index) async { - if (index == state.queueState.queueIndex) return; await audioHandler.skipToQueueItem(index); if (!audioHandler.player.playing) { await play(); @@ -131,10 +130,11 @@ class MediaPlayerCubit extends AppCubit Future skipToMediaItem(PlayableMedia mediaItem) async { final index = state.queueState.queue.indexOf(mediaItem.toMediaItem()); if (index == -1) return; - final configCubit = configCubitGetter(); - final controller = configCubit.miniPlayerPageController; await skipToIndex(index); - controller?.animateToPage(index); + } + + Future seek(Duration position) { + return audioHandler.seek(position); } @override @@ -160,6 +160,12 @@ class MediaPlayerCubit extends AppCubit if (value.$2 != null) { palette = await configCubitGetter() .generatePalleteGenerator(value.$2?.artUri.toString() ?? ''); + final configCubit = configCubitGetter(); + final controller = configCubit.miniPlayerPageController; + final carouselController = configCubit.playerPageController; + final index = value.$3.queueIndex ?? 0; + controller?.animateToPage(index); + carouselController?.animateToPage(index); } else { palette = null; } @@ -182,9 +188,4 @@ class MediaPlayerCubit extends AppCubit if (state.paletteGenerator == null) return null; return MediaColorPalette.fromPaletteGenerator(state.paletteGenerator!); } - - void recreateMiniPlayerController() { - final currentIndex = state.queueState.queueIndex; - configCubitGetter().recreateMiniPlayerController(currentIndex); - } } diff --git a/lib/cubits/player/player_state.dart b/lib/cubits/player/player_state.dart index f38c169..e77dbf7 100644 --- a/lib/cubits/player/player_state.dart +++ b/lib/cubits/player/player_state.dart @@ -40,4 +40,23 @@ class MediaPlayerState extends Equatable { queueState, paletteGenerator ]; + + Gradient? get gradient { + if (paletteGenerator == null) return null; + final palette = paletteGenerator!; + final PaletteColor? selectedColor = palette.darkVibrantColor ?? + palette.darkMutedColor ?? + palette.dominantColor; + final Color? color1 = selectedColor?.color.withOpacity(1); + const Color color2 = Colors.black; + return LinearGradient( + colors: [ + color1 ?? color2, + color2, + ], + begin: Alignment.topCenter, + end: FractionalOffset.bottomCenter, + stops: const [0, 0.8], + ); + } } diff --git a/lib/widgets/page_with_navbar.dart b/lib/widgets/page_with_navbar.dart index 3c5cdb3..85ebb14 100644 --- a/lib/widgets/page_with_navbar.dart +++ b/lib/widgets/page_with_navbar.dart @@ -1,51 +1,89 @@ import 'package:another_flushbar/flushbar_helper.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; +import 'package:sliding_up_panel/sliding_up_panel.dart'; +import 'package:varanasi_mobile_app/cubits/config/config_cubit.dart'; +import 'package:varanasi_mobile_app/cubits/player/player_cubit.dart'; +import 'package:varanasi_mobile_app/utils/extensions/media_query.dart'; +import 'package:varanasi_mobile_app/widgets/player/full_screen_player/flutter_screen_player.dart'; import 'player/mini_player.dart'; -class PageWithNavbar extends StatelessWidget { +const bottomNavHeight = 114.0; + +class PageWithNavbar extends HookWidget { final StatefulNavigationShell child; const PageWithNavbar({super.key, required this.child}); @override Widget build(BuildContext context) { + final positionState = useState(0.0); + final position = positionState.value; + final opacity = 1 - position; + final currentHeight = bottomNavHeight * (1 - position); + final controller = context.select((ConfigCubit cubit) => + cubit.state is ConfigLoaded + ? (cubit.state as ConfigLoaded).panelController + : null); + final queue = context + .select((MediaPlayerCubit cubit) => cubit.state.queueState.queue); return Scaffold( body: Stack( children: [ Positioned.fill(child: child), - const Positioned( - bottom: 0, - right: 8, - left: 8, - height: 56, - child: MiniPlayer(), - ), + if (controller != null && queue.isNotEmpty) + SlidingUpPanel( + controller: controller, + renderPanelSheet: false, + backdropEnabled: true, + color: Colors.transparent, + borderRadius: BorderRadius.circular(8), + collapsed: MiniPlayer(panelController: controller), + minHeight: 56, + maxHeight: context.height, + panel: Player(panelController: controller), + onPanelSlide: (pos) => positionState.value = pos, + ), ], ), - bottomNavigationBar: NavigationBar( - surfaceTintColor: Colors.transparent, - selectedIndex: child.currentIndex, - onDestinationSelected: (value) { - FlushbarHelper.createError( - message: 'Coming soon!', - duration: const Duration(seconds: 1), - ).show(context); - }, - destinations: const [ - NavigationDestination( - icon: Icon(Icons.home_filled), - label: 'Home', - ), - NavigationDestination( - icon: Icon(Icons.search_rounded), - label: 'Search', + bottomNavigationBar: SizedBox( + height: currentHeight, + child: Transform.translate( + offset: Offset(0.0, currentHeight * position * 0.5), + child: AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: opacity, + child: OverflowBox( + maxHeight: bottomNavHeight, + child: NavigationBar( + surfaceTintColor: Colors.transparent, + selectedIndex: child.currentIndex, + onDestinationSelected: (value) { + FlushbarHelper.createError( + message: 'Coming soon!', + duration: const Duration(seconds: 1), + ).show(context); + }, + destinations: const [ + NavigationDestination( + icon: Icon(Icons.home_filled), + label: 'Home', + ), + NavigationDestination( + icon: Icon(Icons.search_rounded), + label: 'Search', + ), + NavigationDestination( + icon: Icon(Icons.library_music_outlined), + label: 'Library', + ), + ], + ), + ), ), - NavigationDestination( - icon: Icon(Icons.library_music_outlined), - label: 'Library', - ), - ], + ), ), ); } diff --git a/lib/widgets/player/full_screen_player/flutter_screen_player.dart b/lib/widgets/player/full_screen_player/flutter_screen_player.dart new file mode 100644 index 0000000..083d70c --- /dev/null +++ b/lib/widgets/player/full_screen_player/flutter_screen_player.dart @@ -0,0 +1,104 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:sliding_up_panel/sliding_up_panel.dart'; +import 'package:varanasi_mobile_app/cubits/config/config_cubit.dart'; +import 'package:varanasi_mobile_app/cubits/player/player_cubit.dart'; +import 'package:varanasi_mobile_app/utils/extensions/media_query.dart'; +import 'package:varanasi_mobile_app/widgets/seek_bar.dart'; + +class Player extends StatefulWidget { + const Player({super.key, required this.panelController}); + final PanelController? panelController; + + @override + State createState() => _PlayerState(); +} + +class _PlayerState extends State { + @override + Widget build(BuildContext context) { + final controller = context.select((ConfigCubit cubit) => + cubit.state is ConfigLoaded + ? (cubit.state as ConfigLoaded).playerPageController + : null); + final (state, audioHandler) = context + .select((MediaPlayerCubit cubit) => (cubit.state, cubit.audioHandler)); + final queueState = state.queueState; + final queue = queueState.queue; + + return Scaffold( + extendBodyBehindAppBar: true, + appBar: AppBar( + backgroundColor: Colors.transparent, + leading: IconButton( + onPressed: () => widget.panelController?.close(), + icon: const Icon( + Icons.arrow_back_ios_new, + ), + ), + ), + body: AnimatedContainer( + padding: EdgeInsets.only(top: kToolbarHeight + context.topPadding), + width: context.width, + duration: kThemeAnimationDuration, + decoration: BoxDecoration(gradient: state.gradient), + child: Column( + children: [ + SizedBox.square( + dimension: context.width, + child: CarouselSlider.builder( + carouselController: controller, + itemCount: queue.length, + itemBuilder: (context, index, _) { + final media = queue[index]; + return Padding( + key: ValueKey(media.id), + padding: EdgeInsets.all(context.width * 0.05), + child: CachedNetworkImage( + imageUrl: media.artUri.toString(), + ), + ); + }, + options: CarouselOptions( + initialPage: queueState.queueIndex ?? 0, + height: context.width, + viewportFraction: 1.0, + enableInfiniteScroll: false, + disableCenter: true, + onPageChanged: (index, reason) { + if (reason == CarouselPageChangedReason.manual) { + context.read().skipToIndex(index); + } + }, + ), + ), + ), + StreamBuilder( + stream: Rx.combineLatest2( + audioHandler.player.positionStream, + audioHandler.mediaItem, + (a, b) => (a, b), + ), + builder: (context, snapshot) { + final position = snapshot.data?.$1 ?? Duration.zero; + final media = snapshot.data?.$2; + return SeekBar( + key: ValueKey('${media?.id}seekbar'), + color: Colors.white, + duration: media?.duration ?? Duration.zero, + position: position, + onChanged: (value) { + context.read().seek(value); + }, + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/player/mini_player.dart b/lib/widgets/player/mini_player.dart index 665a0b6..e97fc17 100644 --- a/lib/widgets/player/mini_player.dart +++ b/lib/widgets/player/mini_player.dart @@ -1,6 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart' hide Title; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:sliding_up_panel/sliding_up_panel.dart'; import 'package:varanasi_mobile_app/cubits/player/player_cubit.dart'; import 'package:varanasi_mobile_app/utils/extensions/theme.dart'; import 'package:varanasi_mobile_app/widgets/play_pause_button.dart'; @@ -9,19 +10,15 @@ import 'package:varanasi_mobile_app/widgets/player/current_duration.dart'; import 'title.dart'; class MiniPlayer extends StatefulWidget { - const MiniPlayer({super.key}); + const MiniPlayer({super.key, required this.panelController}); + + final PanelController? panelController; @override State createState() => _MiniPlayerState(); } class _MiniPlayerState extends State { - @override - void initState() { - super.initState(); - context.readMediaPlayerCubit.recreateMiniPlayerController(); - } - @override Widget build(BuildContext context) { final (media, isPlaying, queueState, colorPalette) = context.select( @@ -75,34 +72,38 @@ class _MiniPlayerState extends State { ); } - return AnimatedContainer( - duration: const Duration(milliseconds: 300), - decoration: BoxDecoration( - color: colorPalette?.backgroundColor ?? context.theme.primaryColor, - borderRadius: BorderRadius.circular(8), - ), - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8).copyWith(left: 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - buildLeading(), - const SizedBox(width: 8), - Expanded( - child: Title( - key: ValueKey('${media.id}pageview'), - queueState: queueState, - colorPalette: colorPalette, + return GestureDetector( + onTap: () => widget.panelController?.open(), + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + decoration: BoxDecoration( + color: colorPalette?.backgroundColor ?? context.theme.primaryColor, + borderRadius: BorderRadius.circular(8), + ), + child: Stack( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(vertical: 8).copyWith(left: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + buildLeading(), + const SizedBox(width: 8), + Expanded( + child: Title( + key: ValueKey('${media.id}pageview'), + queueState: queueState, + colorPalette: colorPalette, + ), ), - ), - buildTrailing(), - ], + buildTrailing(), + ], + ), ), - ), - CurrentDuration(media: media, colorPalette: colorPalette), - ], + CurrentDuration(media: media, colorPalette: colorPalette), + ], + ), ), ); } diff --git a/lib/widgets/player/title.dart b/lib/widgets/player/title.dart index 56c7bae..db814ea 100644 --- a/lib/widgets/player/title.dart +++ b/lib/widgets/player/title.dart @@ -19,10 +19,13 @@ class Title extends StatelessWidget { @override Widget build(BuildContext context) { - final controller = context.select((ConfigCubit cubit) => - cubit.state is ConfigLoaded - ? (cubit.state as ConfigLoaded).miniPlayerPageController - : null); + final (controller, playerController) = context.select((ConfigCubit cubit) { + if (cubit.state is! ConfigLoaded) return (null, null); + return ( + (cubit.state as ConfigLoaded).miniPlayerPageController, + (cubit.state as ConfigLoaded).playerPageController + ); + }); return SizedBox( height: 40, child: CarouselSlider.builder( @@ -70,9 +73,14 @@ class Title extends StatelessWidget { initialPage: queueState.queueIndex ?? 0, height: 40, viewportFraction: 1.0, + enableInfiniteScroll: false, onPageChanged: (index, reason) { if (reason == CarouselPageChangedReason.manual) { context.read().skipToIndex(index); + playerController?.jumpToPage(index); + } + if (reason == CarouselPageChangedReason.controller) { + playerController?.jumpToPage(index); } }, ), diff --git a/lib/widgets/seek_bar.dart b/lib/widgets/seek_bar.dart new file mode 100644 index 0000000..fdeac40 --- /dev/null +++ b/lib/widgets/seek_bar.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:varanasi_mobile_app/utils/extensions/theme.dart'; + +class SeekBar extends StatefulWidget { + final Duration duration; + final Duration position; + final Color? color; + final ValueChanged? onChanged; + final ValueChanged? onChangeEnd; + + const SeekBar({ + super.key, + required this.duration, + required this.position, + this.color, + this.onChanged, + this.onChangeEnd, + }); + + @override + SeekBarState createState() => SeekBarState(); +} + +class SeekBarState extends State { + late double _dragValue; + + @override + void initState() { + super.initState(); + _dragValue = widget.position.inMilliseconds.toDouble(); + } + + @override + Widget build(BuildContext context) { + final value = _dragValue; + final position = Duration(milliseconds: value.round()); + return Stack( + clipBehavior: Clip.none, + children: [ + Slider( + activeColor: widget.color ?? context.colorScheme.primary, + inactiveColor: widget.color?.withOpacity(0.25) ?? + context.colorScheme.primary.withOpacity(0.25), + max: widget.duration.inMilliseconds.toDouble(), + value: value, + onChanged: (value) { + setState(() { + _dragValue = value; + }); + widget.onChanged?.call(Duration(milliseconds: value.round())); + }, + onChangeEnd: (value) { + widget.onChangeEnd?.call(Duration(milliseconds: value.round())); + }, + ), + Positioned( + right: 24.0, + bottom: -8.0, + left: 24.0, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + RegExp(r'((^0*[1-9]\d*:)?\d{2}:\d{2})\.\d+$') + .firstMatch("$position") + ?.group(1) ?? + '$_remaining', + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith(color: widget.color), + ), + Text( + '-${RegExp(r'((^0*[1-9]\d*:)?\d{2}:\d{2})\.\d+$').firstMatch("$_remaining")?.group(1) ?? '$_remaining'}', + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith(color: widget.color), + ), + ], + ), + ), + ], + ); + } + + Duration get _remaining => + widget.duration - Duration(milliseconds: _dragValue.round()); +} From 9a47a7f7ea0572ef7a9c1c78413418acdb9bd9fe Mon Sep 17 00:00:00 2001 From: devaryakumar Date: Sat, 23 Sep 2023 23:59:48 +0530 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20Seekbar=20usage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flutter_screen_player.dart | 2 +- lib/widgets/seek_bar.dart | 33 ++++++++++--------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/lib/widgets/player/full_screen_player/flutter_screen_player.dart b/lib/widgets/player/full_screen_player/flutter_screen_player.dart index 083d70c..dd3b1bd 100644 --- a/lib/widgets/player/full_screen_player/flutter_screen_player.dart +++ b/lib/widgets/player/full_screen_player/flutter_screen_player.dart @@ -90,7 +90,7 @@ class _PlayerState extends State { color: Colors.white, duration: media?.duration ?? Duration.zero, position: position, - onChanged: (value) { + onChangeEnd: (value) { context.read().seek(value); }, ); diff --git a/lib/widgets/seek_bar.dart b/lib/widgets/seek_bar.dart index fdeac40..f4b0076 100644 --- a/lib/widgets/seek_bar.dart +++ b/lib/widgets/seek_bar.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:varanasi_mobile_app/utils/extensions/theme.dart'; @@ -22,18 +24,18 @@ class SeekBar extends StatefulWidget { } class SeekBarState extends State { - late double _dragValue; - - @override - void initState() { - super.initState(); - _dragValue = widget.position.inMilliseconds.toDouble(); - } + double? _dragValue; + bool _dragging = false; @override Widget build(BuildContext context) { - final value = _dragValue; - final position = Duration(milliseconds: value.round()); + final value = min( + _dragValue ?? widget.position.inMilliseconds.toDouble(), + widget.duration.inMilliseconds.toDouble(), + ); + if (_dragValue != null && !_dragging) { + _dragValue = null; + } return Stack( clipBehavior: Clip.none, children: [ @@ -43,14 +45,16 @@ class SeekBarState extends State { context.colorScheme.primary.withOpacity(0.25), max: widget.duration.inMilliseconds.toDouble(), value: value, + onChangeStart: (value) { + _dragging = true; + }, onChanged: (value) { - setState(() { - _dragValue = value; - }); + setState(() => _dragValue = value); widget.onChanged?.call(Duration(milliseconds: value.round())); }, onChangeEnd: (value) { widget.onChangeEnd?.call(Duration(milliseconds: value.round())); + _dragging = false; }, ), Positioned( @@ -62,7 +66,7 @@ class SeekBarState extends State { children: [ Text( RegExp(r'((^0*[1-9]\d*:)?\d{2}:\d{2})\.\d+$') - .firstMatch("$position") + .firstMatch("${widget.position}") ?.group(1) ?? '$_remaining', style: Theme.of(context) @@ -84,6 +88,5 @@ class SeekBarState extends State { ); } - Duration get _remaining => - widget.duration - Duration(milliseconds: _dragValue.round()); + Duration get _remaining => widget.duration - widget.position; } From dcd215e4bb322dfbb4e1cab98af876c561cb8f0f Mon Sep 17 00:00:00 2001 From: devaryakumar Date: Sun, 24 Sep 2023 02:42:34 +0530 Subject: [PATCH 5/8] =?UTF-8?q?=E2=9C=A8=20New:=20Full=20Screen=20player?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../full_screen_player/audio_seekbar.dart | 39 +++++++++++++ .../flutter_screen_player.dart | 53 +++++++++++------- .../player/full_screen_player/media_info.dart | 41 ++++++++++++++ .../full_screen_player/repeat_toggle.dart | 56 +++++++++++++++++++ .../shuffle_mode_toggle.dart | 50 +++++++++++++++++ .../full_screen_player/skip_to_next.dart | 26 +++++++++ .../full_screen_player/skip_to_previous.dart | 26 +++++++++ 7 files changed, 270 insertions(+), 21 deletions(-) create mode 100644 lib/widgets/player/full_screen_player/audio_seekbar.dart create mode 100644 lib/widgets/player/full_screen_player/media_info.dart create mode 100644 lib/widgets/player/full_screen_player/repeat_toggle.dart create mode 100644 lib/widgets/player/full_screen_player/shuffle_mode_toggle.dart create mode 100644 lib/widgets/player/full_screen_player/skip_to_next.dart create mode 100644 lib/widgets/player/full_screen_player/skip_to_previous.dart diff --git a/lib/widgets/player/full_screen_player/audio_seekbar.dart b/lib/widgets/player/full_screen_player/audio_seekbar.dart new file mode 100644 index 0000000..f4289d4 --- /dev/null +++ b/lib/widgets/player/full_screen_player/audio_seekbar.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:varanasi_mobile_app/cubits/player/player_cubit.dart'; +import 'package:varanasi_mobile_app/utils/player/audio_handler_impl.dart'; +import 'package:varanasi_mobile_app/widgets/seek_bar.dart'; + +class AudioSeekbar extends StatelessWidget { + const AudioSeekbar({ + super.key, + required this.audioHandler, + }); + + final AudioHandlerImpl audioHandler; + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: Rx.combineLatest2( + audioHandler.player.positionStream, + audioHandler.mediaItem, + (a, b) => (a, b), + ), + builder: (context, snapshot) { + final position = snapshot.data?.$1 ?? Duration.zero; + final media = snapshot.data?.$2; + return SeekBar( + key: ValueKey('${media?.id}seekbar'), + color: Colors.white, + duration: media?.duration ?? Duration.zero, + position: position, + onChangeEnd: (value) { + context.read().seek(value); + }, + ); + }, + ); + } +} diff --git a/lib/widgets/player/full_screen_player/flutter_screen_player.dart b/lib/widgets/player/full_screen_player/flutter_screen_player.dart index dd3b1bd..3fd3641 100644 --- a/lib/widgets/player/full_screen_player/flutter_screen_player.dart +++ b/lib/widgets/player/full_screen_player/flutter_screen_player.dart @@ -2,12 +2,18 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:rxdart/rxdart.dart'; import 'package:sliding_up_panel/sliding_up_panel.dart'; import 'package:varanasi_mobile_app/cubits/config/config_cubit.dart'; import 'package:varanasi_mobile_app/cubits/player/player_cubit.dart'; import 'package:varanasi_mobile_app/utils/extensions/media_query.dart'; -import 'package:varanasi_mobile_app/widgets/seek_bar.dart'; +import 'package:varanasi_mobile_app/widgets/play_pause_button.dart'; + +import 'audio_seekbar.dart'; +import 'media_info.dart'; +import 'repeat_toggle.dart'; +import 'shuffle_mode_toggle.dart'; +import 'skip_to_next.dart'; +import 'skip_to_previous.dart'; class Player extends StatefulWidget { const Player({super.key, required this.panelController}); @@ -28,6 +34,7 @@ class _PlayerState extends State { .select((MediaPlayerCubit cubit) => (cubit.state, cubit.audioHandler)); final queueState = state.queueState; final queue = queueState.queue; + final mediaItem = queue[queueState.queueIndex ?? 0]; return Scaffold( extendBodyBehindAppBar: true, @@ -76,26 +83,30 @@ class _PlayerState extends State { ), ), ), - StreamBuilder( - stream: Rx.combineLatest2( - audioHandler.player.positionStream, - audioHandler.mediaItem, - (a, b) => (a, b), - ), - builder: (context, snapshot) { - final position = snapshot.data?.$1 ?? Duration.zero; - final media = snapshot.data?.$2; - return SeekBar( - key: ValueKey('${media?.id}seekbar'), - color: Colors.white, - duration: media?.duration ?? Duration.zero, - position: position, - onChangeEnd: (value) { - context.read().seek(value); + MediaInfo(mediaItem: mediaItem), + AudioSeekbar(audioHandler: audioHandler), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ShuffleModeToggle(audioHandler: audioHandler), + SkipToPrevious(queueState: queueState), + PlayPauseMediaButton( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + onPressed: () { + if (state.isPlaying) { + context.read().pause(); + } else { + context.read().play(); + } }, - ); - }, - ), + isPlaying: state.isPlaying, + ), + SkipToNext(queueState: queueState), + RepeatToggle(audioHandler: audioHandler), + ], + ) ], ), ), diff --git a/lib/widgets/player/full_screen_player/media_info.dart b/lib/widgets/player/full_screen_player/media_info.dart new file mode 100644 index 0000000..2527926 --- /dev/null +++ b/lib/widgets/player/full_screen_player/media_info.dart @@ -0,0 +1,41 @@ +import 'package:audio_service/audio_service.dart'; +import 'package:flutter/material.dart'; +import 'package:varanasi_mobile_app/utils/extensions/media_query.dart'; +import 'package:varanasi_mobile_app/utils/extensions/theme.dart'; + +class MediaInfo extends StatelessWidget { + const MediaInfo({ + super.key, + required this.mediaItem, + }); + + final MediaItem mediaItem; + + @override + Widget build(BuildContext context) { + return ListTile( + contentPadding: EdgeInsets.symmetric(horizontal: context.width * 0.05), + title: Text( + mediaItem.displayTitle ?? '', + style: context.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + mediaItem.displaySubtitle ?? '', + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: IconButton( + onPressed: () {}, + icon: const Icon(Icons.add_circle_outline_rounded), + iconSize: 32, + ), + ); + } +} diff --git a/lib/widgets/player/full_screen_player/repeat_toggle.dart b/lib/widgets/player/full_screen_player/repeat_toggle.dart new file mode 100644 index 0000000..72311bc --- /dev/null +++ b/lib/widgets/player/full_screen_player/repeat_toggle.dart @@ -0,0 +1,56 @@ +import 'package:audio_service/audio_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:varanasi_mobile_app/cubits/player/player_cubit.dart'; +import 'package:varanasi_mobile_app/utils/extensions/theme.dart'; +import 'package:varanasi_mobile_app/utils/player/audio_handler_impl.dart'; + +class RepeatToggle extends StatelessWidget { + const RepeatToggle({ + super.key, + required this.audioHandler, + }); + + final AudioHandlerImpl audioHandler; + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: audioHandler.playbackState.map((event) => event.repeatMode), + builder: (context, snapshot) { + final repeatOn = + ![null, AudioServiceRepeatMode.none].contains(snapshot.data); + final icon = { + AudioServiceRepeatMode.one: const Icon(Icons.repeat_one), + AudioServiceRepeatMode.all: const Icon(Icons.repeat), + }[snapshot.data] ?? + const Icon(Icons.repeat); + return IconButton( + onPressed: () { + final nextState = { + AudioServiceRepeatMode.none: AudioServiceRepeatMode.one, + AudioServiceRepeatMode.one: AudioServiceRepeatMode.all, + AudioServiceRepeatMode.all: AudioServiceRepeatMode.none, + }[snapshot.data ?? AudioServiceRepeatMode.none] ?? + AudioServiceRepeatMode.none; + context + .read() + .audioHandler + .setRepeatMode(nextState); + }, + icon: Badge( + alignment: Alignment.bottomCenter, + isLabelVisible: repeatOn, + backgroundColor: context.colorScheme.primary, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: icon, + ), + ), + color: repeatOn ? context.colorScheme.primary : null, + iconSize: 32, + ); + }, + ); + } +} diff --git a/lib/widgets/player/full_screen_player/shuffle_mode_toggle.dart b/lib/widgets/player/full_screen_player/shuffle_mode_toggle.dart new file mode 100644 index 0000000..07b51f5 --- /dev/null +++ b/lib/widgets/player/full_screen_player/shuffle_mode_toggle.dart @@ -0,0 +1,50 @@ +import 'package:audio_service/audio_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:varanasi_mobile_app/cubits/player/player_cubit.dart'; +import 'package:varanasi_mobile_app/utils/extensions/theme.dart'; +import 'package:varanasi_mobile_app/utils/player/audio_handler_impl.dart'; + +class ShuffleModeToggle extends StatelessWidget { + const ShuffleModeToggle({ + super.key, + required this.audioHandler, + }); + + final AudioHandlerImpl audioHandler; + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: audioHandler.playbackState.map((event) => event.shuffleMode), + builder: (context, snapshot) { + final shuffleEnabled = + ![null, AudioServiceShuffleMode.none].contains(snapshot.data); + return IconButton( + onPressed: () { + final nextMode = { + AudioServiceShuffleMode.none: AudioServiceShuffleMode.all, + AudioServiceShuffleMode.all: AudioServiceShuffleMode.none, + }[snapshot.data ?? AudioServiceShuffleMode.none] ?? + AudioServiceShuffleMode.none; + context + .read() + .audioHandler + .setShuffleMode(nextMode); + }, + icon: Badge( + alignment: Alignment.bottomCenter, + isLabelVisible: shuffleEnabled, + backgroundColor: context.colorScheme.primary, + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 4.0), + child: Icon(Icons.shuffle), + ), + ), + color: shuffleEnabled ? context.colorScheme.primary : null, + iconSize: 32, + ); + }, + ); + } +} diff --git a/lib/widgets/player/full_screen_player/skip_to_next.dart b/lib/widgets/player/full_screen_player/skip_to_next.dart new file mode 100644 index 0000000..0708d5b --- /dev/null +++ b/lib/widgets/player/full_screen_player/skip_to_next.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:varanasi_mobile_app/cubits/player/player_cubit.dart'; +import 'package:varanasi_mobile_app/utils/player/typings.dart'; + +class SkipToNext extends StatelessWidget { + const SkipToNext({ + super.key, + required this.queueState, + }); + + final QueueState queueState; + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: queueState.hasNext + ? () { + context.read().skipToNext(); + } + : null, + icon: const Icon(Icons.skip_next), + iconSize: 32, + ); + } +} diff --git a/lib/widgets/player/full_screen_player/skip_to_previous.dart b/lib/widgets/player/full_screen_player/skip_to_previous.dart new file mode 100644 index 0000000..f2627fd --- /dev/null +++ b/lib/widgets/player/full_screen_player/skip_to_previous.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:varanasi_mobile_app/cubits/player/player_cubit.dart'; +import 'package:varanasi_mobile_app/utils/player/typings.dart'; + +class SkipToPrevious extends StatelessWidget { + const SkipToPrevious({ + super.key, + required this.queueState, + }); + + final QueueState queueState; + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: queueState.hasPrevious + ? () { + context.read().skipToPrevious(); + } + : null, + icon: const Icon(Icons.skip_previous), + iconSize: 32, + ); + } +} From c7ec923fa5a37720b1dee19413c947f45004d9fb Mon Sep 17 00:00:00 2001 From: devaryakumar Date: Sun, 24 Sep 2023 02:54:55 +0530 Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20rounded=20corners=20n?= =?UTF-8?q?ot=20visible=20for=20mini=20player?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/widgets/page_with_navbar.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/widgets/page_with_navbar.dart b/lib/widgets/page_with_navbar.dart index 85ebb14..762d68c 100644 --- a/lib/widgets/page_with_navbar.dart +++ b/lib/widgets/page_with_navbar.dart @@ -43,7 +43,9 @@ class PageWithNavbar extends HookWidget { collapsed: MiniPlayer(panelController: controller), minHeight: 56, maxHeight: context.height, - panel: Player(panelController: controller), + panel: position > 0 + ? Player(panelController: controller) + : const SizedBox.shrink(), onPanelSlide: (pos) => positionState.value = pos, ), ], From c0ec49db27066e273236275a83171e8568de3db7 Mon Sep 17 00:00:00 2001 From: devaryakumar Date: Sun, 24 Sep 2023 03:05:56 +0530 Subject: [PATCH 7/8] Added safer `animateToPage` function --- lib/cubits/player/player_cubit.dart | 5 +++-- lib/utils/safe_animate_to_pageview.dart | 6 ++++++ lib/widgets/player/title.dart | 4 +++- 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 lib/utils/safe_animate_to_pageview.dart diff --git a/lib/cubits/player/player_cubit.dart b/lib/cubits/player/player_cubit.dart index a8fc8af..b5c90f6 100644 --- a/lib/cubits/player/player_cubit.dart +++ b/lib/cubits/player/player_cubit.dart @@ -17,6 +17,7 @@ import 'package:varanasi_mobile_app/utils/mixins/cachable_mixin.dart'; import 'package:varanasi_mobile_app/utils/mixins/repository_protocol.dart'; import 'package:varanasi_mobile_app/utils/player/audio_handler_impl.dart'; import 'package:varanasi_mobile_app/utils/player/typings.dart'; +import 'package:varanasi_mobile_app/utils/safe_animate_to_pageview.dart'; import 'package:varanasi_mobile_app/utils/services/http_services.dart'; part 'player_state.dart'; @@ -164,8 +165,8 @@ class MediaPlayerCubit extends AppCubit final controller = configCubit.miniPlayerPageController; final carouselController = configCubit.playerPageController; final index = value.$3.queueIndex ?? 0; - controller?.animateToPage(index); - carouselController?.animateToPage(index); + animateToPage(index, controller); + animateToPage(index, carouselController); } else { palette = null; } diff --git a/lib/utils/safe_animate_to_pageview.dart b/lib/utils/safe_animate_to_pageview.dart new file mode 100644 index 0000000..766170b --- /dev/null +++ b/lib/utils/safe_animate_to_pageview.dart @@ -0,0 +1,6 @@ +import 'package:carousel_slider/carousel_controller.dart'; + +void animateToPage(int index, CarouselController? controller) { + if (controller == null || !controller.ready) return; + controller.animateToPage(index); +} diff --git a/lib/widgets/player/title.dart b/lib/widgets/player/title.dart index db814ea..15ce7a4 100644 --- a/lib/widgets/player/title.dart +++ b/lib/widgets/player/title.dart @@ -5,6 +5,7 @@ import 'package:varanasi_mobile_app/cubits/config/config_cubit.dart'; import 'package:varanasi_mobile_app/cubits/player/player_cubit.dart'; import 'package:varanasi_mobile_app/utils/extensions/theme.dart'; import 'package:varanasi_mobile_app/utils/player/typings.dart'; +import 'package:varanasi_mobile_app/utils/safe_animate_to_pageview.dart'; import 'package:varanasi_mobile_app/widgets/animated_overflow_text.dart'; class Title extends StatelessWidget { @@ -78,9 +79,10 @@ class Title extends StatelessWidget { if (reason == CarouselPageChangedReason.manual) { context.read().skipToIndex(index); playerController?.jumpToPage(index); + animateToPage(index, playerController); } if (reason == CarouselPageChangedReason.controller) { - playerController?.jumpToPage(index); + animateToPage(index, playerController); } }, ), From 97ca58cce0e6e52a5025c5f1b4bdd94117ee1db1 Mon Sep 17 00:00:00 2001 From: devaryakumar Date: Sun, 24 Sep 2023 03:06:25 +0530 Subject: [PATCH 8/8] Updated app version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 70afbca..2eb177e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.0.6+3 +version: 0.0.7+1 environment: sdk: ">=3.0.6 <4.0.0"