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
+3 -1
View File
@@ -275,7 +275,9 @@ bare system prompt gives no context on its own.
`MainShellView` builds its chrome from the Noo nav kit `MainShellView` builds its chrome from the Noo nav kit
(`widgets/noo/nav/`) and switches between two layouts on (`widgets/noo/nav/`) and switches between two layouts on
`NooLayout.isDesktop`: `NooLayout.isDesktop` (width >= 900dp *and* shortest side >= 600dp, so a
phone in landscape keeps the mobile layout and only tablet-class windows get
the sidebar):
- **Mobile:** `AppTopBar` (`widgets/app_top_bar.dart`) wraps `NooTopBar` - **Mobile:** `AppTopBar` (`widgets/app_top_bar.dart`) wraps `NooTopBar`
for *every* tab (previously only Files had shell-level top chrome, with for *every* tab (previously only Files had shell-level top chrome, with
+16 -2
View File
@@ -80,10 +80,10 @@ Catalog:
| Folder | Components | | Folder | Components |
|---|---| |---|---|
| `core/` | `NooButton`, `NooFab`, `NooChip`, `NooSegmentedControl`, `NooToggle`, `NooSearchField`, `NooAvatar`, `NooBadge`, `NooProgressBar` | | `core/` | `NooButton`, `NooFab`, `NooChip`, `NooSegmentedControl`, `NooToggle`, `NooSearchField`, `NooAvatar`, `NooBadge`, `NooProgressBar` |
| `lists/` | `NooGroupedList`, `NooSettingsRow`, `NooTabOrderRow`, `NooBanner`, `NooSummaryCard`, `NooSelectionBar` | | `lists/` | `NooGroupedList`, `NooSettingsRow`, `NooTabOrderRow`, `NooBanner`, `NooInfoNote` (tinted info callout; `SettingsSection(notice:)`), `NooSummaryCard`, `NooSelectionBar` |
| `files/` | `NooFileKind` (spec §1.2 tiles; `NooFileKind.from(name:, mimeType:, isDirectory:)`), `NooFileTile`, `NooStatusIcon`/`NooSyncStatus`, `NooFileRow` (mobile 64px), `NooFileTableHeader`/`NooFileTableRow` (desktop), `NooSwipeAction` | | `files/` | `NooFileKind` (spec §1.2 tiles; `NooFileKind.from(name:, mimeType:, isDirectory:)`), `NooFileTile`, `NooStatusIcon`/`NooSyncStatus`, `NooFileRow` (mobile 64px), `NooFileTableHeader`/`NooFileTableRow` (desktop), `NooSwipeAction` |
| `media/` | `NooGridCard`, `NooPhotoTile` (video badge, selection), `NooPhotoGroupHeader`/`NooPhotoGrid` (sliver, or `.box`), `NooActivityItem`, `NooStatCard` | | `media/` | `NooGridCard`, `NooPhotoTile` (video badge, selection), `NooPhotoGroupHeader`/`NooPhotoGrid` (sliver, or `.box`), `NooActivityItem`, `NooStatCard` |
| `nav/` | `NooBottomBar`, `NooTopBar` (a `PreferredSizeWidget`) with `NooTopBarButton`/`NooTopBarBack`, `NooDrawer` with its `Account`/`Storage`/`Item`/`Link` parts, `NooSidebar` with `NooSidebarItem`/`Divider`/`Account`/`Storage`, `NooToolbar` | | `nav/` | `NooBottomBar`, `NooTopBar` (a `PreferredSizeWidget`) with `NooTopBarButton`/`NooTopBarBack`, `NooDrawer` with its `Account`/`Storage`/`Item`/`Link` parts, `NooSidebar` with `NooSidebarItem`/`Divider`/`Account`/`Storage`, `NooToolbar` (floating rounded card with margin; `NooToolbar.outerHeight` for `appBar` sizing) |
| `overlays/` | `showNooSheet`, `showNooDialog`/`NooDialog`, `NooOverlayHeader`, `NooTextField`, and the share parts `NooShareSection`, `NooPersonAccessRow`, `NooPermissionPill` | | `overlays/` | `showNooSheet`, `showNooDialog`/`NooDialog`, `NooOverlayHeader`, `NooTextField`, and the share parts `NooShareSection`, `NooPersonAccessRow`, `NooPermissionPill` |
Gotchas: Gotchas:
@@ -336,3 +336,17 @@ blocks are noted where they matter:
Markdown files (`.md`/`.markdown`) in `MediaTextPreview` open rendered via `flutter_markdown_plus` (`Markdown`, styled from Noo tokens in `_markdownStyle`), with a top-right Edit/Preview toggle (hidden for read-only offline copies). Other text files go straight to the editor. Markdown files (`.md`/`.markdown`) in `MediaTextPreview` open rendered via `flutter_markdown_plus` (`Markdown`, styled from Noo tokens in `_markdownStyle`), with a top-right Edit/Preview toggle (hidden for read-only offline copies). Other text files go straight to the editor.
`ShareSheet`: focusing the people search field does not scroll; once the user types, `_revealPeopleSection` animates the "Share with people" section (keyed by `_peopleKey` on the `NooShareSection`, not the inner column) to just below the sheet's top edge with a small gap. `ShareSheet`: focusing the people search field does not scroll; once the user types, `_revealPeopleSection` animates the "Share with people" section (keyed by `_peopleKey` on the `NooShareSection`, not the inner column) to just below the sheet's top edge with a small gap.
## Desktop/tablet layout notes
- The desktop branch in `main.dart` is wrapped in `SafeArea` so tablets keep
system bars clear of the sidebar; the sidebar has no divider between pinned
and hidden tabs.
- Desktop Settings is the same two-level menu as mobile (centered 640px column, back button in the floating toolbar); the
pushed category screens render the desktop card layout.
- Settings that don't apply in wide-tablet (Appearance nav options,
Swipe on a file) show a `NooInfoNote` via `SettingsSection.notice`.
- Mobile-layout grids (Files, Favorites, move/copy picker, share upload,
Photos) take their column count from `NooLayout.gridColumns` (min tile
width, never below the phone count), so a tablet in portrait gets more
columns rather than a few huge tiles.
+1 -1
View File
@@ -1 +1 @@
Redesigned Noo interface, multi-account support, app locks, offline sync, appearance customization, and a markdown viewer and editor. Improved tablet layout: sidebar navigation, floating title bars, and adaptive grids. Long-press selection and Settings submenus now work on tablets.
+64 -61
View File
@@ -444,70 +444,73 @@ class _MainShellViewState extends State<MainShellView> {
if (isDesktop) { if (isDesktop) {
scaffold = Scaffold( scaffold = Scaffold(
backgroundColor: colors.bg, backgroundColor: colors.bg,
body: Row( // Tablets keep the system bars, so inset for them (desktop's
children: [ // insets are zero).
NooSidebar( body: SafeArea(
account: NooSidebarAccount( child: Row(
avatar: NooAvatar( children: [
initials: accountInitial(session.username), NooSidebar(
current: true, account: NooSidebarAccount(
size: 32, avatar: NooAvatar(
initials: accountInitial(session.username),
current: true,
size: 32,
),
name: session.username,
subtitle: serverHost(session.serverUrl),
onTap: () => showAccountSwitcher(context),
), ),
name: session.username, items: [
subtitle: serverHost(session.serverUrl), for (final tab in pinnedTabs)
onTap: () => showAccountSwitcher(context), NooSidebarItem(
), icon: tab.icon,
items: [ label: tab.label,
for (final tab in pinnedTabs) count: trashBadge(tab),
NooSidebarItem( selected: tab == selectedTab,
icon: tab.icon, onTap: () => selectTab(tab),
label: tab.label, ),
count: trashBadge(tab), for (final tab in hiddenTabs)
selected: tab == selectedTab, NooSidebarItem(
onTap: () => selectTab(tab), icon: tab.icon,
), label: tab.label,
if (hiddenTabs.isNotEmpty) const NooSidebarDivider(), count: trashBadge(tab),
for (final tab in hiddenTabs) selected: tab == selectedTab,
NooSidebarItem( onTap: () => selectTab(tab),
icon: tab.icon, ),
label: tab.label,
count: trashBadge(tab),
selected: tab == selectedTab,
onTap: () => selectTab(tab),
),
],
storage: NooSidebarStorage(
value: quotaFraction(quota),
detail: quotaDetail(quota),
),
settings: NooSidebarItem(
icon: LucideIcons.settings,
label: 'Settings',
onTap: () => openSettings(context),
),
),
Expanded(
child: Column(
children: [
NooToolbar(
title: selectedTab.label,
search: const ShellSearchLauncher(onSurface: true),
actions: [
if (canUpload)
NooButton(
icon: LucideIcons.upload,
onTap: () => showCreateMenu(context),
child: const Text('Upload'),
),
],
),
Expanded(
child: ColoredBox(color: colors.surface, child: tabStack),
),
], ],
storage: NooSidebarStorage(
value: quotaFraction(quota),
detail: quotaDetail(quota),
),
settings: NooSidebarItem(
icon: LucideIcons.settings,
label: 'Settings',
onTap: () => openSettings(context),
),
), ),
), Expanded(
], child: Column(
children: [
NooToolbar(
title: selectedTab.label,
search: const ShellSearchLauncher(onSurface: true),
actions: [
if (canUpload)
NooButton(
icon: LucideIcons.upload,
onTap: () => showCreateMenu(context),
child: const Text('Upload'),
),
],
),
Expanded(
child: ColoredBox(color: colors.surface, child: tabStack),
),
],
),
),
],
),
), ),
); );
} else { } else {
+50 -77
View File
@@ -20,11 +20,8 @@ import '../widgets/settings/settings_tabs.dart';
/// order: account card, accounts, security, file sync, files cache, /// order: account card, accounts, security, file sync, files cache,
/// appearance, tabs, action bar, swipe on a file). /// appearance, tabs, action bar, swipe on a file).
/// ///
/// Desktop is unchanged: a 2-column grid of cards ([_DesktopGrid]) wide /// Every layout (including wide tablet, in a centered 640px column) is
/// enough to see every section at once, so it has no scroll-depth problem /// a two-level menu, the way native iOS/Android Settings apps
/// and needs no menu.
///
/// Mobile is a two-level menu, the way native iOS/Android Settings apps
/// work: [SettingsAccountCard] (the account summary, not a settings picker) /// work: [SettingsAccountCard] (the account summary, not a settings picker)
/// stays pinned at the top of a single top-level list ([_MobileMenu]), and /// stays pinned at the top of a single top-level list ([_MobileMenu]), and
/// every other section becomes one tappable [NooSettingsRow] - icon, title, /// every other section becomes one tappable [NooSettingsRow] - icon, title,
@@ -47,18 +44,6 @@ import '../widgets/settings/settings_tabs.dart';
class AccountView extends StatelessWidget { class AccountView extends StatelessWidget {
const AccountView({super.key}); const AccountView({super.key});
static const _sections = <Widget>[
SettingsAccountCard(),
SettingsAccountsSection(),
SettingsSecuritySection(),
SettingsFileSyncSection(),
SettingsFilesCacheSection(),
SettingsAppearanceSection(),
SettingsTabsSection(),
SettingsActionBarSection(),
SettingsSwipeSection(),
];
static final _categories = <_SettingsCategory>[ static final _categories = <_SettingsCategory>[
_SettingsCategory( _SettingsCategory(
title: 'Accounts', title: 'Accounts',
@@ -110,7 +95,7 @@ class AccountView extends StatelessWidget {
return Scaffold( return Scaffold(
backgroundColor: colors.bg, backgroundColor: colors.bg,
appBar: desktop appBar: desktop
? const NooToolbar(title: 'Settings') ? _desktopBar(context, 'Settings')
: NooTopBar( : NooTopBar(
style: NooLayout.navStyle(context), style: NooLayout.navStyle(context),
title: 'Settings', title: 'Settings',
@@ -119,7 +104,12 @@ class AccountView extends StatelessWidget {
body: SafeArea( body: SafeArea(
top: false, top: false,
child: desktop child: desktop
? _DesktopGrid(sections: _sections) ? Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 640),
child: _MobileMenu(categories: _categories),
),
)
: _MobileMenu(categories: _categories), : _MobileMenu(categories: _categories),
), ),
); );
@@ -207,78 +197,61 @@ class _SettingsCategoryScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = context.nooColors; final colors = context.nooColors;
final desktop = NooLayout.isDesktop(context);
return Scaffold( return Scaffold(
backgroundColor: colors.bg, backgroundColor: colors.bg,
appBar: NooTopBar( appBar: desktop
style: NooLayout.navStyle(context), ? _desktopBar(context, category.title)
title: category.title, : NooTopBar(
leading: const NooTopBarBack(), style: NooLayout.navStyle(context),
), title: category.title,
leading: const NooTopBarBack(),
),
body: SafeArea( body: SafeArea(
top: false, top: false,
child: ListView( child: Center(
padding: const EdgeInsets.fromLTRB( child: ConstrainedBox(
NooSpace.sm, constraints: BoxConstraints(
NooSpace.sm, maxWidth: desktop ? 640 : double.infinity,
NooSpace.sm, ),
NooSpace.xxl, 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 PreferredSizeWidget _desktopBar(BuildContext context, String title) {
/// each card's content height varies a lot (the tab reorder list and the final top = MediaQuery.paddingOf(context).top;
/// accounts list can both run much taller than, say, Security) - a fixed return PreferredSize(
/// grid would either clip content or leave large gaps. preferredSize: Size.fromHeight(NooToolbar.outerHeight + top),
class _DesktopGrid extends StatelessWidget { child: Padding(
final List<Widget> sections; padding: EdgeInsets.only(top: top),
const _DesktopGrid({required this.sections}); child: NooToolbar(
titleWidget: Row(
@override mainAxisSize: MainAxisSize.min,
Widget build(BuildContext context) { spacing: NooSpace.sm,
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,
children: [ children: [
Expanded(child: _Column(children: left)), IconButton(
const SizedBox(width: NooSpace.xl), icon: const Icon(LucideIcons.arrowLeft),
Expanded(child: _Column(children: right)), 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( sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: isDesktop ? 5 : 2, crossAxisCount: isDesktop
? 5
: NooLayout.gridColumns(context, phone: 2, minTile: 180),
crossAxisSpacing: isDesktop ? 16 : 10, crossAxisSpacing: isDesktop ? 16 : 10,
mainAxisSpacing: isDesktop ? 16 : 10, mainAxisSpacing: isDesktop ? 16 : 10,
childAspectRatio: isDesktop ? 0.92 : 0.85, childAspectRatio: isDesktop ? 0.92 : 0.85,
+6 -1
View File
@@ -754,7 +754,9 @@ class _FilesViewState extends State<FilesView> {
), ),
sliver: SliverGrid( sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: isDesktop ? 5 : 2, crossAxisCount: isDesktop
? 5
: NooLayout.gridColumns(context, phone: 2, minTile: 180),
childAspectRatio: isDesktop ? 1.05 : 0.92, childAspectRatio: isDesktop ? 1.05 : 0.92,
crossAxisSpacing: isDesktop ? 16 : 10, crossAxisSpacing: isDesktop ? 16 : 10,
mainAxisSpacing: isDesktop ? 16 : 10, mainAxisSpacing: isDesktop ? 16 : 10,
@@ -1323,6 +1325,9 @@ class _FilesViewState extends State<FilesView> {
selected: isSelected, selected: isSelected,
thumbnail: _rowThumbnail(context, item, session, NooFileTileSize.desktop), thumbnail: _rowThumbnail(context, item, session, NooFileTileSize.desktop),
onTap: () => _handleItemTap(context, item, picking: picking), onTap: () => _handleItemTap(context, item, picking: picking),
onLongPress: _offline || (picking && !pick.pickRequest!.allowMultiple)
? null
: () => _toggleSelection(item),
onMore: !_isSelecting && !picking onMore: !_isSelecting && !picking
? () => _showItemActions(context, item) ? () => _showItemActions(context, item)
: null, : null,
+3 -1
View File
@@ -284,7 +284,9 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
padding: EdgeInsets.symmetric(horizontal: gutter), padding: EdgeInsets.symmetric(horizontal: gutter),
sliver: SliverGrid( sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: desktop ? 5 : 2, crossAxisCount: desktop
? 5
: NooLayout.gridColumns(context, phone: 2, minTile: 180),
childAspectRatio: desktop ? 1.05 : 0.92, childAspectRatio: desktop ? 1.05 : 0.92,
crossAxisSpacing: desktop ? 16 : 10, crossAxisSpacing: desktop ? 16 : 10,
mainAxisSpacing: desktop ? 16 : 10, mainAxisSpacing: desktop ? 16 : 10,
+5 -1
View File
@@ -342,7 +342,11 @@ class _PhotosViewState extends State<PhotosView> {
itemCount: group.items.length, itemCount: group.items.length,
columns: isDesktop columns: isDesktop
? NooPhotoGrid.desktopColumns ? NooPhotoGrid.desktopColumns
: NooPhotoGrid.mobileColumns, : NooLayout.gridColumns(
context,
phone: NooPhotoGrid.mobileColumns,
minTile: 130,
),
gap: isDesktop ? NooPhotoGrid.desktopGap : NooPhotoGrid.mobileGap, gap: isDesktop ? NooPhotoGrid.desktopGap : NooPhotoGrid.mobileGap,
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: isDesktop ? NooLayout.gutter(context) : 0, horizontal: isDesktop ? NooLayout.gutter(context) : 0,
+3 -1
View File
@@ -209,7 +209,9 @@ class _ShareUploadViewState extends State<ShareUploadView> {
padding: EdgeInsets.symmetric(horizontal: gutter), padding: EdgeInsets.symmetric(horizontal: gutter),
sliver: SliverGrid( sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: desktop ? 5 : 2, crossAxisCount: desktop
? 5
: NooLayout.gridColumns(context, phone: 2, minTile: 180),
childAspectRatio: desktop ? 1.05 : 0.92, childAspectRatio: desktop ? 1.05 : 0.92,
crossAxisSpacing: desktop ? 16 : 10, crossAxisSpacing: desktop ? 16 : 10,
mainAxisSpacing: desktop ? 16 : 10, mainAxisSpacing: desktop ? 16 : 10,
@@ -154,6 +154,7 @@ class NooFileTableRow extends StatelessWidget {
final VoidCallback? onTap; final VoidCallback? onTap;
final VoidCallback? onDoubleTap; final VoidCallback? onDoubleTap;
final VoidCallback? onSecondaryTap; final VoidCallback? onSecondaryTap;
final VoidCallback? onLongPress;
/// Overflow menu tap. The button is hidden when null. /// Overflow menu tap. The button is hidden when null.
final VoidCallback? onMore; final VoidCallback? onMore;
@@ -171,6 +172,7 @@ class NooFileTableRow extends StatelessWidget {
this.onTap, this.onTap,
this.onDoubleTap, this.onDoubleTap,
this.onSecondaryTap, this.onSecondaryTap,
this.onLongPress,
this.onMore, this.onMore,
}); });
@@ -193,6 +195,7 @@ class NooFileTableRow extends StatelessWidget {
onTap: onTap, onTap: onTap,
onDoubleTap: onDoubleTap, onDoubleTap: onDoubleTap,
onSecondaryTap: onSecondaryTap, onSecondaryTap: onSecondaryTap,
onLongPress: onLongPress,
child: SizedBox( child: SizedBox(
height: NooSizes.rowDesktop, height: NooSizes.rowDesktop,
child: _TableColumns( 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 @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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = context.nooColors; final colors = context.nooColors;
final titleStyle = NooText.title.copyWith(height: 1, color: colors.fg1); final titleStyle = NooText.title.copyWith(height: 1, color: colors.fg1);
return Material( return Padding(
color: backgroundColor ?? colors.surface, padding: _margin,
child: Container( child: Material(
height: NooSizes.toolbar, color: backgroundColor ?? colors.surface,
padding: const EdgeInsetsDirectional.only(start: 24, end: 20), clipBehavior: Clip.antiAlias,
decoration: BoxDecoration( shape: RoundedRectangleBorder(
border: Border(bottom: BorderSide(color: colors.line)), borderRadius: BorderRadius.circular(NooRadii.card),
side: BorderSide(color: colors.line),
), ),
child: Row( child: Container(
spacing: 12, height: NooSizes.toolbar,
children: [ padding: const EdgeInsetsDirectional.only(start: 24, end: 20),
if (showHistoryNav) child: Row(
Padding( spacing: 12,
padding: const EdgeInsetsDirectional.only(end: 4), children: [
child: Row( if (showHistoryNav)
mainAxisSize: MainAxisSize.min, Padding(
spacing: 4, padding: const EdgeInsetsDirectional.only(end: 4),
children: [ child: Row(
_HistoryButton( mainAxisSize: MainAxisSize.min,
icon: LucideIcons.chevronLeft, spacing: 4,
tooltip: 'Back', children: [
onTap: onBack, _HistoryButton(
), icon: LucideIcons.chevronLeft,
_HistoryButton( tooltip: 'Back',
icon: LucideIcons.chevronRight, onTap: onBack,
tooltip: 'Forward', ),
onTap: onForward, _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( if (search != null) SizedBox(width: 260, child: search),
child: Align( ...actions,
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,
],
), ),
), ),
); );
+22 -2
View File
@@ -11,8 +11,16 @@ class NooLayout {
/// (table rows, 5-column grids, 8-column photos, 24px gutter). /// (table rows, 5-column grids, 8-column photos, 24px gutter).
static const double desktopBreakpoint = 900; static const double desktopBreakpoint = 900;
static bool isDesktop(BuildContext context) => /// Shortest window side that counts as a tablet-class window (the usual
MediaQuery.sizeOf(context).width >= desktopBreakpoint; /// 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) => static NooNavStyle navStyle(BuildContext context) =>
NooNavStyle.fromPlatform(Theme.of(context).platform); NooNavStyle.fromPlatform(Theme.of(context).platform);
@@ -24,4 +32,16 @@ class NooLayout {
/// Horizontal content gutter: 12 on mobile (cards), 24 on desktop. /// Horizontal content gutter: 12 on mobile (cards), 24 on desktop.
static double gutter(BuildContext context) => isDesktop(context) ? 24 : 12; 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: [ children: [
SettingsSection( SettingsSection(
title: 'Appearance', title: 'Appearance',
notice:
'Bottom bar style, menu style and search in bottom bar have no effect on wide tablets, which use a sidebar.',
children: [ children: [
_ThemeRow(settings: settings), _ThemeRow(settings: settings),
_BottomBarStyleRow(settings: settings), _BottomBarStyleRow(settings: settings),
+19 -1
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../theme/design_tokens.dart'; import '../../theme/design_tokens.dart';
import '../noo/lists/noo_grouped_list.dart'; import '../noo/lists/noo_grouped_list.dart';
import '../noo/lists/noo_info_note.dart';
import '../noo/noo_layout.dart'; import '../noo/noo_layout.dart';
import '../noo/overlays/noo_dialog.dart'; import '../noo/overlays/noo_dialog.dart';
import '../noo/overlays/noo_sheet.dart'; import '../noo/overlays/noo_sheet.dart';
@@ -17,10 +18,14 @@ class SettingsSection extends StatelessWidget {
final String? subtitle; final String? subtitle;
final List<Widget> children; final List<Widget> children;
/// Shown as a prominent [NooInfoNote] above the rows.
final String? notice;
const SettingsSection({ const SettingsSection({
super.key, super.key,
required this.title, required this.title,
this.subtitle, this.subtitle,
this.notice,
required this.children, required this.children,
}); });
@@ -28,7 +33,7 @@ class SettingsSection extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = context.nooColors; final colors = context.nooColors;
if (!NooLayout.isDesktop(context)) { if (!NooLayout.isDesktop(context)) {
return NooGroupedList( final list = NooGroupedList(
label: title, label: title,
footer: subtitle != null ? Text(subtitle!) : null, footer: subtitle != null ? Text(subtitle!) : null,
// Not collapsible: each mobile Settings section now renders on its // 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. // one is already isolated on its own screen.
children: children, children: children,
); );
if (notice == null) return list;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
NooInfoNote(message: notice!),
const SizedBox(height: NooSpace.md),
list,
],
);
} }
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -57,6 +71,10 @@ class SettingsSection extends StatelessWidget {
Text(subtitle!, style: NooText.meta.copyWith(color: colors.fg3)), Text(subtitle!, style: NooText.meta.copyWith(color: colors.fg3)),
], ],
const SizedBox(height: NooSpace.md), const SizedBox(height: NooSpace.md),
if (notice != null) ...[
NooInfoNote(message: notice!),
const SizedBox(height: NooSpace.md),
],
FlatRowGroup(children: children), FlatRowGroup(children: children),
], ],
), ),
+6 -1
View File
@@ -37,7 +37,11 @@ class SettingsSwipeSection extends StatelessWidget {
NooSettingsRow( NooSettingsRow(
label: Text(_swipeActionLabel(action)), label: Text(_swipeActionLabel(action)),
trailing: action == current trailing: action == current
? Icon(LucideIcons.check, size: 18, color: context.nooColors.accentText) ? Icon(
LucideIcons.check,
size: 18,
color: context.nooColors.accentText,
)
: null, : null,
onTap: () { onTap: () {
onChanged(action); onChanged(action);
@@ -55,6 +59,7 @@ class SettingsSwipeSection extends StatelessWidget {
return SettingsSection( return SettingsSection(
title: 'Swipe on a file', title: 'Swipe on a file',
subtitle: 'Choose what swiping a file left or right does in list view', subtitle: 'Choose what swiping a file left or right does in list view',
notice: 'Swipe actions have no effect on wide tablets.',
children: [ children: [
NooSettingsRow( NooSettingsRow(
icon: LucideIcons.chevronsRight, icon: LucideIcons.chevronsRight,
+1 -1
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+3 version: 1.0.1+4
environment: environment:
sdk: ^3.12.2 sdk: ^3.12.2