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>
This commit is contained in:
@@ -11,7 +11,11 @@ class NooSegmentOption<T> {
|
||||
|
||||
enum NooSegmentedSize { md, sm, xs }
|
||||
|
||||
const _heights = {NooSegmentedSize.md: 34.0, NooSegmentedSize.sm: 30.0, NooSegmentedSize.xs: 28.0};
|
||||
const _heights = {
|
||||
NooSegmentedSize.md: 34.0,
|
||||
NooSegmentedSize.sm: 30.0,
|
||||
NooSegmentedSize.xs: 28.0,
|
||||
};
|
||||
|
||||
/// Pill track with an accent-soft active segment - List/Grid, Shares scope,
|
||||
/// Theme (Noo Design System project, `components/core/SegmentedControl.jsx`).
|
||||
@@ -21,12 +25,22 @@ class NooSegmentedControl<T> extends StatelessWidget {
|
||||
final List<NooSegmentOption<T>> options;
|
||||
final T value;
|
||||
final ValueChanged<T>? onChanged;
|
||||
|
||||
/// Use surface on bg, surface-2 on surface.
|
||||
final bool onSurface;
|
||||
final NooSegmentedSize size;
|
||||
final bool fill;
|
||||
final bool iconOnly;
|
||||
|
||||
/// Every segment always shows its icon, but only the *selected* one also
|
||||
/// shows its label - a middle ground between `iconOnly` (no segment is
|
||||
/// ever distinguishable without a label) and the default (every segment's
|
||||
/// label always visible, crowding a 3+-option control). Used for Files'/
|
||||
/// Photos' type filter (All/Files/Folders, All/Photos/Videos) so both use
|
||||
/// the same List/Grid-toggle-style pill instead of Files' old checkmark
|
||||
/// list and Photos' own always-labelled control.
|
||||
final bool labelOnlySelected;
|
||||
|
||||
const NooSegmentedControl({
|
||||
super.key,
|
||||
required this.options,
|
||||
@@ -36,6 +50,7 @@ class NooSegmentedControl<T> extends StatelessWidget {
|
||||
this.size = NooSegmentedSize.md,
|
||||
this.fill = false,
|
||||
this.iconOnly = false,
|
||||
this.labelOnlySelected = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -44,10 +59,13 @@ class NooSegmentedControl<T> extends StatelessWidget {
|
||||
final h = iconOnly ? 28.0 : (_heights[size] ?? 34.0);
|
||||
final segments = options.map((o) {
|
||||
final on = o.value == value;
|
||||
final showLabel = labelOnlySelected ? on : !iconOnly;
|
||||
final child = Container(
|
||||
height: h,
|
||||
width: iconOnly ? 36 : null,
|
||||
padding: iconOnly || fill ? EdgeInsets.zero : const EdgeInsets.symmetric(horizontal: 12),
|
||||
padding: iconOnly || fill
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.symmetric(horizontal: 12),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: on ? colors.accentSoft : Colors.transparent,
|
||||
@@ -62,8 +80,9 @@ class NooSegmentedControl<T> extends StatelessWidget {
|
||||
size: iconOnly ? (size == NooSegmentedSize.md ? 18 : 16) : 14,
|
||||
color: on ? colors.accentText : colors.fg2,
|
||||
),
|
||||
if (o.icon != null && !iconOnly && o.label != null) const SizedBox(width: 6),
|
||||
if (!iconOnly && o.label != null)
|
||||
if (o.icon != null && showLabel && o.label != null)
|
||||
const SizedBox(width: 6),
|
||||
if (showLabel && o.label != null)
|
||||
Text(
|
||||
o.label!,
|
||||
style: NooText.body.copyWith(
|
||||
@@ -89,7 +108,10 @@ class NooSegmentedControl<T> extends StatelessWidget {
|
||||
color: onSurface ? colors.surface2 : colors.surface,
|
||||
borderRadius: BorderRadius.circular(NooRadii.pill),
|
||||
),
|
||||
child: Row(mainAxisSize: fill ? MainAxisSize.max : MainAxisSize.min, children: segments),
|
||||
child: Row(
|
||||
mainAxisSize: fill ? MainAxisSize.max : MainAxisSize.min,
|
||||
children: segments,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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
|
||||
@@ -7,12 +8,22 @@ import '../../../theme/design_tokens.dart';
|
||||
/// 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.
|
||||
class NooGroupedList extends StatelessWidget {
|
||||
///
|
||||
/// [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,
|
||||
@@ -20,60 +31,115 @@ class NooGroupedList extends StatelessWidget {
|
||||
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)
|
||||
if (label != null) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label!, style: NooText.label.copyWith(color: colors.fg2)),
|
||||
if (aside != null)
|
||||
Text(
|
||||
aside!,
|
||||
style: NooText.body.copyWith(
|
||||
fontSize: 13,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colors.fg3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: _buildLabelRow(label, colors, expanded),
|
||||
),
|
||||
if (label != null) const SizedBox(height: 8),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(NooRadii.card),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(color: colors.line),
|
||||
child: Column(
|
||||
children: [
|
||||
for (var i = 0; i < children.length; i++) ...[
|
||||
if (i > 0) const SizedBox(height: 1),
|
||||
children[i],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (footer != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: DefaultTextStyle(
|
||||
style: NooText.meta.copyWith(color: colors.fg3),
|
||||
child: footer!,
|
||||
),
|
||||
),
|
||||
],
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user