Build App Bundle / build (push) Successful in 6m0s
Co-Authored-By: Claude Code <noreply@anthropic.com>
258 lines
8.7 KiB
Dart
258 lines
8.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
|
import '../theme/design_tokens.dart';
|
|
import '../widgets/noo/lists/noo_grouped_list.dart';
|
|
import '../widgets/noo/lists/noo_settings_row.dart';
|
|
import '../widgets/noo/nav/noo_top_bar.dart';
|
|
import '../widgets/noo/nav/noo_toolbar.dart';
|
|
import '../widgets/noo/noo_layout.dart';
|
|
import '../widgets/settings/settings_account_card.dart';
|
|
import '../widgets/settings/settings_accounts.dart';
|
|
import '../widgets/settings/settings_action_bar.dart';
|
|
import '../widgets/settings/settings_appearance.dart';
|
|
import '../widgets/settings/settings_file_sync.dart';
|
|
import '../widgets/settings/settings_files_cache.dart';
|
|
import '../widgets/settings/settings_security.dart';
|
|
import '../widgets/settings/settings_swipe.dart';
|
|
import '../widgets/settings/settings_tabs.dart';
|
|
|
|
/// 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).
|
|
///
|
|
/// Every layout (including wide tablet, in a centered 640px column) is
|
|
/// a two-level menu, the way native iOS/Android Settings apps
|
|
/// work: [SettingsAccountCard] (the account summary, not a settings picker)
|
|
/// stays pinned at the top of a single top-level list ([_MobileMenu]), and
|
|
/// every other section becomes one tappable [NooSettingsRow] - icon, title,
|
|
/// chevron - in a [NooGroupedList] below it. Tapping a row pushes a new
|
|
/// [_SettingsCategoryScreen] with its own [NooTopBar]/[NooTopBarBack],
|
|
/// containing just that section's content full-screen. This replaces an
|
|
/// earlier design where every section rendered inline in one long
|
|
/// collapsible-sections column (`_MobileList`, since removed) - and before
|
|
/// that, a trailing jump rail (an even earlier version) that scrolled that
|
|
/// *same* page to an anchor. Both were rejected: the jump rail added a
|
|
/// second, redundant way to navigate on top of plain scrolling without
|
|
/// shortening the page, and the collapsible-sections column still left a
|
|
/// long page to scroll past even collapsed. A genuinely separate pushed
|
|
/// screen per category removes the scroll-depth problem outright, so
|
|
/// neither a jump rail nor per-section collapsing is needed any more - see
|
|
/// [SettingsSection]'s doc comment for how that reflects in its mobile
|
|
/// layout. 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 final _categories = <_SettingsCategory>[
|
|
_SettingsCategory(
|
|
title: 'Accounts',
|
|
icon: LucideIcons.users,
|
|
builder: (_) => const SettingsAccountsSection(),
|
|
),
|
|
_SettingsCategory(
|
|
title: 'Security',
|
|
icon: LucideIcons.lock,
|
|
builder: (_) => const SettingsSecuritySection(),
|
|
),
|
|
_SettingsCategory(
|
|
title: 'File sync',
|
|
icon: LucideIcons.cloud,
|
|
builder: (_) => const SettingsFileSyncSection(),
|
|
),
|
|
_SettingsCategory(
|
|
title: 'Files cache',
|
|
icon: LucideIcons.database,
|
|
builder: (_) => const SettingsFilesCacheSection(),
|
|
),
|
|
_SettingsCategory(
|
|
title: 'Appearance',
|
|
icon: LucideIcons.sunMoon,
|
|
builder: (_) => const SettingsAppearanceSection(),
|
|
),
|
|
_SettingsCategory(
|
|
title: 'Tabs',
|
|
icon: LucideIcons.layoutGrid,
|
|
builder: (_) => const SettingsTabsSection(),
|
|
),
|
|
_SettingsCategory(
|
|
title: 'Action bar',
|
|
icon: LucideIcons.slidersHorizontal,
|
|
builder: (_) => const SettingsActionBarSection(),
|
|
),
|
|
_SettingsCategory(
|
|
title: 'Swipe on a file',
|
|
icon: LucideIcons.chevronsLeftRight,
|
|
builder: (_) => const SettingsSwipeSection(),
|
|
),
|
|
];
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colors = context.nooColors;
|
|
final desktop = NooLayout.isDesktop(context);
|
|
|
|
return Scaffold(
|
|
backgroundColor: colors.bg,
|
|
appBar: desktop
|
|
? _desktopBar(context, 'Settings')
|
|
: NooTopBar(
|
|
style: NooLayout.navStyle(context),
|
|
title: 'Settings',
|
|
leading: const NooTopBarBack(),
|
|
),
|
|
body: SafeArea(
|
|
top: false,
|
|
child: desktop
|
|
? Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 640),
|
|
child: _MobileMenu(categories: _categories),
|
|
),
|
|
)
|
|
: _MobileMenu(categories: _categories),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// One row of [AccountView]'s mobile top-level menu: a title, a leading
|
|
/// icon (reused from that section's own first/most-representative row, so
|
|
/// the menu icon and the content the user lands on agree), and a builder
|
|
/// for the section content shown on [_SettingsCategoryScreen].
|
|
class _SettingsCategory {
|
|
final String title;
|
|
final IconData icon;
|
|
final WidgetBuilder builder;
|
|
|
|
_SettingsCategory({
|
|
required this.title,
|
|
required this.icon,
|
|
required this.builder,
|
|
});
|
|
}
|
|
|
|
/// The mobile top-level Settings list: [SettingsAccountCard] pinned above a
|
|
/// single [NooGroupedList] of category rows, one per [AccountView._categories]
|
|
/// entry - the menu half of the menu-then-pushed-screen pattern described on
|
|
/// [AccountView]'s own doc comment.
|
|
class _MobileMenu extends StatelessWidget {
|
|
final List<_SettingsCategory> categories;
|
|
const _MobileMenu({required this.categories});
|
|
|
|
void _open(BuildContext context, _SettingsCategory category) {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => _SettingsCategoryScreen(category: category),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colors = context.nooColors;
|
|
|
|
return ListView(
|
|
padding: const EdgeInsets.fromLTRB(
|
|
NooSpace.sm,
|
|
NooSpace.sm,
|
|
NooSpace.sm,
|
|
NooSpace.xxl,
|
|
),
|
|
physics: const BouncingScrollPhysics(),
|
|
children: [
|
|
const SettingsAccountCard(),
|
|
const SizedBox(height: NooSpace.xl),
|
|
NooGroupedList(
|
|
children: [
|
|
for (final category in categories)
|
|
NooSettingsRow(
|
|
icon: category.icon,
|
|
label: Text(category.title),
|
|
trailing: Icon(
|
|
LucideIcons.chevronRight,
|
|
size: 18,
|
|
color: colors.fg3,
|
|
),
|
|
onTap: () => _open(context, category),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A pushed, single-category Settings screen: [NooTopBar] titled with the
|
|
/// category, a back button, and just that section's own content - the
|
|
/// pushed half of [AccountView]'s mobile menu-then-screen pattern. Always
|
|
/// built in a mobile-width context (desktop never opens this screen; it
|
|
/// shows every section inline in its own grid instead), so the section
|
|
/// widgets inside render their normal mobile layout unchanged.
|
|
class _SettingsCategoryScreen extends StatelessWidget {
|
|
final _SettingsCategory category;
|
|
const _SettingsCategoryScreen({required this.category});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colors = context.nooColors;
|
|
final desktop = NooLayout.isDesktop(context);
|
|
|
|
return Scaffold(
|
|
backgroundColor: colors.bg,
|
|
appBar: desktop
|
|
? _desktopBar(context, category.title)
|
|
: NooTopBar(
|
|
style: NooLayout.navStyle(context),
|
|
title: category.title,
|
|
leading: const NooTopBarBack(),
|
|
),
|
|
body: SafeArea(
|
|
top: false,
|
|
child: Center(
|
|
child: ConstrainedBox(
|
|
constraints: BoxConstraints(
|
|
maxWidth: desktop ? 640 : double.infinity,
|
|
),
|
|
child: ListView(
|
|
padding: const EdgeInsets.fromLTRB(
|
|
NooSpace.sm,
|
|
NooSpace.sm,
|
|
NooSpace.sm,
|
|
NooSpace.xxl,
|
|
),
|
|
physics: const BouncingScrollPhysics(),
|
|
children: [category.builder(context)],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
PreferredSizeWidget _desktopBar(BuildContext context, String title) {
|
|
final top = MediaQuery.paddingOf(context).top;
|
|
return PreferredSize(
|
|
preferredSize: Size.fromHeight(NooToolbar.outerHeight + top),
|
|
child: Padding(
|
|
padding: EdgeInsets.only(top: top),
|
|
child: NooToolbar(
|
|
titleWidget: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
spacing: NooSpace.sm,
|
|
children: [
|
|
IconButton(
|
|
icon: const Icon(LucideIcons.arrowLeft),
|
|
tooltip: 'Back',
|
|
onPressed: () => Navigator.maybePop(context),
|
|
),
|
|
Text(title),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|