From e21977b894441448d1a4b5c5fba86e03992817ee Mon Sep 17 00:00:00 2001 From: "Dr.Blank" <64108942+Dr-Blank@users.noreply.github.com> Date: Fri, 18 Apr 2025 12:38:51 +0530 Subject: [PATCH] feat: implement library selection in YouPage --- lib/api/library_provider.dart | 42 +++++ lib/api/library_provider.g.dart | 176 +++++++++++++++++++ lib/features/logging/view/logs_page.dart | 4 - lib/features/you/view/you_page.dart | 210 ++++++++++++++++++++--- lib/shared/icons/abs_icons.dart | 1 + lib/shared/widgets/vaani_logo.dart | 27 +++ shelfsdk | 2 +- 7 files changed, 430 insertions(+), 32 deletions(-) create mode 100644 lib/api/library_provider.dart create mode 100644 lib/api/library_provider.g.dart create mode 100644 lib/shared/widgets/vaani_logo.dart diff --git a/lib/api/library_provider.dart b/lib/api/library_provider.dart new file mode 100644 index 0000000..a138a7a --- /dev/null +++ b/lib/api/library_provider.dart @@ -0,0 +1,42 @@ +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:vaani/api/api_provider.dart' show authenticatedApiProvider; +part 'library_provider.g.dart'; + +final _logger = Logger('LibraryProvider'); + +@riverpod +Future currentLibrary(Ref ref, String id) async { + final api = ref.watch(authenticatedApiProvider); + final library = await api.libraries.get(libraryId: id); + if (library == null) { + _logger.warning('No library found through id: $id'); + // try to get the library from the list of libraries + final libraries = await ref.watch(librariesProvider.future); + for (final lib in libraries) { + if (lib.id == id) { + return lib; + } + } + _logger.warning('No library found in the list of libraries'); + return null; + } + return library.library; +} + +@riverpod +class Libraries extends _$Libraries { + @override + FutureOr> build() async { + final api = ref.watch(authenticatedApiProvider); + final libraries = await api.libraries.getAll(); + if (libraries == null) { + _logger.warning('Failed to fetch libraries'); + return []; + } + return libraries; + } +} diff --git a/lib/api/library_provider.g.dart b/lib/api/library_provider.g.dart new file mode 100644 index 0000000..3161d34 --- /dev/null +++ b/lib/api/library_provider.g.dart @@ -0,0 +1,176 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'library_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$currentLibraryHash() => r'f37904b8b43c88a523696d1ed7acf871c3e3326f'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [currentLibrary]. +@ProviderFor(currentLibrary) +const currentLibraryProvider = CurrentLibraryFamily(); + +/// See also [currentLibrary]. +class CurrentLibraryFamily extends Family> { + /// See also [currentLibrary]. + const CurrentLibraryFamily(); + + /// See also [currentLibrary]. + CurrentLibraryProvider call( + String id, + ) { + return CurrentLibraryProvider( + id, + ); + } + + @override + CurrentLibraryProvider getProviderOverride( + covariant CurrentLibraryProvider provider, + ) { + return call( + provider.id, + ); + } + + 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'currentLibraryProvider'; +} + +/// See also [currentLibrary]. +class CurrentLibraryProvider extends AutoDisposeFutureProvider { + /// See also [currentLibrary]. + CurrentLibraryProvider( + String id, + ) : this._internal( + (ref) => currentLibrary( + ref as CurrentLibraryRef, + id, + ), + from: currentLibraryProvider, + name: r'currentLibraryProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$currentLibraryHash, + dependencies: CurrentLibraryFamily._dependencies, + allTransitiveDependencies: + CurrentLibraryFamily._allTransitiveDependencies, + id: id, + ); + + CurrentLibraryProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.id, + }) : super.internal(); + + final String id; + + @override + Override overrideWith( + FutureOr Function(CurrentLibraryRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: CurrentLibraryProvider._internal( + (ref) => create(ref as CurrentLibraryRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + id: id, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _CurrentLibraryProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is CurrentLibraryProvider && other.id == id; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, id.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin CurrentLibraryRef on AutoDisposeFutureProviderRef { + /// The parameter `id` of this provider. + String get id; +} + +class _CurrentLibraryProviderElement + extends AutoDisposeFutureProviderElement with CurrentLibraryRef { + _CurrentLibraryProviderElement(super.provider); + + @override + String get id => (origin as CurrentLibraryProvider).id; +} + +String _$librariesHash() => r'a79954d0b68a8265859c577e36d5596620a72843'; + +/// See also [Libraries]. +@ProviderFor(Libraries) +final librariesProvider = + AutoDisposeAsyncNotifierProvider>.internal( + Libraries.new, + name: r'librariesProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$librariesHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$Libraries = 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/logging/view/logs_page.dart b/lib/features/logging/view/logs_page.dart index ad2c764..b774963 100644 --- a/lib/features/logging/view/logs_page.dart +++ b/lib/features/logging/view/logs_page.dart @@ -1,15 +1,11 @@ -import 'dart:io'; -import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:logging/logging.dart'; -import 'package:permission_handler/permission_handler.dart'; import 'package:share_plus/share_plus.dart'; import 'package:vaani/features/logging/providers/logs_provider.dart'; import 'package:vaani/main.dart'; -import 'package:vaani/settings/metadata/metadata_provider.dart'; class LogsPage extends HookConsumerWidget { const LogsPage({super.key}); diff --git a/lib/features/you/view/you_page.dart b/lib/features/you/view/you_page.dart index c15e80f..69eeecc 100644 --- a/lib/features/you/view/you_page.dart +++ b/lib/features/you/view/you_page.dart @@ -1,12 +1,22 @@ +import 'dart:io' show Platform; + +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shelfsdk/audiobookshelf_api.dart' show Library; import 'package:vaani/api/api_provider.dart'; +import 'package:vaani/api/library_provider.dart' show librariesProvider; import 'package:vaani/features/player/view/mini_player_bottom_padding.dart'; +import 'package:vaani/main.dart' show appLogger; import 'package:vaani/router/router.dart'; +import 'package:vaani/settings/api_settings_provider.dart' + show apiSettingsProvider; import 'package:vaani/settings/constants.dart'; +import 'package:vaani/shared/icons/abs_icons.dart'; import 'package:vaani/shared/utils.dart'; import 'package:vaani/shared/widgets/not_implemented.dart'; +import 'package:vaani/shared/widgets/vaani_logo.dart'; class YouPage extends HookConsumerWidget { const YouPage({ @@ -16,6 +26,9 @@ class YouPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final api = ref.watch(authenticatedApiProvider); + final librariesAsyncValue = ref.watch(librariesProvider); + // Get current settings to know the active library ID later + final apiSettings = ref.watch(apiSettingsProvider); return Scaffold( appBar: AppBar( // title: const Text('You'), @@ -63,7 +76,58 @@ class YouPage extends HookConsumerWidget { context.pushNamed(Routes.userManagement.name); }, ), - // ActionChip( + librariesAsyncValue.when( + data: (libraries) => ActionChip( + avatar: Icon( + AbsIcons.getIconByName( + apiSettings.activeLibraryId != null + ? libraries + .firstWhere( + (lib) => + lib.id == + apiSettings.activeLibraryId, + ) + .icon + : libraries.first.icon, + ), + ), // Replace with your icon + label: const Text('Change Library'), + // Enable only if libraries are loaded and not empty + onPressed: libraries.isNotEmpty + ? () => _showLibrarySwitcher( + context, + ref, + libraries, + apiSettings.activeLibraryId, + ) + : null, // Disable if no libraries + ), + loading: () => const ActionChip( + avatar: SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ), + label: Text('Loading Libs...'), + onPressed: null, // Disable while loading + ), + error: (error, stack) => ActionChip( + avatar: Icon( + Icons.error_outline, + color: Theme.of(context).colorScheme.error, + ), + label: const Text('Error Loading Libs'), + onPressed: () { + // Maybe show error details or allow retry + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text('Failed to load libraries: $error'), + ), + ); + }, + ), + ), // ActionChip( // avatar: const Icon(Icons.logout), // label: const Text('Logout'), // onPressed: () { @@ -144,6 +208,124 @@ class YouPage extends HookConsumerWidget { } } +// --- Helper Function to Show the Switcher --- +void _showLibrarySwitcher( + BuildContext context, + WidgetRef ref, + List libraries, // Pass loaded libraries + String? currentLibraryId, // Pass current ID +) { + final content = _LibrarySelectionContent( + libraries: libraries, + currentLibraryId: currentLibraryId, + ); + + // --- Platform-Specific UI --- + bool isDesktop = false; + if (!kIsWeb) { + // dart:io Platform is not available on web + isDesktop = Platform.isLinux || Platform.isMacOS || Platform.isWindows; + } else { + // Basic web detection (might need refinement based on screen size) + // Consider using MediaQuery for a size-based check instead for web/tablet + final size = MediaQuery.of(context).size; + isDesktop = size.width > 600; // Example threshold for "desktop-like" layout + } + + if (isDesktop) { + // --- Desktop: Use AlertDialog --- + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: const Text('Select Library'), + content: SizedBox( + // Constrain size for dialogs + width: 300, // Adjust as needed + // Make content scrollable if list is long + child: Scrollbar(child: content), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: const Text('Cancel'), + ), + ], + ), + ); + } else { + // --- Mobile/Tablet: Use BottomSheet --- + showModalBottomSheet( + context: context, + // Make it scrollable and control height + isScrollControlled: true, + constraints: BoxConstraints( + maxHeight: + MediaQuery.of(context).size.height * 0.6, // Max 60% of screen + ), + builder: (sheetContext) => Padding( + // Add padding within the bottom sheet + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, // Take minimum necessary height + children: [ + const Text( + 'Select Library', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + const Divider(), + Flexible( + // Allow the list to take remaining space and scroll + child: Scrollbar(child: content), + ), + ], + ), + ), + ); + } +} + +// --- Widget for the Selection List Content (Reusable) --- +class _LibrarySelectionContent extends ConsumerWidget { + final List libraries; + final String? currentLibraryId; + + const _LibrarySelectionContent({ + required this.libraries, + this.currentLibraryId, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ListView.builder( + shrinkWrap: true, // Important for Dialog/BottomSheet sizing + itemCount: libraries.length, + itemBuilder: (context, index) { + final library = libraries[index]; + final bool isSelected = library.id == currentLibraryId; + + return ListTile( + title: Text(library.name), + leading: Icon(AbsIcons.getIconByName(library.icon)), + selected: isSelected, // Makes the tile visually selected + onTap: () { + appLogger + .info('Selected library: ${library.name} (ID: ${library.id})'); + // Get current settings state + final currentSettings = ref.read(apiSettingsProvider); + // Update the active library ID + ref.read(apiSettingsProvider.notifier).updateState( + currentSettings.copyWith(activeLibraryId: library.id), + ); + // Close the dialog/bottom sheet + Navigator.pop(context); + }, + ); + }, + ); + } +} + class UserBar extends HookConsumerWidget { const UserBar({ super.key, @@ -185,29 +367,3 @@ class UserBar extends HookConsumerWidget { ); } } - -class VaaniLogo extends StatelessWidget { - const VaaniLogo({ - super.key, - this.size, - this.duration = const Duration(milliseconds: 750), - this.curve = Curves.fastOutSlowIn, - }); - - final double? size; - final Duration duration; - final Curve curve; - - @override - Widget build(BuildContext context) { - final IconThemeData iconTheme = IconTheme.of(context); - final double? iconSize = size ?? iconTheme.size; - return AnimatedContainer( - width: iconSize, - height: iconSize, - duration: duration, - curve: curve, - child: Image.asset('assets/images/vaani_logo_foreground.png'), - ); - } -} diff --git a/lib/shared/icons/abs_icons.dart b/lib/shared/icons/abs_icons.dart index f112d0b..eca3e1a 100644 --- a/lib/shared/icons/abs_icons.dart +++ b/lib/shared/icons/abs_icons.dart @@ -13,6 +13,7 @@ /// /// /// +library; // ignore_for_file: constant_identifier_names import 'package:flutter/widgets.dart' show IconData; diff --git a/lib/shared/widgets/vaani_logo.dart b/lib/shared/widgets/vaani_logo.dart new file mode 100644 index 0000000..a5b4e11 --- /dev/null +++ b/lib/shared/widgets/vaani_logo.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class VaaniLogo extends StatelessWidget { + const VaaniLogo({ + super.key, + this.size, + this.duration = const Duration(milliseconds: 750), + this.curve = Curves.fastOutSlowIn, + }); + + final double? size; + final Duration duration; + final Curve curve; + + @override + Widget build(BuildContext context) { + final IconThemeData iconTheme = IconTheme.of(context); + final double? iconSize = size ?? iconTheme.size; + return AnimatedContainer( + width: iconSize, + height: iconSize, + duration: duration, + curve: curve, + child: Image.asset('assets/images/vaani_logo_foreground.png'), + ); + } +} diff --git a/shelfsdk b/shelfsdk index 5cc545c..e1848a4 160000 --- a/shelfsdk +++ b/shelfsdk @@ -1 +1 @@ -Subproject commit 5cc545ca87c05615473ab9c363cfa29e341d1e2a +Subproject commit e1848a42c27257146015a33e9427f197f522fe03