diff --git a/.vscode/settings.json b/.vscode/settings.json index fc25963..73fb4a2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,5 +17,6 @@ "riverpod", "shelfsdk", "tapable" - ] + ], + "cmake.configureOnOpen": false } \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 0557e86..6f73d06 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,10 +1,18 @@ - + + + + + + + + android:name="io.flutter.embedding.android.NormalTheme" + android:resource="@style/NormalTheme" + /> - - + + + + + + + + + + + + + + - - + + - + \ No newline at end of file diff --git a/lib/features/item_viewer/view/library_item_page.dart b/lib/features/item_viewer/view/library_item_page.dart index 72d8053..e82a650 100644 --- a/lib/features/item_viewer/view/library_item_page.dart +++ b/lib/features/item_viewer/view/library_item_page.dart @@ -12,8 +12,8 @@ import 'package:whispering_pages/constants/hero_tag_conventions.dart'; import 'package:whispering_pages/features/player/providers/audiobook_player.dart'; import 'package:whispering_pages/router/models/library_item_extras.dart'; import 'package:whispering_pages/settings/app_settings_provider.dart'; -import 'package:whispering_pages/theme/theme_from_cover_provider.dart'; import 'package:whispering_pages/shared/widgets/shelves/book_shelf.dart'; +import 'package:whispering_pages/theme/theme_from_cover_provider.dart'; import '../../../shared/widgets/expandable_description.dart'; import 'library_item_sliver_app_bar.dart'; @@ -219,7 +219,7 @@ class LibraryItemMetadata extends StatelessWidget { return null; } final codec = book.audioFiles.first.codec.toUpperCase(); - final bitrate = book.audioFiles.first.bitRate; + // final bitrate = book.audioFiles.first.bitRate; return codec; } } @@ -277,85 +277,83 @@ class LibraryItemActions extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final player = ref.read(audiobookPlayerProvider); return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Container( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - // play/resume button the same width as image - LayoutBuilder( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + // play/resume button the same width as image + LayoutBuilder( + builder: (context, constraints) { + return SizedBox( + width: calculateWidth(context, constraints), + // a boxy button with icon and text but little rounded corner + child: ElevatedButton.icon( + onPressed: () async { + // play the book + debugPrint('Pressed play/resume button'); + // set the book to the player if not already set + if (player.book != book) { + debugPrint('Setting the book ${book.libraryItemId}'); + await player.setSourceAudioBook(book); + ref + .read(audiobookPlayerProvider.notifier) + .notifyListeners(); + } + // toggle play/pause + player.togglePlayPause(); + }, + icon: const Icon(Icons.play_arrow_rounded), + label: const Text('Play/Resume'), + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + ), + ); + }, + ), + Expanded( + child: LayoutBuilder( builder: (context, constraints) { return SizedBox( - width: calculateWidth(context, constraints), - // a boxy button with icon and text but little rounded corner - child: ElevatedButton.icon( - onPressed: () async { - // play the book - debugPrint('Pressed play/resume button'); - // set the book to the player if not already set - if (player.book != book) { - debugPrint('Setting the book ${book.libraryItemId}'); - await player.setSourceAudioBook(book); - ref - .read(audiobookPlayerProvider.notifier) - .notifyListeners(); - } - // toggle play/pause - player.togglePlayPause(); - }, - icon: const Icon(Icons.play_arrow_rounded), - label: const Text('Play/Resume'), - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), + width: constraints.maxWidth * 0.6, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // read list button + IconButton( + onPressed: () {}, + icon: const Icon( + Icons.playlist_add_rounded, + ), ), - ), + // share button + IconButton( + onPressed: () {}, + icon: const Icon(Icons.share_rounded), + ), + // download button + IconButton( + onPressed: () {}, + icon: const Icon( + Icons.download_rounded, + ), + ), + // more button + IconButton( + onPressed: () {}, + icon: const Icon( + Icons.more_vert_rounded, + ), + ), + ], ), ); }, ), - Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - return SizedBox( - width: constraints.maxWidth * 0.6, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - // read list button - IconButton( - onPressed: () {}, - icon: const Icon( - Icons.playlist_add_rounded, - ), - ), - // share button - IconButton( - onPressed: () {}, - icon: const Icon(Icons.share_rounded), - ), - // download button - IconButton( - onPressed: () {}, - icon: const Icon( - Icons.download_rounded, - ), - ), - // more button - IconButton( - onPressed: () {}, - icon: const Icon( - Icons.more_vert_rounded, - ), - ), - ], - ), - ); - }, - ), - ), - ], - ), + ), + ], ), ); } diff --git a/lib/features/player/core/audiobook_player.dart b/lib/features/player/core/audiobook_player.dart index 6adbb3e..8e8a78d 100644 --- a/lib/features/player/core/audiobook_player.dart +++ b/lib/features/player/core/audiobook_player.dart @@ -3,13 +3,15 @@ /// this is needed as audiobook can be a list of audio files instead of a single file library; -import 'package:audioplayers/audioplayers.dart'; +import 'package:flutter/foundation.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:just_audio_background/just_audio_background.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; /// will manage the audio player instance class AudiobookPlayer extends AudioPlayer { // constructor which takes in the BookExpanded object - AudiobookPlayer(this.token, this.baseUrl, {super.playerId}) : super() { + AudiobookPlayer(this.token, this.baseUrl) : super() { // set the source of the player to the first track in the book } @@ -28,7 +30,7 @@ class AudiobookPlayer extends AudioPlayer { final Uri baseUrl; // the current index of the audio file in the [book] - final int _currentIndex = 0; + // final int _currentIndex = 0; // available audio tracks int? get availableTracks => _book?.tracks.length; @@ -46,15 +48,32 @@ class AudiobookPlayer extends AudioPlayer { // if the book is the same, do nothing return; } - // first stop the player + // first stop the player and clear the source await stop(); - var track = book.tracks[_currentIndex]; - var url = '$baseUrl${track.contentUrl}?token=$token'; - await setSourceUrl( - url, - mimeType: track.mimeType, - ); + await setAudioSource( + ConcatenatingAudioSource( + useLazyPreparation: true, + children: book.tracks.map((track) { + return AudioSource.uri( + Uri.parse('$baseUrl${track.contentUrl}?token=$token'), + tag: MediaItem( + // Specify a unique ID for each media item: + id: book.libraryItemId + track.index.toString(), + // Metadata to display in the notification: + album: book.metadata.title, + title: book.metadata.title ?? track.title, + artUri: Uri.parse( + '$baseUrl/api/items/${book.libraryItemId}/cover?token=$token&width=800', + ), + ), + ); + }).toList(), + ), + ).catchError((error) { + debugPrint('Error: $error'); + }); + _book = book; } @@ -64,56 +83,37 @@ class AudiobookPlayer extends AudioPlayer { if (_book == null) { throw StateError('No book is set'); } - return switch (state) { - PlayerState.playing => pause(), - PlayerState.paused || - PlayerState.stopped || - PlayerState.completed => - resume(), - // do nothing if the player is disposed - PlayerState.disposed => throw StateError('Player is disposed'), + + // ! refactor this + return switch (playerState) { + PlayerState(playing: var isPlaying) => isPlaying ? pause() : play(), }; } - /// override resume to set the source if the book is not set - @override - Future resume() async { - if (_book == null) { - throw StateError('No book is set'); - } - return super.resume(); - } - - /// a convenience stream for onPositionEveryXSeconds - Stream onPositionEvery(Duration duration) => TimerPositionUpdater( - getPosition: getCurrentPosition, - interval: duration, - ).positionStream; - /// need to override getDuration and getCurrentPosition to return according to the book instead of the current track /// this is because the book can be a list of audio files and the player is only aware of the current track /// so we need to calculate the duration and current position based on the book - @override - Future getDuration() async { - if (_book == null) { - return null; - } - return _book!.tracks.fold( - Duration.zero, - (previousValue, element) => previousValue + element.duration, - ); - } + // @override + // Future getDuration() async { + // if (_book == null) { + // return null; + // } + // return _book!.tracks.fold( + // Duration.zero, + // (previousValue, element) => previousValue + element.duration, + // ); +// } - @override - Future getCurrentPosition() async { - if (_book == null) { - return null; - } - var currentTrack = _book!.tracks[_currentIndex]; - var currentTrackDuration = currentTrack.duration; - var currentTrackPosition = await super.getCurrentPosition(); - return currentTrackPosition != null - ? currentTrackPosition + currentTrackDuration - : null; - } + // @override + // Future getCurrentPosition() async { + // if (_book == null) { + // return null; + // } + // var currentTrack = _book!.tracks[_currentIndex]; + // var currentTrackDuration = currentTrack.duration; + // var currentTrackPosition = await super.getCurrentPosition(); + // return currentTrackPosition != null + // ? currentTrackPosition + currentTrackDuration + // : null; + // } } diff --git a/lib/features/player/providers/audiobook_player.dart b/lib/features/player/providers/audiobook_player.dart index bac5cda..56e86d7 100644 --- a/lib/features/player/providers/audiobook_player.dart +++ b/lib/features/player/providers/audiobook_player.dart @@ -24,12 +24,12 @@ class AudiobookPlayer extends _$AudiobookPlayer { abp.AudiobookPlayer build() { final api = ref.watch(authenticatedApiProvider); final player = - abp.AudiobookPlayer(api.token!, api.baseUrl, playerId: playerId); + abp.AudiobookPlayer(api.token!, api.baseUrl); ref.onDispose(player.dispose); // bind notify listeners to the player - player.onPlayerStateChanged.listen((_) { + player.playerStateStream.listen((_) { notifyListeners(); }); diff --git a/lib/features/player/providers/player_form.dart b/lib/features/player/providers/player_form.dart new file mode 100644 index 0000000..47371f9 --- /dev/null +++ b/lib/features/player/providers/player_form.dart @@ -0,0 +1,67 @@ +// this provider is used to manage the player form state +// it will inform about the percentage of the player expanded + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:miniplayer/miniplayer.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'player_form.g.dart'; + +const double playerMinHeight = 70; +const miniplayerPercentageDeclaration = 0.2; + +extension on Ref { + // We can move the previous logic to a Ref extension. + // This enables reusing the logic between providers + T disposeAndListenChangeNotifier(T notifier) { + onDispose(notifier.dispose); + notifier.addListener(notifyListeners); + // We return the notifier to ease the usage a bit + return notifier; + } +} + +@Riverpod(keepAlive: true) +Raw> playerExpandProgressNotifier( + PlayerExpandProgressNotifierRef ref, +) { + final ValueNotifier playerExpandProgress = + ValueNotifier(playerMinHeight); + + return ref.disposeAndListenChangeNotifier(playerExpandProgress); +} + +// @Riverpod(keepAlive: true) +// Raw> dragDownPercentageNotifier( +// DragDownPercentageNotifierRef ref, +// ) { +// final ValueNotifier notifier = ValueNotifier(0); + +// return ref.disposeAndListenChangeNotifier(notifier); +// } + +// a provider that will listen to the playerExpandProgressNotifier and return the percentage of the player expanded +@Riverpod(keepAlive: true) +double playerHeight( + PlayerHeightRef ref, +) { + final playerExpandProgress = ref.watch(playerExpandProgressNotifierProvider); + + // on change of the playerExpandProgress invalidate + playerExpandProgress.addListener(() { + ref.invalidateSelf(); + }); + + // listen to the playerExpandProgressNotifier and return the value + return playerExpandProgress.value; +} + +// a final MiniplayerController controller = MiniplayerController(); +@Riverpod(keepAlive: true) +MiniplayerController miniplayerController( + MiniplayerControllerRef ref, +) { + return MiniplayerController(); +} diff --git a/lib/features/player/providers/player_form.g.dart b/lib/features/player/providers/player_form.g.dart new file mode 100644 index 0000000..5222543 --- /dev/null +++ b/lib/features/player/providers/player_form.g.dart @@ -0,0 +1,58 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'player_form.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$playerExpandProgressNotifierHash() => + r'e4817361b9a311b61ca23e51082ed11b0a1120ab'; + +/// See also [playerExpandProgressNotifier]. +@ProviderFor(playerExpandProgressNotifier) +final playerExpandProgressNotifierProvider = + Provider>>.internal( + playerExpandProgressNotifier, + name: r'playerExpandProgressNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$playerExpandProgressNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef PlayerExpandProgressNotifierRef + = ProviderRef>>; +String _$playerHeightHash() => r'26dbcb180d494575488d700bd5bdb58c02c224a9'; + +/// See also [playerHeight]. +@ProviderFor(playerHeight) +final playerHeightProvider = Provider.internal( + playerHeight, + name: r'playerHeightProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$playerHeightHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef PlayerHeightRef = ProviderRef; +String _$miniplayerControllerHash() => + r'489579a18f4e08793de08a4828172bd924768301'; + +/// See also [miniplayerController]. +@ProviderFor(miniplayerController) +final miniplayerControllerProvider = Provider.internal( + miniplayerController, + name: r'miniplayerControllerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$miniplayerControllerHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef MiniplayerControllerRef = ProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/features/player/view/audiobook_player.dart b/lib/features/player/view/audiobook_player.dart index b9ccb9f..f35b418 100644 --- a/lib/features/player/view/audiobook_player.dart +++ b/lib/features/player/view/audiobook_player.dart @@ -1,13 +1,14 @@ import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; -import 'package:audioplayers/audioplayers.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:just_audio/just_audio.dart'; import 'package:miniplayer/miniplayer.dart'; import 'package:whispering_pages/api/image_provider.dart'; import 'package:whispering_pages/api/library_item_provider.dart'; import 'package:whispering_pages/features/player/providers/audiobook_player.dart'; import 'package:whispering_pages/features/player/providers/currently_playing_provider.dart'; +import 'package:whispering_pages/features/player/providers/player_form.dart'; import 'package:whispering_pages/shared/widgets/shelves/book_shelf.dart'; import 'package:whispering_pages/theme/theme_from_cover_provider.dart'; @@ -26,13 +27,7 @@ double percentageFromValueInRange({required final double min, max, value}) { return (value - min) / (max - min); } -const double playerMinHeight = 70; -const double playerMaxHeight = 500; -const miniplayerPercentageDeclaration = 0.2; -final ValueNotifier playerExpandProgress = - ValueNotifier(playerMinHeight); -final MiniplayerController controller = MiniplayerController(); class AudiobookPlayer extends HookConsumerWidget { const AudiobookPlayer({super.key}); @@ -64,39 +59,19 @@ class AudiobookPlayer extends HookConsumerWidget { ); // add controller to the player state listener - player.onPlayerStateChanged.listen((state) { - if (state == PlayerState.playing) { - playPauseController.reverse(); - } else { + player.playerStateStream.listen((state) { + if (state.playing) { playPauseController.forward(); + } else { + playPauseController.reverse(); } }); - final playPauseButton = IconButton( - onPressed: () async { - await player.togglePlayPause(); - }, - icon: AnimatedIcon( - icon: AnimatedIcons.pause_play, - progress: playPauseController, - size: 50, - ), + final playPauseButton = AudiobookPlayerPlayPauseButton( + playPauseController: playPauseController, ); - // player.onPositionChanged.listen((event) { - // currentProgress.value = event.inSeconds.toDouble(); - // }); - // final progressStream = TimerPositionUpdater( - // getPosition: player.getCurrentPosition, - // interval: const Duration(milliseconds: 500), - // ).positionStream; - // // a debug that will print the current position of the player - // progressStream.listen((event) { - // debugPrint('Current position: ${event.inSeconds}'); - // }); - - // the widget that will be displayed when the player is expanded - const progressBar = PlayerProgressBar(); + const progressBar = AudiobookTotalProgressBar(); // theme from image final imageTheme = ref.watch( @@ -105,30 +80,38 @@ class AudiobookPlayer extends HookConsumerWidget { brightness: Theme.of(context).brightness, ), ); - return Theme( - // get the theme from imageThemeProvider + // max height of the player is the height of the screen + final playerMaxHeight = MediaQuery.of(context).size.height; + + + return Theme( data: ThemeData( colorScheme: imageTheme.valueOrNull ?? Theme.of(context).colorScheme, ), child: Miniplayer( - valueNotifier: playerExpandProgress, + valueNotifier: ref.watch(playerExpandProgressNotifierProvider), minHeight: playerMinHeight, maxHeight: playerMaxHeight, - controller: controller, + controller: ref.watch(miniplayerControllerProvider), elevation: 4, onDismissed: () { player.setSourceAudioBook(null); }, curve: Curves.easeOut, builder: (height, percentage) { + // return SafeArea( + // child: Text( + // 'percentage: ${percentage.toStringAsFixed(2)}, height: ${height.toStringAsFixed(2)}', + // ), + // ); final bool isFormMiniplayer = percentage < miniplayerPercentageDeclaration; final double availWidth = MediaQuery.of(context).size.width; final maxImgSize = availWidth * 0.4; - + final bookTitle = Text(player.book?.metadata.title ?? ''); - + //Declare additional widgets (eg. SkipButton) and variables if (!isFormMiniplayer) { var percentageExpandedPlayer = percentageFromValueInRange( @@ -153,7 +136,7 @@ class AudiobookPlayer extends HookConsumerWidget { percentage: percentageExpandedPlayer, ) / 2; - + const buttonSkipForward = IconButton( icon: Icon(Icons.forward_30), iconSize: 33, @@ -164,12 +147,6 @@ class AudiobookPlayer extends HookConsumerWidget { iconSize: 33, onPressed: onTap, ); - const buttonPlayExpanded = IconButton( - icon: Icon(Icons.pause_circle_filled), - iconSize: 50, - onPressed: onTap, - ); - return PlayerWhenExpanded( imgPaddingLeft: paddingLeft, imgPaddingVertical: paddingVertical, @@ -178,12 +155,12 @@ class AudiobookPlayer extends HookConsumerWidget { percentageExpandedPlayer: percentageExpandedPlayer, text: bookTitle, buttonSkipBackwards: buttonSkipBackwards, - buttonPlayExpanded: playPauseButton, + playPauseButton: playPauseButton, buttonSkipForward: buttonSkipForward, progressIndicator: progressBar, ); } - + //Miniplayer final percentageMiniplayer = percentageFromValueInRange( min: playerMinHeight, @@ -191,16 +168,14 @@ class AudiobookPlayer extends HookConsumerWidget { playerMinHeight, value: height, ); - + final elementOpacity = 1 - 1 * percentageMiniplayer; - final progressIndicatorHeight = 4 - 4 * percentageMiniplayer; - + return PlayerWhenMinimized( maxImgSize: maxImgSize, imgWidget: imgWidget, elementOpacity: elementOpacity, playPauseButton: playPauseButton, - progressIndicatorHeight: progressIndicatorHeight, progressIndicator: progressBar, ); }, @@ -209,34 +184,102 @@ class AudiobookPlayer extends HookConsumerWidget { } } -class PlayerProgressBar extends HookConsumerWidget { - const PlayerProgressBar({ +class AudiobookPlayerPlayPauseButton extends HookConsumerWidget { + const AudiobookPlayerPlayPauseButton({ + super.key, + required this.playPauseController, + }); + + final AnimationController playPauseController; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final player = ref.watch(audiobookPlayerProvider); + return switch (player.processingState) { + ProcessingState.loading || + ProcessingState.buffering => + const CircularProgressIndicator(), + ProcessingState.completed => IconButton( + onPressed: () async { + await player.seek(const Duration(seconds: 0)); + await player.play(); + }, + icon: const Icon(Icons.replay), + ), + ProcessingState.ready => IconButton( + onPressed: () async { + await player.togglePlayPause(); + }, + icon: AnimatedIcon( + icon: AnimatedIcons.play_pause, + progress: playPauseController, + size: 50, + ), + ), + ProcessingState.idle => const SizedBox.shrink(), + }; + } +} + +/// A progress bar that shows the total progress of the audiobook +/// +/// for chapter progress, use [AudiobookChapterProgressBar] +class AudiobookTotalProgressBar extends HookConsumerWidget { + const AudiobookTotalProgressBar({ super.key, }); @override Widget build(BuildContext context, WidgetRef ref) { final player = ref.watch(audiobookPlayerProvider); - final playerState = useState(player.state); + // final playerState = useState(player.processingState); // add a listener to the player state - player.onPlayerStateChanged.listen((state) { - playerState.value = state; - }); - return StreamBuilder( - stream: player.onPositionChanged, - builder: (context, snapshot) { - return ProgressBar( - progress: snapshot.data ?? const Duration(seconds: 0), - total: player.book?.duration ?? const Duration(seconds: 0), - onSeek: player.seek, - thumbRadius: 8, - // thumbColor: Theme.of(context).colorScheme.secondary, - thumbGlowColor: Theme.of(context).colorScheme.secondary, - thumbGlowRadius: playerState.value == PlayerState.playing ? 10 : 0, + // player.processingStateStream.listen((state) { + // playerState.value = state; + // }); + return StreamBuilder( + stream: player.currentIndexStream, + builder: (context, currentTrackIndex) { + return StreamBuilder( + stream: player.positionStream, + builder: (context, progress) { + // totalProgress is the sum of the duration of all the tracks before the current track + the current track position + final totalProgress = + player.book?.tracks.sublist(0, currentTrackIndex.data).fold( + const Duration(seconds: 0), + (previousValue, element) => + previousValue + element.duration, + ) ?? + const Duration(seconds: 0) + + (progress.data ?? const Duration(seconds: 0)); + + return StreamBuilder( + stream: player.bufferedPositionStream, + builder: (context, buffered) { + final totalBuffered = + player.book?.tracks.sublist(0, currentTrackIndex.data).fold( + const Duration(seconds: 0), + (previousValue, element) => + previousValue + element.duration, + ) ?? + const Duration(seconds: 0) + + (buffered.data ?? const Duration(seconds: 0)); + return ProgressBar( + progress: totalProgress, + total: player.book?.duration ?? const Duration(seconds: 0), + onSeek: player.seek, + thumbRadius: 8, + buffered: totalBuffered, + bufferedBarColor: Theme.of(context).colorScheme.secondary, + ); + }, + ); + }, ); }, ); } } +// ! TODO remove onTap void onTap() {} diff --git a/lib/features/player/view/player_when_expanded.dart b/lib/features/player/view/player_when_expanded.dart index b4f03ea..02fe37f 100644 --- a/lib/features/player/view/player_when_expanded.dart +++ b/lib/features/player/view/player_when_expanded.dart @@ -10,7 +10,7 @@ class PlayerWhenExpanded extends StatelessWidget { required this.percentageExpandedPlayer, required this.text, required this.buttonSkipBackwards, - required this.buttonPlayExpanded, + required this.playPauseButton, required this.buttonSkipForward, required this.progressIndicator, }); @@ -23,7 +23,7 @@ class PlayerWhenExpanded extends StatelessWidget { final double percentageExpandedPlayer; final Text text; final IconButton buttonSkipBackwards; - final IconButton buttonPlayExpanded; + final Widget playPauseButton; final IconButton buttonSkipForward; final Widget progressIndicator; @@ -62,7 +62,7 @@ class PlayerWhenExpanded extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ buttonSkipBackwards, - buttonPlayExpanded, + playPauseButton, buttonSkipForward, ], ), diff --git a/lib/features/player/view/player_when_minimized.dart b/lib/features/player/view/player_when_minimized.dart index 5c74f17..4a9cbe1 100644 --- a/lib/features/player/view/player_when_minimized.dart +++ b/lib/features/player/view/player_when_minimized.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:miniplayer/miniplayer.dart'; import 'package:whispering_pages/features/player/providers/audiobook_player.dart'; -import 'package:whispering_pages/features/player/view/audiobook_player.dart'; +import 'package:whispering_pages/features/player/providers/player_form.dart'; class PlayerWhenMinimized extends HookConsumerWidget { const PlayerWhenMinimized({ @@ -11,20 +10,19 @@ class PlayerWhenMinimized extends HookConsumerWidget { required this.imgWidget, required this.elementOpacity, required this.playPauseButton, - required this.progressIndicatorHeight, required this.progressIndicator, }); final double maxImgSize; final Widget imgWidget; final double elementOpacity; - final IconButton playPauseButton; - final double progressIndicatorHeight; + final Widget playPauseButton; final Widget progressIndicator; @override Widget build(BuildContext context, WidgetRef ref) { final player = ref.watch(audiobookPlayerProvider); + final controller = ref.watch(miniplayerControllerProvider); return Column( children: [ Expanded( @@ -67,14 +65,14 @@ class PlayerWhenMinimized extends HookConsumerWidget { ), ), ), - IconButton( - icon: const Icon(Icons.fullscreen), - onPressed: () { - controller.animateToHeight(state: PanelState.MAX); - }, - ), + // IconButton( + // icon: const Icon(Icons.fullscreen), + // onPressed: () { + // controller.animateToHeight(state: PanelState.MAX); + // }, + // ), Padding( - padding: const EdgeInsets.only(right: 3), + padding: const EdgeInsets.all(8), child: Opacity( opacity: elementOpacity, child: playPauseButton, @@ -83,13 +81,13 @@ class PlayerWhenMinimized extends HookConsumerWidget { ], ), ), - SizedBox( - height: progressIndicatorHeight, - child: Opacity( - opacity: elementOpacity, - child: progressIndicator, - ), - ), + // SizedBox( + // height: progressIndicatorHeight, + // child: Opacity( + // opacity: elementOpacity, + // child: progressIndicator, + // ), + // ), ], ); } diff --git a/lib/main.dart b/lib/main.dart index 791774f..fb5bc3e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,10 @@ +import 'package:audio_session/audio_session.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:just_audio_background/just_audio_background.dart' + show JustAudioBackground; +import 'package:just_audio_media_kit/just_audio_media_kit.dart' + show JustAudioMediaKit; import 'package:whispering_pages/api/server_provider.dart'; import 'package:whispering_pages/db/storage.dart'; import 'package:whispering_pages/router/router.dart'; @@ -10,9 +15,24 @@ import 'package:whispering_pages/theme/theme.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + // for playing audio on windows, linux + JustAudioMediaKit.ensureInitialized(); + // initialize the storage await initStorage(); + // for configuring how this app will interact with other audio apps + final session = await AudioSession.instance; + await session.configure(const AudioSessionConfiguration.speech()); + + // for playing audio in the background + await JustAudioBackground.init( + androidNotificationChannelId: 'com.whispering_pages.bg_demo.channel.audio', + androidNotificationChannelName: 'Audio playback', + androidNotificationOngoing: true, + ); + + // run the app runApp( const ProviderScope( child: MyApp(), diff --git a/lib/router/scaffold_with_nav_bar.dart b/lib/router/scaffold_with_nav_bar.dart index d68a9bd..dc04f3c 100644 --- a/lib/router/scaffold_with_nav_bar.dart +++ b/lib/router/scaffold_with_nav_bar.dart @@ -1,6 +1,9 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:whispering_pages/features/player/providers/player_form.dart'; import 'package:whispering_pages/features/player/view/audiobook_player.dart'; /// Builds the "shell" for the app by building a Scaffold with a @@ -17,6 +20,16 @@ class ScaffoldWithNavBar extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + // playerExpandProgress is used to animate bottom navigation bar to opacity 0 and slide down when player is expanded + // final playerProgress = + // useValueListenable(ref.watch(playerExpandProgressNotifierProvider)); + final playerProgress = ref.watch(playerHeightProvider); + final playerMaxHeight = MediaQuery.of(context).size.height; + var percentExpanded = (playerProgress - playerMinHeight) / + (playerMaxHeight - playerMinHeight); + // Clamp the value between 0 and 1 + percentExpanded = max(0, min(1, percentExpanded)); + return Scaffold( body: Stack( children: [ @@ -24,33 +37,44 @@ class ScaffoldWithNavBar extends HookConsumerWidget { const AudiobookPlayer(), ], ), - bottomNavigationBar: BottomNavigationBar( - elevation: 0.0, - landscapeLayout: BottomNavigationBarLandscapeLayout.centered, - selectedFontSize: Theme.of(context).textTheme.labelMedium!.fontSize!, - unselectedFontSize: Theme.of(context).textTheme.labelMedium!.fontSize!, - showUnselectedLabels: false, - fixedColor: Theme.of(context).colorScheme.onBackground, - // type: BottomNavigationBarType.fixed, + bottomNavigationBar: Opacity( + // Opacity is interpolated from 1 to 0 when player is expanded + opacity: 1 - percentExpanded, + child: SizedBox( + // height is interpolated from 0 to 56 when player is expanded + height: 56 * (1 - percentExpanded), - // Here, the items of BottomNavigationBar are hard coded. In a real - // world scenario, the items would most likely be generated from the - // branches of the shell route, which can be fetched using - // `navigationShell.route.branches`. - items: const [ - BottomNavigationBarItem( - label: 'Home', - icon: Icon(Icons.home_outlined), - activeIcon: Icon(Icons.home), + child: BottomNavigationBar( + elevation: 0.0, + landscapeLayout: BottomNavigationBarLandscapeLayout.centered, + selectedFontSize: + Theme.of(context).textTheme.labelMedium!.fontSize!, + unselectedFontSize: + Theme.of(context).textTheme.labelMedium!.fontSize!, + showUnselectedLabels: false, + fixedColor: Theme.of(context).colorScheme.onBackground, + // type: BottomNavigationBarType.fixed, + + // Here, the items of BottomNavigationBar are hard coded. In a real + // world scenario, the items would most likely be generated from the + // branches of the shell route, which can be fetched using + // `navigationShell.route.branches`. + items: const [ + BottomNavigationBarItem( + label: 'Home', + icon: Icon(Icons.home_outlined), + activeIcon: Icon(Icons.home), + ), + BottomNavigationBarItem( + label: 'Settings', + icon: Icon(Icons.settings_outlined), + activeIcon: Icon(Icons.settings), + ), + ], + currentIndex: navigationShell.currentIndex, + onTap: (int index) => _onTap(context, index), ), - BottomNavigationBarItem( - label: 'Settings', - icon: Icon(Icons.settings_outlined), - activeIcon: Icon(Icons.settings), - ), - ], - currentIndex: navigationShell.currentIndex, - onTap: (int index) => _onTap(context, index), + ), ), ); } diff --git a/lib/settings/models/app_settings.dart b/lib/settings/models/app_settings.dart index 41f032c..7366bdd 100644 --- a/lib/settings/models/app_settings.dart +++ b/lib/settings/models/app_settings.dart @@ -13,8 +13,30 @@ class AppSettings with _$AppSettings { const factory AppSettings({ @Default(true) bool isDarkMode, @Default(false) bool useMaterialThemeOnItemPage, + @Default(PlayerSettings()) PlayerSettings playerSettings, }) = _AppSettings; factory AppSettings.fromJson(Map json) => _$AppSettingsFromJson(json); } + +@freezed +class PlayerSettings with _$PlayerSettings { + const factory PlayerSettings({ + @Default(MinimizedPlayerSettings()) + MinimizedPlayerSettings miniPlayerSettings, + }) = _PlayerSettings; + + factory PlayerSettings.fromJson(Map json) => + _$PlayerSettingsFromJson(json); +} + +@freezed +class MinimizedPlayerSettings with _$MinimizedPlayerSettings { + const factory MinimizedPlayerSettings({ + @Default(false) bool useChapterInfo, + }) = _MiniPlayerSettings; + + factory MinimizedPlayerSettings.fromJson(Map json) => + _$MinimizedPlayerSettingsFromJson(json); +} diff --git a/lib/settings/models/app_settings.freezed.dart b/lib/settings/models/app_settings.freezed.dart index e5eb67c..c2597e7 100644 --- a/lib/settings/models/app_settings.freezed.dart +++ b/lib/settings/models/app_settings.freezed.dart @@ -22,6 +22,7 @@ AppSettings _$AppSettingsFromJson(Map json) { mixin _$AppSettings { bool get isDarkMode => throw _privateConstructorUsedError; bool get useMaterialThemeOnItemPage => throw _privateConstructorUsedError; + PlayerSettings get playerSettings => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -35,7 +36,12 @@ abstract class $AppSettingsCopyWith<$Res> { AppSettings value, $Res Function(AppSettings) then) = _$AppSettingsCopyWithImpl<$Res, AppSettings>; @useResult - $Res call({bool isDarkMode, bool useMaterialThemeOnItemPage}); + $Res call( + {bool isDarkMode, + bool useMaterialThemeOnItemPage, + PlayerSettings playerSettings}); + + $PlayerSettingsCopyWith<$Res> get playerSettings; } /// @nodoc @@ -53,6 +59,7 @@ class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings> $Res call({ Object? isDarkMode = null, Object? useMaterialThemeOnItemPage = null, + Object? playerSettings = null, }) { return _then(_value.copyWith( isDarkMode: null == isDarkMode @@ -63,8 +70,20 @@ class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings> ? _value.useMaterialThemeOnItemPage : useMaterialThemeOnItemPage // ignore: cast_nullable_to_non_nullable as bool, + playerSettings: null == playerSettings + ? _value.playerSettings + : playerSettings // ignore: cast_nullable_to_non_nullable + as PlayerSettings, ) as $Val); } + + @override + @pragma('vm:prefer-inline') + $PlayerSettingsCopyWith<$Res> get playerSettings { + return $PlayerSettingsCopyWith<$Res>(_value.playerSettings, (value) { + return _then(_value.copyWith(playerSettings: value) as $Val); + }); + } } /// @nodoc @@ -75,7 +94,13 @@ abstract class _$$AppSettingsImplCopyWith<$Res> __$$AppSettingsImplCopyWithImpl<$Res>; @override @useResult - $Res call({bool isDarkMode, bool useMaterialThemeOnItemPage}); + $Res call( + {bool isDarkMode, + bool useMaterialThemeOnItemPage, + PlayerSettings playerSettings}); + + @override + $PlayerSettingsCopyWith<$Res> get playerSettings; } /// @nodoc @@ -91,6 +116,7 @@ class __$$AppSettingsImplCopyWithImpl<$Res> $Res call({ Object? isDarkMode = null, Object? useMaterialThemeOnItemPage = null, + Object? playerSettings = null, }) { return _then(_$AppSettingsImpl( isDarkMode: null == isDarkMode @@ -101,6 +127,10 @@ class __$$AppSettingsImplCopyWithImpl<$Res> ? _value.useMaterialThemeOnItemPage : useMaterialThemeOnItemPage // ignore: cast_nullable_to_non_nullable as bool, + playerSettings: null == playerSettings + ? _value.playerSettings + : playerSettings // ignore: cast_nullable_to_non_nullable + as PlayerSettings, )); } } @@ -109,7 +139,9 @@ class __$$AppSettingsImplCopyWithImpl<$Res> @JsonSerializable() class _$AppSettingsImpl implements _AppSettings { const _$AppSettingsImpl( - {this.isDarkMode = true, this.useMaterialThemeOnItemPage = false}); + {this.isDarkMode = true, + this.useMaterialThemeOnItemPage = false, + this.playerSettings = const PlayerSettings()}); factory _$AppSettingsImpl.fromJson(Map json) => _$$AppSettingsImplFromJson(json); @@ -120,10 +152,13 @@ class _$AppSettingsImpl implements _AppSettings { @override @JsonKey() final bool useMaterialThemeOnItemPage; + @override + @JsonKey() + final PlayerSettings playerSettings; @override String toString() { - return 'AppSettings(isDarkMode: $isDarkMode, useMaterialThemeOnItemPage: $useMaterialThemeOnItemPage)'; + return 'AppSettings(isDarkMode: $isDarkMode, useMaterialThemeOnItemPage: $useMaterialThemeOnItemPage, playerSettings: $playerSettings)'; } @override @@ -136,13 +171,15 @@ class _$AppSettingsImpl implements _AppSettings { (identical(other.useMaterialThemeOnItemPage, useMaterialThemeOnItemPage) || other.useMaterialThemeOnItemPage == - useMaterialThemeOnItemPage)); + useMaterialThemeOnItemPage) && + (identical(other.playerSettings, playerSettings) || + other.playerSettings == playerSettings)); } @JsonKey(ignore: true) @override - int get hashCode => - Object.hash(runtimeType, isDarkMode, useMaterialThemeOnItemPage); + int get hashCode => Object.hash( + runtimeType, isDarkMode, useMaterialThemeOnItemPage, playerSettings); @JsonKey(ignore: true) @override @@ -161,7 +198,8 @@ class _$AppSettingsImpl implements _AppSettings { abstract class _AppSettings implements AppSettings { const factory _AppSettings( {final bool isDarkMode, - final bool useMaterialThemeOnItemPage}) = _$AppSettingsImpl; + final bool useMaterialThemeOnItemPage, + final PlayerSettings playerSettings}) = _$AppSettingsImpl; factory _AppSettings.fromJson(Map json) = _$AppSettingsImpl.fromJson; @@ -171,7 +209,309 @@ abstract class _AppSettings implements AppSettings { @override bool get useMaterialThemeOnItemPage; @override + PlayerSettings get playerSettings; + @override @JsonKey(ignore: true) _$$AppSettingsImplCopyWith<_$AppSettingsImpl> get copyWith => throw _privateConstructorUsedError; } + +PlayerSettings _$PlayerSettingsFromJson(Map json) { + return _PlayerSettings.fromJson(json); +} + +/// @nodoc +mixin _$PlayerSettings { + MinimizedPlayerSettings get miniPlayerSettings => + throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $PlayerSettingsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PlayerSettingsCopyWith<$Res> { + factory $PlayerSettingsCopyWith( + PlayerSettings value, $Res Function(PlayerSettings) then) = + _$PlayerSettingsCopyWithImpl<$Res, PlayerSettings>; + @useResult + $Res call({MinimizedPlayerSettings miniPlayerSettings}); + + $MinimizedPlayerSettingsCopyWith<$Res> get miniPlayerSettings; +} + +/// @nodoc +class _$PlayerSettingsCopyWithImpl<$Res, $Val extends PlayerSettings> + implements $PlayerSettingsCopyWith<$Res> { + _$PlayerSettingsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? miniPlayerSettings = null, + }) { + return _then(_value.copyWith( + miniPlayerSettings: null == miniPlayerSettings + ? _value.miniPlayerSettings + : miniPlayerSettings // ignore: cast_nullable_to_non_nullable + as MinimizedPlayerSettings, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $MinimizedPlayerSettingsCopyWith<$Res> get miniPlayerSettings { + return $MinimizedPlayerSettingsCopyWith<$Res>(_value.miniPlayerSettings, + (value) { + return _then(_value.copyWith(miniPlayerSettings: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$PlayerSettingsImplCopyWith<$Res> + implements $PlayerSettingsCopyWith<$Res> { + factory _$$PlayerSettingsImplCopyWith(_$PlayerSettingsImpl value, + $Res Function(_$PlayerSettingsImpl) then) = + __$$PlayerSettingsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({MinimizedPlayerSettings miniPlayerSettings}); + + @override + $MinimizedPlayerSettingsCopyWith<$Res> get miniPlayerSettings; +} + +/// @nodoc +class __$$PlayerSettingsImplCopyWithImpl<$Res> + extends _$PlayerSettingsCopyWithImpl<$Res, _$PlayerSettingsImpl> + implements _$$PlayerSettingsImplCopyWith<$Res> { + __$$PlayerSettingsImplCopyWithImpl( + _$PlayerSettingsImpl _value, $Res Function(_$PlayerSettingsImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? miniPlayerSettings = null, + }) { + return _then(_$PlayerSettingsImpl( + miniPlayerSettings: null == miniPlayerSettings + ? _value.miniPlayerSettings + : miniPlayerSettings // ignore: cast_nullable_to_non_nullable + as MinimizedPlayerSettings, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$PlayerSettingsImpl implements _PlayerSettings { + const _$PlayerSettingsImpl( + {this.miniPlayerSettings = const MinimizedPlayerSettings()}); + + factory _$PlayerSettingsImpl.fromJson(Map json) => + _$$PlayerSettingsImplFromJson(json); + + @override + @JsonKey() + final MinimizedPlayerSettings miniPlayerSettings; + + @override + String toString() { + return 'PlayerSettings(miniPlayerSettings: $miniPlayerSettings)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PlayerSettingsImpl && + (identical(other.miniPlayerSettings, miniPlayerSettings) || + other.miniPlayerSettings == miniPlayerSettings)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, miniPlayerSettings); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$PlayerSettingsImplCopyWith<_$PlayerSettingsImpl> get copyWith => + __$$PlayerSettingsImplCopyWithImpl<_$PlayerSettingsImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$PlayerSettingsImplToJson( + this, + ); + } +} + +abstract class _PlayerSettings implements PlayerSettings { + const factory _PlayerSettings( + {final MinimizedPlayerSettings miniPlayerSettings}) = + _$PlayerSettingsImpl; + + factory _PlayerSettings.fromJson(Map json) = + _$PlayerSettingsImpl.fromJson; + + @override + MinimizedPlayerSettings get miniPlayerSettings; + @override + @JsonKey(ignore: true) + _$$PlayerSettingsImplCopyWith<_$PlayerSettingsImpl> get copyWith => + throw _privateConstructorUsedError; +} + +MinimizedPlayerSettings _$MinimizedPlayerSettingsFromJson( + Map json) { + return _MiniPlayerSettings.fromJson(json); +} + +/// @nodoc +mixin _$MinimizedPlayerSettings { + bool get useChapterInfo => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $MinimizedPlayerSettingsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MinimizedPlayerSettingsCopyWith<$Res> { + factory $MinimizedPlayerSettingsCopyWith(MinimizedPlayerSettings value, + $Res Function(MinimizedPlayerSettings) then) = + _$MinimizedPlayerSettingsCopyWithImpl<$Res, MinimizedPlayerSettings>; + @useResult + $Res call({bool useChapterInfo}); +} + +/// @nodoc +class _$MinimizedPlayerSettingsCopyWithImpl<$Res, + $Val extends MinimizedPlayerSettings> + implements $MinimizedPlayerSettingsCopyWith<$Res> { + _$MinimizedPlayerSettingsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? useChapterInfo = null, + }) { + return _then(_value.copyWith( + useChapterInfo: null == useChapterInfo + ? _value.useChapterInfo + : useChapterInfo // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$MiniPlayerSettingsImplCopyWith<$Res> + implements $MinimizedPlayerSettingsCopyWith<$Res> { + factory _$$MiniPlayerSettingsImplCopyWith(_$MiniPlayerSettingsImpl value, + $Res Function(_$MiniPlayerSettingsImpl) then) = + __$$MiniPlayerSettingsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({bool useChapterInfo}); +} + +/// @nodoc +class __$$MiniPlayerSettingsImplCopyWithImpl<$Res> + extends _$MinimizedPlayerSettingsCopyWithImpl<$Res, + _$MiniPlayerSettingsImpl> + implements _$$MiniPlayerSettingsImplCopyWith<$Res> { + __$$MiniPlayerSettingsImplCopyWithImpl(_$MiniPlayerSettingsImpl _value, + $Res Function(_$MiniPlayerSettingsImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? useChapterInfo = null, + }) { + return _then(_$MiniPlayerSettingsImpl( + useChapterInfo: null == useChapterInfo + ? _value.useChapterInfo + : useChapterInfo // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$MiniPlayerSettingsImpl implements _MiniPlayerSettings { + const _$MiniPlayerSettingsImpl({this.useChapterInfo = false}); + + factory _$MiniPlayerSettingsImpl.fromJson(Map json) => + _$$MiniPlayerSettingsImplFromJson(json); + + @override + @JsonKey() + final bool useChapterInfo; + + @override + String toString() { + return 'MinimizedPlayerSettings(useChapterInfo: $useChapterInfo)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MiniPlayerSettingsImpl && + (identical(other.useChapterInfo, useChapterInfo) || + other.useChapterInfo == useChapterInfo)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, useChapterInfo); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$MiniPlayerSettingsImplCopyWith<_$MiniPlayerSettingsImpl> get copyWith => + __$$MiniPlayerSettingsImplCopyWithImpl<_$MiniPlayerSettingsImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$MiniPlayerSettingsImplToJson( + this, + ); + } +} + +abstract class _MiniPlayerSettings implements MinimizedPlayerSettings { + const factory _MiniPlayerSettings({final bool useChapterInfo}) = + _$MiniPlayerSettingsImpl; + + factory _MiniPlayerSettings.fromJson(Map json) = + _$MiniPlayerSettingsImpl.fromJson; + + @override + bool get useChapterInfo; + @override + @JsonKey(ignore: true) + _$$MiniPlayerSettingsImplCopyWith<_$MiniPlayerSettingsImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/settings/models/app_settings.g.dart b/lib/settings/models/app_settings.g.dart index 533f9c7..b9fdaef 100644 --- a/lib/settings/models/app_settings.g.dart +++ b/lib/settings/models/app_settings.g.dart @@ -11,10 +11,41 @@ _$AppSettingsImpl _$$AppSettingsImplFromJson(Map json) => isDarkMode: json['isDarkMode'] as bool? ?? true, useMaterialThemeOnItemPage: json['useMaterialThemeOnItemPage'] as bool? ?? false, + playerSettings: json['playerSettings'] == null + ? const PlayerSettings() + : PlayerSettings.fromJson( + json['playerSettings'] as Map), ); Map _$$AppSettingsImplToJson(_$AppSettingsImpl instance) => { 'isDarkMode': instance.isDarkMode, 'useMaterialThemeOnItemPage': instance.useMaterialThemeOnItemPage, + 'playerSettings': instance.playerSettings, + }; + +_$PlayerSettingsImpl _$$PlayerSettingsImplFromJson(Map json) => + _$PlayerSettingsImpl( + miniPlayerSettings: json['miniPlayerSettings'] == null + ? const MinimizedPlayerSettings() + : MinimizedPlayerSettings.fromJson( + json['miniPlayerSettings'] as Map), + ); + +Map _$$PlayerSettingsImplToJson( + _$PlayerSettingsImpl instance) => + { + 'miniPlayerSettings': instance.miniPlayerSettings, + }; + +_$MiniPlayerSettingsImpl _$$MiniPlayerSettingsImplFromJson( + Map json) => + _$MiniPlayerSettingsImpl( + useChapterInfo: json['useChapterInfo'] as bool? ?? false, + ); + +Map _$$MiniPlayerSettingsImplToJson( + _$MiniPlayerSettingsImpl instance) => + { + 'useChapterInfo': instance.useChapterInfo, }; diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index a222344..ff22425 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -91,6 +91,8 @@ set_target_properties(${BINARY_NAME} # them to the application. include(flutter/generated_plugins.cmake) +# as suggested by https://pub.dev/packages/just_audio_media_kit +target_link_libraries(${BINARY_NAME} PRIVATE ${MIMALLOC_LIB}) # === Installation === # By default, "installing" just makes a relocatable bundle in the build diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 35ac2ac..879195f 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,17 +6,17 @@ #include "generated_plugin_registrant.h" -#include #include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin"); - audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar); g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin"); isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); + media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 6d6f2a8..026cbff 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,8 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST - audioplayers_linux isar_flutter_libs + media_kit_libs_linux url_launcher_linux ) diff --git a/pubspec.lock b/pubspec.lock index 5bf968a..428a520 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -65,6 +65,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + audio_service: + dependency: transitive + description: + name: audio_service + sha256: "4547c312a94f9cb2c48b60823fb190767cbd63454a83c73049384d5d3cba4650" + url: "https://pub.dev" + source: hosted + version: "0.18.13" + audio_service_platform_interface: + dependency: transitive + description: + name: audio_service_platform_interface + sha256: "8431a455dac9916cc9ee6f7da5620a666436345c906ad2ebb7fa41d18b3c1bf4" + url: "https://pub.dev" + source: hosted + version: "0.1.1" + audio_service_web: + dependency: transitive + description: + name: audio_service_web + sha256: "9d7d5ae5f98a5727f2580fad73062f2484f400eef6cef42919413268e62a363e" + url: "https://pub.dev" + source: hosted + version: "0.1.2" + audio_session: + dependency: "direct main" + description: + name: audio_session + sha256: a49af9981eec5d7cd73b37bacb6ee73f8143a6a9f9bd5b6021e6c346b9b6cf4e + url: "https://pub.dev" + source: hosted + version: "0.1.19" audio_video_progress_bar: dependency: "direct main" description: @@ -73,62 +105,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" - audioplayers: - dependency: "direct main" - description: - name: audioplayers - sha256: "752039d6aa752597c98ec212e9759519061759e402e7da59a511f39d43aa07d2" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - audioplayers_android: - dependency: transitive - description: - name: audioplayers_android - sha256: de576b890befe27175c2f511ba8b742bec83765fa97c3ce4282bba46212f58e4 - url: "https://pub.dev" - source: hosted - version: "5.0.0" - audioplayers_darwin: - dependency: transitive - description: - name: audioplayers_darwin - sha256: e507887f3ff18d8e5a10a668d7bedc28206b12e10b98347797257c6ae1019c3b - url: "https://pub.dev" - source: hosted - version: "6.0.0" - audioplayers_linux: - dependency: transitive - description: - name: audioplayers_linux - sha256: "3d3d244c90436115417f170426ce768856d8fe4dfc5ed66a049d2890acfa82f9" - url: "https://pub.dev" - source: hosted - version: "4.0.0" - audioplayers_platform_interface: - dependency: transitive - description: - name: audioplayers_platform_interface - sha256: "6834dd48dfb7bc6c2404998ebdd161f79cd3774a7e6779e1348d54a3bfdcfaa5" - url: "https://pub.dev" - source: hosted - version: "7.0.0" - audioplayers_web: - dependency: transitive - description: - name: audioplayers_web - sha256: db8fc420dadf80da18e2286c18e746fb4c3b2c5adbf0c963299dde046828886d - url: "https://pub.dev" - source: hosted - version: "5.0.0" - audioplayers_windows: - dependency: transitive - description: - name: audioplayers_windows - sha256: "8605762dddba992138d476f6a0c3afd9df30ac5b96039929063eceed416795c2" - url: "https://pub.dev" - source: hosted - version: "4.0.0" auto_scroll_text: dependency: "direct main" description: @@ -568,6 +544,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" + url: "https://pub.dev" + source: hosted + version: "4.1.7" io: dependency: transitive description: @@ -616,6 +600,46 @@ packages: url: "https://pub.dev" source: hosted version: "6.8.0" + just_audio: + dependency: "direct main" + description: + name: just_audio + sha256: b7cb6bbf3750caa924d03f432ba401ec300fd90936b3f73a9b33d58b1e96286b + url: "https://pub.dev" + source: hosted + version: "0.9.37" + just_audio_background: + dependency: "direct main" + description: + name: just_audio_background + sha256: "3454ffc97edfa1282b7f42759bfa8aa13d9114a24465f4101e0d3ae58a9327fb" + url: "https://pub.dev" + source: hosted + version: "0.0.1-beta.11" + just_audio_media_kit: + dependency: "direct main" + description: + name: just_audio_media_kit + sha256: bbecbd43959c230d9f9610df0e0165855e711b4c960ce730c08f31107cc3bd26 + url: "https://pub.dev" + source: hosted + version: "2.0.4" + just_audio_platform_interface: + dependency: transitive + description: + name: just_audio_platform_interface + sha256: c3dee0014248c97c91fe6299edb73dc4d6c6930a2f4f713579cd692d9e47f4a1 + url: "https://pub.dev" + source: hosted + version: "4.2.2" + just_audio_web: + dependency: transitive + description: + name: just_audio_web + sha256: "134356b0fe3d898293102b33b5fd618831ffdc72bb7a1b726140abdf22772b70" + url: "https://pub.dev" + source: hosted + version: "0.4.9" leak_tracker: dependency: transitive description: @@ -680,6 +704,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.8.0" + media_kit: + dependency: transitive + description: + name: media_kit + sha256: "3289062540e3b8b9746e5c50d95bd78a9289826b7227e253dff806d002b9e67a" + url: "https://pub.dev" + source: hosted + version: "1.1.10+1" + media_kit_libs_linux: + dependency: "direct main" + description: + name: media_kit_libs_linux + sha256: e186891c31daa6bedab4d74dcdb4e8adfccc7d786bfed6ad81fe24a3b3010310 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + media_kit_libs_windows_audio: + dependency: "direct main" + description: + name: media_kit_libs_windows_audio + sha256: c2fd558cc87b9d89a801141fcdffe02e338a3b21a41a18fbd63d5b221a1b8e53 + url: "https://pub.dev" + source: hosted + version: "1.0.9" meta: dependency: transitive description: @@ -699,11 +747,10 @@ packages: miniplayer: dependency: "direct main" description: - name: miniplayer - sha256: "6e12c27aef7432fc16508460a6dc824f3edfeb01761bd0dbfbccc84d516121bf" - url: "https://pub.dev" - source: hosted - version: "1.0.1" + path: "../miniplayer" + relative: true + source: path + version: "1.0.3" octo_image: dependency: transitive description: @@ -776,6 +823,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" platform: dependency: transitive description: @@ -857,13 +912,21 @@ packages: source: hosted version: "2.3.10" rxdart: - dependency: transitive + dependency: "direct main" description: name: rxdart sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" url: "https://pub.dev" source: hosted version: "0.27.7" + safe_local_storage: + dependency: transitive + description: + name: safe_local_storage + sha256: ede4eb6cb7d88a116b3d3bf1df70790b9e2038bc37cb19112e381217c74d9440 + url: "https://pub.dev" + source: hosted + version: "1.0.2" scroll_loop_auto_scroll: dependency: "direct main" description: @@ -1052,6 +1115,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: d315be0f6641898b280ffa34e2ddb14f3d12b1a37882557869646e0cc363d0cc + url: "https://pub.dev" + source: hosted + version: "1.0.0+1" + uri_parser: + dependency: transitive + description: + name: uri_parser + sha256: "6543c9fd86d2862fac55d800a43e67c0dcd1a41677cb69c2f8edfe73bbcf1835" + url: "https://pub.dev" + source: hosted + version: "2.0.2" url_launcher: dependency: "direct main" description: @@ -1180,6 +1259,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index cba6259..68b78b2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,8 +32,8 @@ isar_version: &isar_version ^4.0.0-dev.13 # define the version to be used dependencies: animated_list_plus: ^0.5.2 animated_theme_switcher: ^2.0.10 + audio_session: ^0.1.19 audio_video_progress_bar: ^2.0.2 - audioplayers: ^6.0.0 auto_scroll_text: ^0.0.7 cached_network_image: ^3.3.1 coast: ^2.0.2 @@ -54,11 +54,18 @@ dependencies: isar: ^4.0.0-dev.13 isar_flutter_libs: ^4.0.0-dev.13 json_annotation: ^4.9.0 + just_audio: ^0.9.37 + just_audio_background: ^0.0.1-beta.11 + just_audio_media_kit: ^2.0.4 lottie: ^3.1.0 - miniplayer: ^1.0.1 + media_kit_libs_linux: any + media_kit_libs_windows_audio: any + miniplayer: + path: ../miniplayer path: ^1.9.0 path_provider: ^2.1.0 riverpod_annotation: ^2.3.5 + rxdart: ^0.27.7 scroll_loop_auto_scroll: ^0.0.5 shelfsdk: path: ../../_dart/shelfsdk diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index ab3246f..1e47bc9 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,15 +6,15 @@ #include "generated_plugin_registrant.h" -#include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { - AudioplayersWindowsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); IsarFlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); + MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("MediaKitLibsWindowsAudioPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index eaeb6e5..c41e9ee 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,8 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST - audioplayers_windows isar_flutter_libs + media_kit_libs_windows_audio url_launcher_windows )