Files
noo/lib/widgets/noo/lists/noo_grouped_list.dart
T
ayushyaandClaude Sonnet 5 f9653bc0f2 Fix share copy-link, destination-picker filters, and type-filter UX
- SharesController's fetchShares() built NextcloudShare objects through
  its own thin inline parser instead of _shareFromJson, so url/token/
  permissions/expireDate were always null - a public link share could
  never show "Copy link" in the Shares tab. Reuses _shareFromJson.
- Upload/Move/Copy's destination picker hid External storage in its
  filter sheet (showStorageScope: false) unlike Files' own controls
  row, despite the design doc already calling for full parity.
- Files' and Photos' type filter (All/Files/Folders, All/Photos/Videos)
  now render as one consistent style in both filter sheets: every icon
  always shown, label only on the selected segment
  (NooSegmentedControl's new labelOnlySelected), replacing Files' old
  checkmark list and Photos' own always-labelled track.
- Fixes Photos' type filter only visually updating after closing and
  reopening the filter sheet - it lived on PhotosView's own State, so
  Listenable.merge([photos, files]) never rebuilt the sheet when it
  changed; a StatefulBuilder now gives it that trigger.
- Replaces Settings' trailing section jump rail with per-section
  collapsible cards (NooGroupedList's new collapsible param, expanded
  by default) - one less parallel way to navigate a long screen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:49:24 -04:00

146 lines
4.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../../theme/design_tokens.dart';
/// Radius-20 surface card of rows with 1px line dividers and an optional
/// label above it (Noo Design System project,
/// `components/lists/GroupedList.jsx`, mobile variant - desktop's bordered/
/// titled card isn't built yet). Each child (`SettingsRow`, `FileRow`, ...)
/// paints its own `surface` background; this only supplies the 1px `line`
/// gaps between them and the rounded clip. Stack groups 18-20px apart.
///
/// [collapsible] makes [label] (required alongside it) a tap target that
/// shows/hides the card and [footer] below it, with a trailing chevron -
/// used by `SettingsSection` for Settings' mobile sections so a long
/// Settings screen can be collapsed section by section instead of needing a
/// separate jump rail. Every other call site leaves this false and renders
/// exactly as before, always expanded and non-interactive.
class NooGroupedList extends StatefulWidget {
final String? label;
/// Right-aligned secondary text next to [label].
final String? aside;
final List<Widget> children;
final Widget? footer;
final bool collapsible;
final bool initiallyExpanded;
const NooGroupedList({
super.key,
this.label,
this.aside,
required this.children,
this.footer,
this.collapsible = false,
this.initiallyExpanded = true,
});
@override
State<NooGroupedList> createState() => _NooGroupedListState();
}
class _NooGroupedListState extends State<NooGroupedList> {
late bool _expanded = widget.initiallyExpanded;
@override
Widget build(BuildContext context) {
final colors = context.nooColors;
final expanded = !widget.collapsible || _expanded;
final label = widget.label;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (label != null) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: _buildLabelRow(label, colors, expanded),
),
const SizedBox(height: 8),
],
AnimatedCrossFade(
duration: NooMotion.fast,
sizeCurve: NooMotion.ease,
crossFadeState: expanded
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(NooRadii.card),
child: DecoratedBox(
decoration: BoxDecoration(color: colors.line),
child: Column(
children: [
for (var i = 0; i < widget.children.length; i++) ...[
if (i > 0) const SizedBox(height: 1),
widget.children[i],
],
],
),
),
),
if (widget.footer != null) ...[
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: DefaultTextStyle(
style: NooText.meta.copyWith(color: colors.fg3),
child: widget.footer!,
),
),
],
],
),
secondChild: const SizedBox(width: double.infinity),
),
],
);
}
Widget _buildLabelRow(String label, NooColors colors, bool expanded) {
final row = Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: NooText.label.copyWith(color: colors.fg2)),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.aside != null)
Padding(
padding: EdgeInsets.only(right: widget.collapsible ? 8 : 0),
child: Text(
widget.aside!,
style: NooText.body.copyWith(
fontSize: 13,
height: 1,
fontWeight: FontWeight.w500,
color: colors.fg3,
),
),
),
if (widget.collapsible)
AnimatedRotation(
turns: expanded ? 0.5 : 0,
duration: NooMotion.fast,
curve: NooMotion.ease,
child: Icon(
LucideIcons.chevronDown,
size: 16,
color: colors.fg3,
),
),
],
),
],
);
if (!widget.collapsible) return row;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => setState(() => _expanded = !_expanded),
child: row,
);
}
}