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
@@ -154,6 +154,7 @@ class NooFileTableRow extends StatelessWidget {
final VoidCallback? onTap;
final VoidCallback? onDoubleTap;
final VoidCallback? onSecondaryTap;
final VoidCallback? onLongPress;
/// Overflow menu tap. The button is hidden when null.
final VoidCallback? onMore;
@@ -171,6 +172,7 @@ class NooFileTableRow extends StatelessWidget {
this.onTap,
this.onDoubleTap,
this.onSecondaryTap,
this.onLongPress,
this.onMore,
});
@@ -193,6 +195,7 @@ class NooFileTableRow extends StatelessWidget {
onTap: onTap,
onDoubleTap: onDoubleTap,
onSecondaryTap: onSecondaryTap,
onLongPress: onLongPress,
child: SizedBox(
height: NooSizes.rowDesktop,
child: _TableColumns(
+40
View File
@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../../theme/design_tokens.dart';
/// Tinted info callout (info-soft fill, info icon) for a short notice that
/// must be noticed immediately, unlike a muted footer/subtitle line.
class NooInfoNote extends StatelessWidget {
final String message;
const NooInfoNote({super.key, required this.message});
@override
Widget build(BuildContext context) {
final colors = context.nooColors;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
color: colors.infoSoft,
borderRadius: BorderRadius.circular(NooRadii.input),
border: Border.all(color: colors.info.withValues(alpha: 0.35)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 1),
child: Icon(LucideIcons.info, size: 18, color: colors.info),
),
const SizedBox(width: 10),
Expanded(
child: Text(
message,
style: NooText.body.copyWith(color: colors.fg1, height: 1.35),
),
),
],
),
);
}
}
+53 -43
View File
@@ -48,58 +48,68 @@ class NooToolbar extends StatelessWidget implements PreferredSizeWidget {
);
@override
Size get preferredSize => const Size.fromHeight(NooSizes.toolbar);
Size get preferredSize => const Size.fromHeight(outerHeight);
static const _margin = EdgeInsets.fromLTRB(12, 8, 12, 4);
/// Bar height plus the margin that makes it float.
static const double outerHeight = NooSizes.toolbar + 12;
@override
Widget build(BuildContext context) {
final colors = context.nooColors;
final titleStyle = NooText.title.copyWith(height: 1, color: colors.fg1);
return Material(
color: backgroundColor ?? colors.surface,
child: Container(
height: NooSizes.toolbar,
padding: const EdgeInsetsDirectional.only(start: 24, end: 20),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: colors.line)),
return Padding(
padding: _margin,
child: Material(
color: backgroundColor ?? colors.surface,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(NooRadii.card),
side: BorderSide(color: colors.line),
),
child: Row(
spacing: 12,
children: [
if (showHistoryNav)
Padding(
padding: const EdgeInsetsDirectional.only(end: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 4,
children: [
_HistoryButton(
icon: LucideIcons.chevronLeft,
tooltip: 'Back',
onTap: onBack,
),
_HistoryButton(
icon: LucideIcons.chevronRight,
tooltip: 'Forward',
onTap: onForward,
),
],
child: Container(
height: NooSizes.toolbar,
padding: const EdgeInsetsDirectional.only(start: 24, end: 20),
child: Row(
spacing: 12,
children: [
if (showHistoryNav)
Padding(
padding: const EdgeInsetsDirectional.only(end: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 4,
children: [
_HistoryButton(
icon: LucideIcons.chevronLeft,
tooltip: 'Back',
onTap: onBack,
),
_HistoryButton(
icon: LucideIcons.chevronRight,
tooltip: 'Forward',
onTap: onForward,
),
],
),
),
Expanded(
child: Align(
alignment: AlignmentDirectional.centerStart,
child: DefaultTextStyle(
style: titleStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: titleWidget ?? Text(title ?? ''),
),
),
),
Expanded(
child: Align(
alignment: AlignmentDirectional.centerStart,
child: DefaultTextStyle(
style: titleStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: titleWidget ?? Text(title ?? ''),
),
),
),
if (search != null) SizedBox(width: 260, child: search),
...actions,
],
if (search != null) SizedBox(width: 260, child: search),
...actions,
],
),
),
),
);
+22 -2
View File
@@ -11,8 +11,16 @@ class NooLayout {
/// (table rows, 5-column grids, 8-column photos, 24px gutter).
static const double desktopBreakpoint = 900;
static bool isDesktop(BuildContext context) =>
MediaQuery.sizeOf(context).width >= desktopBreakpoint;
/// Shortest window side that counts as a tablet-class window (the usual
/// 600dp rule). A phone in landscape is wide but short, so it stays on the
/// mobile layout.
static const double tabletShortestSide = 600;
static bool isDesktop(BuildContext context) {
final size = MediaQuery.sizeOf(context);
return size.width >= desktopBreakpoint &&
size.shortestSide >= tabletShortestSide;
}
static NooNavStyle navStyle(BuildContext context) =>
NooNavStyle.fromPlatform(Theme.of(context).platform);
@@ -24,4 +32,16 @@ class NooLayout {
/// Horizontal content gutter: 12 on mobile (cards), 24 on desktop.
static double gutter(BuildContext context) => isDesktop(context) ? 24 : 12;
/// Grid column count for [minTile]-wide tiles, never fewer than [phone]
/// (so phones keep their usual count while wide non-sidebar windows, e.g. a
/// tablet in portrait, get more instead of a few huge tiles).
static int gridColumns(
BuildContext context, {
required int phone,
required double minTile,
}) {
final width = MediaQuery.sizeOf(context).width;
return (width / minTile).floor().clamp(phone, 12);
}
}
@@ -48,6 +48,8 @@ class SettingsAppearanceSection extends StatelessWidget {
children: [
SettingsSection(
title: 'Appearance',
notice:
'Bottom bar style, menu style and search in bottom bar have no effect on wide tablets, which use a sidebar.',
children: [
_ThemeRow(settings: settings),
_BottomBarStyleRow(settings: settings),
+19 -1
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import '../../theme/design_tokens.dart';
import '../noo/lists/noo_grouped_list.dart';
import '../noo/lists/noo_info_note.dart';
import '../noo/noo_layout.dart';
import '../noo/overlays/noo_dialog.dart';
import '../noo/overlays/noo_sheet.dart';
@@ -17,10 +18,14 @@ class SettingsSection extends StatelessWidget {
final String? subtitle;
final List<Widget> children;
/// Shown as a prominent [NooInfoNote] above the rows.
final String? notice;
const SettingsSection({
super.key,
required this.title,
this.subtitle,
this.notice,
required this.children,
});
@@ -28,7 +33,7 @@ class SettingsSection extends StatelessWidget {
Widget build(BuildContext context) {
final colors = context.nooColors;
if (!NooLayout.isDesktop(context)) {
return NooGroupedList(
final list = NooGroupedList(
label: title,
footer: subtitle != null ? Text(subtitle!) : null,
// Not collapsible: each mobile Settings section now renders on its
@@ -40,6 +45,15 @@ class SettingsSection extends StatelessWidget {
// one is already isolated on its own screen.
children: children,
);
if (notice == null) return list;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
NooInfoNote(message: notice!),
const SizedBox(height: NooSpace.md),
list,
],
);
}
return Container(
decoration: BoxDecoration(
@@ -57,6 +71,10 @@ class SettingsSection extends StatelessWidget {
Text(subtitle!, style: NooText.meta.copyWith(color: colors.fg3)),
],
const SizedBox(height: NooSpace.md),
if (notice != null) ...[
NooInfoNote(message: notice!),
const SizedBox(height: NooSpace.md),
],
FlatRowGroup(children: children),
],
),
+6 -1
View File
@@ -37,7 +37,11 @@ class SettingsSwipeSection extends StatelessWidget {
NooSettingsRow(
label: Text(_swipeActionLabel(action)),
trailing: action == current
? Icon(LucideIcons.check, size: 18, color: context.nooColors.accentText)
? Icon(
LucideIcons.check,
size: 18,
color: context.nooColors.accentText,
)
: null,
onTap: () {
onChanged(action);
@@ -55,6 +59,7 @@ class SettingsSwipeSection extends StatelessWidget {
return SettingsSection(
title: 'Swipe on a file',
subtitle: 'Choose what swiping a file left or right does in list view',
notice: 'Swipe actions have no effect on wide tablets.',
children: [
NooSettingsRow(
icon: LucideIcons.chevronsRight,