Tablet layout polish: tablet-only sidebar layout, floating toolbar, Settings submenus, info notes, adaptive grids; bump to 1.0.1+4
Build App Bundle / build (push) Successful in 6m0s

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-10-01 17:08:22 -04:00
co-authored by Claude Code
parent ed747a0407
commit 0b7df833f6
18 changed files with 300 additions and 195 deletions
+50 -77
View File
@@ -20,11 +20,8 @@ import '../widgets/settings/settings_tabs.dart';
/// order: account card, accounts, security, file sync, files cache,
/// appearance, tabs, action bar, swipe on a file).
///
/// Desktop is unchanged: a 2-column grid of cards ([_DesktopGrid]) wide
/// enough to see every section at once, so it has no scroll-depth problem
/// and needs no menu.
///
/// Mobile is a two-level menu, the way native iOS/Android Settings apps
/// 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,
@@ -47,18 +44,6 @@ import '../widgets/settings/settings_tabs.dart';
class AccountView extends StatelessWidget {
const AccountView({super.key});
static const _sections = <Widget>[
SettingsAccountCard(),
SettingsAccountsSection(),
SettingsSecuritySection(),
SettingsFileSyncSection(),
SettingsFilesCacheSection(),
SettingsAppearanceSection(),
SettingsTabsSection(),
SettingsActionBarSection(),
SettingsSwipeSection(),
];
static final _categories = <_SettingsCategory>[
_SettingsCategory(
title: 'Accounts',
@@ -110,7 +95,7 @@ class AccountView extends StatelessWidget {
return Scaffold(
backgroundColor: colors.bg,
appBar: desktop
? const NooToolbar(title: 'Settings')
? _desktopBar(context, 'Settings')
: NooTopBar(
style: NooLayout.navStyle(context),
title: 'Settings',
@@ -119,7 +104,12 @@ class AccountView extends StatelessWidget {
body: SafeArea(
top: false,
child: desktop
? _DesktopGrid(sections: _sections)
? Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 640),
child: _MobileMenu(categories: _categories),
),
)
: _MobileMenu(categories: _categories),
),
);
@@ -207,78 +197,61 @@ class _SettingsCategoryScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colors = context.nooColors;
final desktop = NooLayout.isDesktop(context);
return Scaffold(
backgroundColor: colors.bg,
appBar: NooTopBar(
style: NooLayout.navStyle(context),
title: category.title,
leading: const NooTopBarBack(),
),
appBar: desktop
? _desktopBar(context, category.title)
: NooTopBar(
style: NooLayout.navStyle(context),
title: category.title,
leading: const NooTopBarBack(),
),
body: SafeArea(
top: false,
child: ListView(
padding: const EdgeInsets.fromLTRB(
NooSpace.sm,
NooSpace.sm,
NooSpace.sm,
NooSpace.xxl,
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)],
),
),
physics: const BouncingScrollPhysics(),
children: [category.builder(context)],
),
),
);
}
}
/// Splits the sections between two columns rather than a strict grid, since
/// each card's content height varies a lot (the tab reorder list and the
/// accounts list can both run much taller than, say, Security) - a fixed
/// grid would either clip content or leave large gaps.
class _DesktopGrid extends StatelessWidget {
final List<Widget> sections;
const _DesktopGrid({required this.sections});
@override
Widget build(BuildContext context) {
final left = <Widget>[];
final right = <Widget>[];
for (var i = 0; i < sections.length; i++) {
(i.isEven ? left : right).add(sections[i]);
}
return SingleChildScrollView(
padding: const EdgeInsets.all(NooSpace.xl),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 1040),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
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: [
Expanded(child: _Column(children: left)),
const SizedBox(width: NooSpace.xl),
Expanded(child: _Column(children: right)),
IconButton(
icon: const Icon(LucideIcons.arrowLeft),
tooltip: 'Back',
onPressed: () => Navigator.maybePop(context),
),
Text(title),
],
),
),
);
}
}
class _Column extends StatelessWidget {
final List<Widget> children;
const _Column({required this.children});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final child in children) ...[
child,
const SizedBox(height: NooSpace.xl),
],
],
);
}
),
);
}
+3 -1
View File
@@ -586,7 +586,9 @@ class _FavoritesViewState extends State<FavoritesView> {
),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: isDesktop ? 5 : 2,
crossAxisCount: isDesktop
? 5
: NooLayout.gridColumns(context, phone: 2, minTile: 180),
crossAxisSpacing: isDesktop ? 16 : 10,
mainAxisSpacing: isDesktop ? 16 : 10,
childAspectRatio: isDesktop ? 0.92 : 0.85,
+6 -1
View File
@@ -754,7 +754,9 @@ class _FilesViewState extends State<FilesView> {
),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: isDesktop ? 5 : 2,
crossAxisCount: isDesktop
? 5
: NooLayout.gridColumns(context, phone: 2, minTile: 180),
childAspectRatio: isDesktop ? 1.05 : 0.92,
crossAxisSpacing: isDesktop ? 16 : 10,
mainAxisSpacing: isDesktop ? 16 : 10,
@@ -1323,6 +1325,9 @@ class _FilesViewState extends State<FilesView> {
selected: isSelected,
thumbnail: _rowThumbnail(context, item, session, NooFileTileSize.desktop),
onTap: () => _handleItemTap(context, item, picking: picking),
onLongPress: _offline || (picking && !pick.pickRequest!.allowMultiple)
? null
: () => _toggleSelection(item),
onMore: !_isSelecting && !picking
? () => _showItemActions(context, item)
: null,
+3 -1
View File
@@ -284,7 +284,9 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
padding: EdgeInsets.symmetric(horizontal: gutter),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: desktop ? 5 : 2,
crossAxisCount: desktop
? 5
: NooLayout.gridColumns(context, phone: 2, minTile: 180),
childAspectRatio: desktop ? 1.05 : 0.92,
crossAxisSpacing: desktop ? 16 : 10,
mainAxisSpacing: desktop ? 16 : 10,
+5 -1
View File
@@ -342,7 +342,11 @@ class _PhotosViewState extends State<PhotosView> {
itemCount: group.items.length,
columns: isDesktop
? NooPhotoGrid.desktopColumns
: NooPhotoGrid.mobileColumns,
: NooLayout.gridColumns(
context,
phone: NooPhotoGrid.mobileColumns,
minTile: 130,
),
gap: isDesktop ? NooPhotoGrid.desktopGap : NooPhotoGrid.mobileGap,
padding: EdgeInsets.symmetric(
horizontal: isDesktop ? NooLayout.gutter(context) : 0,
+3 -1
View File
@@ -209,7 +209,9 @@ class _ShareUploadViewState extends State<ShareUploadView> {
padding: EdgeInsets.symmetric(horizontal: gutter),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: desktop ? 5 : 2,
crossAxisCount: desktop
? 5
: NooLayout.gridColumns(context, phone: 2, minTile: 180),
childAspectRatio: desktop ? 1.05 : 0.92,
crossAxisSpacing: desktop ? 16 : 10,
mainAxisSpacing: desktop ? 16 : 10,