diff --git a/.claude/context/architecture.md b/.claude/context/architecture.md index cb533ce..0ac3fad 100644 --- a/.claude/context/architecture.md +++ b/.claude/context/architecture.md @@ -275,7 +275,9 @@ bare system prompt gives no context on its own. `MainShellView` builds its chrome from the Noo nav kit (`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` for *every* tab (previously only Files had shell-level top chrome, with diff --git a/.claude/context/styling.md b/.claude/context/styling.md index 2f29e8a..1a98b36 100644 --- a/.claude/context/styling.md +++ b/.claude/context/styling.md @@ -80,10 +80,10 @@ Catalog: | Folder | Components | |---|---| | `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` | | `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` | 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. `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. diff --git a/distribution/whatsnew/whatsnew-en-US b/distribution/whatsnew/whatsnew-en-US index 9fe35c6..484db34 100644 --- a/distribution/whatsnew/whatsnew-en-US +++ b/distribution/whatsnew/whatsnew-en-US @@ -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. \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 4716603..8ec3766 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -444,70 +444,73 @@ class _MainShellViewState extends State { if (isDesktop) { scaffold = Scaffold( backgroundColor: colors.bg, - body: Row( - children: [ - NooSidebar( - account: NooSidebarAccount( - avatar: NooAvatar( - initials: accountInitial(session.username), - current: true, - size: 32, + // Tablets keep the system bars, so inset for them (desktop's + // insets are zero). + body: SafeArea( + child: Row( + children: [ + NooSidebar( + account: NooSidebarAccount( + avatar: NooAvatar( + initials: accountInitial(session.username), + current: true, + size: 32, + ), + name: session.username, + subtitle: serverHost(session.serverUrl), + onTap: () => showAccountSwitcher(context), ), - name: session.username, - subtitle: serverHost(session.serverUrl), - onTap: () => showAccountSwitcher(context), - ), - items: [ - for (final tab in pinnedTabs) - NooSidebarItem( - icon: tab.icon, - label: tab.label, - count: trashBadge(tab), - selected: tab == selectedTab, - onTap: () => selectTab(tab), - ), - if (hiddenTabs.isNotEmpty) const NooSidebarDivider(), - for (final tab in hiddenTabs) - NooSidebarItem( - 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), - ), + items: [ + for (final tab in pinnedTabs) + NooSidebarItem( + icon: tab.icon, + label: tab.label, + count: trashBadge(tab), + selected: tab == selectedTab, + onTap: () => selectTab(tab), + ), + for (final tab in hiddenTabs) + NooSidebarItem( + 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), + ), + ], + ), + ), + ], + ), ), ); } else { diff --git a/lib/views/account_view.dart b/lib/views/account_view.dart index cdb1a0e..fb008f9 100644 --- a/lib/views/account_view.dart +++ b/lib/views/account_view.dart @@ -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 = [ - 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 sections; - const _DesktopGrid({required this.sections}); - - @override - Widget build(BuildContext context) { - final left = []; - final right = []; - 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 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), - ], - ], - ); - } + ), + ); } diff --git a/lib/views/favorites_view.dart b/lib/views/favorites_view.dart index df68879..3cb6c4d 100644 --- a/lib/views/favorites_view.dart +++ b/lib/views/favorites_view.dart @@ -586,7 +586,9 @@ class _FavoritesViewState extends State { ), 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, diff --git a/lib/views/files_view.dart b/lib/views/files_view.dart index 02d2946..f834097 100644 --- a/lib/views/files_view.dart +++ b/lib/views/files_view.dart @@ -754,7 +754,9 @@ class _FilesViewState extends State { ), 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 { 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, diff --git a/lib/views/move_copy_destination_picker.dart b/lib/views/move_copy_destination_picker.dart index b07022b..8470e27 100644 --- a/lib/views/move_copy_destination_picker.dart +++ b/lib/views/move_copy_destination_picker.dart @@ -284,7 +284,9 @@ class _MoveCopyDestinationPickerState extends State { 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, diff --git a/lib/views/photos_view.dart b/lib/views/photos_view.dart index 7b02541..51bd79b 100644 --- a/lib/views/photos_view.dart +++ b/lib/views/photos_view.dart @@ -342,7 +342,11 @@ class _PhotosViewState extends State { 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, diff --git a/lib/views/share_upload_view.dart b/lib/views/share_upload_view.dart index c7181e2..70c526c 100644 --- a/lib/views/share_upload_view.dart +++ b/lib/views/share_upload_view.dart @@ -209,7 +209,9 @@ class _ShareUploadViewState extends State { 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, diff --git a/lib/widgets/noo/files/noo_file_table.dart b/lib/widgets/noo/files/noo_file_table.dart index 5a13d97..50ea8ae 100644 --- a/lib/widgets/noo/files/noo_file_table.dart +++ b/lib/widgets/noo/files/noo_file_table.dart @@ -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( diff --git a/lib/widgets/noo/lists/noo_info_note.dart b/lib/widgets/noo/lists/noo_info_note.dart new file mode 100644 index 0000000..d434020 --- /dev/null +++ b/lib/widgets/noo/lists/noo_info_note.dart @@ -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), + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/noo/nav/noo_toolbar.dart b/lib/widgets/noo/nav/noo_toolbar.dart index 18e85d6..914f724 100644 --- a/lib/widgets/noo/nav/noo_toolbar.dart +++ b/lib/widgets/noo/nav/noo_toolbar.dart @@ -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, + ], + ), ), ), ); diff --git a/lib/widgets/noo/noo_layout.dart b/lib/widgets/noo/noo_layout.dart index 34c3fb8..239342d 100644 --- a/lib/widgets/noo/noo_layout.dart +++ b/lib/widgets/noo/noo_layout.dart @@ -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); + } } diff --git a/lib/widgets/settings/settings_appearance.dart b/lib/widgets/settings/settings_appearance.dart index a0fd0da..be07076 100644 --- a/lib/widgets/settings/settings_appearance.dart +++ b/lib/widgets/settings/settings_appearance.dart @@ -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), diff --git a/lib/widgets/settings/settings_section.dart b/lib/widgets/settings/settings_section.dart index d2a8407..1c5789a 100644 --- a/lib/widgets/settings/settings_section.dart +++ b/lib/widgets/settings/settings_section.dart @@ -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 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), ], ), diff --git a/lib/widgets/settings/settings_swipe.dart b/lib/widgets/settings/settings_swipe.dart index d4f7613..0165994 100644 --- a/lib/widgets/settings/settings_swipe.dart +++ b/lib/widgets/settings/settings_swipe.dart @@ -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, diff --git a/pubspec.yaml b/pubspec.yaml index 99d9697..834651f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 # 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. -version: 1.0.0+3 +version: 1.0.1+4 environment: sdk: ^3.12.2