diff --git a/lib/api/library_provider.dart b/lib/api/library_provider.dart index 0903015..62c79d8 100644 --- a/lib/api/library_provider.dart +++ b/lib/api/library_provider.dart @@ -26,7 +26,7 @@ Future library(Ref ref, String id) async { _logger.warning('No library found in the list of libraries'); return null; } - _logger.fine('Fetched library: ${library}'); + _logger.fine('Fetched library: $library'); return library.library; } diff --git a/lib/api/library_provider.g.dart b/lib/api/library_provider.g.dart index 8f22251..a8bc88a 100644 --- a/lib/api/library_provider.g.dart +++ b/lib/api/library_provider.g.dart @@ -6,7 +6,7 @@ part of 'library_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$libraryHash() => r'b62d976f8ab83b2f5823a0fb7dac52fde8fcbffc'; +String _$libraryHash() => r'f8a34100acb58f02fa958c71a629577bf815710e'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/features/item_viewer/view/library_item_page.dart b/lib/features/item_viewer/view/library_item_page.dart index 53db17e..e7b9310 100644 --- a/lib/features/item_viewer/view/library_item_page.dart +++ b/lib/features/item_viewer/view/library_item_page.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:animated_theme_switcher/animated_theme_switcher.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/api/library_item_provider.dart'; import 'package:vaani/features/item_viewer/view/library_item_sliver_app_bar.dart'; @@ -23,19 +24,89 @@ class LibraryItemPage extends HookConsumerWidget { final String itemId; final Object? extra; + static const double _showFabThreshold = 300.0; @override Widget build(BuildContext context, WidgetRef ref) { final additionalItemData = extra is LibraryItemExtras ? extra as LibraryItemExtras : null; + final scrollController = useScrollController(); + final showFab = useState(false); + + // Effect to listen to scroll changes and update FAB visibility + useEffect( + () { + void listener() { + if (!scrollController.hasClients) { + return; // Ensure controller is attached + } + final shouldShow = scrollController.offset > _showFabThreshold; + // Update state only if it changes and widget is still mounted + if (showFab.value != shouldShow && context.mounted) { + showFab.value = shouldShow; + } + } + + scrollController.addListener(listener); + // Initial check in case the view starts scrolled (less likely but safe) + WidgetsBinding.instance.addPostFrameCallback((_) { + if (scrollController.hasClients && context.mounted) { + listener(); + } + }); + + // Cleanup: remove the listener when the widget is disposed + return () => scrollController.removeListener(listener); + }, + [scrollController], + ); // Re-run effect if scrollController changes + + // --- FAB Scroll-to-Top Logic --- + void scrollToTop() { + if (scrollController.hasClients) { + scrollController.animateTo( + 0.0, // Target offset (top) + duration: 300.ms, + curve: Curves.easeInOut, + ); + } + } return ThemeProvider( initTheme: Theme.of(context), duration: 200.ms, child: ThemeSwitchingArea( child: Scaffold( + floatingActionButton: AnimatedSwitcher( + duration: 250.ms, + // A common transition for FABs (fade + scale) + transitionBuilder: (Widget child, Animation animation) { + return ScaleTransition( + scale: animation, + child: FadeTransition( + opacity: animation, + child: child, + ), + ); + }, + child: showFab.value + ? FloatingActionButton( + // Key is important for AnimatedSwitcher to differentiate + key: const ValueKey('fab-scroll-top'), + onPressed: scrollToTop, + tooltip: 'Scroll to top', + child: const Icon(Icons.arrow_upward), + ) + : const SizedBox.shrink( + key: ValueKey('fab-empty'), + ), + ), body: CustomScrollView( + controller: scrollController, slivers: [ - const LibraryItemSliverAppBar(), + LibraryItemSliverAppBar( + id: itemId, + scrollController: scrollController, + ), SliverPadding( padding: const EdgeInsets.all(8), sliver: LibraryItemHeroSection( diff --git a/lib/features/item_viewer/view/library_item_sliver_app_bar.dart b/lib/features/item_viewer/view/library_item_sliver_app_bar.dart index ef133ee..fd6b621 100644 --- a/lib/features/item_viewer/view/library_item_sliver_app_bar.dart +++ b/lib/features/item_viewer/view/library_item_sliver_app_bar.dart @@ -1,22 +1,80 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:vaani/api/library_item_provider.dart' show libraryItemProvider; -class LibraryItemSliverAppBar extends StatelessWidget { +class LibraryItemSliverAppBar extends HookConsumerWidget { const LibraryItemSliverAppBar({ super.key, + required this.id, + required this.scrollController, }); + final String id; + final ScrollController scrollController; + + static const double _showTitleThreshold = kToolbarHeight * 0.5; + @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final item = ref.watch(libraryItemProvider(id)).valueOrNull; + + final showTitle = useState(false); + + useEffect( + () { + void listener() { + final shouldShow = scrollController.hasClients && + scrollController.offset > _showTitleThreshold; + if (showTitle.value != shouldShow) { + showTitle.value = shouldShow; + } + } + + scrollController.addListener(listener); + // Trigger listener once initially in case the view starts scrolled + // (though unlikely for this specific use case, it's good practice) + WidgetsBinding.instance.addPostFrameCallback((_) { + if (scrollController.hasClients) { + listener(); + } + }); + return () => scrollController.removeListener(listener); + }, + [scrollController], + ); + return SliverAppBar( elevation: 0, - floating: true, + floating: false, + pinned: true, primary: true, - snap: true, actions: [ - // cast button - IconButton(onPressed: () {}, icon: const Icon(Icons.cast)), - IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)), + // IconButton( + // icon: const Icon(Icons.cast), + // onPressed: () { + // // Handle search action + // }, + // ), ], + title: AnimatedSwitcher( + duration: const Duration(milliseconds: 150), + child: showTitle.value + ? Text( + // Use a Key to help AnimatedSwitcher differentiate widgets + key: const ValueKey('title-text'), + item?.media.metadata.title ?? '', + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium, + ) + : const SizedBox( + // Also give it a key for differentiation + key: ValueKey('empty-title'), + width: 0, // Ensure it takes no space if possible + height: 0, + ), + ), + centerTitle: false, ); } } diff --git a/lib/features/you/view/widgets/library_switch_chip.dart b/lib/features/you/view/widgets/library_switch_chip.dart index 1463b8f..a673332 100644 --- a/lib/features/you/view/widgets/library_switch_chip.dart +++ b/lib/features/you/view/widgets/library_switch_chip.dart @@ -204,7 +204,8 @@ class _LibrarySelectionContent extends ConsumerWidget { trailing: isSelected ? const Icon(Icons.check) : null, onTap: () { appLogger.info( - 'Selected library: ${library.name} (ID: ${library.id})'); + 'Selected library: ${library.name} (ID: ${library.id})', + ); // Get current settings state final currentSettings = ref.read(apiSettingsProvider); // Update the active library ID