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.
98 lines
3.2 KiB
Dart
98 lines
3.2 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../../../utils/scroll_utils.dart';
|
|
import '../../../widgets/overlay_sheet.dart';
|
|
import 'sheet_column_header.dart';
|
|
|
|
/// Per-row handle handed to [SheetSelectionColumn.itemBuilder].
|
|
abstract class SheetSelectionColumnScope {
|
|
/// Key for the row at [index]. Only the first row is keyed, so the one-time
|
|
/// initial scroll can measure a real item height.
|
|
Key? keyFor(int index);
|
|
|
|
/// Runs an async selection: re-entrant taps are ignored while one is in
|
|
/// flight, a progress bar is shown meanwhile, and the sheet is closed once
|
|
/// [action] completes.
|
|
void runExclusive(Future<void> Function() action);
|
|
}
|
|
|
|
/// Shared scaffold for the selectable columns inside the video control sheets:
|
|
/// an optional header, a one-shot scroll to the selected row, the async
|
|
/// selection guard, the scrolling list, and an optional footer.
|
|
class SheetSelectionColumn extends StatefulWidget {
|
|
/// Header text, or null to omit the header entirely.
|
|
final String? headerLabel;
|
|
final int itemCount;
|
|
|
|
/// Row to scroll into view on first build; ignored when null or <= 0.
|
|
final int? initialIndex;
|
|
final Widget Function(BuildContext context, int index, SheetSelectionColumnScope scope) itemBuilder;
|
|
final List<Widget> footer;
|
|
|
|
const SheetSelectionColumn({
|
|
super.key,
|
|
this.headerLabel,
|
|
required this.itemCount,
|
|
required this.initialIndex,
|
|
required this.itemBuilder,
|
|
this.footer = const [],
|
|
});
|
|
|
|
@override
|
|
State<SheetSelectionColumn> createState() => _SheetSelectionColumnState();
|
|
}
|
|
|
|
class _SheetSelectionColumnState extends State<SheetSelectionColumn> implements SheetSelectionColumnScope {
|
|
final _initialScroll = InitialItemScrollController();
|
|
bool _selectionPending = false;
|
|
|
|
@override
|
|
void dispose() {
|
|
_initialScroll.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Key? keyFor(int index) => index == 0 ? _initialScroll.firstItemKey : null;
|
|
|
|
@override
|
|
void runExclusive(Future<void> Function() action) => unawaited(_select(action));
|
|
|
|
Future<void> _select(Future<void> Function() action) async {
|
|
if (_selectionPending) return;
|
|
setState(() => _selectionPending = true);
|
|
try {
|
|
await action();
|
|
if (mounted) OverlaySheetController.of(context).close();
|
|
} finally {
|
|
if (mounted) setState(() => _selectionPending = false);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
_initialScroll.maybeScrollTo(widget.initialIndex);
|
|
|
|
return Column(
|
|
mainAxisSize: .min,
|
|
children: [
|
|
if (widget.headerLabel != null) SheetColumnHeader(label: widget.headerLabel!),
|
|
// Reserve the bar's 2px unconditionally: growing the column mid-tap
|
|
// would nudge a content-sized sheet, and the host eases that as a twitch.
|
|
SizedBox(height: 2, child: _selectionPending ? const LinearProgressIndicator(minHeight: 2) : null),
|
|
Flexible(
|
|
child: ListView.builder(
|
|
shrinkWrap: true,
|
|
controller: _initialScroll.controller,
|
|
itemCount: widget.itemCount,
|
|
itemBuilder: (context, index) => widget.itemBuilder(context, index, this),
|
|
),
|
|
),
|
|
...widget.footer,
|
|
],
|
|
);
|
|
}
|
|
}
|