From 3ba35b31b8d494bfaea7f00670c3022fca77a20f Mon Sep 17 00:00:00 2001 From: rang <378694192@qq.com> Date: Fri, 28 Nov 2025 17:05:35 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=80=E5=A0=86=E4=B9=B1=E4=B8=83=E5=85=AB?= =?UTF-8?q?=E7=B3=9F=E7=9A=84=E4=BF=AE=E6=94=B9=20=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=A2=9E=E5=8A=A0=E6=A1=8C=E9=9D=A2=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 1 + lib/api/api_provider.dart | 24 +- lib/api/api_provider.g.dart | 2 +- lib/api/authenticated_users_provider.dart | 7 +- lib/api/library_item_provider.dart | 42 +- lib/api/library_item_provider.g.dart | 140 ++++- lib/api/library_provider.dart | 32 +- lib/api/library_provider.g.dart | 20 + lib/api/server_provider.dart | 5 +- lib/db/available_boxes.dart | 2 +- lib/db/cache/cache_key.dart | 8 + lib/db/init.dart | 2 +- lib/db/register_models.dart | 2 +- .../downloads/core/download_manager.dart | 20 +- .../downloads/providers/download_manager.dart | 2 +- .../providers/search_result_provider.dart | 2 +- lib/features/explore/view/explore_page.dart | 4 +- .../view/library_item_actions.dart | 5 +- .../view/library_item_hero_section.dart | 9 +- lib/features/logging/core/logger.dart | 2 +- .../onboarding/providers/oauth_provider.dart | 2 +- .../onboarding/view/callback_page.dart | 2 +- .../view/onboarding_single_page.dart | 2 +- lib/features/onboarding/view/user_login.dart | 7 +- .../view/user_login_with_open_id.dart | 4 +- .../view/user_login_with_password.dart | 4 +- .../view/user_login_with_token.dart | 4 +- .../models/nullable_player_settings.dart | 2 +- .../core/playback_reporter.dart | 340 ----------- .../core/playback_reporter_session.dart | 2 +- .../providers/playback_reporter_provider.dart | 22 +- .../playback_reporter_provider.g.dart | 8 +- .../player/core/audiobook_player.dart | 570 ++++++++---------- .../player/core/audiobook_player_session.dart | 365 ----------- lib/features/player/core/init.dart | 62 -- .../player/providers/audiobook_player.dart | 145 +++-- .../player/providers/audiobook_player.g.dart | 61 +- .../providers/currently_playing_provider.dart | 62 +- .../currently_playing_provider.g.dart | 63 +- .../player/providers/player_form.dart | 80 --- .../player/providers/player_form.g.dart | 63 -- .../player/providers/player_providers.dart | 1 - .../player/providers/session_provider.dart | 188 ------ .../player/providers/session_provider.g.dart | 88 --- .../view/mini_player_bottom_padding.dart | 5 +- lib/features/player/view/player_expanded.dart | 285 +++++---- .../player/view/player_expanded_desktop.dart | 174 ++++++ .../player/view/player_minimized.dart | 13 +- .../player/view/player_when_expanded.dart | 293 --------- .../player/view/player_when_minimized.dart | 155 ----- .../widgets/audiobook_player_seek_button.dart | 2 +- .../audiobook_player_seek_chapter_button.dart | 2 +- .../widgets/chapter_selection_button.dart | 3 +- .../widgets/player_player_pause_button.dart | 2 +- .../view/widgets/player_progress_bar.dart | 3 +- .../widgets/player_speed_adjust_button.dart | 11 +- .../player/view/widgets/speed_selector.dart | 4 +- .../{player => playlist}/playlist.dart | 0 .../playlist_provider.dart | 2 +- .../playlist_provider.g.dart | 0 .../settings/api_settings_provider.dart | 2 +- .../settings/api_settings_provider.g.dart | 0 .../settings/app_settings_provider.dart | 2 +- .../settings/app_settings_provider.g.dart | 0 .../settings/models/api_settings.dart | 4 +- .../settings/models/api_settings.freezed.dart | 0 .../settings/models/api_settings.g.dart | 0 .../settings/models/app_settings.dart | 0 .../settings/models/app_settings.freezed.dart | 0 .../settings/models/app_settings.g.dart | 0 .../models/audiobookshelf_server.dart | 0 .../models/audiobookshelf_server.freezed.dart | 0 .../models/audiobookshelf_server.g.dart | 0 .../settings/models/authenticated_user.dart | 2 +- .../models/authenticated_user.freezed.dart | 0 .../settings/models/authenticated_user.g.dart | 0 .../settings/models/models.dart | 0 lib/{ => features}/settings/settings.dart | 0 .../settings/view/app_settings_page.dart | 10 +- .../view/auto_sleep_timer_settings_page.dart | 4 +- lib/{ => features}/settings/view/buttons.dart | 0 .../view/home_page_settings_page.dart | 4 +- .../view/notification_settings_page.dart | 8 +- .../settings/view/player_settings_page.dart | 6 +- .../view/shake_detector_settings_page.dart | 8 +- .../settings/view/simple_settings_page.dart | 0 .../settings/view/theme_settings_page.dart | 4 +- .../widgets/navigation_with_switch_tile.dart | 0 .../shake_detector.dart | 2 +- .../shake_detector_provider.dart} | 10 +- .../shake_detector_provider.g.dart} | 2 +- .../{ => core}/skip_start_end.dart | 2 +- .../skip_start_end_provider.dart | 4 +- .../skip_start_end_provider.g.dart | 0 .../view/skip_start_end_button.dart} | 4 +- .../sleep_timer/core/sleep_timer.dart | 3 +- .../providers/sleep_timer_provider.dart | 6 +- .../providers/sleep_timer_provider.g.dart | 2 +- .../sleep_timer/view/sleep_timer_button.dart | 2 +- lib/features/you/view/server_manager.dart | 4 +- .../you/view/widgets/library_switch_chip.dart | 2 +- lib/framework.dart | 135 +---- lib/globals.dart | 20 +- lib/main.dart | 22 +- lib/pages/home_page.dart | 4 +- lib/pages/library_page.dart | 2 +- lib/pages/player_page.dart | 36 ++ lib/router/router.dart | 20 +- lib/router/scaffold_with_nav_bar.dart | 15 +- lib/shared/extensions/model_conversions.dart | 5 + lib/shared/extensions/obfuscation.dart | 6 +- .../utils}/error_response.dart | 0 lib/shared/utils/{utils.dart => helper.dart} | 2 +- lib/shared/widgets/shelves/book_shelf.dart | 4 +- .../widgets/tray_manager.dart} | 26 +- windows/runner/Runner.rc | 4 +- 116 files changed, 1238 insertions(+), 2592 deletions(-) delete mode 100644 lib/features/playback_reporting/core/playback_reporter.dart delete mode 100644 lib/features/player/core/audiobook_player_session.dart delete mode 100644 lib/features/player/core/init.dart delete mode 100644 lib/features/player/providers/player_form.dart delete mode 100644 lib/features/player/providers/player_form.g.dart delete mode 100644 lib/features/player/providers/player_providers.dart delete mode 100644 lib/features/player/providers/session_provider.dart delete mode 100644 lib/features/player/providers/session_provider.g.dart create mode 100644 lib/features/player/view/player_expanded_desktop.dart delete mode 100644 lib/features/player/view/player_when_expanded.dart delete mode 100644 lib/features/player/view/player_when_minimized.dart rename lib/features/{player => playlist}/playlist.dart (100%) rename lib/features/{player => playlist}/playlist_provider.dart (86%) rename lib/features/{player => playlist}/playlist_provider.g.dart (100%) rename lib/{ => features}/settings/api_settings_provider.dart (95%) rename lib/{ => features}/settings/api_settings_provider.g.dart (100%) rename lib/{ => features}/settings/app_settings_provider.dart (96%) rename lib/{ => features}/settings/app_settings_provider.g.dart (100%) rename lib/{ => features}/settings/models/api_settings.dart (80%) rename lib/{ => features}/settings/models/api_settings.freezed.dart (100%) rename lib/{ => features}/settings/models/api_settings.g.dart (100%) rename lib/{ => features}/settings/models/app_settings.dart (100%) rename lib/{ => features}/settings/models/app_settings.freezed.dart (100%) rename lib/{ => features}/settings/models/app_settings.g.dart (100%) rename lib/{ => features}/settings/models/audiobookshelf_server.dart (100%) rename lib/{ => features}/settings/models/audiobookshelf_server.freezed.dart (100%) rename lib/{ => features}/settings/models/audiobookshelf_server.g.dart (100%) rename lib/{ => features}/settings/models/authenticated_user.dart (87%) rename lib/{ => features}/settings/models/authenticated_user.freezed.dart (100%) rename lib/{ => features}/settings/models/authenticated_user.g.dart (100%) rename lib/{ => features}/settings/models/models.dart (100%) rename lib/{ => features}/settings/settings.dart (100%) rename lib/{ => features}/settings/view/app_settings_page.dart (96%) rename lib/{ => features}/settings/view/auto_sleep_timer_settings_page.dart (97%) rename lib/{ => features}/settings/view/buttons.dart (100%) rename lib/{ => features}/settings/view/home_page_settings_page.dart (96%) rename lib/{ => features}/settings/view/notification_settings_page.dart (98%) rename lib/{ => features}/settings/view/player_settings_page.dart (98%) rename lib/{ => features}/settings/view/shake_detector_settings_page.dart (97%) rename lib/{ => features}/settings/view/simple_settings_page.dart (100%) rename lib/{ => features}/settings/view/theme_settings_page.dart (98%) rename lib/{ => features}/settings/view/widgets/navigation_with_switch_tile.dart (100%) rename lib/features/{shake_detection/core => shake_detector}/shake_detector.dart (96%) rename lib/features/{shake_detection/providers/shake_detector.dart => shake_detector/shake_detector_provider.dart} (95%) rename lib/features/{shake_detection/providers/shake_detector.g.dart => shake_detector/shake_detector_provider.g.dart} (95%) rename lib/features/skip_start_end/{ => core}/skip_start_end.dart (95%) rename lib/features/skip_start_end/{ => providers}/skip_start_end_provider.dart (92%) rename lib/features/skip_start_end/{ => providers}/skip_start_end_provider.g.dart (100%) rename lib/features/{player/view/widgets/player_skip_chapter_start_end.dart => skip_start_end/view/skip_start_end_button.dart} (94%) create mode 100644 lib/pages/player_page.dart rename lib/{models => shared/utils}/error_response.dart (100%) rename lib/shared/utils/{utils.dart => helper.dart} (97%) rename lib/{models/tray.dart => shared/widgets/tray_manager.dart} (78%) diff --git a/.vscode/settings.json b/.vscode/settings.json index 434aa82..a389247 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,7 @@ "deeplinking", "fullscreen", "Lerp", + "Librarys", "miniplayer", "mocktail", "nodename", diff --git a/lib/api/api_provider.dart b/lib/api/api_provider.dart index 477a074..d6e9305 100644 --- a/lib/api/api_provider.dart +++ b/lib/api/api_provider.dart @@ -9,10 +9,11 @@ import 'package:http/http.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; +import 'package:vaani/db/cache/cache_key.dart'; import 'package:vaani/db/cache_manager.dart'; -import 'package:vaani/models/error_response.dart'; -import 'package:vaani/settings/api_settings_provider.dart'; -import 'package:vaani/settings/models/authenticated_user.dart'; +import 'package:vaani/shared/utils/error_response.dart'; +import 'package:vaani/features/settings/api_settings_provider.dart'; +import 'package:vaani/features/settings/models/authenticated_user.dart'; import 'package:vaani/shared/extensions/obfuscation.dart'; part 'api_provider.g.dart'; @@ -154,15 +155,14 @@ class PersonalizedView extends _$PersonalizedView { } // try to find in cache // final cacheKey = 'personalizedView:${apiSettings.activeLibraryId}'; - final key = 'personalizedView:${apiSettings.activeLibraryId! + user.id}'; - final cachedRes = await apiResponseCacheManager.getFileFromMemory( - key, - ) ?? - await apiResponseCacheManager.getFileFromCache(key); - if (cachedRes != null) { - _logger.fine('reading from cache: $cachedRes for key: $key'); + // final key = 'personalizedView:${apiSettings.activeLibraryId! + user.id}'; + final key = CacheKey.personalized(apiSettings.activeLibraryId! + user.id); + final cachedFile = await apiResponseCacheManager.getFileFromCache(key); + if (cachedFile != null) { + _logger.fine('reading from cache: $cachedFile for key: $key'); try { - final resJson = jsonDecode(await cachedRes.file.readAsString()) as List; + final resJson = + jsonDecode(await cachedFile.file.readAsString()) as List; final res = [ for (final item in resJson) Shelf.fromJson(item as Map), @@ -170,7 +170,7 @@ class PersonalizedView extends _$PersonalizedView { _logger.fine('successfully read from cache key: $key'); yield res; } catch (e) { - _logger.warning('error reading from cache: $e\n$cachedRes'); + _logger.warning('error reading from cache: $e\n$cachedFile'); } } diff --git a/lib/api/api_provider.g.dart b/lib/api/api_provider.g.dart index 619a729..c3fa666 100644 --- a/lib/api/api_provider.g.dart +++ b/lib/api/api_provider.g.dart @@ -662,7 +662,7 @@ class _LoginProviderElement AuthenticatedUser? get user => (origin as LoginProvider).user; } -String _$personalizedViewHash() => r'425e89d99d7e4712b4d6a688f3a12442bd66584f'; +String _$personalizedViewHash() => r'e3c3e041f925f041db2145e8ca0dbb07268ecc47'; /// fetch the personalized view /// diff --git a/lib/api/authenticated_users_provider.dart b/lib/api/authenticated_users_provider.dart index 5e78fab..d93be07 100644 --- a/lib/api/authenticated_users_provider.dart +++ b/lib/api/authenticated_users_provider.dart @@ -3,9 +3,10 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:vaani/api/server_provider.dart' show audiobookShelfServerProvider; import 'package:vaani/db/storage.dart'; -import 'package:vaani/settings/api_settings_provider.dart'; -import 'package:vaani/settings/models/audiobookshelf_server.dart'; -import 'package:vaani/settings/models/authenticated_user.dart' as model; +import 'package:vaani/features/settings/api_settings_provider.dart'; +import 'package:vaani/features/settings/models/audiobookshelf_server.dart'; +import 'package:vaani/features/settings/models/authenticated_user.dart' + as model; import 'package:vaani/shared/extensions/obfuscation.dart'; part 'authenticated_users_provider.g.dart'; diff --git a/lib/api/library_item_provider.dart b/lib/api/library_item_provider.dart index 013f62e..c708fdd 100644 --- a/lib/api/library_item_provider.dart +++ b/lib/api/library_item_provider.dart @@ -1,11 +1,13 @@ import 'dart:convert'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk; import 'package:vaani/api/api_provider.dart'; import 'package:vaani/db/cache/cache_key.dart'; import 'package:vaani/db/cache_manager.dart'; +import 'package:vaani/globals.dart'; import 'package:vaani/shared/extensions/model_conversions.dart'; part 'library_item_provider.g.dart'; @@ -26,8 +28,8 @@ class LibraryItem extends _$LibraryItem { // look for the item in the cache final key = CacheKey.libraryItem(id); - final cachedFile = await apiResponseCacheManager.getFileFromMemory(key) ?? - await apiResponseCacheManager.getFileFromCache(key); + final cachedFile = await apiResponseCacheManager.getFileFromCache(key); + if (cachedFile != null) { _logger.fine( 'LibraryItemProvider reading from cache for $id from ${cachedFile.file}', @@ -69,3 +71,39 @@ class LibraryItem extends _$LibraryItem { } } } + +@riverpod +Future playBackSession( + Ref ref, + String libraryItemId, +) async { + final api = ref.watch(authenticatedApiProvider); + final playBack = await api.items.play( + libraryItemId: libraryItemId, + parameters: shelfsdk.PlayItemReqParams( + deviceInfo: shelfsdk.DeviceInfoReqParams( + clientVersion: appVersion, + manufacturer: deviceManufacturer, + model: deviceModel, + sdkVersion: deviceSdkVersion, + clientName: appName, + deviceName: deviceName, + ), + forceDirectPlay: false, + forceTranscode: false, + supportedMimeTypes: [ + "audio/flac", + "audio/mpeg", + "audio/mp4", + "audio/ogg", + "audio/aac", + "audio/webm", + ], + ), + ); + + if (playBack == null) { + return null; + } + return playBack.asExpanded; +} diff --git a/lib/api/library_item_provider.g.dart b/lib/api/library_item_provider.g.dart index 5261d9c..3826338 100644 --- a/lib/api/library_item_provider.g.dart +++ b/lib/api/library_item_provider.g.dart @@ -6,7 +6,7 @@ part of 'library_item_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$libraryItemHash() => r'a3cfa7f912e9498a70b5782899018b6964d6445c'; +String _$playBackSessionHash() => r'1bc00653e0041e839d8569192b6bc90d96b4ca4f'; /// Copied from Dart SDK class _SystemHash { @@ -29,6 +29,144 @@ class _SystemHash { } } +/// See also [playBackSession]. +@ProviderFor(playBackSession) +const playBackSessionProvider = PlayBackSessionFamily(); + +/// See also [playBackSession]. +class PlayBackSessionFamily + extends Family> { + /// See also [playBackSession]. + const PlayBackSessionFamily(); + + /// See also [playBackSession]. + PlayBackSessionProvider call( + String libraryItemId, + ) { + return PlayBackSessionProvider( + libraryItemId, + ); + } + + @override + PlayBackSessionProvider getProviderOverride( + covariant PlayBackSessionProvider provider, + ) { + return call( + provider.libraryItemId, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'playBackSessionProvider'; +} + +/// See also [playBackSession]. +class PlayBackSessionProvider + extends AutoDisposeFutureProvider { + /// See also [playBackSession]. + PlayBackSessionProvider( + String libraryItemId, + ) : this._internal( + (ref) => playBackSession( + ref as PlayBackSessionRef, + libraryItemId, + ), + from: playBackSessionProvider, + name: r'playBackSessionProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$playBackSessionHash, + dependencies: PlayBackSessionFamily._dependencies, + allTransitiveDependencies: + PlayBackSessionFamily._allTransitiveDependencies, + libraryItemId: libraryItemId, + ); + + PlayBackSessionProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.libraryItemId, + }) : super.internal(); + + final String libraryItemId; + + @override + Override overrideWith( + FutureOr Function( + PlayBackSessionRef provider) + create, + ) { + return ProviderOverride( + origin: this, + override: PlayBackSessionProvider._internal( + (ref) => create(ref as PlayBackSessionRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + libraryItemId: libraryItemId, + ), + ); + } + + @override + AutoDisposeFutureProviderElement + createElement() { + return _PlayBackSessionProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is PlayBackSessionProvider && + other.libraryItemId == libraryItemId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, libraryItemId.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin PlayBackSessionRef + on AutoDisposeFutureProviderRef { + /// The parameter `libraryItemId` of this provider. + String get libraryItemId; +} + +class _PlayBackSessionProviderElement + extends AutoDisposeFutureProviderElement + with PlayBackSessionRef { + _PlayBackSessionProviderElement(super.provider); + + @override + String get libraryItemId => (origin as PlayBackSessionProvider).libraryItemId; +} + +String _$libraryItemHash() => r'8032b2d3ca6a8a2a6217cd32f11cd4b35919f49e'; + abstract class _$LibraryItem extends BuildlessStreamNotifier { late final String id; diff --git a/lib/api/library_provider.dart b/lib/api/library_provider.dart index 62c79d8..c5403ae 100644 --- a/lib/api/library_provider.dart +++ b/lib/api/library_provider.dart @@ -1,11 +1,13 @@ import 'package:hooks_riverpod/hooks_riverpod.dart' show Ref; import 'package:logging/logging.dart' show Logger; import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import 'package:shelfsdk/audiobookshelf_api.dart' show Library; +import 'package:shelfsdk/audiobookshelf_api.dart' + show GetLibrarysItemsReqParams, Library, LibraryItemMinified; import 'package:vaani/api/api_provider.dart' show authenticatedApiProvider; -import 'package:vaani/settings/api_settings_provider.dart' +import 'package:vaani/features/settings/api_settings_provider.dart' show apiSettingsProvider; +import 'package:vaani/shared/extensions/model_conversions.dart'; + part 'library_provider.g.dart'; final _logger = Logger('LibraryProvider'); @@ -56,3 +58,27 @@ class Libraries extends _$Libraries { return libraries; } } + +// 查询库下所有项目 +@riverpod +Future> currentLibraryItems(Ref ref) async { + final api = ref.watch(authenticatedApiProvider); + final libraryId = + ref.watch(apiSettingsProvider.select((s) => s.activeLibraryId)); + if (libraryId == null) { + _logger.warning('No active library id found'); + return []; + } + final items = await api.libraries.getItems( + libraryId: libraryId, + parameters: const GetLibrarysItemsReqParams( + limit: 18, + page: 1, + minified: true, + ), + ); + if (items == null) { + return []; + } + return items.results.map((v) => v.asMinified).toList(); +} diff --git a/lib/api/library_provider.g.dart b/lib/api/library_provider.g.dart index a8bc88a..8bcaf44 100644 --- a/lib/api/library_provider.g.dart +++ b/lib/api/library_provider.g.dart @@ -173,6 +173,26 @@ final currentLibraryProvider = AutoDisposeFutureProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef CurrentLibraryRef = AutoDisposeFutureProviderRef; +String _$currentLibraryItemsHash() => + r'2e2ce270c46bedf0b779399772df89a23803fe50'; + +/// See also [currentLibraryItems]. +@ProviderFor(currentLibraryItems) +final currentLibraryItemsProvider = + AutoDisposeFutureProvider>.internal( + currentLibraryItems, + name: r'currentLibraryItemsProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$currentLibraryItemsHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef CurrentLibraryItemsRef + = AutoDisposeFutureProviderRef>; String _$librariesHash() => r'95ebd4d1ac0cc2acf7617dc22895eff0ca30600f'; /// See also [Libraries]. diff --git a/lib/api/server_provider.dart b/lib/api/server_provider.dart index ef1c864..4e5793b 100644 --- a/lib/api/server_provider.dart +++ b/lib/api/server_provider.dart @@ -2,8 +2,9 @@ import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:vaani/api/authenticated_users_provider.dart'; import 'package:vaani/db/storage.dart'; -import 'package:vaani/settings/api_settings_provider.dart'; -import 'package:vaani/settings/models/audiobookshelf_server.dart' as model; +import 'package:vaani/features/settings/api_settings_provider.dart'; +import 'package:vaani/features/settings/models/audiobookshelf_server.dart' + as model; import 'package:vaani/shared/extensions/obfuscation.dart'; part 'server_provider.g.dart'; diff --git a/lib/db/available_boxes.dart b/lib/db/available_boxes.dart index ff01acc..537b7b3 100644 --- a/lib/db/available_boxes.dart +++ b/lib/db/available_boxes.dart @@ -1,7 +1,7 @@ import 'package:flutter/foundation.dart' show immutable; import 'package:hive/hive.dart'; import 'package:vaani/features/per_book_settings/models/book_settings.dart'; -import 'package:vaani/settings/models/models.dart'; +import 'package:vaani/features/settings/models/models.dart'; @immutable class AvailableHiveBoxes { diff --git a/lib/db/cache/cache_key.dart b/lib/db/cache/cache_key.dart index 0f38f92..14cf291 100644 --- a/lib/db/cache/cache_key.dart +++ b/lib/db/cache/cache_key.dart @@ -1,5 +1,13 @@ class CacheKey { + static String personalized(String id) { + return 'personalizedView:$id'; + } + static String libraryItem(String id) { return 'library_item_$id'; } + + static String libraryItems(String id) { + return 'library_items_$id'; + } } diff --git a/lib/db/init.dart b/lib/db/init.dart index ec45bae..1028efb 100644 --- a/lib/db/init.dart +++ b/lib/db/init.dart @@ -13,7 +13,7 @@ Future initStorage() async { // ); // await storageDir.create(recursive: true); - Hive.defaultDirectory = appStorageDir.path; + Hive.defaultDirectory = appDocumentsDir.path; appLogger.config('Hive storage directory init: ${Hive.defaultDirectory}'); await registerModels(); diff --git a/lib/db/register_models.dart b/lib/db/register_models.dart index e185e33..f84a181 100644 --- a/lib/db/register_models.dart +++ b/lib/db/register_models.dart @@ -1,6 +1,6 @@ import 'package:hive/hive.dart'; import 'package:vaani/features/per_book_settings/models/book_settings.dart'; -import 'package:vaani/settings/models/models.dart'; +import 'package:vaani/features/settings/models/models.dart'; // register all models to Hive for serialization Future registerModels() async { diff --git a/lib/features/downloads/core/download_manager.dart b/lib/features/downloads/core/download_manager.dart index 0e808b1..678e6cd 100644 --- a/lib/features/downloads/core/download_manager.dart +++ b/lib/features/downloads/core/download_manager.dart @@ -5,8 +5,8 @@ import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; import 'package:logging/logging.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; +import 'package:vaani/globals.dart'; import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/extensions/obfuscation.dart'; @@ -72,11 +72,10 @@ class AudiobookDownloadManager { ) async { _logger.info('queuing download for item: ${item.id}'); // create a download task for each file in the item - final directory = await getApplicationSupportDirectory(); for (final file in item.libraryFiles) { // check if the file is already downloaded if (isFileDownloaded( - constructFilePath(directory, item, file), + constructFilePath(item, file), )) { _logger.info('file already downloaded: ${file.metadata.filename}'); continue; @@ -102,11 +101,10 @@ class AudiobookDownloadManager { } String constructFilePath( - Directory directory, LibraryItemExpanded item, LibraryFile file, ) => - '${directory.path}/${item.relPath}/${file.metadata.filename}'; + '${appSupportDir.path}/${item.relPath}/${file.metadata.filename}'; void dispose() { _updatesSubscription.cancel(); @@ -125,10 +123,9 @@ class AudiobookDownloadManager { Future> getDownloadedFilesMetadata( LibraryItemExpanded item, ) async { - final directory = await getApplicationSupportDirectory(); final downloadedFiles = []; for (final file in item.libraryFiles) { - final filePath = constructFilePath(directory, item, file); + final filePath = constructFilePath(item, file); if (isFileDownloaded(filePath)) { downloadedFiles.add(file); } @@ -146,9 +143,8 @@ class AudiobookDownloadManager { } Future isItemDownloaded(LibraryItemExpanded item) async { - final directory = await getApplicationSupportDirectory(); for (final file in item.libraryFiles) { - if (!isFileDownloaded(constructFilePath(directory, item, file))) { + if (!isFileDownloaded(constructFilePath(item, file))) { _logger.info('file not downloaded: ${file.metadata.filename}'); return false; } @@ -159,9 +155,8 @@ class AudiobookDownloadManager { Future deleteDownloadedItem(LibraryItemExpanded item) async { _logger.info('deleting downloaded item with id: ${item.id}'); - final directory = await getApplicationSupportDirectory(); for (final file in item.libraryFiles) { - final filePath = constructFilePath(directory, item, file); + final filePath = constructFilePath(item, file); if (isFileDownloaded(filePath)) { File(filePath).deleteSync(); } @@ -170,10 +165,9 @@ class AudiobookDownloadManager { } Future> getDownloadedFilesUri(LibraryItemExpanded item) async { - final directory = await getApplicationSupportDirectory(); final files = []; for (final file in item.libraryFiles) { - final filePath = constructFilePath(directory, item, file); + final filePath = constructFilePath(item, file); if (isFileDownloaded(filePath)) { files.add(Uri.file(filePath)); } diff --git a/lib/features/downloads/providers/download_manager.dart b/lib/features/downloads/providers/download_manager.dart index 6ffdded..8b7c203 100644 --- a/lib/features/downloads/providers/download_manager.dart +++ b/lib/features/downloads/providers/download_manager.dart @@ -6,7 +6,7 @@ import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/library_item_provider.dart'; import 'package:vaani/features/downloads/core/download_manager.dart' as core; -import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/features/settings/app_settings_provider.dart'; import 'package:vaani/shared/extensions/item_files.dart'; part 'download_manager.g.dart'; diff --git a/lib/features/explore/providers/search_result_provider.dart b/lib/features/explore/providers/search_result_provider.dart index 2c903f7..30a550c 100644 --- a/lib/features/explore/providers/search_result_provider.dart +++ b/lib/features/explore/providers/search_result_provider.dart @@ -2,7 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/api/api_provider.dart'; -import 'package:vaani/settings/api_settings_provider.dart'; +import 'package:vaani/features/settings/api_settings_provider.dart'; part 'search_result_provider.g.dart'; diff --git a/lib/features/explore/view/explore_page.dart b/lib/features/explore/view/explore_page.dart index 7d0c2be..9429df5 100644 --- a/lib/features/explore/view/explore_page.dart +++ b/lib/features/explore/view/explore_page.dart @@ -13,8 +13,8 @@ import 'package:vaani/features/explore/providers/search_controller.dart'; import 'package:vaani/features/explore/view/search_result_page.dart'; import 'package:vaani/generated/l10n.dart'; import 'package:vaani/router/router.dart'; -import 'package:vaani/settings/api_settings_provider.dart'; -import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/features/settings/api_settings_provider.dart'; +import 'package:vaani/features/settings/app_settings_provider.dart'; import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; diff --git a/lib/features/item_viewer/view/library_item_actions.dart b/lib/features/item_viewer/view/library_item_actions.dart index 2234091..3a165c1 100644 --- a/lib/features/item_viewer/view/library_item_actions.dart +++ b/lib/features/item_viewer/view/library_item_actions.dart @@ -14,13 +14,12 @@ import 'package:vaani/features/downloads/providers/download_manager.dart' isItemDownloadingProvider, itemDownloadProgressProvider; import 'package:vaani/features/item_viewer/view/library_item_page.dart'; -import 'package:vaani/features/player/providers/player_form.dart'; import 'package:vaani/features/player/providers/player_status_provider.dart'; -import 'package:vaani/features/player/providers/session_provider.dart'; +import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/generated/l10n.dart'; import 'package:vaani/globals.dart'; import 'package:vaani/router/router.dart'; -import 'package:vaani/settings/api_settings_provider.dart'; +import 'package:vaani/features/settings/api_settings_provider.dart'; import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/utils.dart'; diff --git a/lib/features/item_viewer/view/library_item_hero_section.dart b/lib/features/item_viewer/view/library_item_hero_section.dart index bbba5d3..68b641b 100644 --- a/lib/features/item_viewer/view/library_item_hero_section.dart +++ b/lib/features/item_viewer/view/library_item_hero_section.dart @@ -9,7 +9,7 @@ import 'package:vaani/constants/hero_tag_conventions.dart'; import 'package:vaani/features/item_viewer/view/library_item_page.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/router/models/library_item_extras.dart'; -import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/features/settings/app_settings_provider.dart'; import 'package:vaani/shared/extensions/duration_format.dart'; import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; @@ -139,20 +139,21 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final player = ref.watch(audiobookPlayerProvider); + final player = ref.watch(playerProvider); final libraryItem = ref.watch(libraryItemProvider(id)).valueOrNull; if (libraryItem == null) { return const SizedBox.shrink(); } final mediaProgress = libraryItem.userMediaProgress; - if (mediaProgress == null && player.book?.libraryItemId != libraryItem.id) { + if (mediaProgress == null && + player.session?.libraryItemId != libraryItem.id) { return const SizedBox.shrink(); } double progress; Duration remainingTime; - if (player.book?.libraryItemId == libraryItem.id) { + if (player.session?.libraryItemId == libraryItem.id) { // final positionStream = useStream(player.slowPositionStream); progress = (player.positionInBook).inSeconds / libraryItem.media.asBookExpanded.duration.inSeconds; diff --git a/lib/features/logging/core/logger.dart b/lib/features/logging/core/logger.dart index bb914ea..8c63880 100644 --- a/lib/features/logging/core/logger.dart +++ b/lib/features/logging/core/logger.dart @@ -6,7 +6,7 @@ import 'package:vaani/shared/extensions/duration_format.dart'; Future getLoggingFilePath() async { // final Directory directory = await getApplicationDocumentsDirectory(); - return '${appStorageDir.path}/$appName.log'; + return '${appDocumentsDir.path}/$appName.log'; } Future initLogging() async { diff --git a/lib/features/onboarding/providers/oauth_provider.dart b/lib/features/onboarding/providers/oauth_provider.dart index fb7856b..d38a331 100644 --- a/lib/features/onboarding/providers/oauth_provider.dart +++ b/lib/features/onboarding/providers/oauth_provider.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:vaani/api/api_provider.dart'; -import 'package:vaani/models/error_response.dart'; +import 'package:vaani/shared/utils/error_response.dart'; import '../models/flow.dart'; diff --git a/lib/features/onboarding/view/callback_page.dart b/lib/features/onboarding/view/callback_page.dart index f4bb098..54e2a7d 100644 --- a/lib/features/onboarding/view/callback_page.dart +++ b/lib/features/onboarding/view/callback_page.dart @@ -3,7 +3,7 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/features/onboarding/providers/oauth_provider.dart'; import 'package:vaani/features/onboarding/view/user_login_with_password.dart'; -import 'package:vaani/models/error_response.dart'; +import 'package:vaani/shared/utils/error_response.dart'; import 'package:vaani/router/router.dart'; class CallbackPage extends HookConsumerWidget { diff --git a/lib/features/onboarding/view/onboarding_single_page.dart b/lib/features/onboarding/view/onboarding_single_page.dart index 8acbd83..370bfb7 100644 --- a/lib/features/onboarding/view/onboarding_single_page.dart +++ b/lib/features/onboarding/view/onboarding_single_page.dart @@ -6,7 +6,7 @@ import 'package:vaani/api/api_provider.dart'; import 'package:vaani/features/onboarding/view/user_login.dart'; import 'package:vaani/generated/l10n.dart'; import 'package:vaani/globals.dart'; -import 'package:vaani/settings/api_settings_provider.dart'; +import 'package:vaani/features/settings/api_settings_provider.dart'; import 'package:vaani/shared/utils.dart'; import 'package:vaani/shared/widgets/add_new_server.dart'; diff --git a/lib/features/onboarding/view/user_login.dart b/lib/features/onboarding/view/user_login.dart index 40d9863..5a2224c 100644 --- a/lib/features/onboarding/view/user_login.dart +++ b/lib/features/onboarding/view/user_login.dart @@ -17,10 +17,11 @@ import 'package:vaani/features/onboarding/view/user_login_with_token.dart' import 'package:vaani/generated/l10n.dart'; import 'package:vaani/hacks/fix_autofill_losing_focus.dart' show InactiveFocusScopeObserver; -import 'package:vaani/models/error_response.dart' show ErrorResponseHandler; -import 'package:vaani/settings/api_settings_provider.dart' +import 'package:vaani/shared/utils/error_response.dart' + show ErrorResponseHandler; +import 'package:vaani/features/settings/api_settings_provider.dart' show apiSettingsProvider; -import 'package:vaani/settings/models/models.dart' as model; +import 'package:vaani/features/settings/models/models.dart' as model; class UserLoginWidget extends HookConsumerWidget { const UserLoginWidget({ diff --git a/lib/features/onboarding/view/user_login_with_open_id.dart b/lib/features/onboarding/view/user_login_with_open_id.dart index fccfe66..3abf75d 100644 --- a/lib/features/onboarding/view/user_login_with_open_id.dart +++ b/lib/features/onboarding/view/user_login_with_open_id.dart @@ -7,9 +7,9 @@ import 'package:vaani/api/api_provider.dart'; import 'package:vaani/features/onboarding/providers/oauth_provider.dart'; import 'package:vaani/features/onboarding/view/user_login_with_password.dart'; import 'package:vaani/globals.dart'; -import 'package:vaani/models/error_response.dart'; +import 'package:vaani/shared/utils/error_response.dart'; import 'package:vaani/router/router.dart'; -import 'package:vaani/settings/models/models.dart' as model; +import 'package:vaani/features/settings/models/models.dart' as model; import 'package:vaani/shared/extensions/obfuscation.dart'; import 'package:vaani/shared/utils.dart'; diff --git a/lib/features/onboarding/view/user_login_with_password.dart b/lib/features/onboarding/view/user_login_with_password.dart index faab24f..6344b52 100644 --- a/lib/features/onboarding/view/user_login_with_password.dart +++ b/lib/features/onboarding/view/user_login_with_password.dart @@ -9,9 +9,9 @@ import 'package:vaani/api/authenticated_users_provider.dart'; import 'package:vaani/generated/l10n.dart'; import 'package:vaani/globals.dart'; import 'package:vaani/hacks/fix_autofill_losing_focus.dart'; -import 'package:vaani/models/error_response.dart'; +import 'package:vaani/shared/utils/error_response.dart'; import 'package:vaani/router/router.dart'; -import 'package:vaani/settings/models/models.dart' as model; +import 'package:vaani/features/settings/models/models.dart' as model; import 'package:vaani/shared/utils.dart'; class UserLoginWithPassword extends HookConsumerWidget { diff --git a/lib/features/onboarding/view/user_login_with_token.dart b/lib/features/onboarding/view/user_login_with_token.dart index 8da8035..c47afa6 100644 --- a/lib/features/onboarding/view/user_login_with_token.dart +++ b/lib/features/onboarding/view/user_login_with_token.dart @@ -6,9 +6,9 @@ import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/authenticated_users_provider.dart'; import 'package:vaani/generated/l10n.dart'; -import 'package:vaani/models/error_response.dart'; +import 'package:vaani/shared/utils/error_response.dart'; import 'package:vaani/router/router.dart'; -import 'package:vaani/settings/models/models.dart' as model; +import 'package:vaani/features/settings/models/models.dart' as model; class UserLoginWithToken extends HookConsumerWidget { UserLoginWithToken({ diff --git a/lib/features/per_book_settings/models/nullable_player_settings.dart b/lib/features/per_book_settings/models/nullable_player_settings.dart index 4f933e5..1665f6c 100644 --- a/lib/features/per_book_settings/models/nullable_player_settings.dart +++ b/lib/features/per_book_settings/models/nullable_player_settings.dart @@ -1,5 +1,5 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:vaani/settings/models/app_settings.dart'; +import 'package:vaani/features/settings/models/app_settings.dart'; part 'nullable_player_settings.freezed.dart'; part 'nullable_player_settings.g.dart'; diff --git a/lib/features/playback_reporting/core/playback_reporter.dart b/lib/features/playback_reporting/core/playback_reporter.dart deleted file mode 100644 index 5fba5af..0000000 --- a/lib/features/playback_reporting/core/playback_reporter.dart +++ /dev/null @@ -1,340 +0,0 @@ -import 'dart:async'; - -import 'package:http/http.dart' as http; -import 'package:logging/logging.dart'; -import 'package:shelfsdk/audiobookshelf_api.dart'; -import 'package:vaani/features/player/core/audiobook_player.dart'; -import 'package:vaani/shared/extensions/obfuscation.dart'; - -final _logger = Logger('PlaybackReporter'); - -/// this playback reporter will watch the player and report to the server -/// -/// it will by default report every 10 seconds -/// and also report when the player is paused/stopped/finished/playing -class PlaybackReporter { - /// The player to watch - final AudiobookPlayer player; - - /// the api to report to - final AudiobookshelfApi authenticatedApi; - - /// The stopwatch to keep track of the time since the last report - /// - /// this should only run when media is playing - final _stopwatch = Stopwatch(); - - /// subscriptions to listen and then cancel when disposing - final List _subscriptions = []; - - Duration _reportingInterval; - - /// the duration to wait before reporting - Duration get reportingInterval => _reportingInterval; - set reportingInterval(Duration value) { - _reportingInterval = value; - _cancelReportTimer(); - _setReportTimerIfNotAlready(); - _logger.info('set interval: $value'); - } - - /// the minimum duration to report - final Duration reportingDurationThreshold; - - /// the duration to wait before starting the reporting - /// this is to ignore the initial duration in case user is browsing - final Duration? minimumPositionForReporting; - - /// the duration to mark the book as complete when the time left is less than this - final Duration markCompleteWhenTimeLeft; - - /// timer to report every 10 seconds - /// tracking the time since the last report - Timer? _reportTimer; - - /// metadata to report - String? deviceName; - String? deviceModel; - String? deviceSdkVersion; - String? deviceClientName; - String? deviceClientVersion; - String? deviceManufacturer; - - PlaybackReporter( - this.player, - this.authenticatedApi, { - this.deviceName, - this.deviceModel, - this.deviceSdkVersion, - this.deviceClientName, - this.deviceClientVersion, - this.deviceManufacturer, - this.reportingDurationThreshold = const Duration(seconds: 1), - Duration reportingInterval = const Duration(seconds: 10), - this.minimumPositionForReporting, - this.markCompleteWhenTimeLeft = const Duration(seconds: 5), - }) : _reportingInterval = reportingInterval { - // initial conditions - if (player.playing) { - _stopwatch.start(); - _setReportTimerIfNotAlready(); - _logger.fine('starting stopwatch'); - } else { - _logger.fine('not starting stopwatch'); - } - - _subscriptions.add( - player.playerStateStream.listen((state) async { - // set timer if any book is playing and cancel if not - if (player.book != null) { - if (state.playing) { - _setReportTimerIfNotAlready(); - } else { - _cancelReportTimer(); - } - } else if (player.book == null && _reportTimer != null) { - _logger.info('book is null, closing session'); - await closeSession(); - _cancelReportTimer(); - } - - // start or stop the stopwatch based on the playing state - if (state.playing) { - _stopwatch.start(); - _logger.fine( - 'player state observed, starting stopwatch at ${_stopwatch.elapsed}', - ); - } else if (!state.playing) { - _stopwatch.stop(); - _logger.fine( - 'player state observed, stopping stopwatch at ${_stopwatch.elapsed}', - ); - await tryReportPlayback(null); - } - }), - ); - - _logger.fine( - 'initialized with reportingInterval: $reportingInterval, reportingDurationThreshold: $reportingDurationThreshold', - ); - _logger.fine( - 'initialized with minimumPositionForReporting: $minimumPositionForReporting, markCompleteWhenTimeLeft: $markCompleteWhenTimeLeft', - ); - _logger.fine( - 'initialized with deviceModel: $deviceModel, deviceSdkVersion: $deviceSdkVersion, deviceClientName: $deviceClientName, deviceClientVersion: $deviceClientVersion, deviceManufacturer: $deviceManufacturer', - ); - } - - Future tryReportPlayback(_) async { - _logger.fine( - 'callback called when elapsed ${_stopwatch.elapsed}', - ); - if (player.book != null && - player.positionInBook >= - player.book!.duration - markCompleteWhenTimeLeft) { - _logger.info( - 'marking complete as time left is less than $markCompleteWhenTimeLeft', - ); - await markComplete(); - return; - } - if (_stopwatch.elapsed > reportingDurationThreshold) { - _logger.fine( - 'reporting now with elapsed ${_stopwatch.elapsed} > threshold $reportingDurationThreshold', - ); - await syncCurrentPosition(); - } - } - - /// dispose the timer - Future dispose() async { - for (var sub in _subscriptions) { - sub.cancel(); - } - await closeSession(); - _stopwatch.stop(); - _reportTimer?.cancel(); - - _logger.fine('disposed'); - } - - /// current sessionId - /// this is used to report the playback - PlaybackSession? _session; - String? get sessionId => _session?.id; - - Future startSession() async { - if (_session != null) { - return _session!; - } - if (player.book == null) { - _logger.warning('No audiobook playing to start session'); - return null; - } - _session = await authenticatedApi.items.play( - libraryItemId: player.book!.libraryItemId, - parameters: PlayItemReqParams( - deviceInfo: await _getDeviceInfo(), - forceDirectPlay: false, - forceTranscode: false, - supportedMimeTypes: [ - "audio/flac", - "audio/mpeg", - "audio/mp4", - "audio/ogg", - "audio/aac", - "audio/webm", - ], - ), - responseErrorHandler: _responseErrorHandler, - ); - _logger.info('Started session: $sessionId'); - return _session; - } - - Future markComplete() async { - if (player.book == null) { - throw NoAudiobookPlayingError(); - } - await authenticatedApi.me.createUpdateMediaProgress( - libraryItemId: player.book!.libraryItemId, - parameters: CreateUpdateProgressReqParams( - isFinished: true, - currentTime: player.positionInBook, - duration: player.book!.duration, - ), - responseErrorHandler: _responseErrorHandler, - ); - _logger.info('Marked complete for book: ${player.book!.libraryItemId}'); - } - - Future syncCurrentPosition() async { - final data = _getSyncData(); - if (data == null) { - await closeSession(); - } - try { - _session ??= await startSession(); - } on Error catch (e) { - _logger.warning('Error starting session: $e'); - } - if (_session == null) { - _logger.warning('No session to sync position'); - return; - } - final currentPosition = player.positionInBook; - - await authenticatedApi.sessions.syncOpen( - sessionId: sessionId!, - parameters: _getSyncData()!, - responseErrorHandler: _responseErrorHandler, - ); - - _logger.fine( - 'Synced position: $currentPosition with timeListened: ${_stopwatch.elapsed} for session: $sessionId', - ); - - // reset the stopwatch - _stopwatch.reset(); - } - - Future closeSession() async { - if (sessionId == null) { - _logger.warning('No session to close'); - return; - } - - await authenticatedApi.sessions.closeOpen( - sessionId: sessionId!, - parameters: _getSyncData(), - responseErrorHandler: _responseErrorHandler, - ); - _session = null; - _logger.info('Closed session'); - } - - void _setReportTimerIfNotAlready() { - if (_reportTimer != null) return; - _reportTimer = Timer.periodic(_reportingInterval, tryReportPlayback); - _logger.fine('set timer with interval: $_reportingInterval'); - } - - void _cancelReportTimer() { - _reportTimer?.cancel(); - _reportTimer = null; - _logger.fine('cancelled timer'); - } - - void _responseErrorHandler(http.Response response, [error]) { - if (response.statusCode != 200) { - _logger.severe('Error with api: ${response.obfuscate()}, $error'); - throw PlaybackSyncError( - 'Error syncing position: ${response.body}, $error', - ); - } - } - - SyncSessionReqParams? _getSyncData() { - if (player.book?.libraryItemId != _session?.libraryItemId) { - _logger.info( - 'Book changed, not syncing position for session: $sessionId', - ); - return null; - } - - // if in the ignore duration, don't sync - if (minimumPositionForReporting != null && - player.positionInBook < minimumPositionForReporting!) { - // but if elapsed time is more than the minimumPositionForReporting, sync - if (_stopwatch.elapsed > minimumPositionForReporting!) { - _logger.info( - 'Syncing position despite being less than minimumPositionForReporting as elapsed time is more: ${_stopwatch.elapsed}', - ); - } else { - _logger.info( - 'Ignoring sync for position: ${player.positionInBook} < $minimumPositionForReporting', - ); - return null; - } - } - - return SyncSessionReqParams( - currentTime: player.positionInBook, - timeListened: _stopwatch.elapsed, - duration: player.book?.duration ?? Duration.zero, - ); - } - - Future _getDeviceInfo() async { - return DeviceInfoReqParams( - clientVersion: deviceClientVersion, - manufacturer: deviceManufacturer, - model: deviceModel, - sdkVersion: deviceSdkVersion, - clientName: deviceClientName, - deviceName: deviceName, - ); - } -} - -class PlaybackSyncError implements Exception { - String message; - - PlaybackSyncError([this.message = 'Error syncing playback']); - - @override - String toString() { - return 'PlaybackSyncError: $message'; - } -} - -class NoAudiobookPlayingError implements Exception { - String message; - - NoAudiobookPlayingError([this.message = 'No audiobook is playing']); - - @override - String toString() { - return 'NoAudiobookPlayingError: $message'; - } -} diff --git a/lib/features/playback_reporting/core/playback_reporter_session.dart b/lib/features/playback_reporting/core/playback_reporter_session.dart index 1e53dae..d1a3c76 100644 --- a/lib/features/playback_reporting/core/playback_reporter_session.dart +++ b/lib/features/playback_reporting/core/playback_reporter_session.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; -import 'package:vaani/features/player/core/audiobook_player_session.dart'; +import 'package:vaani/features/player/core/audiobook_player.dart'; import 'package:vaani/shared/extensions/obfuscation.dart'; final _logger = Logger('PlaybackReporter'); diff --git a/lib/features/playback_reporting/providers/playback_reporter_provider.dart b/lib/features/playback_reporting/providers/playback_reporter_provider.dart index db75bae..bbeecfa 100644 --- a/lib/features/playback_reporting/providers/playback_reporter_provider.dart +++ b/lib/features/playback_reporting/providers/playback_reporter_provider.dart @@ -1,19 +1,22 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:vaani/api/api_provider.dart'; -import 'package:vaani/features/playback_reporting/core/playback_reporter.dart' +import 'package:vaani/features/playback_reporting/core/playback_reporter_session.dart' as core; import 'package:vaani/features/player/providers/audiobook_player.dart'; -import 'package:vaani/globals.dart'; -import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/features/settings/app_settings_provider.dart'; part 'playback_reporter_provider.g.dart'; -@Riverpod(keepAlive: true) +@riverpod class PlaybackReporter extends _$PlaybackReporter { @override - Future build() async { + Future build() async { + final session = ref.watch(sessionProvider); + if (session == null) { + return null; + } final playerSettings = ref.watch(appSettingsProvider).playerSettings; - final player = ref.watch(simpleAudiobookPlayerProvider); + final player = ref.watch(playerProvider); final api = ref.watch(authenticatedApiProvider); final reporter = core.PlaybackReporter( @@ -22,12 +25,7 @@ class PlaybackReporter extends _$PlaybackReporter { reportingInterval: playerSettings.playbackReportInterval, markCompleteWhenTimeLeft: playerSettings.markCompleteWhenTimeLeft, minimumPositionForReporting: playerSettings.minimumPositionForReporting, - deviceName: deviceName, - deviceModel: deviceModel, - deviceSdkVersion: deviceSdkVersion, - deviceClientName: appName, - deviceClientVersion: appVersion, - deviceManufacturer: deviceManufacturer, + session: session, ); ref.onDispose(reporter.dispose); return reporter; diff --git a/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart b/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart index 7ac1ad6..6abe6c2 100644 --- a/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart +++ b/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart @@ -6,12 +6,12 @@ part of 'playback_reporter_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$playbackReporterHash() => r'43bde2ac163830b6950303a80cdd915ffcb1943b'; +String _$playbackReporterHash() => r'f9be5d6e4b07815ec669406cede4b00d2278e3af'; /// See also [PlaybackReporter]. @ProviderFor(PlaybackReporter) -final playbackReporterProvider = - AsyncNotifierProvider.internal( +final playbackReporterProvider = AutoDisposeAsyncNotifierProvider< + PlaybackReporter, core.PlaybackReporter?>.internal( PlaybackReporter.new, name: r'playbackReporterProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -21,6 +21,6 @@ final playbackReporterProvider = allTransitiveDependencies: null, ); -typedef _$PlaybackReporter = AsyncNotifier; +typedef _$PlaybackReporter = AutoDisposeAsyncNotifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/features/player/core/audiobook_player.dart b/lib/features/player/core/audiobook_player.dart index f179c72..37c2d8f 100644 --- a/lib/features/player/core/audiobook_player.dart +++ b/lib/features/player/core/audiobook_player.dart @@ -1,240 +1,239 @@ -/// a wrapper around the audioplayers package to manage the audio player instance -/// -/// this is needed as audiobook can be a list of audio files instead of a single file -library; +// my_audio_handler.dart +import 'dart:io'; +import 'package:audio_service/audio_service.dart'; import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:just_audio/just_audio.dart'; -// import 'package:just_audio_background/just_audio_background.dart'; -import 'package:logging/logging.dart'; +import 'package:rxdart/rxdart.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; - -import 'package:vaani/settings/app_settings_provider.dart'; -import 'package:vaani/settings/models/app_settings.dart'; -import 'package:vaani/shared/extensions/model_conversions.dart'; - -final _logger = Logger('AudiobookPlayer'); +import 'package:vaani/features/player/core/player_status.dart' as core; +import 'package:vaani/features/player/providers/player_status_provider.dart'; +import 'package:vaani/shared/extensions/chapter.dart'; // add a small offset so the display does not show the previous chapter for a split second final offset = Duration(milliseconds: 10); -/// time into the current chapter to determine if we should go to the previous chapter or the start of the current chapter -final doNotSeekBackIfLessThan = Duration(seconds: 5); +class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { + final AudioPlayer _player = AudioPlayer(); + // final List _playlist = []; + final Ref ref; -/// returns the sum of the duration of all the previous tracks before the [index] -Duration sumOfTracks(BookExpanded book, int? index) { - _logger.fine('Calculating sum of tracks for index: $index'); - // return 0 if index is less than 0 - if (index == null || index < 0) { - _logger.warning('Index is null or less than 0, returning 0'); - return Duration.zero; - } - final total = book.tracks.sublist(0, index).fold( - Duration.zero, - (previousValue, element) => previousValue + element.duration, - ); - _logger.fine('Sum of tracks for index: $index is $total'); - return total; -} + PlaybackSessionExpanded? _session; -/// will manage the audio player instance -class AudiobookPlayer extends AudioPlayer { - // constructor which takes in the BookExpanded object - AudiobookPlayer(this.token, this.baseUrl) : super() { - // set the source of the player to the first track in the book - _logger.config('Setting up audiobook player'); - // playerStateStream.listen((playerState) { - // if (playerState.processingState == ProcessingState.completed) { - // Future.microtask(seekToNext); - // } - // }); + final _currentChapterObject = BehaviorSubject.seeded(null); + AbsAudioHandler(this.ref) { + _setupAudioPlayer(); } - /// the [BookExpanded] being played - BookExpanded? _book; + void _setupAudioPlayer() { + final statusNotifier = ref.read(playerStatusProvider.notifier); - // /// the [BookExpanded] trying to be played - // BookExpanded? _intended_book; - - /// the [BookExpanded] being played - /// - /// to set the book, use [setSourceAudiobook] - BookExpanded? get book => _book; - - /// the authentication token to access the [AudioTrack.contentUrl] - final String token; - - /// the base url for the audio files - final Uri baseUrl; - - // the current index of the audio file in the [book] - // int _currentIndex = 0; - - // available audio tracks - int? get availableTracks => _book?.tracks.length; - - /// sets the current [AudioTrack] as the source of the player - Future setSourceAudiobook( - BookExpanded? book, { - bool preload = true, - int? initialIndex, - Duration? initialPosition, - List? downloadedUris, - Uri? artworkUri, - }) async { - _logger.finer( - 'Initial position: $initialPosition, Downloaded URIs: $downloadedUris', - ); - final appSettings = loadOrCreateAppSettings(); - if (book == null) { - _book = null; - _logger.info('Book is null, stopping player'); - return stop(); - } - - if (_book == book) { - _logger.info('Book is the same, doing nothing'); - return; - } - _logger.info('Setting source for book: $book'); - - _logger.fine('Stopping player'); - await stop(); - - _book = book; - // some calculations to set the initial index and position - // initialPosition is of the entire book not just the current track - // hence first we need to calculate the current track which will be used to set the initial position - // then we set the initial index to the current track index and position as the remaining duration from the position - // after subtracting the duration of all the previous tracks - // initialPosition ; - final trackToPlay = - _book!.findTrackAtTime(initialPosition ?? Duration.zero); - - final initialIndex = book.tracks.indexOf(trackToPlay); - final initialPositionInTrack = initialPosition != null - ? initialPosition - trackToPlay.startOffset - : null; - _logger.finer('Setting audioSource'); - final playlist = book.tracks.map((track) { - final retrievedUri = - _getUri(track, downloadedUris, baseUrl: baseUrl, token: token); - // _logger.fine( - // 'Setting source for track: ${track.title}, URI: ${retrievedUri.obfuscate()}', - // ); - return AudioSource.uri( - retrievedUri, - // tag: MediaItem( - // // Specify a unique ID for each media item: - // id: book.libraryItemId + track.index.toString(), - // // Metadata to display in the notification: - // title: appSettings.notificationSettings.primaryTitle - // .formatNotificationTitle(book), - // album: appSettings.notificationSettings.secondaryTitle - // .formatNotificationTitle(book), - // artUri: artworkUri ?? - // Uri.parse( - // '$baseUrl/api/items/${book.libraryItemId}/cover?token=$token&width=800', - // ), - // ), - ); - }).toList(); - await setAudioSources( - playlist, - preload: preload, - initialIndex: initialIndex, - initialPosition: initialPositionInTrack, - ).catchError((error) { - _logger.shout('Error in setting audio source: $error'); - return null; + // 转发播放状态 + _player.playbackEventStream.map(_transformEvent).pipe(playbackState); + _player.playerStateStream.listen((event) { + if (event.playing) { + statusNotifier.setPlayStatusVerify(core.PlayStatus.playing); + } else { + statusNotifier.setPlayStatusVerify(core.PlayStatus.paused); + } + }); + _player.positionStream.distinct().listen((position) { + final chapter = _session?.findChapterAtTime(positionInBook); + if (chapter != currentChapter) { + _currentChapterObject.sink.add(chapter); + } }); } - /// toggles the player between play and pause - Future togglePlayPause() { - // check if book is set - if (_book == null) { - _logger.warning('No book is set, not toggling play/pause'); + // 加载有声书 + Future setSourceAudiobook( + PlaybackSessionExpanded playbackSession, { + required Uri baseUrl, + required String token, + List? downloadedUris, + }) async { + _session = playbackSession; + + // 添加所有音轨 + List audioSources = []; + for (final track in playbackSession.audioTracks) { + audioSources.add( + AudioSource.uri( + _getUri(track, downloadedUris, baseUrl: baseUrl, token: token), + ), + ); } - // TODO refactor this to cover all the states - return switch (playerState) { - PlayerState(playing: var isPlaying) => isPlaying ? pause() : play(), - }; + playMediaItem( + MediaItem( + id: playbackSession.libraryItemId, + album: playbackSession.mediaMetadata.title, + title: playbackSession.displayTitle, + displaySubtitle: playbackSession.mediaType == MediaType.book + ? (playbackSession.mediaMetadata as BookMetadata).subtitle + : null, + duration: playbackSession.duration, + artUri: Uri.parse( + '$baseUrl/api/items/${playbackSession.libraryItemId}/cover?token=$token', + ), + ), + ); + final track = playbackSession.findTrackAtTime(playbackSession.currentTime); + final index = playbackSession.audioTracks.indexOf(track); + + await _player.setAudioSources( + audioSources, + initialIndex: index, + initialPosition: playbackSession.currentTime - track.startOffset, + ); + _player.seek(playbackSession.currentTime - track.startOffset, index: index); + await play(); + // 恢复上次播放位置(如果有) + // if (initialPosition != null) { + // await seekInBook(initialPosition); + // } } - /// 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 - Future seekInBook(Duration globalPosition) async { - if (_book == null) { - _logger.warning('No book is set, not seeking'); - return; - } - // 找到目标音轨和在音轨内的位置 - final track = _book!.findTrackAtTime(globalPosition); - final index = _book!.tracks.indexOf(track); - Duration positionInTrack = globalPosition - track.startOffset; - if (positionInTrack <= Duration.zero) { - positionInTrack = offset; - } - // 切换到目标音轨具体位置 - if (index != currentIndex) { - await seek(positionInTrack, index: index); - } - await seek(positionInTrack); - } + // // 音轨切换处理 + // void _onTrackChanged(int trackIndex) { + // if (_book == null) return; + + // // 可以在这里处理音轨切换逻辑,比如预加载下一音轨 + // // print('切换到音轨: ${_book!.tracks[trackIndex].title}'); + // } // 核心功能:跳转到指定章节 - Future skipToChapter(int chapterId, {Duration? position}) async { - if (_book == null) return; + Future skipToChapter(int chapterId) async { + if (_session == null) return; - final chapter = _book!.chapters.firstWhere( + final chapter = _session!.chapters.firstWhere( (ch) => ch.id == chapterId, orElse: () => throw Exception('Chapter not found'), ); - if (position != null) { - print('章节开头: ${chapter.start}'); - print('章节开头: ${chapter.start + position}'); - await seekInBook(chapter.start + position); - return; - } await seekInBook(chapter.start + offset); } + PlaybackSessionExpanded? get session => _session; + + // 当前音轨 + AudioTrack? get currentTrack { + if (_session == null || _player.currentIndex == null) { + return null; + } + return _session!.audioTracks[_player.currentIndex!]; + } + + // 当前章节 + BookChapter? get currentChapter { + return _currentChapterObject.value; + } + + Duration get position => _player.position; + Duration get positionInChapter { + return _player.position + + (currentTrack?.startOffset ?? Duration.zero) - + (currentChapter?.start ?? Duration.zero); + } + + Duration get positionInBook { + return _player.position + (currentTrack?.startOffset ?? Duration.zero); + } + + Duration get bufferedPositionInBook { + return _player.bufferedPosition + + (currentTrack?.startOffset ?? Duration.zero); + } + + Duration? get chapterDuration => currentChapter?.duration; + + Stream get playerStateStream => _player.playerStateStream; + + Stream get positionStream => _player.positionStream; + + Stream get positionStreamInBook { + return _player.positionStream.map((position) { + return position + (currentTrack?.startOffset ?? Duration.zero); + }); + } + + Stream get slowPositionStreamInBook { + final superPositionStream = _player.createPositionStream( + steps: 100, + minPeriod: const Duration(milliseconds: 500), + maxPeriod: const Duration(seconds: 1), + ); + return superPositionStream.map((position) { + return position + (currentTrack?.startOffset ?? Duration.zero); + }); + } + + Stream get bufferedPositionStreamInBook { + return _player.bufferedPositionStream.map((position) { + return position + (currentTrack?.startOffset ?? Duration.zero); + }); + } + + Stream get positionStreamInChapter { + return _player.positionStream.distinct().map((position) { + return position + + (currentTrack?.startOffset ?? Duration.zero) - + (currentChapter?.start ?? Duration.zero); + }); + } + + Stream get chapterStream => _currentChapterObject.stream; + + Future togglePlayPause() async { + // check if book is set + if (_session == null) { + return Future.value(); + } + _player.playerState.playing ? await pause() : await play(); + } + + // 播放控制方法 @override - Future seekToNext() async { - if (_book == null) { + Future play() async { + await _player.play(); + } + + @override + Future pause() async { + await _player.pause(); + } + + // 重写上一曲/下一曲为章节导航 + @override + Future skipToNext() async { + if (_session == null) { // 回退到默认行为 - return super.seekToNext(); + return _player.seekToNext(); } final chapter = currentChapter; if (chapter == null) { // 回退到默认行为 - return super.seekToNext(); + return _player.seekToNext(); } - final currentIndex = _book!.chapters.indexOf(chapter); - if (currentIndex < _book!.chapters.length - 1) { + final chapterIndex = _session!.chapters.indexOf(chapter); + if (chapterIndex < _session!.chapters.length - 1) { // 跳到下一章 - final nextChapter = _book!.chapters[currentIndex + 1]; + final nextChapter = _session!.chapters[chapterIndex + 1]; await skipToChapter(nextChapter.id); } } @override - Future seekToPrevious() async { - if (_book == null) { - return super.seekToPrevious(); - } - + Future skipToPrevious() async { final chapter = currentChapter; - if (chapter == null) { - return super.seekToPrevious(); + if (_session == null || chapter == null) { + return _player.seekToPrevious(); } - final currentIndex = _book!.chapters.indexOf(chapter); + final currentIndex = _session!.chapters.indexOf(chapter); if (currentIndex > 0) { // 跳到上一章 - final prevChapter = _book!.chapters[currentIndex - 1]; + final prevChapter = _session!.chapters[currentIndex - 1]; await skipToChapter(prevChapter.id); } else { // 已经是第一章,回到开头 @@ -242,84 +241,77 @@ class AudiobookPlayer extends AudioPlayer { } } - /// a convenience method to get position in the book instead of the current track position - Duration get positionInBook { - if (_book == null || currentIndex == null) { - return Duration.zero; + @override + Future seek(Duration position) async { + // 这个 position 是当前音轨内的位置,我们不直接使用 + // 而是通过全局位置来控制 + final track = currentTrack; + Duration startOffset = Duration.zero; + if (track != null) { + startOffset = track.startOffset; } - return position + _book!.tracks[currentIndex!].startOffset; - // return position + _book!.tracks[sequenceState!.currentIndex].startOffset; + await seekInBook(startOffset + position); } - /// a convenience method to get the buffered position in the book instead of the current track position - Duration get bufferedPositionInBook { - if (_book == null || currentIndex == null) { - return Duration.zero; + Future setVolume(double volume) async { + await _player.setVolume(volume); + } + + @override + Future setSpeed(double speed) async { + await _player.setSpeed(speed); + } + + // 核心功能:跳转到全局时间位置 + Future seekInBook(Duration globalPosition) async { + if (_session == null) return; + // 找到目标音轨和在音轨内的位置 + final track = _session!.findTrackAtTime(globalPosition); + final index = _session!.audioTracks.indexOf(track); + Duration positionInTrack = globalPosition - track.startOffset; + if (positionInTrack < Duration.zero) { + positionInTrack = Duration.zero; } - return bufferedPosition + _book!.tracks[currentIndex!].startOffset; - // return bufferedPosition + _book!.tracks[sequenceState!.currentIndex].startOffset; + // 切换到目标音轨具体位置 + await _player.seek(positionInTrack, index: index); } - // 章节进度 - Stream get positionStreamInChapter { - return super.positionStream.map((position) { - if (_book == null || currentIndex == null) { - return Duration.zero; - } - final globalPosition = - position + _book!.tracks[currentIndex!].startOffset; - final chapter = _book!.findChapterAtTime(globalPosition); - return globalPosition - chapter.start; - }); - } - - /// streams to override to suit the book instead of the current track - // - positionStream - // - bufferedPositionStream - Stream get positionStreamInBook { - // return the positionInBook stream - return super.positionStream.map((position) { - if (_book == null || currentIndex == null) { - return Duration.zero; - } - return position + _book!.tracks[currentIndex!].startOffset; - // return position + _book!.tracks[sequenceState!.currentIndex].startOffset; - }); - } - - Stream get bufferedPositionStreamInBook { - return super.bufferedPositionStream.map((position) { - if (_book == null || currentIndex == null) { - return Duration.zero; - } - return position + _book!.tracks[currentIndex!].startOffset; - // return position + _book!.tracks[sequenceState!.currentIndex].startOffset; - }); - } - - /// a convenience getter for slow position stream - Stream get slowPositionStreamInBook { - final superPositionStream = createPositionStream( - steps: 100, - minPeriod: const Duration(milliseconds: 500), - maxPeriod: const Duration(seconds: 1), + AudioPlayer get player => _player; + PlaybackState _transformEvent(PlaybackEvent event) { + return PlaybackState( + controls: [ + if (kIsWeb || !Platform.isAndroid) MediaControl.skipToPrevious, + MediaControl.rewind, + if (_player.playing) MediaControl.pause else MediaControl.play, + MediaControl.stop, + MediaControl.fastForward, + if (kIsWeb || !Platform.isAndroid) MediaControl.skipToNext, + ], + systemActions: { + if (kIsWeb || !Platform.isAndroid) MediaAction.skipToPrevious, + MediaAction.rewind, + MediaAction.seek, + MediaAction.fastForward, + MediaAction.stop, + MediaAction.setSpeed, + if (kIsWeb || !Platform.isAndroid) MediaAction.skipToNext, + }, + androidCompactActionIndices: const [1, 2, 3], + processingState: const { + ProcessingState.idle: AudioProcessingState.idle, + ProcessingState.loading: AudioProcessingState.loading, + ProcessingState.buffering: AudioProcessingState.buffering, + ProcessingState.ready: AudioProcessingState.ready, + ProcessingState.completed: AudioProcessingState.completed, + }[_player.processingState] ?? + AudioProcessingState.idle, + playing: _player.playing, + updatePosition: _player.position, + bufferedPosition: event.bufferedPosition, + speed: _player.speed, + queueIndex: event.currentIndex, + captioningEnabled: false, ); - // now we need to map the position to the book instead of the current track - return superPositionStream.map((position) { - if (_book == null || currentIndex == null) { - return Duration.zero; - } - return position + _book!.tracks[currentIndex!].startOffset; - // return position + _book!.tracks[sequenceState!.currentIndex].startOffset; - }); - } - - /// get current chapter - BookChapter? get currentChapter { - if (_book == null) { - return null; - } - return _book!.findChapterAtTime(positionInBook); } } @@ -340,46 +332,7 @@ Uri _getUri( Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token'); } -extension FormatNotificationTitle on String { - String formatNotificationTitle(BookExpanded book) { - return replaceAllMapped( - RegExp(r'\$(\w+)'), - (match) { - final type = match.group(1); - return NotificationTitleType.values - .firstWhere((element) => element.name == type) - .extractFrom(book) ?? - match.group(0) ?? - ''; - }, - ); - } -} - -extension NotificationTitleUtils on NotificationTitleType { - String? extractFrom(BookExpanded book) { - var bookMetadataExpanded = book.metadata.asBookMetadataExpanded; - switch (this) { - case NotificationTitleType.bookTitle: - return bookMetadataExpanded.title; - case NotificationTitleType.chapterTitle: - // TODO: implement chapter title; depends on https://github.com/Dr-Blank/Vaani/issues/2 - return bookMetadataExpanded.title; - case NotificationTitleType.author: - return bookMetadataExpanded.authorName; - case NotificationTitleType.narrator: - return bookMetadataExpanded.narratorName; - case NotificationTitleType.series: - return bookMetadataExpanded.seriesName; - case NotificationTitleType.subtitle: - return bookMetadataExpanded.subtitle; - case NotificationTitleType.year: - return bookMetadataExpanded.publishedYear; - } - } -} - -extension BookExpandedExtension on BookExpanded { +extension PlaybackSessionExpandedExtension on PlaybackSessionExpanded { BookChapter findChapterAtTime(Duration position) { return chapters.firstWhere( (element) { @@ -390,16 +343,23 @@ extension BookExpandedExtension on BookExpanded { } AudioTrack findTrackAtTime(Duration position) { - return tracks.firstWhere( + return audioTracks.firstWhere( (element) { return element.startOffset <= position && element.startOffset + element.duration >= position + offset; }, - orElse: () => tracks.first, + orElse: () => audioTracks.first, ); } + int findTrackIndexAtTime(Duration position) { + return audioTracks.indexWhere((element) { + return element.startOffset <= position && + element.startOffset + element.duration >= position + offset; + }); + } + Duration getTrackStartOffset(int index) { - return tracks[index].startOffset; + return audioTracks[index].startOffset; } } diff --git a/lib/features/player/core/audiobook_player_session.dart b/lib/features/player/core/audiobook_player_session.dart deleted file mode 100644 index 37c2d8f..0000000 --- a/lib/features/player/core/audiobook_player_session.dart +++ /dev/null @@ -1,365 +0,0 @@ -// my_audio_handler.dart -import 'dart:io'; - -import 'package:audio_service/audio_service.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:just_audio/just_audio.dart'; -import 'package:rxdart/rxdart.dart'; -import 'package:shelfsdk/audiobookshelf_api.dart'; -import 'package:vaani/features/player/core/player_status.dart' as core; -import 'package:vaani/features/player/providers/player_status_provider.dart'; -import 'package:vaani/shared/extensions/chapter.dart'; - -// add a small offset so the display does not show the previous chapter for a split second -final offset = Duration(milliseconds: 10); - -class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { - final AudioPlayer _player = AudioPlayer(); - // final List _playlist = []; - final Ref ref; - - PlaybackSessionExpanded? _session; - - final _currentChapterObject = BehaviorSubject.seeded(null); - AbsAudioHandler(this.ref) { - _setupAudioPlayer(); - } - - void _setupAudioPlayer() { - final statusNotifier = ref.read(playerStatusProvider.notifier); - - // 转发播放状态 - _player.playbackEventStream.map(_transformEvent).pipe(playbackState); - _player.playerStateStream.listen((event) { - if (event.playing) { - statusNotifier.setPlayStatusVerify(core.PlayStatus.playing); - } else { - statusNotifier.setPlayStatusVerify(core.PlayStatus.paused); - } - }); - _player.positionStream.distinct().listen((position) { - final chapter = _session?.findChapterAtTime(positionInBook); - if (chapter != currentChapter) { - _currentChapterObject.sink.add(chapter); - } - }); - } - - // 加载有声书 - Future setSourceAudiobook( - PlaybackSessionExpanded playbackSession, { - required Uri baseUrl, - required String token, - List? downloadedUris, - }) async { - _session = playbackSession; - - // 添加所有音轨 - List audioSources = []; - for (final track in playbackSession.audioTracks) { - audioSources.add( - AudioSource.uri( - _getUri(track, downloadedUris, baseUrl: baseUrl, token: token), - ), - ); - } - - playMediaItem( - MediaItem( - id: playbackSession.libraryItemId, - album: playbackSession.mediaMetadata.title, - title: playbackSession.displayTitle, - displaySubtitle: playbackSession.mediaType == MediaType.book - ? (playbackSession.mediaMetadata as BookMetadata).subtitle - : null, - duration: playbackSession.duration, - artUri: Uri.parse( - '$baseUrl/api/items/${playbackSession.libraryItemId}/cover?token=$token', - ), - ), - ); - final track = playbackSession.findTrackAtTime(playbackSession.currentTime); - final index = playbackSession.audioTracks.indexOf(track); - - await _player.setAudioSources( - audioSources, - initialIndex: index, - initialPosition: playbackSession.currentTime - track.startOffset, - ); - _player.seek(playbackSession.currentTime - track.startOffset, index: index); - await play(); - // 恢复上次播放位置(如果有) - // if (initialPosition != null) { - // await seekInBook(initialPosition); - // } - } - - // // 音轨切换处理 - // void _onTrackChanged(int trackIndex) { - // if (_book == null) return; - - // // 可以在这里处理音轨切换逻辑,比如预加载下一音轨 - // // print('切换到音轨: ${_book!.tracks[trackIndex].title}'); - // } - - // 核心功能:跳转到指定章节 - Future skipToChapter(int chapterId) async { - if (_session == null) return; - - final chapter = _session!.chapters.firstWhere( - (ch) => ch.id == chapterId, - orElse: () => throw Exception('Chapter not found'), - ); - await seekInBook(chapter.start + offset); - } - - PlaybackSessionExpanded? get session => _session; - - // 当前音轨 - AudioTrack? get currentTrack { - if (_session == null || _player.currentIndex == null) { - return null; - } - return _session!.audioTracks[_player.currentIndex!]; - } - - // 当前章节 - BookChapter? get currentChapter { - return _currentChapterObject.value; - } - - Duration get position => _player.position; - Duration get positionInChapter { - return _player.position + - (currentTrack?.startOffset ?? Duration.zero) - - (currentChapter?.start ?? Duration.zero); - } - - Duration get positionInBook { - return _player.position + (currentTrack?.startOffset ?? Duration.zero); - } - - Duration get bufferedPositionInBook { - return _player.bufferedPosition + - (currentTrack?.startOffset ?? Duration.zero); - } - - Duration? get chapterDuration => currentChapter?.duration; - - Stream get playerStateStream => _player.playerStateStream; - - Stream get positionStream => _player.positionStream; - - Stream get positionStreamInBook { - return _player.positionStream.map((position) { - return position + (currentTrack?.startOffset ?? Duration.zero); - }); - } - - Stream get slowPositionStreamInBook { - final superPositionStream = _player.createPositionStream( - steps: 100, - minPeriod: const Duration(milliseconds: 500), - maxPeriod: const Duration(seconds: 1), - ); - return superPositionStream.map((position) { - return position + (currentTrack?.startOffset ?? Duration.zero); - }); - } - - Stream get bufferedPositionStreamInBook { - return _player.bufferedPositionStream.map((position) { - return position + (currentTrack?.startOffset ?? Duration.zero); - }); - } - - Stream get positionStreamInChapter { - return _player.positionStream.distinct().map((position) { - return position + - (currentTrack?.startOffset ?? Duration.zero) - - (currentChapter?.start ?? Duration.zero); - }); - } - - Stream get chapterStream => _currentChapterObject.stream; - - Future togglePlayPause() async { - // check if book is set - if (_session == null) { - return Future.value(); - } - _player.playerState.playing ? await pause() : await play(); - } - - // 播放控制方法 - @override - Future play() async { - await _player.play(); - } - - @override - Future pause() async { - await _player.pause(); - } - - // 重写上一曲/下一曲为章节导航 - @override - Future skipToNext() async { - if (_session == null) { - // 回退到默认行为 - return _player.seekToNext(); - } - final chapter = currentChapter; - if (chapter == null) { - // 回退到默认行为 - return _player.seekToNext(); - } - final chapterIndex = _session!.chapters.indexOf(chapter); - if (chapterIndex < _session!.chapters.length - 1) { - // 跳到下一章 - final nextChapter = _session!.chapters[chapterIndex + 1]; - await skipToChapter(nextChapter.id); - } - } - - @override - Future skipToPrevious() async { - final chapter = currentChapter; - if (_session == null || chapter == null) { - return _player.seekToPrevious(); - } - final currentIndex = _session!.chapters.indexOf(chapter); - if (currentIndex > 0) { - // 跳到上一章 - final prevChapter = _session!.chapters[currentIndex - 1]; - await skipToChapter(prevChapter.id); - } else { - // 已经是第一章,回到开头 - await seekInBook(Duration.zero); - } - } - - @override - Future seek(Duration position) async { - // 这个 position 是当前音轨内的位置,我们不直接使用 - // 而是通过全局位置来控制 - final track = currentTrack; - Duration startOffset = Duration.zero; - if (track != null) { - startOffset = track.startOffset; - } - await seekInBook(startOffset + position); - } - - Future setVolume(double volume) async { - await _player.setVolume(volume); - } - - @override - Future setSpeed(double speed) async { - await _player.setSpeed(speed); - } - - // 核心功能:跳转到全局时间位置 - Future seekInBook(Duration globalPosition) async { - if (_session == null) return; - // 找到目标音轨和在音轨内的位置 - final track = _session!.findTrackAtTime(globalPosition); - final index = _session!.audioTracks.indexOf(track); - Duration positionInTrack = globalPosition - track.startOffset; - if (positionInTrack < Duration.zero) { - positionInTrack = Duration.zero; - } - // 切换到目标音轨具体位置 - await _player.seek(positionInTrack, index: index); - } - - AudioPlayer get player => _player; - PlaybackState _transformEvent(PlaybackEvent event) { - return PlaybackState( - controls: [ - if (kIsWeb || !Platform.isAndroid) MediaControl.skipToPrevious, - MediaControl.rewind, - if (_player.playing) MediaControl.pause else MediaControl.play, - MediaControl.stop, - MediaControl.fastForward, - if (kIsWeb || !Platform.isAndroid) MediaControl.skipToNext, - ], - systemActions: { - if (kIsWeb || !Platform.isAndroid) MediaAction.skipToPrevious, - MediaAction.rewind, - MediaAction.seek, - MediaAction.fastForward, - MediaAction.stop, - MediaAction.setSpeed, - if (kIsWeb || !Platform.isAndroid) MediaAction.skipToNext, - }, - androidCompactActionIndices: const [1, 2, 3], - processingState: const { - ProcessingState.idle: AudioProcessingState.idle, - ProcessingState.loading: AudioProcessingState.loading, - ProcessingState.buffering: AudioProcessingState.buffering, - ProcessingState.ready: AudioProcessingState.ready, - ProcessingState.completed: AudioProcessingState.completed, - }[_player.processingState] ?? - AudioProcessingState.idle, - playing: _player.playing, - updatePosition: _player.position, - bufferedPosition: event.bufferedPosition, - speed: _player.speed, - queueIndex: event.currentIndex, - captioningEnabled: false, - ); - } -} - -Uri _getUri( - AudioTrack track, - List? downloadedUris, { - required Uri baseUrl, - required String token, -}) { - // check if the track is in the downloadedUris - final uri = downloadedUris?.firstWhereOrNull( - (element) { - return element.pathSegments.last == track.metadata?.filename; - }, - ); - - return uri ?? - Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token'); -} - -extension PlaybackSessionExpandedExtension on PlaybackSessionExpanded { - BookChapter findChapterAtTime(Duration position) { - return chapters.firstWhere( - (element) { - return element.start <= position && element.end >= position + offset; - }, - orElse: () => chapters.first, - ); - } - - AudioTrack findTrackAtTime(Duration position) { - return audioTracks.firstWhere( - (element) { - return element.startOffset <= position && - element.startOffset + element.duration >= position + offset; - }, - orElse: () => audioTracks.first, - ); - } - - int findTrackIndexAtTime(Duration position) { - return audioTracks.indexWhere((element) { - return element.startOffset <= position && - element.startOffset + element.duration >= position + offset; - }); - } - - Duration getTrackStartOffset(int index) { - return audioTracks[index].startOffset; - } -} diff --git a/lib/features/player/core/init.dart b/lib/features/player/core/init.dart deleted file mode 100644 index 8891556..0000000 --- a/lib/features/player/core/init.dart +++ /dev/null @@ -1,62 +0,0 @@ -// import 'package:audio_service/audio_service.dart'; -// import 'package:audio_session/audio_session.dart'; -// import 'package:just_audio_background/just_audio_background.dart' -// show JustAudioBackground, NotificationConfig; -// import 'package:just_audio_media_kit/just_audio_media_kit.dart' -// show JustAudioMediaKit; -// import 'package:vaani/settings/app_settings_provider.dart'; -// import 'package:vaani/settings/models/app_settings.dart'; - -// Future configurePlayer() async { -// // for playing audio on windows, linux -// JustAudioMediaKit.ensureInitialized(windows: false); - -// // for configuring how this app will interact with other audio apps -// final session = await AudioSession.instance; -// await session.configure(const AudioSessionConfiguration.speech()); - -// final appSettings = loadOrCreateAppSettings(); - -// // for playing audio in the background -// await JustAudioBackground.init( -// androidNotificationChannelId: 'com.vaani.bg_demo.channel.audio', -// androidNotificationChannelName: 'Audio playback', -// androidNotificationOngoing: false, -// androidStopForegroundOnPause: false, -// androidNotificationChannelDescription: 'Audio playback in the background', -// androidNotificationIcon: 'drawable/ic_stat_logo', -// rewindInterval: appSettings.notificationSettings.rewindInterval, -// fastForwardInterval: appSettings.notificationSettings.fastForwardInterval, -// androidShowNotificationBadge: false, -// notificationConfigBuilder: (state) { -// final controls = [ -// if (appSettings.notificationSettings.mediaControls -// .contains(NotificationMediaControl.skipToPreviousChapter) && -// state.hasPrevious) -// MediaControl.skipToPrevious, -// if (appSettings.notificationSettings.mediaControls -// .contains(NotificationMediaControl.rewind)) -// MediaControl.rewind, -// if (state.playing) MediaControl.pause else MediaControl.play, -// if (appSettings.notificationSettings.mediaControls -// .contains(NotificationMediaControl.fastForward)) -// MediaControl.fastForward, -// if (appSettings.notificationSettings.mediaControls -// .contains(NotificationMediaControl.skipToNextChapter) && -// state.hasNext) -// MediaControl.skipToNext, -// if (appSettings.notificationSettings.mediaControls -// .contains(NotificationMediaControl.stop)) -// MediaControl.stop, -// ]; -// return NotificationConfig( -// controls: controls, -// systemActions: const { -// MediaAction.seek, -// MediaAction.seekForward, -// MediaAction.seekBackward, -// }, -// ); -// }, -// ); -// } diff --git a/lib/features/player/providers/audiobook_player.dart b/lib/features/player/providers/audiobook_player.dart index 6335535..955b9cd 100644 --- a/lib/features/player/providers/audiobook_player.dart +++ b/lib/features/player/providers/audiobook_player.dart @@ -1,71 +1,112 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:logging/logging.dart'; +import 'package:audio_service/audio_service.dart'; +import 'package:just_audio_media_kit/just_audio_media_kit.dart'; +import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk; +import 'package:shelfsdk/audiobookshelf_api.dart' as core; import 'package:vaani/api/api_provider.dart'; -import 'package:vaani/features/player/core/audiobook_player.dart' as core; +import 'package:vaani/api/library_item_provider.dart'; +import 'package:vaani/features/downloads/providers/download_manager.dart'; +import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart'; +import 'package:vaani/features/player/core/audiobook_player.dart'; +import 'package:vaani/features/player/providers/player_status_provider.dart'; +import 'package:vaani/features/settings/app_settings_provider.dart'; +import 'package:vaani/shared/extensions/model_conversions.dart'; +import 'package:vaani/shared/utils/helper.dart'; part 'audiobook_player.g.dart'; -final _logger = Logger('AudiobookPlayerProvider'); - -const playerId = 'audiobook_player'; - -/// Simple because it doesn't rebuild when the player state changes -/// it only rebuilds when the token changes @Riverpod(keepAlive: true) -class SimpleAudiobookPlayer extends _$SimpleAudiobookPlayer { +Future audioHandlerInit(Ref ref) async { + if (Helper.isWindows() || Helper.isLinux()) { + // JustAudioMediaKit.ensureInitialized(windows: false); + JustAudioMediaKit.ensureInitialized(); + } + + final audioService = await AudioService.init( + builder: () => AbsAudioHandler(ref), + config: const AudioServiceConfig( + androidNotificationChannelId: 'dr.blank.vaani.channel.audio', + androidNotificationChannelName: 'ABSPlayback', + androidNotificationChannelDescription: + 'Needed to control audio from lock screen', + androidNotificationOngoing: false, + androidStopForegroundOnPause: false, + androidNotificationIcon: 'drawable/ic_stat_logo', + preloadArtwork: true, + ), + ); + return audioService; +} + +@Riverpod(keepAlive: true) +class Player extends _$Player { @override - core.AudiobookPlayer build() { - final api = ref.watch(authenticatedApiProvider); - final player = core.AudiobookPlayer( - api.token!, - api.baseUrl, - ); - - ref.onDispose(player.dispose); - _logger.finer('created simple player'); - - return player; + AbsAudioHandler build() { + return ref.watch(audioHandlerInitProvider).requireValue; } } @Riverpod(keepAlive: true) -class AudiobookPlayer extends _$AudiobookPlayer { +class Session extends _$Session { @override - core.AudiobookPlayer build() { - final player = ref.watch(simpleAudiobookPlayerProvider); - - ref.onDispose(player.dispose); - - // bind notify listeners to the player - // player.playerStateStream.listen((_) { - // ref.notifyListeners(); - // }); - - _logger.finer('created player'); - - return player; + core.PlaybackSessionExpanded? build() { + return null; } - Future setSpeed(double speed) async { - await state.setSpeed(speed); - ref.notifyListeners(); - } + Future load(String id, String? episodeId) async { + final audioService = ref.read(playerProvider); + await audioService.pause(); + ref.read(playerStatusProvider.notifier).setLoading(id); + final api = ref.read(authenticatedApiProvider); + final playBack = await ref.watch(playBackSessionProvider(id).future); + if (playBack == null) { + return; + } + state = playBack.asExpanded; + final downloadManager = ref.read(simpleDownloadManagerProvider); + final libItem = + await ref.read(libraryItemProvider(state!.libraryItemId).future); + final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem); - Future setSourceAudiobook({ - required shelfsdk.BookExpanded book, - shelfsdk.MediaProgress? userMediaProgress, - }) async { - ref.notifyListeners(); + var bookPlayerSettings = + ref.read(bookSettingsProvider(state!.libraryItemId)).playerSettings; + var appPlayerSettings = ref.read(appSettingsProvider).playerSettings; + + var configurePlayerForEveryBook = + appPlayerSettings.configurePlayerForEveryBook; + + await Future.wait([ + audioService.setSourceAudiobook( + state!.asExpanded, + baseUrl: api.baseUrl, + token: api.token!, + downloadedUris: downloadedUris, + ), + // set the volume + audioService.setVolume( + configurePlayerForEveryBook + ? bookPlayerSettings.preferredDefaultVolume ?? + appPlayerSettings.preferredDefaultVolume + : appPlayerSettings.preferredDefaultVolume, + ), + // set the speed + audioService.setSpeed( + configurePlayerForEveryBook + ? bookPlayerSettings.preferredDefaultSpeed ?? + appPlayerSettings.preferredDefaultSpeed + : appPlayerSettings.preferredDefaultSpeed, + ), + ]); } } -@riverpod -bool isPlayerPlaying( - Ref ref, -) { - final player = ref.watch(audiobookPlayerProvider); - print("playing: ${player.playing}"); - return player.playing; +class PlaybackSyncError implements Exception { + String message; + + PlaybackSyncError([this.message = 'Error syncing playback']); + + @override + String toString() { + return 'PlaybackSyncError: $message'; + } } diff --git a/lib/features/player/providers/audiobook_player.g.dart b/lib/features/player/providers/audiobook_player.g.dart index a4f5451..ee8520f 100644 --- a/lib/features/player/providers/audiobook_player.g.dart +++ b/lib/features/player/providers/audiobook_player.g.dart @@ -6,58 +6,51 @@ part of 'audiobook_player.dart'; // RiverpodGenerator // ************************************************************************** -String _$isPlayerPlayingHash() => r'b81fa9cfb51c88c8d9e8f5c1f4f6a12d9e5a0cc1'; +String _$audioHandlerInitHash() => r'6e4662a45c1c6e84aa16436f71ffcfecc3d4bdab'; -/// See also [isPlayerPlaying]. -@ProviderFor(isPlayerPlaying) -final isPlayerPlayingProvider = AutoDisposeProvider.internal( - isPlayerPlaying, - name: r'isPlayerPlayingProvider', +/// See also [audioHandlerInit]. +@ProviderFor(audioHandlerInit) +final audioHandlerInitProvider = FutureProvider.internal( + audioHandlerInit, + name: r'audioHandlerInitProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$isPlayerPlayingHash, + : _$audioHandlerInitHash, dependencies: null, allTransitiveDependencies: null, ); @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -typedef IsPlayerPlayingRef = AutoDisposeProviderRef; -String _$simpleAudiobookPlayerHash() => - r'5e94bbff4314adceb5affa704fc4d079d4016afa'; +typedef AudioHandlerInitRef = FutureProviderRef; +String _$playerHash() => r'9599b094cdd9eca614c27ec5bdf2d5259d20ac5f'; -/// Simple because it doesn't rebuild when the player state changes -/// it only rebuilds when the token changes -/// -/// Copied from [SimpleAudiobookPlayer]. -@ProviderFor(SimpleAudiobookPlayer) -final simpleAudiobookPlayerProvider = - NotifierProvider.internal( - SimpleAudiobookPlayer.new, - name: r'simpleAudiobookPlayerProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$simpleAudiobookPlayerHash, +/// See also [Player]. +@ProviderFor(Player) +final playerProvider = NotifierProvider.internal( + Player.new, + name: r'playerProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$playerHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$SimpleAudiobookPlayer = Notifier; -String _$audiobookPlayerHash() => r'04448247e79c5d60b9fd6f98eeeb865f1e8d0ff8'; +typedef _$Player = Notifier; +String _$sessionHash() => r'c171809249c3021dc445dc1ba90fe8626a3d3b54'; -/// See also [AudiobookPlayer]. -@ProviderFor(AudiobookPlayer) -final audiobookPlayerProvider = - NotifierProvider.internal( - AudiobookPlayer.new, - name: r'audiobookPlayerProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$audiobookPlayerHash, +/// See also [Session]. +@ProviderFor(Session) +final sessionProvider = + NotifierProvider.internal( + Session.new, + name: r'sessionProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$sessionHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$AudiobookPlayer = Notifier; +typedef _$Session = Notifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/features/player/providers/currently_playing_provider.dart b/lib/features/player/providers/currently_playing_provider.dart index dc44548..0fc0c3f 100644 --- a/lib/features/player/providers/currently_playing_provider.dart +++ b/lib/features/player/providers/currently_playing_provider.dart @@ -1,46 +1,40 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:shelfsdk/audiobookshelf_api.dart'; +import 'package:shelfsdk/audiobookshelf_api.dart' as core; import 'package:vaani/features/player/providers/audiobook_player.dart'; -import 'package:vaani/shared/extensions/model_conversions.dart'; part 'currently_playing_provider.g.dart'; -final _logger = Logger('CurrentlyPlayingProvider'); - @riverpod -BookExpanded? currentlyPlayingBook(Ref ref) { - try { - final book = ref.watch(simpleAudiobookPlayerProvider.select((v) => v.book)); - return book; - } catch (e) { - _logger.warning('Error getting currently playing book: $e'); - return null; +class CurrentChapter extends _$CurrentChapter { + @override + core.BookChapter? build() { + final player = ref.watch(playerProvider); + player.chapterStream.distinct().listen((chapter) { + update(chapter); + }); + return player.currentChapter; + } + + void update(core.BookChapter? chapter) { + if (state != chapter) { + state = chapter; + } } } -/// provided the current chapter of the book being played @riverpod -BookChapter? currentPlayingChapter(Ref ref) { - final player = ref.watch(audiobookPlayerProvider); - player.slowPositionStreamInBook.listen((_) { - ref.invalidateSelf(); - }); - - return player.currentChapter; +List currentChapters(Ref ref) { + final session = ref.watch(sessionProvider); + if (session == null) { + return []; + } + final currentChapter = ref.watch(currentChapterProvider); + if (currentChapter == null) { + return []; + } + final index = session.chapters.indexOf(currentChapter); + final total = session.chapters.length; + return session.chapters + .sublist(index - 3, (total - 3) <= (index + 17) ? total : index + 17); } - -/// provides the book metadata of the currently playing book -@riverpod -BookMetadataExpanded? currentBookMetadata(Ref ref) { - final player = ref.watch(audiobookPlayerProvider); - if (player.book == null) return null; - return player.book!.metadata.asBookMetadataExpanded; -} - -// /// volume of the player [0, 1] -// @riverpod -// double currentVolume(CurrentVolumeRef ref) { -// return 1; -// } diff --git a/lib/features/player/providers/currently_playing_provider.g.dart b/lib/features/player/providers/currently_playing_provider.g.dart index 430089a..7678af0 100644 --- a/lib/features/player/providers/currently_playing_provider.g.dart +++ b/lib/features/player/providers/currently_playing_provider.g.dart @@ -6,66 +6,39 @@ part of 'currently_playing_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$currentlyPlayingBookHash() => - r'f2c47028340d253be9440dc29f835328ff30c0e6'; +String _$currentChaptersHash() => r'f2cc6ec31b5a3a9471775b1c96b2bfc3a91f1c90'; -/// See also [currentlyPlayingBook]. -@ProviderFor(currentlyPlayingBook) -final currentlyPlayingBookProvider = - AutoDisposeProvider.internal( - currentlyPlayingBook, - name: r'currentlyPlayingBookProvider', +/// See also [currentChapters]. +@ProviderFor(currentChapters) +final currentChaptersProvider = + AutoDisposeProvider>.internal( + currentChapters, + name: r'currentChaptersProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$currentlyPlayingBookHash, + : _$currentChaptersHash, dependencies: null, allTransitiveDependencies: null, ); @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -typedef CurrentlyPlayingBookRef = AutoDisposeProviderRef; -String _$currentPlayingChapterHash() => - r'4a64157089279c71279ccfdbcfc7b32543ecc88c'; +typedef CurrentChaptersRef = AutoDisposeProviderRef>; +String _$currentChapterHash() => r'f5f6d9e49cb7e455d032f7370f364d9ce30b8eb1'; -/// provided the current chapter of the book being played -/// -/// Copied from [currentPlayingChapter]. -@ProviderFor(currentPlayingChapter) -final currentPlayingChapterProvider = - AutoDisposeProvider.internal( - currentPlayingChapter, - name: r'currentPlayingChapterProvider', +/// See also [CurrentChapter]. +@ProviderFor(CurrentChapter) +final currentChapterProvider = + AutoDisposeNotifierProvider.internal( + CurrentChapter.new, + name: r'currentChapterProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$currentPlayingChapterHash, + : _$currentChapterHash, dependencies: null, allTransitiveDependencies: null, ); -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef CurrentPlayingChapterRef = AutoDisposeProviderRef; -String _$currentBookMetadataHash() => - r'f537ef4ef19280bc952de658ecf6520c535ae344'; - -/// provides the book metadata of the currently playing book -/// -/// Copied from [currentBookMetadata]. -@ProviderFor(currentBookMetadata) -final currentBookMetadataProvider = - AutoDisposeProvider.internal( - currentBookMetadata, - name: r'currentBookMetadataProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$currentBookMetadataHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef CurrentBookMetadataRef = AutoDisposeProviderRef; +typedef _$CurrentChapter = AutoDisposeNotifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/features/player/providers/player_form.dart b/lib/features/player/providers/player_form.dart deleted file mode 100644 index 5c47ac1..0000000 --- a/lib/features/player/providers/player_form.dart +++ /dev/null @@ -1,80 +0,0 @@ -// 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'; -import 'package:vaani/features/player/providers/audiobook_player.dart'; - -part 'player_form.g.dart'; - -/// The height of the player when it is minimized -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( - Ref 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( - Ref 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; -} - -// final audioBookMiniplayerController = MiniplayerController(); - -@Riverpod(keepAlive: true) -bool isPlayerActive( - Ref ref, -) { - try { - final player = ref.watch(audiobookPlayerProvider); - if (player.book != null) { - return true; - } else { - final playerHeight = ref.watch(playerHeightProvider); - return playerHeight < playerMinHeight; - } - } catch (e) { - return false; - } -} diff --git a/lib/features/player/providers/player_form.g.dart b/lib/features/player/providers/player_form.g.dart deleted file mode 100644 index 6dcfcf9..0000000 --- a/lib/features/player/providers/player_form.g.dart +++ /dev/null @@ -1,63 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'player_form.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$playerExpandProgressNotifierHash() => - r'1ac7172d90a070f96222286edd1a176be197f378'; - -/// 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, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef PlayerExpandProgressNotifierRef - = ProviderRef>>; -String _$playerHeightHash() => r'3f031eaffdffbb2c6ddf7eb1aba31bf1619260fc'; - -/// 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, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef PlayerHeightRef = ProviderRef; -String _$isPlayerActiveHash() => r'2c7ca125423126fb5f0ef218d37bc8fe0ca9ec98'; - -/// See also [isPlayerActive]. -@ProviderFor(isPlayerActive) -final isPlayerActiveProvider = Provider.internal( - isPlayerActive, - name: r'isPlayerActiveProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$isPlayerActiveHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef IsPlayerActiveRef = 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, deprecated_member_use_from_same_package diff --git a/lib/features/player/providers/player_providers.dart b/lib/features/player/providers/player_providers.dart deleted file mode 100644 index 8b13789..0000000 --- a/lib/features/player/providers/player_providers.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/features/player/providers/session_provider.dart b/lib/features/player/providers/session_provider.dart deleted file mode 100644 index 4097bb7..0000000 --- a/lib/features/player/providers/session_provider.dart +++ /dev/null @@ -1,188 +0,0 @@ -import 'package:audio_service/audio_service.dart'; -import 'package:http/http.dart' as http; -import 'package:just_audio_media_kit/just_audio_media_kit.dart'; -import 'package:riverpod/riverpod.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:shelfsdk/audiobookshelf_api.dart' as core; -import 'package:vaani/api/api_provider.dart'; -import 'package:vaani/api/library_item_provider.dart'; -import 'package:vaani/features/downloads/providers/download_manager.dart'; -import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart'; -import 'package:vaani/features/playback_reporting/core/playback_reporter_session.dart' - as core; -import 'package:vaani/features/player/core/audiobook_player_session.dart'; -import 'package:vaani/features/player/providers/player_status_provider.dart'; -import 'package:vaani/globals.dart'; -import 'package:vaani/settings/app_settings_provider.dart'; -import 'package:vaani/shared/extensions/obfuscation.dart'; -import 'package:vaani/shared/utils/utils.dart'; - -part 'session_provider.g.dart'; - -@Riverpod(keepAlive: true) -Future audioHandlerInit(Ref ref) async { - if (Utils.isWindows() || Utils.isLinux()) { - // JustAudioMediaKit.ensureInitialized(windows: false); - JustAudioMediaKit.ensureInitialized(); - } - - final audioService = await AudioService.init( - builder: () => AbsAudioHandler(ref), - config: const AudioServiceConfig( - androidNotificationChannelId: 'com.vaani.rang.channel.audio', - androidNotificationChannelName: 'ABSPlayback', - androidNotificationChannelDescription: - 'Needed to control audio from lock screen', - androidNotificationOngoing: false, - androidStopForegroundOnPause: false, - androidNotificationIcon: 'drawable/ic_stat_logo', - preloadArtwork: true, - ), - ); - return audioService; -} - -@Riverpod(keepAlive: true) -class Player extends _$Player { - @override - AbsAudioHandler build() { - return ref.watch(audioHandlerInitProvider).requireValue; - } -} - -@Riverpod(keepAlive: true) -class Session extends _$Session { - @override - core.PlaybackSessionExpanded? build() { - return null; - } - - Future load(String id, String? episodeId) async { - final audioService = ref.read(playerProvider); - await audioService.pause(); - ref.read(playerStatusProvider.notifier).setLoading(id); - final api = ref.read(authenticatedApiProvider); - final playBack = await api.items.play( - libraryItemId: id, - parameters: core.PlayItemReqParams( - deviceInfo: core.DeviceInfoReqParams( - clientVersion: appVersion, - manufacturer: deviceManufacturer, - model: deviceModel, - sdkVersion: deviceSdkVersion, - clientName: appName, - deviceName: deviceName, - ), - forceDirectPlay: false, - forceTranscode: false, - supportedMimeTypes: [ - "audio/flac", - "audio/mpeg", - "audio/mp4", - "audio/ogg", - "audio/aac", - "audio/webm", - ], - ), - responseErrorHandler: _responseErrorHandler, - ) as core.PlaybackSessionExpanded; - state = playBack; - final downloadManager = ref.read(simpleDownloadManagerProvider); - final libItem = - await ref.read(libraryItemProvider(playBack.libraryItemId).future); - final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem); - - var bookPlayerSettings = - ref.read(bookSettingsProvider(playBack.libraryItemId)).playerSettings; - var appPlayerSettings = ref.read(appSettingsProvider).playerSettings; - - var configurePlayerForEveryBook = - appPlayerSettings.configurePlayerForEveryBook; - - await Future.wait([ - audioService.setSourceAudiobook( - playBack, - baseUrl: api.baseUrl, - token: api.token!, - downloadedUris: downloadedUris, - ), - // set the volume - audioService.setVolume( - configurePlayerForEveryBook - ? bookPlayerSettings.preferredDefaultVolume ?? - appPlayerSettings.preferredDefaultVolume - : appPlayerSettings.preferredDefaultVolume, - ), - // set the speed - audioService.setSpeed( - configurePlayerForEveryBook - ? bookPlayerSettings.preferredDefaultSpeed ?? - appPlayerSettings.preferredDefaultSpeed - : appPlayerSettings.preferredDefaultSpeed, - ), - ]); - } - - void _responseErrorHandler(http.Response response, [error]) { - if (response.statusCode != 200) { - appLogger.severe('Error with api: ${response.obfuscate()}, $error'); - throw PlaybackSyncError( - 'Error syncing position: ${response.body}, $error', - ); - } - } -} - -@Riverpod(keepAlive: true) -class CurrentChapter extends _$CurrentChapter { - @override - core.BookChapter? build() { - final player = ref.watch(playerProvider); - player.chapterStream.distinct().listen((chapter) { - update(chapter); - }); - return player.currentChapter; - } - - void update(core.BookChapter? chapter) { - if (state != chapter) { - state = chapter; - } - } -} - -@Riverpod(keepAlive: true) -class PlaybackReporter extends _$PlaybackReporter { - @override - Future build() async { - final session = ref.watch(sessionProvider); - if (session == null) { - return null; - } - final playerSettings = ref.watch(appSettingsProvider).playerSettings; - final player = ref.watch(playerProvider); - final api = ref.watch(authenticatedApiProvider); - - final reporter = core.PlaybackReporter( - player, - api, - reportingInterval: playerSettings.playbackReportInterval, - markCompleteWhenTimeLeft: playerSettings.markCompleteWhenTimeLeft, - minimumPositionForReporting: playerSettings.minimumPositionForReporting, - session: session, - ); - ref.onDispose(reporter.dispose); - return reporter; - } -} - -class PlaybackSyncError implements Exception { - String message; - - PlaybackSyncError([this.message = 'Error syncing playback']); - - @override - String toString() { - return 'PlaybackSyncError: $message'; - } -} diff --git a/lib/features/player/providers/session_provider.g.dart b/lib/features/player/providers/session_provider.g.dart deleted file mode 100644 index 4073b51..0000000 --- a/lib/features/player/providers/session_provider.g.dart +++ /dev/null @@ -1,88 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'session_provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$audioHandlerInitHash() => r'c54f17757807f8bc14daff5095c34eb88ff2037b'; - -/// See also [audioHandlerInit]. -@ProviderFor(audioHandlerInit) -final audioHandlerInitProvider = FutureProvider.internal( - audioHandlerInit, - name: r'audioHandlerInitProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$audioHandlerInitHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef AudioHandlerInitRef = FutureProviderRef; -String _$playerHash() => r'9599b094cdd9eca614c27ec5bdf2d5259d20ac5f'; - -/// See also [Player]. -@ProviderFor(Player) -final playerProvider = NotifierProvider.internal( - Player.new, - name: r'playerProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$playerHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$Player = Notifier; -String _$sessionHash() => r'ce9405cc0b8247014924a005fa60fbc3bf92d5c6'; - -/// See also [Session]. -@ProviderFor(Session) -final sessionProvider = - NotifierProvider.internal( - Session.new, - name: r'sessionProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$sessionHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$Session = Notifier; -String _$currentChapterHash() => r'0be02fbc4f65b30da4ce18964ee457a18c4d1073'; - -/// See also [CurrentChapter]. -@ProviderFor(CurrentChapter) -final currentChapterProvider = - NotifierProvider.internal( - CurrentChapter.new, - name: r'currentChapterProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$currentChapterHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$CurrentChapter = Notifier; -String _$playbackReporterHash() => r'b9a2197a12e83f9760bd61ae2a1ad8dff82042e9'; - -/// See also [PlaybackReporter]. -@ProviderFor(PlaybackReporter) -final playbackReporterProvider = - AsyncNotifierProvider.internal( - PlaybackReporter.new, - name: r'playbackReporterProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$playbackReporterHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$PlaybackReporter = AsyncNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/features/player/view/mini_player_bottom_padding.dart b/lib/features/player/view/mini_player_bottom_padding.dart index c403361..996a70b 100644 --- a/lib/features/player/view/mini_player_bottom_padding.dart +++ b/lib/features/player/view/mini_player_bottom_padding.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:vaani/features/player/providers/player_form.dart'; +import 'package:vaani/features/player/providers/player_status_provider.dart'; +import 'package:vaani/globals.dart' show playerMinHeight; class MiniPlayerBottomPadding extends HookConsumerWidget { const MiniPlayerBottomPadding({super.key}); @@ -8,7 +9,7 @@ class MiniPlayerBottomPadding extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return AnimatedSize( duration: const Duration(milliseconds: 200), - child: ref.watch(isPlayerActiveProvider) + child: ref.watch(playerStatusProvider).isPlaying() ? const SizedBox(height: playerMinHeight + 8) : const SizedBox.shrink(), ); diff --git a/lib/features/player/view/player_expanded.dart b/lib/features/player/view/player_expanded.dart index 9775064..6067529 100644 --- a/lib/features/player/view/player_expanded.dart +++ b/lib/features/player/view/player_expanded.dart @@ -1,15 +1,14 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/constants/sizes.dart'; -import 'package:vaani/features/player/providers/session_provider.dart'; +import 'package:vaani/features/player/providers/audiobook_player.dart'; +import 'package:vaani/features/player/providers/currently_playing_provider.dart'; import 'package:vaani/features/player/view/widgets/player_player_pause_button.dart'; import 'package:vaani/features/player/view/widgets/player_progress_bar.dart'; -import 'package:vaani/features/player/view/widgets/player_skip_chapter_start_end.dart'; +import 'package:vaani/features/skip_start_end/view/skip_start_end_button.dart'; import 'package:vaani/features/sleep_timer/view/sleep_timer_button.dart'; -import 'package:vaani/shared/widgets/not_implemented.dart'; import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; import 'widgets/audiobook_player_seek_button.dart'; @@ -40,172 +39,162 @@ class PlayerExpanded extends HookConsumerWidget { final availWidth = MediaQuery.of(context).size.width; // the image width when the player is expanded final imageSize = min(playerMaxHeight * 0.5, availWidth * 0.9); - return Scaffold( - appBar: AppBar( - leading: IconButton( - iconSize: 30, - icon: const Icon(Icons.keyboard_arrow_down), - onPressed: () => context.pop(), + return Column( + children: [ + // sized box for system status bar; not needed as not full screen + SizedBox( + height: MediaQuery.of(context).padding.top, ), - actions: [ - IconButton( - icon: const Icon(Icons.cast), - onPressed: () { - showNotImplementedToast(context); - }, - ), - ], - ), - body: Column( - children: [ - // sized box for system status bar; not needed as not full screen - SizedBox( - height: MediaQuery.of(context).padding.top, - ), - // the image - Padding( - padding: EdgeInsets.only(top: AppElementSizes.paddingLarge), - child: Align( - alignment: Alignment.center, - // add a shadow to the image elevation hovering effect - child: Container( - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - color: Theme.of(context) - .colorScheme - .primary - .withValues(alpha: 0.1), - blurRadius: 32, - spreadRadius: 8, - ), - ], - ), - child: SizedBox( - height: imageSize, - child: InkWell( - onTap: () {}, - child: ClipRRect( - borderRadius: BorderRadius.circular( - AppElementSizes.borderRadiusRegular, - ), - child: BookCoverWidget(), - ), + // the image + Padding( + padding: EdgeInsets.only(top: AppElementSizes.paddingLarge), + child: Align( + alignment: Alignment.center, + // add a shadow to the image elevation hovering effect + child: PlayerExpandedImage(imageSize), + ), + ), + + // the chapter title + Expanded( + child: Padding( + padding: EdgeInsets.only(top: AppElementSizes.paddingRegular), + child: currentChapter == null + ? const SizedBox() + : Text( + currentChapter.title, + style: Theme.of(context).textTheme.titleLarge, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - ), - ), + ), + ), + + // the book name and author + Expanded( + child: Padding( + padding: EdgeInsets.only(bottom: AppElementSizes.paddingRegular), + child: Text( + [ + session.displayTitle, + session.displayAuthor, + ].join(' - '), + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.7), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), + ), - // the chapter title - Expanded( - child: Padding( - padding: EdgeInsets.only(top: AppElementSizes.paddingRegular), - child: currentChapter == null - ? const SizedBox() - : Text( - currentChapter.title, - style: Theme.of(context).textTheme.titleLarge, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ), - - // the book name and author - Expanded( - child: Padding( - padding: EdgeInsets.only(bottom: AppElementSizes.paddingRegular), - child: Text( - [ - session.displayTitle, - session.displayAuthor, - ].join(' - '), - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurface - .withValues(alpha: 0.7), - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ), - - // the progress bar - Expanded( - child: SizedBox( - width: imageSize, - child: Padding( - padding: EdgeInsets.only( - left: AppElementSizes.paddingRegular, - right: AppElementSizes.paddingRegular, - ), - child: const AudiobookChapterProgressBar(), - ), - ), - ), - - SizedBox( + // the progress bar + Expanded( + child: SizedBox( width: imageSize, child: Padding( padding: EdgeInsets.only( left: AppElementSizes.paddingRegular, right: AppElementSizes.paddingRegular, ), - child: const AudiobookProgressBar(), + child: const AudiobookChapterProgressBar(), ), ), + ), - // the chapter skip buttons, seek 30 seconds back and forward, and play/pause button - Expanded( - flex: 2, - child: SizedBox( - width: imageSize, - height: AppElementSizes.iconSizeRegular, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // previous chapter - const AudiobookPlayerSeekChapterButton(isForward: false), - // buttonSkipBackwards - const AudiobookPlayerSeekButton(isForward: false), - AudiobookPlayerPlayPauseButton(), - // buttonSkipForwards - const AudiobookPlayerSeekButton(isForward: true), - // next chapter - const AudiobookPlayerSeekChapterButton(isForward: true), - ], - ), + SizedBox( + width: imageSize, + child: Padding( + padding: EdgeInsets.only( + left: AppElementSizes.paddingRegular, + right: AppElementSizes.paddingRegular, + ), + child: const AudiobookProgressBar(), + ), + ), + + // the chapter skip buttons, seek 30 seconds back and forward, and play/pause button + Expanded( + flex: 2, + child: SizedBox( + width: imageSize, + height: AppElementSizes.iconSizeRegular, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // previous chapter + const AudiobookPlayerSeekChapterButton(isForward: false), + // buttonSkipBackwards + const AudiobookPlayerSeekButton(isForward: false), + AudiobookPlayerPlayPauseButton(), + // buttonSkipForwards + const AudiobookPlayerSeekButton(isForward: true), + // next chapter + const AudiobookPlayerSeekChapterButton(isForward: true), + ], ), ), + ), - // speed control, sleep timer, chapter list, and settings - Expanded( - child: SizedBox( - width: imageSize, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - // speed control - const PlayerSpeedAdjustButton(), - const Spacer(), - // sleep timer - const SleepTimerButton(), - const Spacer(), - // chapter list - const ChapterSelectionButton(), - const Spacer(), - // 跳过片头片尾 - SkipChapterStartEndButton(), - ], - ), + // speed control, sleep timer, chapter list, and settings + Expanded( + child: SizedBox( + width: imageSize, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // speed control + const PlayerSpeedAdjustButton(), + const Spacer(), + // sleep timer + const SleepTimerButton(), + const Spacer(), + // chapter list + const ChapterSelectionButton(), + const Spacer(), + // 跳过片头片尾 + SkipChapterStartEndButton(), + ], ), ), + ), + ], + ); + } +} + +class PlayerExpandedImage extends StatelessWidget { + final double imageSize; + + const PlayerExpandedImage(this.imageSize, {super.key}); + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), + blurRadius: 32, + spreadRadius: 8, + ), ], ), + child: SizedBox( + height: imageSize, + child: InkWell( + onTap: () {}, + child: ClipRRect( + borderRadius: BorderRadius.circular( + AppElementSizes.borderRadiusRegular, + ), + child: BookCoverWidget(), + ), + ), + ), ); } } diff --git a/lib/features/player/view/player_expanded_desktop.dart b/lib/features/player/view/player_expanded_desktop.dart new file mode 100644 index 0000000..06bf6e7 --- /dev/null +++ b/lib/features/player/view/player_expanded_desktop.dart @@ -0,0 +1,174 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:vaani/constants/sizes.dart'; +import 'package:vaani/features/player/providers/audiobook_player.dart'; +import 'package:vaani/features/player/providers/currently_playing_provider.dart'; +import 'package:vaani/features/player/view/player_expanded.dart' + show PlayerExpandedImage; +import 'package:vaani/features/player/view/player_minimized.dart'; +import 'package:vaani/features/player/view/widgets/audiobook_player_seek_button.dart'; +import 'package:vaani/features/player/view/widgets/audiobook_player_seek_chapter_button.dart'; +import 'package:vaani/features/player/view/widgets/player_player_pause_button.dart'; +import 'package:vaani/features/player/view/widgets/player_speed_adjust_button.dart'; +import 'package:vaani/features/skip_start_end/view/skip_start_end_button.dart'; +import 'package:vaani/features/sleep_timer/view/sleep_timer_button.dart'; +import 'package:vaani/globals.dart'; +import 'package:vaani/shared/extensions/chapter.dart'; +import 'package:vaani/shared/extensions/duration_format.dart'; + +var pendingPlayerModals = 0; + +class PlayerExpandedDesktop extends HookConsumerWidget { + const PlayerExpandedDesktop({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final session = ref.watch(sessionProvider); + if (session == null) { + return SizedBox.shrink(); + } + + /// all the properties that help in building the widget are calculated from the [percentageExpandedPlayer] + /// however, some properties need to start later than 0% and end before 100% + final currentChapter = ref.watch(currentChapterProvider); + // final currentBookMetadata = ref.watch(currentBookMetadataProvider); + // max height of the player is the height of the screen + final playerMaxHeight = MediaQuery.of(context).size.height; + final availWidth = MediaQuery.of(context).size.width; + // the image width when the player is expanded + final imageSize = min(playerMaxHeight * 0.5, availWidth * 0.9); + + return Stack( + alignment: Alignment.bottomCenter, + children: [ + Scaffold( + body: Padding( + padding: EdgeInsets.only( + top: AppElementSizes.paddingLarge, + bottom: playerMinHeight + 40, + ), + child: Row( + children: [ + Expanded( + flex: 45, + child: Column( + children: [ + Align( + alignment: Alignment.topCenter, + // add a shadow to the image elevation hovering effect + child: PlayerExpandedImage(imageSize), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // previous chapter + const AudiobookPlayerSeekChapterButton( + isForward: false), + // buttonSkipBackwards + const AudiobookPlayerSeekButton(isForward: false), + AudiobookPlayerPlayPauseButton(), + // buttonSkipForwards + const AudiobookPlayerSeekButton(isForward: true), + // next chapter + const AudiobookPlayerSeekChapterButton( + isForward: true), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // speed control + const PlayerSpeedAdjustButton(), + const Spacer(), + // sleep timer + const SleepTimerButton(), + const Spacer(), + // 跳过片头片尾 + SkipChapterStartEndButton(), + ], + ), + ], + ), + ), + Expanded( + flex: 65, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + currentChapter == null + ? SizedBox.shrink() + : Text( + currentChapter.title, + style: Theme.of(context).textTheme.titleLarge, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Expanded( + child: ChapterSelection(), + ), + ], + ), + ), + ], + ), + ), + ), + Hero(tag: 'player_hero', child: const PlayerMinimized()), + ], + ); + } +} + +class ChapterSelection extends HookConsumerWidget { + const ChapterSelection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final currentChapter = ref.watch(currentChapterProvider); + if (currentChapter == null) { + return SizedBox.shrink(); + } + + final currentChapters = ref.watch(currentChaptersProvider); + final currentChapterIndex = currentChapters.indexOf(currentChapter); + final theme = Theme.of(context); + return Scrollbar( + child: ListView.builder( + itemCount: currentChapters.length, + itemBuilder: (context, index) { + final chapter = currentChapters[index]; + final isCurrent = currentChapterIndex == index; + final isPlayed = index < currentChapterIndex; + return ListTile( + autofocus: isCurrent, + iconColor: isPlayed && !isCurrent ? theme.disabledColor : null, + title: Text( + chapter.title, + style: isPlayed && !isCurrent + ? TextStyle(color: theme.disabledColor) + : null, + ), + subtitle: Text( + '(${chapter.duration.smartBinaryFormat})', + style: isPlayed && !isCurrent + ? TextStyle(color: theme.disabledColor) + : null, + ), + // trailing: isCurrent + // ? const PlayingIndicatorIcon() + // : const Icon(Icons.play_arrow), + selected: isCurrent, + // key: isCurrent ? chapterKey : null, + onTap: () { + ref.read(playerProvider).skipToChapter(chapter.id); + }, + ); + }, + ), + ); + } +} diff --git a/lib/features/player/view/player_minimized.dart b/lib/features/player/view/player_minimized.dart index 2824e26..63f8d8c 100644 --- a/lib/features/player/view/player_minimized.dart +++ b/lib/features/player/view/player_minimized.dart @@ -4,7 +4,8 @@ import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/constants/sizes.dart'; -import 'package:vaani/features/player/providers/session_provider.dart'; +import 'package:vaani/features/player/providers/audiobook_player.dart'; +import 'package:vaani/features/player/providers/currently_playing_provider.dart'; import 'package:vaani/features/player/view/widgets/player_player_pause_button.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; @@ -28,7 +29,7 @@ class PlayerMinimized extends HookConsumerWidget { // image Padding( padding: EdgeInsets.all(AppElementSizes.paddingSmall), - child: InkWell( + child: GestureDetector( onTap: () { // navigate to item page context.pushNamed( @@ -114,7 +115,13 @@ class PlayerMinimizedFramework extends HookConsumerWidget { final progress = useStream(player.positionStreamInChapter, initialData: Duration.zero); return GestureDetector( - onTap: () => context.pushNamed(Routes.player.name), + onTap: () { + if (GoRouterState.of(context).topRoute?.name != Routes.player.name) { + context.pushNamed(Routes.player.name); + } else { + context.pop(); + } + }, child: Container( height: playerMinimizedHeight, color: Theme.of(context).colorScheme.surface, diff --git a/lib/features/player/view/player_when_expanded.dart b/lib/features/player/view/player_when_expanded.dart deleted file mode 100644 index 2e021b7..0000000 --- a/lib/features/player/view/player_when_expanded.dart +++ /dev/null @@ -1,293 +0,0 @@ -// import 'package:flutter/material.dart'; -// import 'package:hooks_riverpod/hooks_riverpod.dart'; -// import 'package:miniplayer/miniplayer.dart'; -// import 'package:vaani/constants/sizes.dart'; -// import 'package:vaani/features/player/providers/currently_playing_provider.dart'; -// import 'package:vaani/features/player/providers/player_form.dart'; -// import 'package:vaani/features/player/view/audiobook_player.dart'; -// import 'package:vaani/features/player/view/widgets/player_progress_bar.dart'; -// import 'package:vaani/features/skip_start_end/player_skip_chapter_start_end.dart'; -// import 'package:vaani/features/sleep_timer/view/sleep_timer_button.dart'; -// import 'package:vaani/shared/extensions/inverse_lerp.dart'; -// import 'package:vaani/shared/widgets/not_implemented.dart'; - -// import 'widgets/audiobook_player_seek_button.dart'; -// import 'widgets/audiobook_player_seek_chapter_button.dart'; -// import 'widgets/chapter_selection_button.dart'; -// import 'widgets/player_speed_adjust_button.dart'; - -// var pendingPlayerModals = 0; - -// class PlayerWhenExpanded extends HookConsumerWidget { -// const PlayerWhenExpanded({ -// super.key, -// required this.imageSize, -// required this.img, -// required this.percentageExpandedPlayer, -// required this.playPauseController, -// }); - -// /// padding values control the position of the image -// final double imageSize; -// final Widget img; -// final double percentageExpandedPlayer; -// final AnimationController playPauseController; - -// @override -// Widget build(BuildContext context, WidgetRef ref) { -// /// all the properties that help in building the widget are calculated from the [percentageExpandedPlayer] -// /// however, some properties need to start later than 0% and end before 100% -// const lateStart = 0.4; -// const earlyEnd = 1; -// final earlyPercentage = percentageExpandedPlayer -// .inverseLerp( -// lateStart, -// earlyEnd, -// ) -// .clamp(0.0, 1.0); -// final currentChapter = ref.watch(currentPlayingChapterProvider); -// final currentBookMetadata = ref.watch(currentBookMetadataProvider); - -// return Column( -// children: [ -// // sized box for system status bar; not needed as not full screen -// SizedBox( -// height: MediaQuery.of(context).padding.top * earlyPercentage, -// ), - -// // a row with a down arrow to minimize the player, a pill shaped container to drag the player, and a cast button -// ConstrainedBox( -// constraints: BoxConstraints( -// maxHeight: 100 * earlyPercentage, -// ), -// child: Opacity( -// opacity: earlyPercentage, -// child: Padding( -// padding: EdgeInsets.only(top: 8.0 * earlyPercentage), -// child: Row( -// crossAxisAlignment: CrossAxisAlignment.center, -// mainAxisSize: MainAxisSize.max, -// mainAxisAlignment: MainAxisAlignment.spaceBetween, -// children: [ -// // the down arrow -// IconButton( -// iconSize: 30, -// icon: const Icon(Icons.keyboard_arrow_down), -// onPressed: () { -// // minimize the player -// audioBookMiniplayerController.animateToHeight( -// state: PanelState.MIN, -// ); -// }, -// ), - -// // the cast button -// IconButton( -// icon: const Icon(Icons.cast), -// onPressed: () { -// showNotImplementedToast(context); -// }, -// ), -// ], -// ), -// ), -// ), -// ), - -// // the image -// Padding( -// padding: EdgeInsets.only( -// top: AppElementSizes.paddingLarge * earlyPercentage, -// ), -// child: Align( -// alignment: Alignment.center, -// // add a shadow to the image elevation hovering effect -// child: Container( -// decoration: BoxDecoration( -// boxShadow: [ -// BoxShadow( -// color: Theme.of(context) -// .colorScheme -// .primary -// .withValues(alpha: 0.1), -// blurRadius: 32 * earlyPercentage, -// spreadRadius: 8 * earlyPercentage, -// // offset: Offset(0, 16 * earlyPercentage), -// ), -// ], -// ), -// child: SizedBox( -// height: imageSize, -// child: InkWell( -// onTap: () {}, -// child: ClipRRect( -// borderRadius: BorderRadius.circular( -// AppElementSizes.borderRadiusRegular * earlyPercentage, -// ), -// child: img, -// ), -// ), -// ), -// ), -// ), -// ), - -// // the chapter title -// Expanded( -// child: Opacity( -// opacity: earlyPercentage, -// child: Padding( -// padding: EdgeInsets.only( -// top: AppElementSizes.paddingRegular * earlyPercentage, -// // horizontal: 16.0, -// ), -// // child: SizedBox( -// // same as the image width -// // width: imageSize, -// child: currentChapter == null -// ? const SizedBox() -// : Text( -// currentChapter.title, -// style: Theme.of(context).textTheme.titleLarge, -// maxLines: 1, -// overflow: TextOverflow.ellipsis, -// ), -// // ), -// ), -// ), -// ), - -// // the book name and author -// Expanded( -// child: Opacity( -// opacity: earlyPercentage, -// child: Padding( -// padding: EdgeInsets.only( -// bottom: AppElementSizes.paddingRegular * earlyPercentage, -// // horizontal: 16.0, -// ), -// // child: SizedBox( -// // same as the image width -// // width: imageSize, -// child: Text( -// [ -// currentBookMetadata?.title ?? '', -// currentBookMetadata?.authorName ?? '', -// ].join(' - '), -// style: Theme.of(context).textTheme.titleMedium?.copyWith( -// color: Theme.of(context) -// .colorScheme -// .onSurface -// .withValues(alpha: 0.7), -// ), -// maxLines: 1, -// overflow: TextOverflow.ellipsis, -// ), -// // ), -// ), -// ), -// ), - -// // the progress bar -// Expanded( -// child: Opacity( -// opacity: earlyPercentage, -// child: SizedBox( -// width: imageSize, -// child: Padding( -// padding: EdgeInsets.only( -// // top: AppElementSizes.paddingRegular * earlyPercentage, -// left: AppElementSizes.paddingRegular * earlyPercentage, -// right: AppElementSizes.paddingRegular * earlyPercentage, -// ), -// child: const AudiobookChapterProgressBar(), -// ), -// ), -// ), -// ), - -// Expanded( -// child: Opacity( -// opacity: earlyPercentage, -// child: SizedBox( -// width: imageSize, -// child: Padding( -// padding: EdgeInsets.only( -// // top: AppElementSizes.paddingRegular * earlyPercentage, -// left: AppElementSizes.paddingRegular * earlyPercentage, -// right: AppElementSizes.paddingRegular * earlyPercentage, -// ), -// child: const AudiobookProgressBar(), -// ), -// ), -// ), -// ), - -// // the chapter skip buttons, seek 30 seconds back and forward, and play/pause button -// Expanded( -// flex: 2, -// child: Opacity( -// opacity: earlyPercentage, -// child: SizedBox( -// width: imageSize, -// height: AppElementSizes.iconSizeRegular, -// child: Row( -// mainAxisAlignment: MainAxisAlignment.spaceBetween, -// children: [ -// // previous chapter -// const AudiobookPlayerSeekChapterButton(isForward: false), -// // buttonSkipBackwards -// const AudiobookPlayerSeekButton(isForward: false), -// AudiobookPlayerPlayPauseButton( -// playPauseController: playPauseController, -// ), -// // buttonSkipForwards -// const AudiobookPlayerSeekButton(isForward: true), -// // next chapter -// const AudiobookPlayerSeekChapterButton(isForward: true), -// ], -// ), -// ), -// ), -// ), - -// // speed control, sleep timer, chapter list, and settings -// Expanded( -// child: Opacity( -// opacity: earlyPercentage, -// child: SizedBox( -// // padding: EdgeInsets.only( -// // bottom: AppElementSizes.paddingRegular * 4 * earlyPercentage, -// // ), -// width: imageSize, -// child: Row( -// mainAxisAlignment: MainAxisAlignment.spaceEvenly, -// children: [ -// // speed control -// const PlayerSpeedAdjustButton(), -// const Spacer(), -// // sleep timer -// const SleepTimerButton(), -// const Spacer(), -// // chapter list -// const ChapterSelectionButton(), -// const Spacer(), -// // 跳过片头片尾 -// SkipChapterStartEndButton(), -// // settings -// // IconButton( -// // icon: const Icon(Icons.more_horiz), -// // onPressed: () { -// // // show toast -// // showNotImplementedToast(context); -// // }, -// // ), -// ], -// ), -// ), -// ), -// ), -// ], -// ); -// } -// } diff --git a/lib/features/player/view/player_when_minimized.dart b/lib/features/player/view/player_when_minimized.dart deleted file mode 100644 index e090a6a..0000000 --- a/lib/features/player/view/player_when_minimized.dart +++ /dev/null @@ -1,155 +0,0 @@ -// import 'package:flutter/material.dart'; -// import 'package:flutter_hooks/flutter_hooks.dart'; -// import 'package:go_router/go_router.dart'; -// import 'package:hooks_riverpod/hooks_riverpod.dart'; -// import 'package:vaani/constants/sizes.dart'; -// import 'package:vaani/features/player/providers/audiobook_player.dart'; -// import 'package:vaani/features/player/providers/currently_playing_provider.dart'; -// import 'package:vaani/features/player/view/audiobook_player.dart'; -// import 'package:vaani/router/router.dart'; - -// class PlayerWhenMinimized extends HookConsumerWidget { -// const PlayerWhenMinimized({ -// super.key, -// required this.availWidth, -// required this.maxImgSize, -// required this.imgWidget, -// required this.playPauseController, -// required this.percentageMiniplayer, -// }); - -// final double availWidth; -// final double maxImgSize; -// final Widget imgWidget; -// final AnimationController playPauseController; - -// /// 0 - 1, from minimized to when switched to expanded player -// /// -// /// by the time 1 is reached only image should be visible in the center of the widget -// final double percentageMiniplayer; - -// @override -// Widget build(BuildContext context, WidgetRef ref) { -// final player = ref.watch(audiobookPlayerProvider); -// final currentChapter = ref.watch(currentPlayingChapterProvider); - -// final vanishingPercentage = 1 - percentageMiniplayer; -// // final progress = -// // useStream(player.slowPositionStreamInBook, initialData: Duration.zero); -// final progress = -// useStream(player.positionStream, initialData: Duration.zero); - -// final bookMetaExpanded = ref.watch(currentBookMetadataProvider); - -// var barHeight = vanishingPercentage * 3; - -// return Stack( -// alignment: Alignment.topCenter, -// children: [ -// Row( -// children: [ -// // image -// Padding( -// padding: EdgeInsets.only( -// left: ((availWidth - maxImgSize) / 2) * percentageMiniplayer, -// ), -// child: InkWell( -// onTap: () { -// // navigate to item page -// context.pushNamed( -// Routes.libraryItem.name, -// pathParameters: { -// Routes.libraryItem.pathParamName!: -// player.book!.libraryItemId, -// }, -// ); -// }, -// child: ConstrainedBox( -// constraints: BoxConstraints( -// maxWidth: maxImgSize, -// ), -// child: imgWidget, -// ), -// ), -// ), -// // author and title of the book -// Expanded( -// child: Padding( -// padding: const EdgeInsets.only(left: 8), -// child: Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// mainAxisAlignment: MainAxisAlignment.center, -// mainAxisSize: MainAxisSize.min, -// children: [ -// // AutoScrollText( -// Text( -// '${bookMetaExpanded?.title ?? ''} - ${currentChapter?.title ?? ''}', -// maxLines: 1, overflow: TextOverflow.ellipsis, -// // velocity: -// // const Velocity(pixelsPerSecond: Offset(16, 0)), -// style: Theme.of(context).textTheme.bodyLarge, -// ), -// Text( -// bookMetaExpanded?.authorName ?? '', -// maxLines: 1, -// overflow: TextOverflow.ellipsis, -// style: Theme.of(context).textTheme.bodyMedium!.copyWith( -// color: Theme.of(context) -// .colorScheme -// .onSurface -// .withValues(alpha: 0.7), -// ), -// ), -// ], -// ), -// ), -// ), -// // IconButton( -// // icon: const Icon(Icons.fullscreen), -// // onPressed: () { -// // controller.animateToHeight(state: PanelState.MAX); -// // }, -// // ), - -// // rewind button -// Opacity( -// opacity: vanishingPercentage, -// child: Padding( -// padding: const EdgeInsets.only(left: 8), -// child: IconButton( -// icon: const Icon( -// Icons.replay_30, -// size: AppElementSizes.iconSizeSmall, -// ), -// onPressed: () {}, -// ), -// ), -// ), - -// // play/pause button -// Opacity( -// opacity: vanishingPercentage, -// child: Padding( -// padding: const EdgeInsets.only(right: 8), -// child: AudiobookPlayerPlayPauseButton( -// playPauseController: playPauseController, -// ), -// ), -// ), -// ], -// ), -// SizedBox( -// height: barHeight, -// child: LinearProgressIndicator( -// // value: (progress.data ?? Duration.zero).inSeconds / -// // player.book!.duration.inSeconds, -// value: (progress.data ?? Duration.zero).inSeconds / -// (player.duration?.inSeconds ?? 1), -// color: Theme.of(context).colorScheme.onPrimaryContainer, -// backgroundColor: Theme.of(context).colorScheme.primaryContainer, -// ), -// ), -// ], -// ); -// } -// } diff --git a/lib/features/player/view/widgets/audiobook_player_seek_button.dart b/lib/features/player/view/widgets/audiobook_player_seek_button.dart index 152fa90..43d4ffa 100644 --- a/lib/features/player/view/widgets/audiobook_player_seek_button.dart +++ b/lib/features/player/view/widgets/audiobook_player_seek_button.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/constants/sizes.dart'; -import 'package:vaani/features/player/providers/session_provider.dart'; +import 'package:vaani/features/player/providers/audiobook_player.dart'; class AudiobookPlayerSeekButton extends HookConsumerWidget { const AudiobookPlayerSeekButton({ diff --git a/lib/features/player/view/widgets/audiobook_player_seek_chapter_button.dart b/lib/features/player/view/widgets/audiobook_player_seek_chapter_button.dart index 282c7ce..dd1fa72 100644 --- a/lib/features/player/view/widgets/audiobook_player_seek_chapter_button.dart +++ b/lib/features/player/view/widgets/audiobook_player_seek_chapter_button.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/constants/sizes.dart'; -import 'package:vaani/features/player/providers/session_provider.dart'; +import 'package:vaani/features/player/providers/audiobook_player.dart'; class AudiobookPlayerSeekChapterButton extends HookConsumerWidget { const AudiobookPlayerSeekChapterButton({ diff --git a/lib/features/player/view/widgets/chapter_selection_button.dart b/lib/features/player/view/widgets/chapter_selection_button.dart index 988b020..8b31ea5 100644 --- a/lib/features/player/view/widgets/chapter_selection_button.dart +++ b/lib/features/player/view/widgets/chapter_selection_button.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:vaani/features/player/providers/session_provider.dart'; +import 'package:vaani/features/player/providers/audiobook_player.dart'; +import 'package:vaani/features/player/providers/currently_playing_provider.dart'; import 'package:vaani/features/player/view/player_expanded.dart' show pendingPlayerModals; import 'package:vaani/features/player/view/widgets/playing_indicator_icon.dart'; diff --git a/lib/features/player/view/widgets/player_player_pause_button.dart b/lib/features/player/view/widgets/player_player_pause_button.dart index cdc6b02..e850bb2 100644 --- a/lib/features/player/view/widgets/player_player_pause_button.dart +++ b/lib/features/player/view/widgets/player_player_pause_button.dart @@ -3,7 +3,7 @@ import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/features/player/core/player_status.dart'; import 'package:vaani/features/player/providers/player_status_provider.dart'; -import 'package:vaani/features/player/providers/session_provider.dart'; +import 'package:vaani/features/player/providers/audiobook_player.dart'; class AudiobookPlayerPlayPauseButton extends HookConsumerWidget { const AudiobookPlayerPlayPauseButton({ diff --git a/lib/features/player/view/widgets/player_progress_bar.dart b/lib/features/player/view/widgets/player_progress_bar.dart index 1b35bc2..0981e72 100644 --- a/lib/features/player/view/widgets/player_progress_bar.dart +++ b/lib/features/player/view/widgets/player_progress_bar.dart @@ -3,7 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/constants/sizes.dart'; -import 'package:vaani/features/player/providers/session_provider.dart'; +import 'package:vaani/features/player/providers/audiobook_player.dart'; +import 'package:vaani/features/player/providers/currently_playing_provider.dart'; class AudiobookChapterProgressBar extends HookConsumerWidget { const AudiobookChapterProgressBar({ diff --git a/lib/features/player/view/widgets/player_speed_adjust_button.dart b/lib/features/player/view/widgets/player_speed_adjust_button.dart index 963ddd2..66274f9 100644 --- a/lib/features/player/view/widgets/player_speed_adjust_button.dart +++ b/lib/features/player/view/widgets/player_speed_adjust_button.dart @@ -5,7 +5,7 @@ import 'package:vaani/features/per_book_settings/providers/book_settings_provide import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/features/player/view/player_expanded.dart'; import 'package:vaani/features/player/view/widgets/speed_selector.dart'; -import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/features/settings/app_settings_provider.dart'; final _logger = Logger('PlayerSpeedAdjustButton'); @@ -16,13 +16,12 @@ class PlayerSpeedAdjustButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final player = ref.watch(audiobookPlayerProvider); - final bookId = player.book?.libraryItemId ?? '_'; + final player = ref.watch(playerProvider); + final bookId = player.session?.libraryItemId ?? '_'; final bookSettings = ref.watch(bookSettingsProvider(bookId)); final appSettings = ref.watch(appSettingsProvider); - final notifier = ref.watch(audiobookPlayerProvider.notifier); return TextButton( - child: Text('${player.speed}x'), + child: Text('${player.player.speed}x'), onPressed: () async { pendingPlayerModals++; _logger.fine('opening speed selector'); @@ -32,7 +31,7 @@ class PlayerSpeedAdjustButton extends HookConsumerWidget { builder: (context) { return SpeedSelector( onSpeedSelected: (speed) { - notifier.setSpeed(speed); + player.setSpeed(speed); if (appSettings.playerSettings.configurePlayerForEveryBook) { ref .read( diff --git a/lib/features/player/view/widgets/speed_selector.dart b/lib/features/player/view/widgets/speed_selector.dart index 7e6ad89..222a6b0 100644 --- a/lib/features/player/view/widgets/speed_selector.dart +++ b/lib/features/player/view/widgets/speed_selector.dart @@ -5,7 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:list_wheel_scroll_view_nls/list_wheel_scroll_view_nls.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; -import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/features/settings/app_settings_provider.dart'; const double itemExtent = 25; @@ -22,7 +22,7 @@ class SpeedSelector extends HookConsumerWidget { final appSettings = ref.watch(appSettingsProvider); final playerSettings = appSettings.playerSettings; final speeds = playerSettings.speedOptions; - final currentSpeed = ref.watch(audiobookPlayerProvider).speed; + final currentSpeed = ref.watch(playerProvider).player.speed; final speedState = useState(currentSpeed); // hook the onSpeedSelected function to the state diff --git a/lib/features/player/playlist.dart b/lib/features/playlist/playlist.dart similarity index 100% rename from lib/features/player/playlist.dart rename to lib/features/playlist/playlist.dart diff --git a/lib/features/player/playlist_provider.dart b/lib/features/playlist/playlist_provider.dart similarity index 86% rename from lib/features/player/playlist_provider.dart rename to lib/features/playlist/playlist_provider.dart index b8df4f4..fefbdfa 100644 --- a/lib/features/player/playlist_provider.dart +++ b/lib/features/playlist/playlist_provider.dart @@ -1,6 +1,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; -import 'package:vaani/features/player/playlist.dart'; +import 'package:vaani/features/playlist/playlist.dart'; part 'playlist_provider.g.dart'; diff --git a/lib/features/player/playlist_provider.g.dart b/lib/features/playlist/playlist_provider.g.dart similarity index 100% rename from lib/features/player/playlist_provider.g.dart rename to lib/features/playlist/playlist_provider.g.dart diff --git a/lib/settings/api_settings_provider.dart b/lib/features/settings/api_settings_provider.dart similarity index 95% rename from lib/settings/api_settings_provider.dart rename to lib/features/settings/api_settings_provider.dart index 91334f0..22bfd51 100644 --- a/lib/settings/api_settings_provider.dart +++ b/lib/features/settings/api_settings_provider.dart @@ -3,7 +3,7 @@ import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:vaani/db/available_boxes.dart'; -import 'package:vaani/settings/models/api_settings.dart' as model; +import 'package:vaani/features/settings/models/api_settings.dart' as model; import 'package:vaani/shared/extensions/obfuscation.dart'; part 'api_settings_provider.g.dart'; diff --git a/lib/settings/api_settings_provider.g.dart b/lib/features/settings/api_settings_provider.g.dart similarity index 100% rename from lib/settings/api_settings_provider.g.dart rename to lib/features/settings/api_settings_provider.g.dart diff --git a/lib/settings/app_settings_provider.dart b/lib/features/settings/app_settings_provider.dart similarity index 96% rename from lib/settings/app_settings_provider.dart rename to lib/features/settings/app_settings_provider.dart index b86e736..cceec68 100644 --- a/lib/settings/app_settings_provider.dart +++ b/lib/features/settings/app_settings_provider.dart @@ -3,7 +3,7 @@ import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:vaani/db/available_boxes.dart'; -import 'package:vaani/settings/models/app_settings.dart' as model; +import 'package:vaani/features/settings/models/app_settings.dart' as model; part 'app_settings_provider.g.dart'; diff --git a/lib/settings/app_settings_provider.g.dart b/lib/features/settings/app_settings_provider.g.dart similarity index 100% rename from lib/settings/app_settings_provider.g.dart rename to lib/features/settings/app_settings_provider.g.dart diff --git a/lib/settings/models/api_settings.dart b/lib/features/settings/models/api_settings.dart similarity index 80% rename from lib/settings/models/api_settings.dart rename to lib/features/settings/models/api_settings.dart index 6410938..f46c0e4 100644 --- a/lib/settings/models/api_settings.dart +++ b/lib/features/settings/models/api_settings.dart @@ -1,8 +1,8 @@ // a freezed class to store the settings of the app import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:vaani/settings/models/audiobookshelf_server.dart'; -import 'package:vaani/settings/models/authenticated_user.dart'; +import 'package:vaani/features/settings/models/audiobookshelf_server.dart'; +import 'package:vaani/features/settings/models/authenticated_user.dart'; part 'api_settings.freezed.dart'; part 'api_settings.g.dart'; diff --git a/lib/settings/models/api_settings.freezed.dart b/lib/features/settings/models/api_settings.freezed.dart similarity index 100% rename from lib/settings/models/api_settings.freezed.dart rename to lib/features/settings/models/api_settings.freezed.dart diff --git a/lib/settings/models/api_settings.g.dart b/lib/features/settings/models/api_settings.g.dart similarity index 100% rename from lib/settings/models/api_settings.g.dart rename to lib/features/settings/models/api_settings.g.dart diff --git a/lib/settings/models/app_settings.dart b/lib/features/settings/models/app_settings.dart similarity index 100% rename from lib/settings/models/app_settings.dart rename to lib/features/settings/models/app_settings.dart diff --git a/lib/settings/models/app_settings.freezed.dart b/lib/features/settings/models/app_settings.freezed.dart similarity index 100% rename from lib/settings/models/app_settings.freezed.dart rename to lib/features/settings/models/app_settings.freezed.dart diff --git a/lib/settings/models/app_settings.g.dart b/lib/features/settings/models/app_settings.g.dart similarity index 100% rename from lib/settings/models/app_settings.g.dart rename to lib/features/settings/models/app_settings.g.dart diff --git a/lib/settings/models/audiobookshelf_server.dart b/lib/features/settings/models/audiobookshelf_server.dart similarity index 100% rename from lib/settings/models/audiobookshelf_server.dart rename to lib/features/settings/models/audiobookshelf_server.dart diff --git a/lib/settings/models/audiobookshelf_server.freezed.dart b/lib/features/settings/models/audiobookshelf_server.freezed.dart similarity index 100% rename from lib/settings/models/audiobookshelf_server.freezed.dart rename to lib/features/settings/models/audiobookshelf_server.freezed.dart diff --git a/lib/settings/models/audiobookshelf_server.g.dart b/lib/features/settings/models/audiobookshelf_server.g.dart similarity index 100% rename from lib/settings/models/audiobookshelf_server.g.dart rename to lib/features/settings/models/audiobookshelf_server.g.dart diff --git a/lib/settings/models/authenticated_user.dart b/lib/features/settings/models/authenticated_user.dart similarity index 87% rename from lib/settings/models/authenticated_user.dart rename to lib/features/settings/models/authenticated_user.dart index 321c885..3337bfd 100644 --- a/lib/settings/models/authenticated_user.dart +++ b/lib/features/settings/models/authenticated_user.dart @@ -1,5 +1,5 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:vaani/settings/models/audiobookshelf_server.dart'; +import 'package:vaani/features/settings/models/audiobookshelf_server.dart'; part 'authenticated_user.freezed.dart'; part 'authenticated_user.g.dart'; diff --git a/lib/settings/models/authenticated_user.freezed.dart b/lib/features/settings/models/authenticated_user.freezed.dart similarity index 100% rename from lib/settings/models/authenticated_user.freezed.dart rename to lib/features/settings/models/authenticated_user.freezed.dart diff --git a/lib/settings/models/authenticated_user.g.dart b/lib/features/settings/models/authenticated_user.g.dart similarity index 100% rename from lib/settings/models/authenticated_user.g.dart rename to lib/features/settings/models/authenticated_user.g.dart diff --git a/lib/settings/models/models.dart b/lib/features/settings/models/models.dart similarity index 100% rename from lib/settings/models/models.dart rename to lib/features/settings/models/models.dart diff --git a/lib/settings/settings.dart b/lib/features/settings/settings.dart similarity index 100% rename from lib/settings/settings.dart rename to lib/features/settings/settings.dart diff --git a/lib/settings/view/app_settings_page.dart b/lib/features/settings/view/app_settings_page.dart similarity index 96% rename from lib/settings/view/app_settings_page.dart rename to lib/features/settings/view/app_settings_page.dart index 37c1cd5..844d3e7 100644 --- a/lib/settings/view/app_settings_page.dart +++ b/lib/features/settings/view/app_settings_page.dart @@ -8,11 +8,11 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/generated/l10n.dart'; import 'package:vaani/router/router.dart'; -import 'package:vaani/settings/app_settings_provider.dart'; -import 'package:vaani/settings/models/app_settings.dart' as model; -import 'package:vaani/settings/view/buttons.dart'; -import 'package:vaani/settings/view/simple_settings_page.dart'; -import 'package:vaani/settings/view/widgets/navigation_with_switch_tile.dart'; +import 'package:vaani/features/settings/app_settings_provider.dart'; +import 'package:vaani/features/settings/models/app_settings.dart' as model; +import 'package:vaani/features/settings/view/buttons.dart'; +import 'package:vaani/features/settings/view/simple_settings_page.dart'; +import 'package:vaani/features/settings/view/widgets/navigation_with_switch_tile.dart'; class AppSettingsPage extends HookConsumerWidget { const AppSettingsPage({ diff --git a/lib/settings/view/auto_sleep_timer_settings_page.dart b/lib/features/settings/view/auto_sleep_timer_settings_page.dart similarity index 97% rename from lib/settings/view/auto_sleep_timer_settings_page.dart rename to lib/features/settings/view/auto_sleep_timer_settings_page.dart index 8dd0179..f745d42 100644 --- a/lib/settings/view/auto_sleep_timer_settings_page.dart +++ b/lib/features/settings/view/auto_sleep_timer_settings_page.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/generated/l10n.dart'; -import 'package:vaani/settings/app_settings_provider.dart'; -import 'package:vaani/settings/view/simple_settings_page.dart'; +import 'package:vaani/features/settings/app_settings_provider.dart'; +import 'package:vaani/features/settings/view/simple_settings_page.dart'; import 'package:vaani/shared/extensions/time_of_day.dart'; class AutoSleepTimerSettingsPage extends HookConsumerWidget { diff --git a/lib/settings/view/buttons.dart b/lib/features/settings/view/buttons.dart similarity index 100% rename from lib/settings/view/buttons.dart rename to lib/features/settings/view/buttons.dart diff --git a/lib/settings/view/home_page_settings_page.dart b/lib/features/settings/view/home_page_settings_page.dart similarity index 96% rename from lib/settings/view/home_page_settings_page.dart rename to lib/features/settings/view/home_page_settings_page.dart index f6f24be..3f5adf7 100644 --- a/lib/settings/view/home_page_settings_page.dart +++ b/lib/features/settings/view/home_page_settings_page.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:vaani/generated/l10n.dart'; -import 'package:vaani/settings/app_settings_provider.dart'; -import 'package:vaani/settings/view/simple_settings_page.dart' +import 'package:vaani/features/settings/app_settings_provider.dart'; +import 'package:vaani/features/settings/view/simple_settings_page.dart' show SimpleSettingsPage; class HomePageSettingsPage extends HookConsumerWidget { diff --git a/lib/settings/view/notification_settings_page.dart b/lib/features/settings/view/notification_settings_page.dart similarity index 98% rename from lib/settings/view/notification_settings_page.dart rename to lib/features/settings/view/notification_settings_page.dart index 72dd566..b8e63af 100644 --- a/lib/settings/view/notification_settings_page.dart +++ b/lib/features/settings/view/notification_settings_page.dart @@ -3,10 +3,10 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/generated/l10n.dart'; -import 'package:vaani/settings/app_settings_provider.dart'; -import 'package:vaani/settings/models/app_settings.dart'; -import 'package:vaani/settings/view/buttons.dart'; -import 'package:vaani/settings/view/simple_settings_page.dart'; +import 'package:vaani/features/settings/app_settings_provider.dart'; +import 'package:vaani/features/settings/models/app_settings.dart'; +import 'package:vaani/features/settings/view/buttons.dart'; +import 'package:vaani/features/settings/view/simple_settings_page.dart'; import 'package:vaani/shared/extensions/enum.dart'; class NotificationSettingsPage extends HookConsumerWidget { diff --git a/lib/settings/view/player_settings_page.dart b/lib/features/settings/view/player_settings_page.dart similarity index 98% rename from lib/settings/view/player_settings_page.dart rename to lib/features/settings/view/player_settings_page.dart index e09dc5b..ed6709a 100644 --- a/lib/settings/view/player_settings_page.dart +++ b/lib/features/settings/view/player_settings_page.dart @@ -4,9 +4,9 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/generated/l10n.dart'; -import 'package:vaani/settings/app_settings_provider.dart'; -import 'package:vaani/settings/view/buttons.dart'; -import 'package:vaani/settings/view/simple_settings_page.dart'; +import 'package:vaani/features/settings/app_settings_provider.dart'; +import 'package:vaani/features/settings/view/buttons.dart'; +import 'package:vaani/features/settings/view/simple_settings_page.dart'; import 'package:vaani/shared/extensions/duration_format.dart'; class PlayerSettingsPage extends HookConsumerWidget { diff --git a/lib/settings/view/shake_detector_settings_page.dart b/lib/features/settings/view/shake_detector_settings_page.dart similarity index 97% rename from lib/settings/view/shake_detector_settings_page.dart rename to lib/features/settings/view/shake_detector_settings_page.dart index 32a54b9..4896485 100644 --- a/lib/settings/view/shake_detector_settings_page.dart +++ b/lib/features/settings/view/shake_detector_settings_page.dart @@ -3,10 +3,10 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/generated/l10n.dart'; -import 'package:vaani/settings/app_settings_provider.dart'; -import 'package:vaani/settings/models/app_settings.dart'; -import 'package:vaani/settings/view/buttons.dart'; -import 'package:vaani/settings/view/simple_settings_page.dart'; +import 'package:vaani/features/settings/app_settings_provider.dart'; +import 'package:vaani/features/settings/models/app_settings.dart'; +import 'package:vaani/features/settings/view/buttons.dart'; +import 'package:vaani/features/settings/view/simple_settings_page.dart'; import 'package:vaani/shared/extensions/enum.dart'; class ShakeDetectorSettingsPage extends HookConsumerWidget { diff --git a/lib/settings/view/simple_settings_page.dart b/lib/features/settings/view/simple_settings_page.dart similarity index 100% rename from lib/settings/view/simple_settings_page.dart rename to lib/features/settings/view/simple_settings_page.dart diff --git a/lib/settings/view/theme_settings_page.dart b/lib/features/settings/view/theme_settings_page.dart similarity index 98% rename from lib/settings/view/theme_settings_page.dart rename to lib/features/settings/view/theme_settings_page.dart index 9ef1fc2..82e7f48 100644 --- a/lib/settings/view/theme_settings_page.dart +++ b/lib/features/settings/view/theme_settings_page.dart @@ -4,8 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/generated/l10n.dart'; -import 'package:vaani/settings/app_settings_provider.dart'; -import 'package:vaani/settings/view/simple_settings_page.dart'; +import 'package:vaani/features/settings/app_settings_provider.dart'; +import 'package:vaani/features/settings/view/simple_settings_page.dart'; class ThemeSettingsPage extends HookConsumerWidget { const ThemeSettingsPage({ diff --git a/lib/settings/view/widgets/navigation_with_switch_tile.dart b/lib/features/settings/view/widgets/navigation_with_switch_tile.dart similarity index 100% rename from lib/settings/view/widgets/navigation_with_switch_tile.dart rename to lib/features/settings/view/widgets/navigation_with_switch_tile.dart diff --git a/lib/features/shake_detection/core/shake_detector.dart b/lib/features/shake_detector/shake_detector.dart similarity index 96% rename from lib/features/shake_detection/core/shake_detector.dart rename to lib/features/shake_detector/shake_detector.dart index 023a8ab..ee409ea 100644 --- a/lib/features/shake_detection/core/shake_detector.dart +++ b/lib/features/shake_detector/shake_detector.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:logging/logging.dart'; import 'package:sensors_plus/sensors_plus.dart'; -import 'package:vaani/settings/models/app_settings.dart'; +import 'package:vaani/features/settings/models/app_settings.dart'; final _logger = Logger('ShakeDetector'); diff --git a/lib/features/shake_detection/providers/shake_detector.dart b/lib/features/shake_detector/shake_detector_provider.dart similarity index 95% rename from lib/features/shake_detection/providers/shake_detector.dart rename to lib/features/shake_detector/shake_detector_provider.dart index 1c17a13..8a84c65 100644 --- a/lib/features/shake_detection/providers/shake_detector.dart +++ b/lib/features/shake_detector/shake_detector_provider.dart @@ -2,17 +2,17 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:just_audio/just_audio.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:vaani/features/player/providers/session_provider.dart'; +import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart' show sleepTimerProvider; -import 'package:vaani/settings/app_settings_provider.dart' +import 'package:vaani/features/settings/app_settings_provider.dart' show appSettingsProvider; -import 'package:vaani/settings/models/app_settings.dart'; +import 'package:vaani/features/settings/models/app_settings.dart'; import 'package:vibration/vibration.dart'; -import '../core/shake_detector.dart' as core; +import 'shake_detector.dart' as core; -part 'shake_detector.g.dart'; +part 'shake_detector_provider.g.dart'; Logger _logger = Logger('ShakeDetector'); diff --git a/lib/features/shake_detection/providers/shake_detector.g.dart b/lib/features/shake_detector/shake_detector_provider.g.dart similarity index 95% rename from lib/features/shake_detection/providers/shake_detector.g.dart rename to lib/features/shake_detector/shake_detector_provider.g.dart index d4fb7c6..a6934f5 100644 --- a/lib/features/shake_detection/providers/shake_detector.g.dart +++ b/lib/features/shake_detector/shake_detector_provider.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'shake_detector.dart'; +part of 'shake_detector_provider.dart'; // ************************************************************************** // RiverpodGenerator diff --git a/lib/features/skip_start_end/skip_start_end.dart b/lib/features/skip_start_end/core/skip_start_end.dart similarity index 95% rename from lib/features/skip_start_end/skip_start_end.dart rename to lib/features/skip_start_end/core/skip_start_end.dart index 465961a..c158923 100644 --- a/lib/features/skip_start_end/skip_start_end.dart +++ b/lib/features/skip_start_end/core/skip_start_end.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:vaani/features/player/core/audiobook_player_session.dart'; +import 'package:vaani/features/player/core/audiobook_player.dart'; import 'package:vaani/shared/extensions/chapter.dart'; import 'package:vaani/shared/utils/throttler.dart'; diff --git a/lib/features/skip_start_end/skip_start_end_provider.dart b/lib/features/skip_start_end/providers/skip_start_end_provider.dart similarity index 92% rename from lib/features/skip_start_end/skip_start_end_provider.dart rename to lib/features/skip_start_end/providers/skip_start_end_provider.dart index da7f9dc..93e143b 100644 --- a/lib/features/skip_start_end/skip_start_end_provider.dart +++ b/lib/features/skip_start_end/providers/skip_start_end_provider.dart @@ -1,7 +1,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart'; -import 'package:vaani/features/player/providers/session_provider.dart'; -import 'package:vaani/features/skip_start_end/skip_start_end.dart' as core; +import 'package:vaani/features/player/providers/audiobook_player.dart'; +import 'package:vaani/features/skip_start_end/core/skip_start_end.dart' as core; part 'skip_start_end_provider.g.dart'; diff --git a/lib/features/skip_start_end/skip_start_end_provider.g.dart b/lib/features/skip_start_end/providers/skip_start_end_provider.g.dart similarity index 100% rename from lib/features/skip_start_end/skip_start_end_provider.g.dart rename to lib/features/skip_start_end/providers/skip_start_end_provider.g.dart diff --git a/lib/features/player/view/widgets/player_skip_chapter_start_end.dart b/lib/features/skip_start_end/view/skip_start_end_button.dart similarity index 94% rename from lib/features/player/view/widgets/player_skip_chapter_start_end.dart rename to lib/features/skip_start_end/view/skip_start_end_button.dart index 943f27c..7d7236e 100644 --- a/lib/features/player/view/widgets/player_skip_chapter_start_end.dart +++ b/lib/features/skip_start_end/view/skip_start_end_button.dart @@ -3,10 +3,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:icons_plus/icons_plus.dart'; import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; -import 'package:vaani/features/player/providers/session_provider.dart'; import 'package:vaani/features/player/view/player_expanded.dart'; import 'package:vaani/generated/l10n.dart'; -import 'package:vaani/settings/view/notification_settings_page.dart'; +import 'package:vaani/features/settings/view/notification_settings_page.dart'; class SkipChapterStartEndButton extends HookConsumerWidget { const SkipChapterStartEndButton({super.key}); @@ -74,7 +73,6 @@ class PlayerSkipChapterStartEnd extends HookConsumerWidget { bookSettings.copyWith .playerSettings(skipChapterStart: interval), ); - ref.read(audiobookPlayerProvider).setClip(start: interval); }, ), ), diff --git a/lib/features/sleep_timer/core/sleep_timer.dart b/lib/features/sleep_timer/core/sleep_timer.dart index 2d9ea31..e57f0d2 100644 --- a/lib/features/sleep_timer/core/sleep_timer.dart +++ b/lib/features/sleep_timer/core/sleep_timer.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:just_audio/just_audio.dart'; import 'package:logging/logging.dart'; -import 'package:vaani/features/player/core/audiobook_player.dart'; /// this timer pauses the music player after a certain duration /// @@ -33,7 +32,7 @@ class SleepTimer { } /// The player to be paused - final AudiobookPlayer player; + final AudioPlayer player; /// The timer that will pause the player Timer? timer; diff --git a/lib/features/sleep_timer/providers/sleep_timer_provider.dart b/lib/features/sleep_timer/providers/sleep_timer_provider.dart index 5c9ac2d..3b84bc7 100644 --- a/lib/features/sleep_timer/providers/sleep_timer_provider.dart +++ b/lib/features/sleep_timer/providers/sleep_timer_provider.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/features/sleep_timer/core/sleep_timer.dart' as core; -import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/features/settings/app_settings_provider.dart'; import 'package:vaani/shared/extensions/time_of_day.dart'; part 'sleep_timer_provider.g.dart'; @@ -26,7 +26,7 @@ class SleepTimer extends _$SleepTimer { var sleepTimer = core.SleepTimer( duration: sleepTimerSettings.defaultDuration, - player: ref.watch(simpleAudiobookPlayerProvider), + player: ref.watch(playerProvider).player, ); ref.onDispose(sleepTimer.dispose); return sleepTimer; @@ -45,7 +45,7 @@ class SleepTimer extends _$SleepTimer { } else { final timer = core.SleepTimer( duration: resultingDuration, - player: ref.watch(simpleAudiobookPlayerProvider), + player: ref.watch(playerProvider).player, ); ref.onDispose(timer.dispose); state = timer; diff --git a/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart b/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart index 0b1db3f..a1471ba 100644 --- a/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart +++ b/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart @@ -6,7 +6,7 @@ part of 'sleep_timer_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$sleepTimerHash() => r'2679454a217d0630a833d730557ab4e4feac2e56'; +String _$sleepTimerHash() => r'daaaf63d599fb991e71a0da0ca1075fb46ccc6be'; /// See also [SleepTimer]. @ProviderFor(SleepTimer) diff --git a/lib/features/sleep_timer/view/sleep_timer_button.dart b/lib/features/sleep_timer/view/sleep_timer_button.dart index ed7a372..5523c29 100644 --- a/lib/features/sleep_timer/view/sleep_timer_button.dart +++ b/lib/features/sleep_timer/view/sleep_timer_button.dart @@ -8,7 +8,7 @@ import 'package:vaani/features/sleep_timer/core/sleep_timer.dart'; import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart' show sleepTimerProvider; import 'package:vaani/globals.dart'; -import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/features/settings/app_settings_provider.dart'; import 'package:vaani/shared/extensions/duration_format.dart'; class SleepTimerButton extends HookConsumerWidget { diff --git a/lib/features/you/view/server_manager.dart b/lib/features/you/view/server_manager.dart index e43cebe..c274a05 100644 --- a/lib/features/you/view/server_manager.dart +++ b/lib/features/you/view/server_manager.dart @@ -14,9 +14,9 @@ import 'package:vaani/features/player/view/mini_player_bottom_padding.dart' import 'package:vaani/generated/l10n.dart'; import 'package:vaani/globals.dart'; import 'package:vaani/router/router.dart' show Routes; -import 'package:vaani/settings/api_settings_provider.dart' +import 'package:vaani/features/settings/api_settings_provider.dart' show apiSettingsProvider; -import 'package:vaani/settings/models/models.dart' as model; +import 'package:vaani/features/settings/models/models.dart' as model; import 'package:vaani/shared/extensions/obfuscation.dart' show ObfuscateSet; import 'package:vaani/shared/widgets/add_new_server.dart' show AddNewServer; diff --git a/lib/features/you/view/widgets/library_switch_chip.dart b/lib/features/you/view/widgets/library_switch_chip.dart index 1da5cca..a525ba5 100644 --- a/lib/features/you/view/widgets/library_switch_chip.dart +++ b/lib/features/you/view/widgets/library_switch_chip.dart @@ -7,7 +7,7 @@ import 'package:shelfsdk/audiobookshelf_api.dart' show Library; import 'package:vaani/api/library_provider.dart'; import 'package:vaani/generated/l10n.dart'; import 'package:vaani/globals.dart'; -import 'package:vaani/settings/api_settings_provider.dart' +import 'package:vaani/features/settings/api_settings_provider.dart' show apiSettingsProvider; import 'package:vaani/shared/icons/abs_icons.dart'; diff --git a/lib/framework.dart b/lib/framework.dart index 3c08e2a..6b85c34 100644 --- a/lib/framework.dart +++ b/lib/framework.dart @@ -1,94 +1,24 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:tray_manager/tray_manager.dart'; import 'package:vaani/features/downloads/providers/download_manager.dart'; -import 'package:vaani/features/player/core/audiobook_player_session.dart'; -import 'package:vaani/features/player/providers/audiobook_player.dart'; -import 'package:vaani/features/player/providers/session_provider.dart'; -import 'package:vaani/features/shake_detection/providers/shake_detector.dart'; -import 'package:vaani/features/skip_start_end/skip_start_end_provider.dart'; +import 'package:vaani/features/playback_reporting/providers/playback_reporter_provider.dart'; +import 'package:vaani/features/skip_start_end/providers/skip_start_end_provider.dart'; +import 'package:vaani/features/shake_detector/shake_detector_provider.dart'; import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart'; import 'package:vaani/globals.dart'; -import 'package:vaani/shared/utils/utils.dart'; -import 'package:window_manager/window_manager.dart'; +import 'package:vaani/shared/utils/helper.dart'; -class Framework extends ConsumerStatefulWidget { +class Framework extends ConsumerWidget { final Widget child; - final AbsAudioHandler? audioHandler; - const Framework({required this.child, this.audioHandler, super.key}); + const Framework(this.child, {super.key}); @override - ConsumerState createState() => _FrameworkState(); -} - -class _FrameworkState extends ConsumerState - with TrayListener, WindowListener { - @override - void initState() { - if (Utils.isDesktop()) { - windowManager.addListener(this); - _init(); - } - super.initState(); - } - - @override - void dispose() { - trayManager.removeListener(this); - super.dispose(); - } - - void _init() async { - await trayManager.setIcon( - Utils.isWindows() ? 'assets/icon/logo.ico' : 'assets/icon/logo.png', - ); - await trayManager.setToolTip(appName); - Menu menu = Menu( - items: [ - MenuItem( - key: 'show_window', - // label: 'Show Window', - label: '显示主窗口', - onClick: (menuItem) => windowManager.show(), - ), - MenuItem.separator(), - MenuItem( - key: 'play_pause', - label: '播放/暂停', - onClick: (menuItem) => - ref.read(audiobookPlayerProvider).togglePlayPause(), - ), - MenuItem( - key: 'previous', - label: '上一个', - onClick: (menuItem) => - ref.read(audiobookPlayerProvider).seekToPrevious(), - ), - MenuItem( - key: 'next', - label: '下一个', - onClick: (menuItem) => ref.read(audiobookPlayerProvider).seekToNext(), - ), - MenuItem.separator(), - MenuItem( - key: 'exit_app', - // label: 'Exit App', - label: '退出', - onClick: (menuItem) => windowManager.destroy(), - ), - ], - ); - await trayManager.setContextMenu(menu); - trayManager.addListener(this); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { // Eagerly initialize providers by watching them. // By using "watch", the provider will stay alive and not be disposed. try { ref.watch(simpleDownloadManagerProvider); - if (Utils.isAndroid()) ref.watch(shakeDetectorProvider); + if (Helper.isAndroid()) ref.watch(shakeDetectorProvider); ref.watch(sleepTimerProvider); ref.watch(skipStartEndProvider); ref.watch(playbackReporterProvider); @@ -96,53 +26,6 @@ class _FrameworkState extends ConsumerState debugPrintStack(stackTrace: StackTrace.current, label: e.toString()); appLogger.severe(e.toString()); } - return widget.child; - } - - @override - void onTrayIconMouseDown() { - // do something, for example pop up the menu - // print('onTrayIconMouseDown'); - windowManager.show(); - } - - @override - void onTrayIconMouseUp() { - // do something, for example pop up the menu - // print('onTrayIconMouseUp'); - } - - @override - void onTrayIconRightMouseDown() { - // do something - // print('onTrayIconRightMouseDown'); - trayManager.popUpContextMenu(bringAppToFront: true); - } - - @override - void onTrayIconRightMouseUp() { - // do something - // print('onTrayIconRightMouseUp'); - } - - // @override - // void onTrayMenuItemClick(MenuItem menuItem) { - // print(menuItem.key); - // if (menuItem.key == 'show_window') { - // // do something - // } else if (menuItem.key == 'exit_app') { - // // do something - - // } else if (menuItem.key == 'play_pause'){ - - // } - // } - - @override - void onWindowClose() async { - final isPreventClose = await windowManager.isPreventClose(); - if (isPreventClose) { - windowManager.hide(); - } + return child; } } diff --git a/lib/globals.dart b/lib/globals.dart index 829c3d1..665d910 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -29,11 +29,12 @@ Future initialize() async { deviceSdkVersion = getDeviceSdkVersion(deviceData); deviceManufacturer = getDeviceManufacturer(deviceData); appLogger = Logger(appName); - final dir = await getApplicationDocumentsDirectory(); - appStorageDir = Directory( - p.join(dir.path, appName), + final dirDocuments = await getApplicationDocumentsDirectory(); + appDocumentsDir = Directory( + p.join(dirDocuments.path, appName), ); - await appStorageDir.create(recursive: true); + await appDocumentsDir.create(recursive: true); + appSupportDir = await getApplicationSupportDirectory(); } late Map deviceData; @@ -44,7 +45,16 @@ late String deviceSdkVersion; late String deviceManufacturer; late Logger appLogger; -late Directory appStorageDir; + +/// 文档目录 getApplicationDocumentsDirectory +/// 存放用户生成的、不可重建的持久化数据。在iOS端,此目录可能会通过iCloud自动同步。 +late Directory appDocumentsDir; + +/// 应用支持目录 getApplicationSupportDirectory +/// 存放应用程序的支持文件。在iOS上,此目录是持久性的,且会备份到iCloud。 +late Directory appSupportDir; + +const double playerMinHeight = 70; var routerConfig = const MyAppRouter().config; diff --git a/lib/main.dart b/lib/main.dart index b0e5ca0..adccd23 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,13 +9,14 @@ import 'package:vaani/api/server_provider.dart'; import 'package:vaani/db/storage.dart'; import 'package:vaani/features/logging/core/logger.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; -import 'package:vaani/features/player/providers/session_provider.dart'; import 'package:vaani/framework.dart'; import 'package:vaani/generated/l10n.dart'; import 'package:vaani/globals.dart'; +import 'package:vaani/shared/widgets/tray_manager.dart'; import 'package:vaani/router/router.dart'; -import 'package:vaani/settings/api_settings_provider.dart'; -import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/features/settings/api_settings_provider.dart'; +import 'package:vaani/features/settings/app_settings_provider.dart'; +import 'package:vaani/shared/utils/Helper.dart'; import 'package:vaani/theme/providers/system_theme_provider.dart'; import 'package:vaani/theme/providers/theme_from_cover_provider.dart'; import 'package:vaani/theme/theme.dart'; @@ -43,10 +44,9 @@ void main() async { runApp( UncontrolledProviderScope( container: container, - child: const Framework( - // audioHandler: , - child: AbsApp(), - ), + child: Helper.isDesktop() + ? Framework(TrayManager(AbsApp())) + : Framework(AbsApp()), ), ); } @@ -121,18 +121,18 @@ class AbsApp extends ConsumerWidget { if (themeSettings.useCurrentPlayerThemeThroughoutApp) { try { - final player = ref.watch(audiobookPlayerProvider); - if (player.book != null) { + final session = ref.watch(sessionProvider); + if (session != null) { final themeLight = ref.watch( themeOfLibraryItemProvider( - player.book!.libraryItemId, + session.libraryItemId, highContrast: shouldUseHighContrast, brightness: Brightness.light, ), ); final themeDark = ref.watch( themeOfLibraryItemProvider( - player.book!.libraryItemId, + session.libraryItemId, highContrast: shouldUseHighContrast, brightness: Brightness.dark, ), diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index f04ea70..856c725 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -6,8 +6,8 @@ import 'package:vaani/api/api_provider.dart'; import 'package:vaani/generated/l10n.dart'; import 'package:vaani/globals.dart'; import 'package:vaani/router/router.dart'; -import 'package:vaani/settings/api_settings_provider.dart'; -import 'package:vaani/settings/app_settings_provider.dart' +import 'package:vaani/features/settings/api_settings_provider.dart'; +import 'package:vaani/features/settings/app_settings_provider.dart' show appSettingsProvider; import '../shared/widgets/shelves/home_shelf.dart'; diff --git a/lib/pages/library_page.dart b/lib/pages/library_page.dart index 2506614..89c3780 100644 --- a/lib/pages/library_page.dart +++ b/lib/pages/library_page.dart @@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/api/api_provider.dart'; import 'package:vaani/globals.dart'; -import 'package:vaani/settings/api_settings_provider.dart'; +import 'package:vaani/features/settings/api_settings_provider.dart'; import '../shared/widgets/drawer.dart'; import '../shared/widgets/shelves/home_shelf.dart'; diff --git a/lib/pages/player_page.dart b/lib/pages/player_page.dart new file mode 100644 index 0000000..bad1159 --- /dev/null +++ b/lib/pages/player_page.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:vaani/features/player/view/player_expanded.dart'; +import 'package:vaani/features/player/view/player_expanded_desktop.dart'; +import 'package:vaani/shared/widgets/not_implemented.dart'; + +class PlayerPage extends HookConsumerWidget { + const PlayerPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final size = MediaQuery.of(context).size; + // 竖屏 + final isVertical = size.height > size.width; + return Scaffold( + appBar: AppBar( + shadowColor: Theme.of(context).colorScheme.onPrimary, + leading: IconButton( + iconSize: 30, + icon: const Icon(Icons.keyboard_arrow_down), + onPressed: () => context.pop(), + ), + actions: [ + IconButton( + icon: const Icon(Icons.cast), + onPressed: () { + showNotImplementedToast(context); + }, + ), + ], + ), + body: isVertical ? PlayerExpanded() : PlayerExpandedDesktop(), + ); + } +} diff --git a/lib/router/router.dart b/lib/router/router.dart index b123307..7397f05 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -8,18 +8,18 @@ import 'package:vaani/features/library_browser/view/library_browser_page.dart'; import 'package:vaani/features/logging/view/logs_page.dart'; import 'package:vaani/features/onboarding/view/callback_page.dart'; import 'package:vaani/features/onboarding/view/onboarding_single_page.dart'; -import 'package:vaani/features/player/view/player_expanded.dart'; +import 'package:vaani/features/settings/view/app_settings_page.dart'; +import 'package:vaani/features/settings/view/auto_sleep_timer_settings_page.dart'; +import 'package:vaani/features/settings/view/home_page_settings_page.dart'; +import 'package:vaani/features/settings/view/notification_settings_page.dart'; +import 'package:vaani/features/settings/view/player_settings_page.dart'; +import 'package:vaani/features/settings/view/shake_detector_settings_page.dart'; +import 'package:vaani/features/settings/view/theme_settings_page.dart'; import 'package:vaani/features/you/view/server_manager.dart'; import 'package:vaani/features/you/view/you_page.dart'; import 'package:vaani/globals.dart'; import 'package:vaani/pages/home_page.dart'; -import 'package:vaani/settings/view/app_settings_page.dart'; -import 'package:vaani/settings/view/auto_sleep_timer_settings_page.dart'; -import 'package:vaani/settings/view/home_page_settings_page.dart'; -import 'package:vaani/settings/view/notification_settings_page.dart'; -import 'package:vaani/settings/view/player_settings_page.dart'; -import 'package:vaani/settings/view/shake_detector_settings_page.dart'; -import 'package:vaani/settings/view/theme_settings_page.dart'; +import 'package:vaani/pages/player_page.dart'; import 'scaffold_with_nav_bar.dart'; import 'transitions/slide.dart'; @@ -235,11 +235,11 @@ class MyAppRouter { ], ), - // loggers page + // player full page GoRoute( path: Routes.player.localPath, name: Routes.player.name, - pageBuilder: defaultPageBuilder(const PlayerExpanded()), + pageBuilder: defaultPageBuilder(const PlayerPage()), ), // loggers page diff --git a/lib/router/scaffold_with_nav_bar.dart b/lib/router/scaffold_with_nav_bar.dart index 978016c..85b672d 100644 --- a/lib/router/scaffold_with_nav_bar.dart +++ b/lib/router/scaffold_with_nav_bar.dart @@ -3,15 +3,13 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/api/library_provider.dart' show currentLibraryProvider; import 'package:vaani/features/explore/providers/search_controller.dart'; -import 'package:vaani/features/player/providers/player_form.dart'; -import 'package:vaani/features/player/providers/session_provider.dart'; +import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/features/player/view/player_minimized.dart'; import 'package:vaani/features/you/view/widgets/library_switch_chip.dart'; import 'package:vaani/generated/l10n.dart'; import 'package:vaani/globals.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/shared/icons/abs_icons.dart' show AbsIcons; -import 'package:vaani/shared/utils/utils.dart'; // stack to track changes in navigationShell.currentIndex // home is always at index 0 and at the start and should be the last before popping @@ -35,21 +33,18 @@ class ScaffoldWithNavBar extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final size = MediaQuery.of(context).size; + // 竖屏 final isVertical = size.height > size.width; return Scaffold( body: Stack( alignment: Alignment.bottomCenter, children: [ - Utils.isMobile() || isVertical - ? navigationShell - : buildNavLeft(context, ref), - // const AudiobookPlayer(), - const PlayerMinimized(), + isVertical ? navigationShell : buildNavLeft(context, ref), + Hero(tag: 'player_hero', child: const PlayerMinimized()), ], ), - bottomNavigationBar: - Utils.isMobile() || isVertical ? buildNavBottom(context, ref) : null, + bottomNavigationBar: isVertical ? buildNavBottom(context, ref) : null, ); } diff --git a/lib/shared/extensions/model_conversions.dart b/lib/shared/extensions/model_conversions.dart index 38403d8..14966c2 100644 --- a/lib/shared/extensions/model_conversions.dart +++ b/lib/shared/extensions/model_conversions.dart @@ -79,3 +79,8 @@ extension ContentUrl on LibraryFile { ); } } + +extension PlaybackSessionConversion on PlaybackSession { + PlaybackSessionExpanded get asExpanded => + PlaybackSessionExpanded.fromJson(toJson()); +} diff --git a/lib/shared/extensions/obfuscation.dart b/lib/shared/extensions/obfuscation.dart index 6ff85fe..82603d0 100644 --- a/lib/shared/extensions/obfuscation.dart +++ b/lib/shared/extensions/obfuscation.dart @@ -1,9 +1,9 @@ import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk; -import 'package:vaani/settings/models/api_settings.dart'; -import 'package:vaani/settings/models/audiobookshelf_server.dart'; -import 'package:vaani/settings/models/authenticated_user.dart'; +import 'package:vaani/features/settings/models/api_settings.dart'; +import 'package:vaani/features/settings/models/audiobookshelf_server.dart'; +import 'package:vaani/features/settings/models/authenticated_user.dart'; // bool kReleaseMode = true; diff --git a/lib/models/error_response.dart b/lib/shared/utils/error_response.dart similarity index 100% rename from lib/models/error_response.dart rename to lib/shared/utils/error_response.dart diff --git a/lib/shared/utils/utils.dart b/lib/shared/utils/helper.dart similarity index 97% rename from lib/shared/utils/utils.dart rename to lib/shared/utils/helper.dart index 93a14d6..52a5244 100644 --- a/lib/shared/utils/utils.dart +++ b/lib/shared/utils/helper.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; -class Utils { +class Helper { static isAndroid() { return !kIsWeb && Platform.isAndroid; } diff --git a/lib/shared/widgets/shelves/book_shelf.dart b/lib/shared/widgets/shelves/book_shelf.dart index ec5a745..0c653da 100644 --- a/lib/shared/widgets/shelves/book_shelf.dart +++ b/lib/shared/widgets/shelves/book_shelf.dart @@ -12,10 +12,10 @@ import 'package:vaani/api/library_item_provider.dart' show libraryItemProvider; import 'package:vaani/constants/hero_tag_conventions.dart'; import 'package:vaani/features/item_viewer/view/library_item_actions.dart'; import 'package:vaani/features/player/providers/player_status_provider.dart'; -import 'package:vaani/features/player/providers/session_provider.dart'; +import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/router/models/library_item_extras.dart'; import 'package:vaani/router/router.dart'; -import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/features/settings/app_settings_provider.dart'; import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/widgets/shelves/home_shelf.dart'; import 'package:vaani/theme/providers/theme_from_cover_provider.dart'; diff --git a/lib/models/tray.dart b/lib/shared/widgets/tray_manager.dart similarity index 78% rename from lib/models/tray.dart rename to lib/shared/widgets/tray_manager.dart index fe3c6d3..3c79e9f 100644 --- a/lib/models/tray.dart +++ b/lib/shared/widgets/tray_manager.dart @@ -3,25 +3,23 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:tray_manager/tray_manager.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/globals.dart'; -import 'package:vaani/shared/utils/utils.dart'; +import 'package:vaani/shared/utils/helper.dart'; import 'package:window_manager/window_manager.dart'; -class TrayFramework extends ConsumerStatefulWidget { +class TrayManager extends ConsumerStatefulWidget { final Widget child; - const TrayFramework(this.child, {super.key}); + const TrayManager(this.child, {super.key}); @override - ConsumerState createState() => _TrayFrameworkState(); + ConsumerState createState() => _TrayManagerState(); } -class _TrayFrameworkState extends ConsumerState +class _TrayManagerState extends ConsumerState with TrayListener, WindowListener { @override void initState() { - if (Utils.isDesktop()) { - windowManager.addListener(this); - _init(); - } + windowManager.addListener(this); + _init(); super.initState(); } @@ -33,7 +31,7 @@ class _TrayFrameworkState extends ConsumerState void _init() async { await trayManager.setIcon( - Utils.isWindows() ? 'assets/icon/logo.ico' : 'assets/icon/logo.png', + Helper.isWindows() ? 'assets/icon/logo.ico' : 'assets/icon/logo.png', ); await trayManager.setToolTip(appName); Menu menu = Menu( @@ -48,19 +46,17 @@ class _TrayFrameworkState extends ConsumerState MenuItem( key: 'play_pause', label: '播放/暂停', - onClick: (menuItem) => - ref.read(audiobookPlayerProvider).togglePlayPause(), + onClick: (menuItem) => ref.read(playerProvider).togglePlayPause(), ), MenuItem( key: 'previous', label: '上一个', - onClick: (menuItem) => - ref.read(audiobookPlayerProvider).seekToPrevious(), + onClick: (menuItem) => ref.read(playerProvider).skipToPrevious(), ), MenuItem( key: 'next', label: '下一个', - onClick: (menuItem) => ref.read(audiobookPlayerProvider).seekToNext(), + onClick: (menuItem) => ref.read(playerProvider).skipToNext(), ), MenuItem.separator(), MenuItem( diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index c4d1ea9..899b745 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -89,11 +89,11 @@ BEGIN BEGIN BLOCK "040904e4" BEGIN - VALUE "CompanyName", "com.example" "\0" + VALUE "CompanyName", "dr.blank" "\0" VALUE "FileDescription", "vaani" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "vaani" "\0" - VALUE "LegalCopyright", "Copyright (C) 2024 com.example. All rights reserved." "\0" + VALUE "LegalCopyright", "Copyright (C) 2024 dr.blank. All rights reserved." "\0" VALUE "OriginalFilename", "vaani.exe" "\0" VALUE "ProductName", "vaani" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0"