Ten fixes: controls-row alignment, banners, shares, viewer bounds

- FilesControlsRow (and Photos' own controls row) wrapped its Row in a
  SingleChildScrollView, which gives a child unbounded width - silently
  breaking the trailing Spacer (a flex child needs bounded width) and
  leaving the List/Grid toggle sitting right after the filter chip
  instead of pinned to the row's right edge, "not enough controls to
  fill the row" or not. Removed the scroll wrapper (two chips and an
  icon toggle never need it) and the dead scroll-hint plumbing that
  went with it in files_view.dart.
- Photos' All/Photos/Videos chips moved into the filter sheet as a
  full-width segmented control (matching Settings' Theme control),
  instead of their own row of chips competing for space with the sort
  and filter chips.
- Settings' Theme row: the System/Light/Dark segmented control now
  sits on its own row below the label instead of squeezed into a
  trailing slot beside it, where three icon+label segments were too
  cramped to read comfortably - same shape as the sort sheet's
  Ascending/Descending control.
- NooBanner (Trash's "Empty trash") and NooSummaryCard (Offline's
  "Sync now") both move their action onto its own row below the
  description text instead of squeezed onto the same line, where a
  longer caption had no room to share with it.
- SharesController now caches each scope (with you / by you, which
  also covers links) after its first fetch instead of re-fetching on
  every single switch, including switching straight back to a scope
  already loaded seconds ago.
- Shares' per-row overflow menu jumped straight to the unshare
  confirmation with no menu at all - a link share had no way to copy
  its own URL short of opening the full Share sheet. It's a real menu
  now: "Copy link" (link shares only) plus remove/unshare.
- Settings gained a trailing jump rail on mobile (one small icon per
  section) so a specific section can be reached in one tap instead of
  scrolling the whole list by hand; desktop's 2-column grid doesn't
  need one.
- The media viewer's PageView now disables its own left/right swipe
  while the current photo is zoomed in, so panning around it doesn't
  also swipe to the next item.
- The PDF preview no longer sets minScale below 1.0: pdfx's
  PdfViewPinch hard-codes an *infinite* pan boundary whenever minScale
  < 1 (not an exposed parameter, so there's no way to override it),
  which let a page be panned arbitrarily far off-screen without ever
  zooming.
This commit is contained in:
2026-09-28 22:55:31 -04:00
parent 0abea46b1c
commit a127d72d56
13 changed files with 556 additions and 314 deletions
+1 -85
View File
@@ -227,12 +227,9 @@ class _GridThumbnail extends StatelessWidget {
}
}
class _FilesViewState extends State<FilesView>
with SingleTickerProviderStateMixin {
class _FilesViewState extends State<FilesView> {
final Set<String> _selectedIds = {};
int _lastPathDepth = 1;
final ScrollController _controlsScrollController = ScrollController();
final List<AnimationController> _scrollHintControllers = [];
bool get _isSelecting => _selectedIds.isNotEmpty;
@@ -264,84 +261,6 @@ class _FilesViewState extends State<FilesView>
// that just never got its initial fetch.
bool _requestedInitialLoad = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback(
(_) => _playScrollHint(_controlsScrollController),
);
}
/// A one-shot hint that a horizontally-scrollable row actually scrolls:
/// nudges it a little to the right and back, once - a motion cue instead
/// of a persistent widget (a chevron badge, an edge fade) sitting on top
/// of the actual controls the whole time. Shared by the controls row
/// (played once it first appears) and the selection actions row (played
/// the first time a selection starts, see `_toggleSelection`). No-ops if
/// there's nothing to scroll (row already fits).
Future<void> _playScrollHint(ScrollController scrollController) async {
// The delay lets the row's first frame (and its actual layout/max
// scroll extent) settle before nudging it, and reads more like a
// deliberate hint than something that happens to fire on load.
await Future.delayed(const Duration(milliseconds: 500));
// `hasClients` only means a position is attached, not that it's
// finished its first layout - reading maxScrollExtent before that
// throws (min/maxScrollExtent are still null internally).
if (!mounted ||
!scrollController.hasClients ||
!scrollController.position.hasContentDimensions) {
return;
}
final maxExtent = scrollController.position.maxScrollExtent;
if (maxExtent <= 0) return;
final double peak = maxExtent < 36 ? maxExtent : 36;
// Driven as a single controller (rather than two chained animateTo
// calls) with mirrored ease-in-out halves, so the motion decelerates
// smoothly into the peak and back out instead of visibly changing
// pace where the two legs meet.
final controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
);
_scrollHintControllers.add(controller);
final hint = TweenSequence<double>([
TweenSequenceItem(
tween: Tween(
begin: 0.0,
end: peak,
).chain(CurveTween(curve: Curves.easeInOutSine)),
weight: 50,
),
TweenSequenceItem(
tween: Tween(
begin: peak,
end: 0.0,
).chain(CurveTween(curve: Curves.easeInOutSine)),
weight: 50,
),
]).animate(controller);
void onTick() {
if (scrollController.hasClients) {
scrollController.jumpTo(hint.value);
}
}
hint.addListener(onTick);
await controller.forward();
hint.removeListener(onTick);
_scrollHintControllers.remove(controller);
controller.dispose();
}
@override
void dispose() {
for (final controller in _scrollHintControllers) {
controller.dispose();
}
_controlsScrollController.dispose();
super.dispose();
}
void _toggleSelection(NextcloudItem item) {
HapticFeedback.selectionClick();
setState(() {
@@ -703,13 +622,10 @@ class _FilesViewState extends State<FilesView>
children: [
Row(
children: [
// The scroll-hint nudge (see _playScrollHint) plays on
// this row once, right after it first appears.
Expanded(
child: FilesControlsRow(
folderPath: browser.currentFolderPath,
showStorageScope: !_offline,
scrollController: _controlsScrollController,
),
),
if (_offline) ...[