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
)