From 90111551d0d15b92bc8b5a0bc9e381a93fc9e08b Mon Sep 17 00:00:00 2001 From: "Dr.Blank" <64108942+Dr-Blank@users.noreply.github.com> Date: Sat, 19 Apr 2025 15:00:23 +0530 Subject: [PATCH] feat: add LibrarySwitchChip widget and integrate it into YouPage and ScaffoldWithNavBar --- lib/features/logging/view/logs_page.dart | 1 - .../you/view/widgets/library_switch_chip.dart | 168 ++++++++++++++++++ lib/features/you/view/you_page.dart | 156 +--------------- lib/router/scaffold_with_nav_bar.dart | 33 ++-- 4 files changed, 193 insertions(+), 165 deletions(-) create mode 100644 lib/features/you/view/widgets/library_switch_chip.dart diff --git a/lib/features/logging/view/logs_page.dart b/lib/features/logging/view/logs_page.dart index b774963..74d1ad3 100644 --- a/lib/features/logging/view/logs_page.dart +++ b/lib/features/logging/view/logs_page.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; diff --git a/lib/features/you/view/widgets/library_switch_chip.dart b/lib/features/you/view/widgets/library_switch_chip.dart new file mode 100644 index 0000000..c4ed4d0 --- /dev/null +++ b/lib/features/you/view/widgets/library_switch_chip.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shelfsdk/audiobookshelf_api.dart' show Library; +import 'package:vaani/api/library_provider.dart'; +import 'package:vaani/settings/api_settings_provider.dart' + show apiSettingsProvider; +import 'package:vaani/shared/icons/abs_icons.dart'; +import 'dart:io' show Platform; + +import 'package:flutter/foundation.dart'; +import 'package:vaani/main.dart' show appLogger; + +class LibrarySwitchChip extends HookConsumerWidget { + const LibrarySwitchChip({ + super.key, + required this.libraries, + }); + final List libraries; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final apiSettings = ref.watch(apiSettingsProvider); + + return 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, + ) + : null, // Disable if no libraries + ); + } +} + +// --- Helper Function to Show the Switcher --- +void showLibrarySwitcher( + BuildContext context, + WidgetRef ref, +) { + final content = _LibrarySelectionContent(); + + // --- 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 { + @override + Widget build(BuildContext context, WidgetRef ref) { + // Get the current library ID from the settings + final currentLibraryId = ref.watch( + apiSettingsProvider.select( + (settings) => settings.activeLibraryId, + ), + ); + // Get the list of libraries from the settings + final libraries = ref.watch(librariesProvider).valueOrNull ?? + []; // Use null-aware operator to handle null case + + // If no libraries are available, show a message + if (libraries.isEmpty) { + return const Center( + child: Text('No libraries available'), + ); + } + 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, + trailing: isSelected ? const Icon(Icons.check) : null, + 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); + }, + ); + }, + ); + } +} diff --git a/lib/features/you/view/you_page.dart b/lib/features/you/view/you_page.dart index 69eeecc..cfe15cd 100644 --- a/lib/features/you/view/you_page.dart +++ b/lib/features/you/view/you_page.dart @@ -1,19 +1,12 @@ -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/features/you/view/widgets/library_switch_chip.dart'; 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'; @@ -27,8 +20,6 @@ class YouPage extends HookConsumerWidget { 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'), @@ -77,31 +68,8 @@ class YouPage extends HookConsumerWidget { }, ), 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 - ), + data: (libraries) => + LibrarySwitchChip(libraries: libraries), loading: () => const ActionChip( avatar: SizedBox( width: 18, @@ -208,124 +176,6 @@ 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, diff --git a/lib/router/scaffold_with_nav_bar.dart b/lib/router/scaffold_with_nav_bar.dart index 0a89162..979dc91 100644 --- a/lib/router/scaffold_with_nav_bar.dart +++ b/lib/router/scaffold_with_nav_bar.dart @@ -6,8 +6,10 @@ import 'package:vaani/features/explore/providers/search_controller.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/player_when_expanded.dart'; +import 'package:vaani/features/you/view/widgets/library_switch_chip.dart'; import 'package:vaani/main.dart'; import 'package:vaani/router/router.dart'; +import 'package:vaani/shared/widgets/not_implemented.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 @@ -111,17 +113,26 @@ class ScaffoldWithNavBar extends HookConsumerWidget { // world scenario, the items would most likely be generated from the // branches of the shell route, which can be fetched using // `navigationShell.route.branches`. - destinations: _navigationItems - .map( - (item) => NavigationDestination( - icon: Icon(item.icon), - selectedIcon: item.activeIcon != null - ? Icon(item.activeIcon) - : Icon(item.icon), - label: item.name, - ), - ) - .toList(), + destinations: _navigationItems.map((item) { + final destinationWidget = NavigationDestination( + icon: Icon(item.icon), + selectedIcon: item.activeIcon != null + ? Icon(item.activeIcon) + : Icon(item.icon), + label: item.name, + ); + if (item.name == 'Library') { + return GestureDetector( + onSecondaryTap: () => showLibrarySwitcher(context, ref), + onLongPress: () => showLibrarySwitcher(context, ref), + child: + destinationWidget, // Wrap the actual NavigationDestination + ); + } else { + // Return the unwrapped destination for other items + return destinationWidget; + } + }).toList(), selectedIndex: navigationShell.currentIndex, onDestinationSelected: (int index) => _onTap(context, index, ref), ),