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:
+35
-124
@@ -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),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user