fix(sheets): size sheets to their content instead of 75% of the window
Sheets rendered at the host's maximum height regardless of content, so a one-item player queue or a two-track picker filled ~75% of a desktop window with empty space. BottomSheetPageScaffold now always lays out Column(mainAxisSize: .min) plus Flexible(child:), and each sheet body shrink-wraps its own scrollable. The scaffold's shrinkWrap flag is gone: its old true branch put the child on an unbounded axis, where an over-tall list overflowed instead of clamping and scrolling. Measured on a 1600x1000 window, the chapter sheet goes from 750px to 118px for one chapter and the two-column track sheet from 750px to 154px for one audio and one subtitle track, both still clamping at the cap. Add SheetSplitColumns for the three side-by-side sheet layouts. A bare VerticalDivider has no intrinsic height, so it inflated those rows to the cap on its own; the rule now paints from a Positioned.fill that cannot size the Stack. IntrinsicHeight is not an option because a Viewport has no intrinsics. Because sheets are bottom-anchored, a content-driven height moves the sheet's top edge and everything above the change point. Three surfaces opt out for that reason and say so at the call site: SubtitleSearchSheet and its language picker keep filling, since both refilter under an autofocused field; FiltersBottomSheet holds the outgoing page's height through its loading transient; and RatingBottomSheet no longer hides MAL/AniList rows asynchronously, which used to slide live rating controls down two rows several hundred ms after open. Wrap the shared StateMessageWidget at the filters sheet boundary rather than editing a widget with 33 filling call sites. The host gains an AnimatedSize keyed per sheet session so nested pushes ease while a replacing show adopts its own height, a 720px absolute height ceiling on desktop windows only, and a min(max(25%, 96px), 60%) drag-dismiss threshold so short sheets neither close on a nudge nor become undismissable. Add videoControls.noAudioDevicesAvailable so the audio output page shows a placeholder instead of a bare header while devices load.
This commit is contained in:
@@ -51,6 +51,8 @@ class _FiltersBottomSheetState extends State<FiltersBottomSheet> {
|
||||
bool _isLoadingValues = false;
|
||||
String? _filterValuesError;
|
||||
int _filterValuesLoadGeneration = 0;
|
||||
final _contentKey = GlobalKey();
|
||||
double? _transitionMinHeight;
|
||||
final Map<String, String> _tempSelectedFilters = {};
|
||||
static final Map<String, String> _filterDisplayNames = {}; // Cache for display names
|
||||
static const int _maxCachedDisplayNames = 1000;
|
||||
@@ -116,6 +118,11 @@ class _FiltersBottomSheetState extends State<FiltersBottomSheet> {
|
||||
final libraryKey = widget.libraryKey;
|
||||
final cachedValues = widget.cachedValues;
|
||||
final loader = widget.loadFilterValues;
|
||||
// Drilling in is a setState page swap inside one sheet, and sheets are
|
||||
// bottom-anchored, so any height change during the load drags the header
|
||||
// and its Back button. Hold the outgoing page's height for the transient
|
||||
// spinner; the settled states below are free to hug again.
|
||||
_transitionMinHeight = _contentHeight();
|
||||
setState(() {
|
||||
_currentFilter = filter;
|
||||
_filterValues = [];
|
||||
@@ -235,26 +242,52 @@ class _FiltersBottomSheetState extends State<FiltersBottomSheet> {
|
||||
),
|
||||
)
|
||||
: null,
|
||||
child: currentFilter != null ? _buildFilterValuesView(currentFilter) : _buildFiltersView(),
|
||||
child: KeyedSubtree(
|
||||
key: _contentKey,
|
||||
child: currentFilter != null ? _buildFilterValuesView(currentFilter) : _buildFiltersView(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Height the content area currently occupies, used to hold the sheet steady
|
||||
/// across a page swap. Null before first layout.
|
||||
double? _contentHeight() {
|
||||
final box = _contentKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
return box?.hasSize == true ? box!.size.height : null;
|
||||
}
|
||||
|
||||
Widget _buildFilterValuesView(MediaFilter filter) {
|
||||
final error = _filterValuesError;
|
||||
if (error != null) {
|
||||
return ErrorStateWidget(
|
||||
message: error,
|
||||
onRetry: () => _loadFilterValues(filter),
|
||||
actionFocusNode: _initialFocusNode,
|
||||
onActionBack: _goBack,
|
||||
actionAutofocus: InputModeTracker.isKeyboardMode(context),
|
||||
actionUseBackgroundFocus: true,
|
||||
// The StateMessageWidget family is filling by design — 33 other sites
|
||||
// render it in page bodies and SliverFillRemaining. The unbounded scroll
|
||||
// axis here is what lets its inner Center shrink to content, so the sheet
|
||||
// does not stretch to the full height cap for one line of text.
|
||||
return SingleChildScrollView(
|
||||
primary: false,
|
||||
child: ErrorStateWidget(
|
||||
message: error,
|
||||
onRetry: () => _loadFilterValues(filter),
|
||||
actionFocusNode: _initialFocusNode,
|
||||
onActionBack: _goBack,
|
||||
actionAutofocus: InputModeTracker.isKeyboardMode(context),
|
||||
actionUseBackgroundFocus: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_isLoadingValues) {
|
||||
assert(_transitionMinHeight != null, '_transitionMinHeight must be set before entering the loading state');
|
||||
// Held at the outgoing page's height (see [_loadFilterValues]) so the
|
||||
// transient spinner cannot move the header. Settled states below hug.
|
||||
return Focus(
|
||||
autofocus: InputModeTracker.isKeyboardMode(context),
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
// Exactly the outgoing height, so the swap moves nothing.
|
||||
// [_loadFilterValues] assigns it immediately before setting
|
||||
// `_isLoadingValues`, so it is never null here.
|
||||
child: SizedBox(
|
||||
height: _transitionMinHeight,
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -262,6 +295,7 @@ class _FiltersBottomSheetState extends State<FiltersBottomSheet> {
|
||||
return ListView.builder(
|
||||
controller: _valuesScrollController,
|
||||
primary: false,
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
itemCount: _filterValues.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
@@ -309,6 +343,7 @@ class _FiltersBottomSheetState extends State<FiltersBottomSheet> {
|
||||
final autofocusFirst = InputModeTracker.isKeyboardMode(context);
|
||||
return ListView.builder(
|
||||
primary: false,
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
itemCount: _sortedFilters.length,
|
||||
itemBuilder: (context, index) {
|
||||
|
||||
@@ -836,7 +836,6 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
||||
return BottomSheetPageScaffold(
|
||||
title: t.libraries.libraryOptions,
|
||||
icon: Symbols.tune_rounded,
|
||||
shrinkWrap: true,
|
||||
child: ListView(
|
||||
primary: false,
|
||||
shrinkWrap: true,
|
||||
@@ -917,7 +916,6 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
||||
title: t.libraries.groupings.title,
|
||||
icon: Symbols.category_rounded,
|
||||
onBack: onBack,
|
||||
shrinkWrap: true,
|
||||
child: ListView(
|
||||
primary: false,
|
||||
shrinkWrap: true,
|
||||
|
||||
Reference in New Issue
Block a user