Fix sheet peek for async content, and rework several screens' details
showNooSheet's peek detection previously measured content once, right
after the first frame - Details' Versions/Activity tabs fetch their
data asynchronously, so that one-shot measurement only ever saw the
initial loading-spinner height, never the eventual (often long) list,
and the sheet still snapped to full height once data arrived. Replaced
it with a ScrollMetricsNotification-based check that keeps content
capped at the peek height from frame one and upgrades to a draggable
sheet whenever it actually overflows that cap, however and whenever
that happens - fixed for good this time, with a regression test that
mirrors the async-load shape.
Also, in the same pass:
- Upload/Move/Copy's bottom bar meta line now always reads as a count
("Uploading 3 items"), never a filename for the single-item case,
and is centered rather than left-aligned - matches the approved
design and removes a bespoke marquee/chip implementation that only
Upload had.
- Upload/Move/Copy now show the same FilesControlsRow (sort, filter,
List/Grid) Files itself uses, including a folder-grid rendering
path for when List/Grid is set to grid.
- Files' filter chip reads "Filters" instead of "All types" when idle.
- Trash's overflow button - whose only action is "Delete forever" -
is now a direct delete icon (mobile) / "Delete" danger pill
(desktop) instead of a generic "..." menu that hid a single action
behind an extra tap.
- The media viewer's floating chrome (top bar, bottom action bar,
video transport row) now follows the app's light/dark theme instead
of a fixed dark panel - a dark panel read as out of place over a
light-theme stage. Panel tint and icon/text colors come from the
theme's own surface/fg1 tokens (at a higher blur opacity to stay
legible over arbitrary photo/video brightness), and the edge scrim
gradients fade toward the panel's own tint instead of a fixed black.
Mirrored the same change into the design canvas's Main.dc.html/
MainAndroid.dc.html mockups (now with a theme prop) so they match.
This commit is contained in:
@@ -38,15 +38,20 @@ Future<T?> showNooSheet<T>(
|
||||
);
|
||||
}
|
||||
|
||||
/// Renders [children] once, unconstrained, to measure their natural height
|
||||
/// against the sheet's available height. Content that fits within
|
||||
/// [_kNooSheetPeekFraction] just stays as that first (plain, content-sized)
|
||||
/// render - the common case for a short menu/option list. Content taller
|
||||
/// than that switches to a [DraggableScrollableSheet] opened at the peek
|
||||
/// fraction instead, so it never jumps straight to (near) full height with
|
||||
/// no room left to adjust. The one extra layout pass for tall content
|
||||
/// happens within the sheet's own entrance animation, so it isn't visible
|
||||
/// as a jump.
|
||||
/// Caps content at [_kNooSheetPeekFraction] of the available height from the
|
||||
/// very first frame - never a one-shot "measure, then decide" pass, since
|
||||
/// some sheets (Details' Versions/Activity tabs) fetch their content
|
||||
/// asynchronously and only reach their real (long) size well after that
|
||||
/// first frame, which a one-shot measurement would miss entirely. Content
|
||||
/// that never needs more than that cap just sizes to itself as a plain
|
||||
/// scroll view, at whatever height that is - the common case for a short
|
||||
/// menu/option list, and visually identical to before. [_needsPeek] flips
|
||||
/// true - upgrading to a draggable [DraggableScrollableSheet], opened at the
|
||||
/// exact same peek height so nothing visibly jumps - the moment a
|
||||
/// [ScrollMetricsNotification] reports the content actually overflowing
|
||||
/// that cap, however and whenever that happens (immediately for a long
|
||||
/// static list, or later, once async content finishes loading and grows
|
||||
/// past it).
|
||||
class _NooSheetBody extends StatefulWidget {
|
||||
final List<Widget> children;
|
||||
|
||||
@@ -57,44 +62,44 @@ class _NooSheetBody extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _NooSheetBodyState extends State<_NooSheetBody> {
|
||||
final _contentKey = GlobalKey();
|
||||
bool _measured = false;
|
||||
bool _needsPeek = false;
|
||||
|
||||
void _measureAfterFrame(double availableHeight) {
|
||||
void _handleOverflow(ScrollMetrics metrics) {
|
||||
if (_needsPeek || metrics.maxScrollExtent <= 0) return;
|
||||
// `ScrollMetricsNotification` is dispatched mid-layout; deferring avoids
|
||||
// a "setState during build" error.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted || _measured) return;
|
||||
final box = _contentKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
final contentHeight = box?.size.height ?? 0;
|
||||
setState(() {
|
||||
_measured = true;
|
||||
_needsPeek = contentHeight > availableHeight * _kNooSheetPeekFraction;
|
||||
});
|
||||
if (mounted && !_needsPeek) setState(() => _needsPeek = true);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildContent(ScrollController? scrollController) {
|
||||
Widget _buildScrollable(ScrollController? scrollController) {
|
||||
final colors = context.nooColors;
|
||||
return SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
|
||||
child: Column(
|
||||
key: _measured ? null : _contentKey,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface3,
|
||||
borderRadius: BorderRadius.circular(NooRadii.pill),
|
||||
return NotificationListener<ScrollMetricsNotification>(
|
||||
onNotification: (notification) {
|
||||
_handleOverflow(notification.metrics);
|
||||
return false;
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface3,
|
||||
borderRadius: BorderRadius.circular(NooRadii.pill),
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final child in widget.children) ...[
|
||||
const SizedBox(height: 22),
|
||||
child,
|
||||
for (final child in widget.children) ...[
|
||||
const SizedBox(height: 22),
|
||||
child,
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -104,12 +109,13 @@ class _NooSheetBodyState extends State<_NooSheetBody> {
|
||||
return SafeArea(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (!_measured) {
|
||||
_measureAfterFrame(constraints.maxHeight);
|
||||
return _buildContent(null);
|
||||
}
|
||||
if (!_needsPeek) {
|
||||
return _buildContent(null);
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: constraints.maxHeight * _kNooSheetPeekFraction,
|
||||
),
|
||||
child: _buildScrollable(null),
|
||||
);
|
||||
}
|
||||
return DraggableScrollableSheet(
|
||||
initialChildSize: _kNooSheetPeekFraction,
|
||||
@@ -129,7 +135,7 @@ class _NooSheetBodyState extends State<_NooSheetBody> {
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: _buildContent(scrollController),
|
||||
child: _buildScrollable(scrollController),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user