Compare commits

...

2 commits

Author SHA1 Message Date
Dr.Blank
ad0cd6e2ad
fix: run dart fix
Some checks are pending
Flutter CI & Release / Test (push) Waiting to run
Flutter CI & Release / Build Android APKs (push) Blocked by required conditions
Flutter CI & Release / build_linux (push) Blocked by required conditions
Flutter CI & Release / Create GitHub Release (push) Blocked by required conditions
2025-04-23 00:29:02 +05:30
Dr.Blank
2cb00c451e
feat: implement scroll-to-top FAB and enhance library item app bar with scroll listener 2025-04-23 00:22:32 +05:30
5 changed files with 141 additions and 11 deletions

View file

@ -26,7 +26,7 @@ Future<Library?> 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;
}

View file

@ -6,7 +6,7 @@ part of 'library_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$libraryHash() => r'b62d976f8ab83b2f5823a0fb7dac52fde8fcbffc';
String _$libraryHash() => r'f8a34100acb58f02fa958c71a629577bf815710e';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -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<double> 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(

View file

@ -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,
);
}
}

View file

@ -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