Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b7df833f6 |
@@ -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
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
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.
|
||||||
+5
-2
@@ -444,7 +444,10 @@ 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
|
||||||
|
// insets are zero).
|
||||||
|
body: SafeArea(
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
NooSidebar(
|
NooSidebar(
|
||||||
account: NooSidebarAccount(
|
account: NooSidebarAccount(
|
||||||
@@ -466,7 +469,6 @@ class _MainShellViewState extends State<MainShellView> {
|
|||||||
selected: tab == selectedTab,
|
selected: tab == selectedTab,
|
||||||
onTap: () => selectTab(tab),
|
onTap: () => selectTab(tab),
|
||||||
),
|
),
|
||||||
if (hiddenTabs.isNotEmpty) const NooSidebarDivider(),
|
|
||||||
for (final tab in hiddenTabs)
|
for (final tab in hiddenTabs)
|
||||||
NooSidebarItem(
|
NooSidebarItem(
|
||||||
icon: tab.icon,
|
icon: tab.icon,
|
||||||
@@ -509,6 +511,7 @@ class _MainShellViewState extends State<MainShellView> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final bottomBarStyle = settings.bottomBarStyle;
|
final bottomBarStyle = settings.bottomBarStyle;
|
||||||
|
|||||||
+37
-64
@@ -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,16 +197,24 @@ 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
|
||||||
|
? _desktopBar(context, category.title)
|
||||||
|
: NooTopBar(
|
||||||
style: NooLayout.navStyle(context),
|
style: NooLayout.navStyle(context),
|
||||||
title: category.title,
|
title: category.title,
|
||||||
leading: const NooTopBarBack(),
|
leading: const NooTopBarBack(),
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
|
child: Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: desktop ? 640 : double.infinity,
|
||||||
|
),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: const EdgeInsets.fromLTRB(
|
||||||
NooSpace.sm,
|
NooSpace.sm,
|
||||||
@@ -228,57 +226,32 @@ class _SettingsCategoryScreen extends StatelessWidget {
|
|||||||
children: [category.builder(context)],
|
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,
|
|
||||||
children: [
|
|
||||||
Expanded(child: _Column(children: left)),
|
|
||||||
const SizedBox(width: NooSpace.xl),
|
|
||||||
Expanded(child: _Column(children: right)),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Column extends StatelessWidget {
|
PreferredSizeWidget _desktopBar(BuildContext context, String title) {
|
||||||
final List<Widget> children;
|
final top = MediaQuery.paddingOf(context).top;
|
||||||
const _Column({required this.children});
|
return PreferredSize(
|
||||||
|
preferredSize: Size.fromHeight(NooToolbar.outerHeight + top),
|
||||||
@override
|
child: Padding(
|
||||||
Widget build(BuildContext context) {
|
padding: EdgeInsets.only(top: top),
|
||||||
return Column(
|
child: NooToolbar(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
titleWidget: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
spacing: NooSpace.sm,
|
||||||
children: [
|
children: [
|
||||||
for (final child in children) ...[
|
IconButton(
|
||||||
child,
|
icon: const Icon(LucideIcons.arrowLeft),
|
||||||
const SizedBox(height: NooSpace.xl),
|
tooltip: 'Back',
|
||||||
],
|
onPressed: () => Navigator.maybePop(context),
|
||||||
|
),
|
||||||
|
Text(title),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,21 +48,30 @@ 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(
|
||||||
|
padding: _margin,
|
||||||
|
child: Material(
|
||||||
color: backgroundColor ?? colors.surface,
|
color: backgroundColor ?? colors.surface,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(NooRadii.card),
|
||||||
|
side: BorderSide(color: colors.line),
|
||||||
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
height: NooSizes.toolbar,
|
height: NooSizes.toolbar,
|
||||||
padding: const EdgeInsetsDirectional.only(start: 24, end: 20),
|
padding: const EdgeInsetsDirectional.only(start: 24, end: 20),
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border(bottom: BorderSide(color: colors.line)),
|
|
||||||
),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
spacing: 12,
|
spacing: 12,
|
||||||
children: [
|
children: [
|
||||||
@@ -102,6 +111,7 @@ class NooToolbar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user