mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2025-12-24 20:09:30 +00:00
Compare commits
2 commits
c3d3a3900d
...
ad0cd6e2ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad0cd6e2ad | ||
|
|
2cb00c451e |
5 changed files with 141 additions and 11 deletions
|
|
@ -26,7 +26,7 @@ Future<Library?> library(Ref ref, String id) async {
|
||||||
_logger.warning('No library found in the list of libraries');
|
_logger.warning('No library found in the list of libraries');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
_logger.fine('Fetched library: ${library}');
|
_logger.fine('Fetched library: $library');
|
||||||
return library.library;
|
return library.library;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ part of 'library_provider.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$libraryHash() => r'b62d976f8ab83b2f5823a0fb7dac52fde8fcbffc';
|
String _$libraryHash() => r'f8a34100acb58f02fa958c71a629577bf815710e';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'dart:math';
|
||||||
import 'package:animated_theme_switcher/animated_theme_switcher.dart';
|
import 'package:animated_theme_switcher/animated_theme_switcher.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:vaani/api/library_item_provider.dart';
|
import 'package:vaani/api/library_item_provider.dart';
|
||||||
import 'package:vaani/features/item_viewer/view/library_item_sliver_app_bar.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 String itemId;
|
||||||
final Object? extra;
|
final Object? extra;
|
||||||
|
static const double _showFabThreshold = 300.0;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final additionalItemData =
|
final additionalItemData =
|
||||||
extra is LibraryItemExtras ? extra as LibraryItemExtras : null;
|
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(
|
return ThemeProvider(
|
||||||
initTheme: Theme.of(context),
|
initTheme: Theme.of(context),
|
||||||
duration: 200.ms,
|
duration: 200.ms,
|
||||||
child: ThemeSwitchingArea(
|
child: ThemeSwitchingArea(
|
||||||
child: Scaffold(
|
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(
|
body: CustomScrollView(
|
||||||
|
controller: scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
const LibraryItemSliverAppBar(),
|
LibraryItemSliverAppBar(
|
||||||
|
id: itemId,
|
||||||
|
scrollController: scrollController,
|
||||||
|
),
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
sliver: LibraryItemHeroSection(
|
sliver: LibraryItemHeroSection(
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,80 @@
|
||||||
import 'package:flutter/material.dart';
|
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({
|
const LibraryItemSliverAppBar({
|
||||||
super.key,
|
super.key,
|
||||||
|
required this.id,
|
||||||
|
required this.scrollController,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final ScrollController scrollController;
|
||||||
|
|
||||||
|
static const double _showTitleThreshold = kToolbarHeight * 0.5;
|
||||||
|
|
||||||
@override
|
@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(
|
return SliverAppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
floating: true,
|
floating: false,
|
||||||
|
pinned: true,
|
||||||
primary: true,
|
primary: true,
|
||||||
snap: true,
|
|
||||||
actions: [
|
actions: [
|
||||||
// cast button
|
// IconButton(
|
||||||
IconButton(onPressed: () {}, icon: const Icon(Icons.cast)),
|
// icon: const Icon(Icons.cast),
|
||||||
IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)),
|
// 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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,8 @@ class _LibrarySelectionContent extends ConsumerWidget {
|
||||||
trailing: isSelected ? const Icon(Icons.check) : null,
|
trailing: isSelected ? const Icon(Icons.check) : null,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
appLogger.info(
|
appLogger.info(
|
||||||
'Selected library: ${library.name} (ID: ${library.id})');
|
'Selected library: ${library.name} (ID: ${library.id})',
|
||||||
|
);
|
||||||
// Get current settings state
|
// Get current settings state
|
||||||
final currentSettings = ref.read(apiSettingsProvider);
|
final currentSettings = ref.read(apiSettingsProvider);
|
||||||
// Update the active library ID
|
// Update the active library ID
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue