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:
2026-09-29 03:49:24 -04:00
co-authored by Claude Sonnet 5
parent 4e6477a3ae
commit f9653bc0f2
14 changed files with 417 additions and 326 deletions
+11 -22
View File
@@ -1471,28 +1471,17 @@ class NextcloudService {
final rawList = data['ocs']?['data'];
if (rawList is! List) return [];
return rawList.map<NextcloudShare>((raw) {
final path = (raw['path'] ?? '/').toString();
final segments = path.split('/').where((s) => s.isNotEmpty).toList();
final name = segments.isNotEmpty ? segments.last : path;
final isFolder = (raw['item_type'] ?? '').toString() == 'folder';
final mimeType = raw['mimetype'] as String?;
return NextcloudShare(
id: (raw['id'] ?? '').toString(),
path: path,
name: name,
itemType: NextcloudItem.deduceType(name, isFolder, mimeType),
shareType: _mapShareType((raw['share_type'] as num?)?.toInt() ?? -1),
ownerDisplayName: (raw['displayname_owner'] ?? raw['uid_owner'] ?? '')
.toString(),
sharedWithDisplayName: raw['share_with_displayname'] as String?,
sharedAt: DateTime.fromMillisecondsSinceEpoch(
((raw['stime'] as num?)?.toInt() ?? 0) * 1000,
),
sharedWithMe: sharedWithMe,
);
}).toList();
// Shares out to `_shareFromJson` (the same parser `fetchSharesForPath`/
// `fetchInheritedShares` use) rather than its own duplicated, thinner
// inline parsing - that copy never set `url`/`token`/`permissions`/
// `expireDate` at all, so a public-link share always came back with
// `url: null` here and the Shares tab's own "Copy link" action could
// never show up for it.
return rawList
.map<NextcloudShare>(
(raw) => _shareFromJson(raw, sharedWithMe: sharedWithMe),
)
.toList();
}
/// Creates a public link share for [path] and returns its share URL.
+35 -124
View File
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../theme/design_tokens.dart';
import '../widgets/noo/nav/noo_top_bar.dart';
import '../widgets/noo/nav/noo_toolbar.dart';
@@ -14,35 +13,33 @@ import '../widgets/settings/settings_security.dart';
import '../widgets/settings/settings_swipe.dart';
import '../widgets/settings/settings_tabs.dart';
/// One entry in [AccountView._sections] - [icon] and [label] are only used
/// by [_SectionJumpRail]'s tap targets, not shown next to the section
/// itself (each section already titles itself).
typedef _Section = (String label, IconData icon, Widget child);
/// Settings, pushed on top of the shell (DESIGN_SYSTEM.md 4's 9-section
/// order: account card, accounts, security, file sync, files cache,
/// appearance, tabs, action bar, swipe on a file). One column of
/// [SettingsSection]s (and [SettingsActionBarSection], which has no option
/// rows of its own to put in one - just the reorder list) on
/// mobile, with a trailing jump rail (see [_SectionJumpRail]) since the
/// list runs long enough that finding a specific section by scrolling
/// alone is slow; a 2-column grid of cards on desktop, wide enough to see
/// most sections without scrolling, so it doesn't get one. See each
/// `widgets/settings/*.dart` file for a section's own content and any
/// rows of its own to put in one - just the reorder list) on mobile, each
/// individually collapsible (expanded by default, tap its label to
/// collapse - see `SettingsSection`/`NooGroupedList`'s `collapsible` param)
/// so a long Settings screen can be collapsed down rather than needing a
/// separate jump rail (an earlier version had one; it added a second,
/// redundant way to navigate on top of plain scrolling and still didn't
/// shorten the page). Desktop uses a 2-column grid of cards instead, wide
/// enough to see most sections without scrolling, so it gets neither. See
/// each `widgets/settings/*.dart` file for a section's own content and any
/// setting that had to be slotted in or grouped under "Advanced appearance".
class AccountView extends StatelessWidget {
const AccountView({super.key});
static const _sections = <_Section>[
('Account', LucideIcons.circleUser, SettingsAccountCard()),
('Accounts', LucideIcons.users, SettingsAccountsSection()),
('Security', LucideIcons.shield, SettingsSecuritySection()),
('File sync', LucideIcons.folderSync, SettingsFileSyncSection()),
('Files cache', LucideIcons.hardDrive, SettingsFilesCacheSection()),
('Appearance', LucideIcons.sunMoon, SettingsAppearanceSection()),
('Tabs', LucideIcons.layoutGrid, SettingsTabsSection()),
('Action bar', LucideIcons.slidersHorizontal, SettingsActionBarSection()),
('Swipe', LucideIcons.moveHorizontal, SettingsSwipeSection()),
static const _sections = <Widget>[
SettingsAccountCard(),
SettingsAccountsSection(),
SettingsSecuritySection(),
SettingsFileSyncSection(),
SettingsFilesCacheSection(),
SettingsAppearanceSection(),
SettingsTabsSection(),
SettingsActionBarSection(),
SettingsSwipeSection(),
];
@override
@@ -62,119 +59,33 @@ class AccountView extends StatelessWidget {
body: SafeArea(
top: false,
child: desktop
? _DesktopGrid(sections: [for (final s in _sections) s.$3])
? _DesktopGrid(sections: _sections)
: _MobileList(sections: _sections),
),
);
}
}
/// A stable per-section identity, shared between the [KeyedSubtree] each
/// section is wrapped in and [_SectionJumpRail]'s lookup - [GlobalObjectKey]
/// compares by [String] equality, not instance identity, so building a new
/// one from the same label on each side still resolves to the same key.
GlobalKey _sectionKey(String label) => GlobalObjectKey(label);
class _MobileList extends StatefulWidget {
final List<_Section> sections;
class _MobileList extends StatelessWidget {
final List<Widget> sections;
const _MobileList({required this.sections});
@override
State<_MobileList> createState() => _MobileListState();
}
class _MobileListState extends State<_MobileList> {
final _scrollController = ScrollController();
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _jumpTo(String label) {
final target = _sectionKey(label).currentContext;
if (target == null) return;
Scrollable.ensureVisible(
target,
duration: NooMotion.slow,
curve: NooMotion.ease,
alignment: 0.05,
);
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
ListView(
controller: _scrollController,
padding: const EdgeInsets.fromLTRB(
NooSpace.sm,
NooSpace.sm,
NooSpace.xxl,
NooSpace.xxl,
),
physics: const BouncingScrollPhysics(),
children: [
for (final (label, _, child) in widget.sections) ...[
KeyedSubtree(key: _sectionKey(label), child: child),
const SizedBox(height: NooSpace.xl),
],
],
),
Positioned(
top: 0,
bottom: 0,
right: 2,
child: _SectionJumpRail(sections: widget.sections, onTap: _jumpTo),
),
],
);
}
}
/// A vertical rail of small per-section icons pinned to the trailing edge,
/// sitting where the scrollbar would otherwise be - tapping one jumps
/// straight to that section instead of scrolling the whole (fairly long)
/// Settings list by hand. Each icon is a generous 32px tap target even
/// though the rail itself is slim, so this stays usable one-handed.
class _SectionJumpRail extends StatelessWidget {
final List<_Section> sections;
final ValueChanged<String> onTap;
const _SectionJumpRail({required this.sections, required this.onTap});
@override
Widget build(BuildContext context) {
final colors = context.nooColors;
return Center(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 2),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(NooRadii.pill),
border: Border.all(color: colors.line),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final (label, icon, _) in sections)
Semantics(
button: true,
label: 'Jump to $label',
child: InkResponse(
onTap: () => onTap(label),
radius: 18,
child: Padding(
padding: const EdgeInsets.all(6),
child: Icon(icon, size: 16, color: colors.fg3),
),
),
),
],
),
return ListView(
padding: const EdgeInsets.fromLTRB(
NooSpace.sm,
NooSpace.sm,
NooSpace.sm,
NooSpace.xxl,
),
physics: const BouncingScrollPhysics(),
children: [
for (final section in sections) ...[
section,
const SizedBox(height: NooSpace.xl),
],
],
);
}
}
+1 -4
View File
@@ -255,10 +255,7 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
children: [
SizedBox(
height: 44,
child: FilesControlsRow(
folderPath: _currentPath,
showStorageScope: false,
),
child: FilesControlsRow(folderPath: _currentPath),
),
if (hasBreadcrumbs) ...[
const SizedBox(height: 10),
+70 -55
View File
@@ -484,65 +484,80 @@ class _PhotosViewState extends State<PhotosView> {
showNooSheet(
context,
children: [
// See `_showSortSheet`'s comment - `Listenable.merge` since these
// toggles span two controllers.
ListenableBuilder(
listenable: Listenable.merge([photos, files]),
builder: (context, _) => Column(
mainAxisSize: MainAxisSize.min,
children: [
// Same segmented-control design as Settings' Theme row - a
// full-width pill track, not chips, and its own row rather
// than a settings row's trailing slot (matches the
// Ascending/Descending control above, in `_showSortSheet`).
NooSegmentedControl<_PhotoTypeFilter>(
fill: true,
value: _typeFilter,
onChanged: (filter) => setState(() => _typeFilter = filter),
options: const [
NooSegmentOption(value: _PhotoTypeFilter.all, label: 'All'),
NooSegmentOption(
value: _PhotoTypeFilter.image,
label: 'Photos',
),
NooSegmentOption(
value: _PhotoTypeFilter.video,
label: 'Videos',
),
],
),
const SizedBox(height: 22),
NooGroupedList(
children: [
NooSettingsRow(
icon: LucideIcons.heart,
label: const Text('Favorites only'),
trailing: NooToggle(
checked: photos.showFavoritesOnly,
onChanged: (_) => photos.toggleFavoritesFilter(),
// `_typeFilter` lives on this State, not a ChangeNotifier, so
// `Listenable.merge([photos, files])` alone doesn't rebuild this
// sheet when it changes - it used to only visibly move once the
// sheet was closed and reopened. `StatefulBuilder` gives it a
// rebuild trigger of its own; see `_showSortSheet`'s comment for
// why the two controllers still need `ListenableBuilder`.
StatefulBuilder(
builder: (context, setSheetState) => ListenableBuilder(
listenable: Listenable.merge([photos, files]),
builder: (context, _) => Column(
mainAxisSize: MainAxisSize.min,
children: [
// Same "icon always, label only when selected" pill as
// Files' own type filter - not a fully-labelled track, so
// the two screens' filter sheets look and behave the same.
NooSegmentedControl<_PhotoTypeFilter>(
fill: true,
labelOnlySelected: true,
value: _typeFilter,
onChanged: (filter) => setState(() {
_typeFilter = filter;
setSheetState(() {});
}),
options: const [
NooSegmentOption(
value: _PhotoTypeFilter.all,
icon: LucideIcons.layoutGrid,
label: 'All',
),
),
NooSettingsRow(
icon: LucideIcons.eye,
label: const Text('Show hidden files'),
trailing: NooToggle(
checked: photos.showHidden,
onChanged: (_) => photos.toggleShowHidden(),
NooSegmentOption(
value: _PhotoTypeFilter.image,
icon: LucideIcons.image,
label: 'Photos',
),
),
NooSettingsRow(
icon: LucideIcons.hardDrive,
label: const Text('External storage'),
trailing: NooToggle(
checked: files.storageScope == StorageScope.external,
onChanged: (external) => files.setStorageScope(
external ? StorageScope.external : StorageScope.cloud,
NooSegmentOption(
value: _PhotoTypeFilter.video,
icon: LucideIcons.film,
label: 'Videos',
),
],
),
const SizedBox(height: 22),
NooGroupedList(
children: [
NooSettingsRow(
icon: LucideIcons.heart,
label: const Text('Favorites only'),
trailing: NooToggle(
checked: photos.showFavoritesOnly,
onChanged: (_) => photos.toggleFavoritesFilter(),
),
),
),
],
),
],
NooSettingsRow(
icon: LucideIcons.eye,
label: const Text('Show hidden files'),
trailing: NooToggle(
checked: photos.showHidden,
onChanged: (_) => photos.toggleShowHidden(),
),
),
NooSettingsRow(
icon: LucideIcons.hardDrive,
label: const Text('External storage'),
trailing: NooToggle(
checked: files.storageScope == StorageScope.external,
onChanged: (external) => files.setStorageScope(
external ? StorageScope.external : StorageScope.cloud,
),
),
),
],
),
],
),
),
),
],
+1 -4
View File
@@ -180,10 +180,7 @@ class _ShareUploadViewState extends State<ShareUploadView> {
children: [
SizedBox(
height: 44,
child: FilesControlsRow(
folderPath: currentPath,
showStorageScope: false,
),
child: FilesControlsRow(folderPath: currentPath),
),
if (hasBreadcrumbs) ...[
const SizedBox(height: 10),
+24 -39
View File
@@ -159,52 +159,37 @@ class FilesControlsRow extends StatelessWidget {
showNooSheet(
context,
children: [
// See `_showSortSheet`'s comment: without this, none of the
// checkmarks/toggles below would move until the sheet is closed
// and reopened.
// See `_showSortSheet`'s comment: without this, neither the type
// filter's selected segment nor the toggles below would move until
// the sheet is closed and reopened.
ListenableBuilder(
listenable: files,
builder: (context, _) => Column(
mainAxisSize: MainAxisSize.min,
children: [
NooGroupedList(
label: 'Show',
children: [
NooSettingsRow(
label: const Text('Files and folders'),
trailing: files.filesTypeFilter == FilesTypeFilter.all
? Icon(
LucideIcons.check,
size: 18,
color: context.nooColors.accentText,
)
: null,
onTap: () => files.setFilesTypeFilter(FilesTypeFilter.all),
// Same "icon always, label only when selected" pill as
// Photos' own type filter below - not a checkmark list, so
// the two screens' filter sheets look and behave the same.
NooSegmentedControl<FilesTypeFilter>(
fill: true,
labelOnlySelected: true,
value: files.filesTypeFilter,
onChanged: files.setFilesTypeFilter,
options: const [
NooSegmentOption(
value: FilesTypeFilter.all,
icon: LucideIcons.layoutGrid,
label: 'All',
),
NooSettingsRow(
label: const Text('Files only'),
trailing: files.filesTypeFilter == FilesTypeFilter.filesOnly
? Icon(
LucideIcons.check,
size: 18,
color: context.nooColors.accentText,
)
: null,
onTap: () =>
files.setFilesTypeFilter(FilesTypeFilter.filesOnly),
NooSegmentOption(
value: FilesTypeFilter.filesOnly,
icon: LucideIcons.file,
label: 'Files',
),
NooSettingsRow(
label: const Text('Folders only'),
trailing:
files.filesTypeFilter == FilesTypeFilter.foldersOnly
? Icon(
LucideIcons.check,
size: 18,
color: context.nooColors.accentText,
)
: null,
onTap: () =>
files.setFilesTypeFilter(FilesTypeFilter.foldersOnly),
NooSegmentOption(
value: FilesTypeFilter.foldersOnly,
icon: LucideIcons.folder,
label: 'Folders',
),
],
),
@@ -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,
),
);
}
}
+107 -41
View File
@@ -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,
);
}
}
+5 -21
View File
@@ -5,6 +5,7 @@ import '../../providers/settings_controller.dart';
import '../../theme/design_tokens.dart';
import '../noo/lists/noo_tab_order_row.dart';
import '../noo/noo_layout.dart';
import 'settings_section.dart';
/// Settings section: the priority order bulk actions (favorite, share,
/// download, ...) show in on the Files/Photos/Favorites multi-select
@@ -40,27 +41,10 @@ class SettingsActionBarSection extends StatelessWidget {
final colors = context.nooColors;
if (!desktop) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
'Action bar',
style: NooText.label.copyWith(color: colors.fg2),
),
),
const SizedBox(height: 4),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
_subtitle,
style: NooText.meta.copyWith(color: colors.fg3),
),
),
const SizedBox(height: 8),
orderList,
],
return SettingsSection(
title: 'Action bar',
subtitle: _subtitle,
children: [orderList],
);
}
+14 -2
View File
@@ -31,6 +31,11 @@ class SettingsSection extends StatelessWidget {
return NooGroupedList(
label: title,
footer: subtitle != null ? Text(subtitle!) : null,
// Every mobile Settings section is individually collapsible,
// expanded by default - replaces the old trailing jump rail (see
// `account_view.dart`'s doc comment) as the way to navigate a long
// Settings screen quickly.
collapsible: true,
children: children,
);
}
@@ -99,8 +104,15 @@ void showSettingsPicker(
required List<Widget> options,
}) {
if (NooLayout.isDesktop(context)) {
showNooDialog(context, title: title, children: [Column(children: options)]);
showNooDialog(
context,
title: title,
children: [Column(children: options)],
);
} else {
showNooSheet(context, children: [NooGroupedList(label: title, children: options)]);
showNooSheet(
context,
children: [NooGroupedList(label: title, children: options)],
);
}
}