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
Build App Bundle / build (push) Successful in 6m0s
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user