Rebuild UI on the Noo design system
Replaces the Material 3 screens with the Noo design system: warm neutrals, one violet accent, pill controls, Schibsted Grotesk/ Instrument Sans, Lucide icons, no gradients/shadows. - New lib/widgets/noo/ component kit (core, lists, files, media, nav, overlays) plus lib/theme/design_tokens.dart for the color/type/space/ radius/motion tokens. - Rebuilt the app shell (top/bottom bars, drawer, desktop sidebar and toolbar), every tab (Files/Offline, Photos, Favorites, Recent, Activity, Trash, Shares), Settings, the lock and login screens, the media viewer, search, the details/share sheets, and the share-upload and move/copy destination pickers. - Added a design canvas (linked from DESIGN_SYSTEM.md) covering the screens the spec didn't already describe, with matching Android and iOS chrome; wrote up the approved recipes into DESIGN_SYSTEM.md §4. - Removed now-dead legacy widgets (media_grid_tile, swipeable_item, sync_status_badge, selectable_thumbnail) and updated architecture.md/styling.md/standards.md to describe the new structure and component/testing conventions. - Added widget tests for the noo/ component kit. This is a UI-only rework: no provider/model/service behavior changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
---
|
||||
name: flutter-kgp-doctor
|
||||
description: Diagnoses and fixes Flutter's "plugin applies Kotlin Gradle Plugin (KGP)" build warning - the one that names specific plugins (e.g. "Your app uses the following plugins that apply Kotlin Gradle Plugin (KGP): file_saver, pdfx") and links to the Built-in Kotlin migration guide. Use whenever `flutter build`/`flutter run` prints this warning, or when checking whether it's safe to upgrade a plugin that previously triggered it.
|
||||
tools: Read, Edit, Grep, Glob, Bash, WebFetch, WebSearch
|
||||
---
|
||||
|
||||
You fix Flutter's Kotlin Gradle Plugin (KGP) migration warning, which looks
|
||||
like:
|
||||
|
||||
```
|
||||
WARNING: Your app uses the following plugins that apply Kotlin Gradle Plugin (KGP): file_saver, pdfx
|
||||
Future versions of Flutter will fail to build if your app uses plugins that apply KGP.
|
||||
```
|
||||
|
||||
## Background
|
||||
|
||||
Flutter is moving to a model where the Flutter Gradle plugin applies Kotlin
|
||||
itself ("Built-in Kotlin"), rather than each Android plugin applying its own
|
||||
copy of the Kotlin Gradle Plugin in its `android/build.gradle`. Plugins that
|
||||
haven't migrated yet trigger this warning today and will hard-fail the build
|
||||
in a future Flutter release. The fix lives upstream, in the plugin's own
|
||||
`android/build.gradle` (removing its own `apply plugin:
|
||||
'kotlin-android'`/`id("kotlin-android")` in favor of Flutter's built-in
|
||||
support) - not in this app.
|
||||
|
||||
Reference: https://docs.flutter.dev/release/breaking-changes/migrate-to-built-in-kotlin/for-app-developers
|
||||
|
||||
## What to do
|
||||
|
||||
1. Run `flutter build apk --release` (faster than `appbundle`) or use the
|
||||
warning text the user already has, to get the exact list of named
|
||||
plugins.
|
||||
2. For each plugin, find its current pinned version in `pubspec.yaml` and
|
||||
the resolved version in `pubspec.lock`.
|
||||
3. Check whether a newer published version has migrated:
|
||||
- Check the plugin's CHANGELOG.md (pub.dev shows it, or the package's
|
||||
GitHub repo) for entries mentioning "Built-in Kotlin", "KGP", or
|
||||
removing `kotlin-android` from its `android/build.gradle`.
|
||||
- If unsure from the changelog alone, fetch the candidate version's
|
||||
`android/build.gradle` from pub.dev/GitHub directly and check whether
|
||||
it still applies the Kotlin Gradle Plugin itself.
|
||||
4. If a migrated version exists and satisfies (or can reasonably relax) the
|
||||
app's other constraints:
|
||||
- Bump the version constraint in `pubspec.yaml`.
|
||||
- Run `flutter pub get`.
|
||||
- Rebuild (`flutter build apk --release` is enough to confirm) and
|
||||
verify that specific plugin no longer appears in the warning's list.
|
||||
- Run `flutter analyze` and skim for anything the upgrade broke (API
|
||||
changes) before calling it done.
|
||||
5. If no migrated version exists yet:
|
||||
- Do not attempt to silence the warning (no Gradle flags, no
|
||||
suppressing build output) - it is telling the truth about a real
|
||||
future break.
|
||||
- Do not fork or hand-patch the plugin's Android Gradle files inside
|
||||
`pub-cache` - that gets overwritten on the next `flutter pub get` and
|
||||
isn't tracked by the repo anyway.
|
||||
- Report which plugin(s) are still blocked, their current version, and
|
||||
whether the upstream issue tracker already has an open issue for this
|
||||
(search it) - file a note for the user rather than opening the issue
|
||||
yourself.
|
||||
|
||||
## Reporting
|
||||
|
||||
For each plugin named in the original warning, state one of:
|
||||
- **Fixed**: upgraded from `X` to `Y` in `pubspec.yaml`, confirmed the
|
||||
warning no longer names it.
|
||||
- **Blocked**: still on `X`, no migrated release exists yet, link to the
|
||||
changelog/issue you checked.
|
||||
|
||||
Keep the report short - a couple of lines per plugin is enough.
|
||||
+125
-46
@@ -13,20 +13,34 @@ lib/
|
||||
# ItemOperations, a plain cross-domain coordinator
|
||||
services/ # network/IO: NextcloudService, LoginFlowService,
|
||||
# AccountStore, AppLockService
|
||||
theme/ # AppTheme (Material 3 ThemeData)
|
||||
theme/ # AppTheme (Material 3 ThemeData, pre-rework
|
||||
# screens) + design_tokens.dart (NooColors/NooText/
|
||||
# NooSpace/NooRadii/NooSizes/NooMotion - see
|
||||
# styling.md's "Noo design-system components")
|
||||
views/ # one screen each (FilesView, PhotosView, TrashView,
|
||||
# SharesView, RecentView, ActivityView, SearchView,
|
||||
# AccountView, LoginView, FileViewerScreen,
|
||||
# ShareUploadView, LockScreenView)
|
||||
widgets/ # reusable pieces shared across views
|
||||
noo/ # the Noo design-system component kit - see
|
||||
# styling.md's catalog
|
||||
details/ # the file-details bottom sheet and its tabs
|
||||
shell/ # pieces shared by the mobile/desktop app shell
|
||||
settings/ # Settings' section widgets
|
||||
tabs/ # loading/error/empty slivers + grouping helpers
|
||||
# shared by Recent/Activity/Trash/Shares
|
||||
files/ # pieces split out of FilesView (e.g. the
|
||||
# breadcrumb row)
|
||||
```
|
||||
|
||||
`views/` files are screens routed to directly (a tab, or pushed via
|
||||
`Navigator`). `widgets/` files are building blocks used by more than one
|
||||
view (or complex enough to warrant their own file) — nothing in `widgets/`
|
||||
owns app state itself; it reads whichever controller(s) it needs via
|
||||
`context.watch`/`context.read`.
|
||||
`context.watch`/`context.read`. Most tabs, the app shell, and Settings are
|
||||
now built from `widgets/noo/` rather than raw Material widgets - see
|
||||
`styling.md`'s "Noo design-system components" for the token/component
|
||||
catalog and the current rebuild status.
|
||||
|
||||
## State management
|
||||
|
||||
@@ -100,8 +114,9 @@ new provider instance):
|
||||
mirror, not a second view. `offline` only swaps the data source, reads
|
||||
images from the local file (`localFileFor`) instead of a server preview,
|
||||
and turns off what needs the server: selection and bulk actions, swipe
|
||||
actions, sync badges, "+" (replaced by "Manage synced folders"), pick
|
||||
mode. The two `FilesView`s are keyed in `buildAppTabView` so the
|
||||
actions, per-item sync status icons, "+" (replaced by "Manage synced
|
||||
folders"), pick mode. The two `FilesView`s are keyed in `buildAppTabView`
|
||||
so the
|
||||
`IndexedStack` never hands one's State to the other.
|
||||
- [`OfflineController`](../../lib/providers/offline_controller.dart) — the
|
||||
Files tab's folder listing read from local storage instead of the
|
||||
@@ -175,15 +190,18 @@ directly, which call `NextcloudService`/`LoginFlowService`/`AccountStore`/
|
||||
- else `session.needsUnlock` → `LockScreenView` (login lock — see above)
|
||||
- else `MainShellView`
|
||||
|
||||
`MainShellView` is a bottom-nav `IndexedStack` over up to 8 tabs — Files,
|
||||
Photos, Favorites, Activity, Trash, Shares, Recent, Offline — user-configurable
|
||||
`MainShellView` is an `IndexedStack` over up to 8 tabs — Files, Photos,
|
||||
Favorites, Activity, Trash, Shares, Recent, Offline — user-configurable
|
||||
(order, visibility up to `maxVisibleTabs`, default tab) via
|
||||
`AppTab`/`SettingsController` and rendered through `buildAppTabView`
|
||||
(`widgets/app_tab_view_builder.dart`). `maxVisibleTabs` (5) is less than the
|
||||
total tab count, and `SettingsController._enforceMaxVisibleTabs` already
|
||||
auto-hides overflow on load (fresh install, or - as when Favorites was
|
||||
added - an existing saved tab order from before a new tab existed), so
|
||||
adding a tab to the `AppTab` enum needs no extra migration.
|
||||
adding a tab to the `AppTab` enum needs no extra migration. Only the (up
|
||||
to) 5 pinned tabs (`settings.visibleTabs`) become bottom-nav/sidebar
|
||||
destinations; the rest sit in the "More" section of the drawer (mobile) or
|
||||
sidebar (desktop) - see below.
|
||||
Each tab keeps its own `ScrollController` (survives tab switches via
|
||||
`IndexedStack`'s built-but-hidden trees) and tapping the already-active tab
|
||||
scrolls it back to top (`tapTabToScrollTop` setting). `MainShellView` also
|
||||
@@ -206,40 +224,85 @@ only two that make sense as external "choose a file" sources. The same
|
||||
override mechanism restricts the visible tabs to just Offline while
|
||||
`ConnectivityController.isOffline` - see `server.md`'s "Working offline"
|
||||
section for the full story, including why session restore itself has to
|
||||
avoid making a network request in that case. `MoreTabsButton` hides
|
||||
entirely in both cases (picking, offline) for the same reason - there's
|
||||
nothing it could usefully open.
|
||||
avoid making a network request in that case. The drawer/sidebar's "More"
|
||||
section is empty in both cases (picking, offline) for the same reason -
|
||||
there's nothing useful in it to switch to - and the mobile drawer doesn't
|
||||
open at all while picking (`Scaffold.drawer` is null).
|
||||
`MainShellView` also fires a one-time notification-permission prompt on
|
||||
its first mount (`_maybeRequestNotificationPermission`, gated by a plain
|
||||
`shared_preferences` flag so it only ever asks once, not on every
|
||||
launch): a plain-language `AlertDialog` explaining why Noo wants it
|
||||
(upload/download progress notifications - see `ShareUploadService.kt`/
|
||||
`DownloadService.kt`) before the OS's own `permission_handler`-driven
|
||||
`Permission.notification.request()`, since the bare system prompt gives
|
||||
no context on its own.
|
||||
launch): a plain-language dialog (`showNooDialog`, two `NooButton`s)
|
||||
explaining why Noo wants it (upload/download progress notifications - see
|
||||
`ShareUploadService.kt`/`DownloadService.kt`) before the OS's own
|
||||
`permission_handler`-driven `Permission.notification.request()`, since the
|
||||
bare system prompt gives no context on its own.
|
||||
|
||||
`FloatingBottomNavBar` (`widgets/floating_bottom_bar.dart`) wraps its
|
||||
pill's item row in a horizontal `SingleChildScrollView` rather than a
|
||||
plain `Row`, so a wide selected-item label plus several icon-only tabs
|
||||
scrolls instead of overflowing on narrower screens.
|
||||
`MainShellView` builds its chrome from the Noo nav kit
|
||||
(`widgets/noo/nav/`) and switches between two layouts on
|
||||
`NooLayout.isDesktop`:
|
||||
|
||||
All eight tabs, plus `ShareUploadView` (the share-to-upload destination
|
||||
picker, pushed rather than a tab - see below), share
|
||||
[`SyncedHeaderScaffold`](../../lib/widgets/synced_header_scaffold.dart) — a
|
||||
- **Mobile:** `AppTopBar` (`widgets/app_top_bar.dart`) wraps `NooTopBar`
|
||||
for *every* tab (previously only Files had shell-level top chrome, with
|
||||
the rest building their own via `SyncedHeaderScaffold`) - iOS gets a
|
||||
large title, an inline search field, a `plus` action on Files only (no
|
||||
other tab has a create/upload flow), and the account avatar; Android
|
||||
gets a compact title row with `search`/avatar actions, relying on an
|
||||
extended `NooFab` ("Upload", Files/Photos only) instead of a top-bar
|
||||
icon for upload. `BottomNavBar` (`widgets/bottom_nav_bar.dart`) adapts
|
||||
the pinned `AppTab`s onto `NooBottomBar`. `AppDrawer`
|
||||
(`widgets/app_drawer.dart`) builds a `NooDrawer`: account block, storage
|
||||
meter, a "More" list of the hidden tabs, Settings, and an "Edit tabs"
|
||||
link (opens Settings - there's no in-page anchor to scroll to its Tabs
|
||||
section yet).
|
||||
- **Desktop:** a `NooSidebar` (account card, pinned tabs, divider,
|
||||
remaining tabs, storage meter, Settings) sits beside a `NooToolbar`
|
||||
(tab title, search, an "Upload" action on Files/Photos) over the same
|
||||
`IndexedStack`, both built inline in `main.dart` rather than as separate
|
||||
widgets.
|
||||
|
||||
Both layouts share one detail: tapping a hidden ("More") tab calls
|
||||
`SettingsController.requestTab` - the same one-shot request
|
||||
`SearchView`/`FavoritesView`/`ShareUploadView` already use to jump the
|
||||
shell to a tab from outside it - instead of pushing that tab as its own
|
||||
screen. `MainShellView` then shows it as the active tab (still just an
|
||||
entry in the `IndexedStack`) with no bottom-bar/sidebar destination
|
||||
highlighted, since it isn't one of the pinned five, until the user taps a
|
||||
pinned or another "More" tab. `widgets/shell/shell_common.dart` holds the
|
||||
pieces both layouts share: account/storage formatting, `openSettings`/
|
||||
`openSearch`, `showAccountSwitcher` (the saved-accounts list behind the
|
||||
drawer's chevron and the sidebar's account card - a sheet on mobile, a
|
||||
dialog on desktop), `ShellAvatarButton` and `ShellSearchLauncher`.
|
||||
|
||||
All eight tabs used to share
|
||||
[`SyncedHeaderScaffold`](../../lib/widgets/synced_header_scaffold.dart) - a
|
||||
`CustomScrollView` with a pull-down "sync status" header (Google
|
||||
Photos-style) and a classic Material refresh spinner shown during a
|
||||
pull-triggered sync. `ShareUploadView` mirrors the Files tab's own
|
||||
controls-row/breadcrumbs sticky header almost exactly, so arriving via
|
||||
another app's "Share to..." sheet still lands on the same top chrome
|
||||
instead of a plain `AppBar` - its `actions` are `[ProfileAvatarButton()]`
|
||||
only (no `MoreTabsButton`: there's nowhere useful for it to go mid-upload,
|
||||
since jumping to another tab would abandon the destination picker);
|
||||
backing out is the system back gesture/button, not a bespoke close icon in
|
||||
the app bar. Its bottom action - "Upload to
|
||||
{folder}" - and the uploading-file-name summary above it (single line,
|
||||
auto-scrolling via `MarqueeTitle` if it doesn't fit) live together in one
|
||||
rounded-top, elevated `Material` bar as `bottomNavigationBar`, reading as a
|
||||
sheet peeking up from the bottom edge rather than a plain flat bar.
|
||||
pull-triggered sync. Now that every tab is rebuilt on Noo, each renders its
|
||||
own content directly in a plain `RefreshIndicator` + `CustomScrollView`
|
||||
instead (`ColoredBox(colors.bg)` background, no shared header widget);
|
||||
device-sync status shows per-row (`NooFileRow`/`NooFileTableRow`'s
|
||||
`NooStatusIcon`s) or in the Offline tab's `NooSummaryCard`, not a shared
|
||||
pinned chip. `SyncedHeaderScaffold` is now unused - the last two screens on
|
||||
it, `ShareUploadView` (the share-to-upload destination picker, pushed
|
||||
rather than a tab - see below) and `MoveCopyDestinationPicker`, are
|
||||
rebuilt too: both are a `Scaffold` with a `NooTopBar`+`NooTopBarBack`
|
||||
(mobile) / `NooToolbar` (desktop) top bar, a noo-styled
|
||||
[`Breadcrumbs`](../../lib/widgets/breadcrumbs.dart) row + folder list
|
||||
(`NooFileRow` mobile, `NooFileTableRow` desktop - both pickers only ever
|
||||
browse *folders*, so the old dimmed-but-visible file rows are gone; a
|
||||
`RefreshIndicator`+`CustomScrollView`) for a body, and a `colors.surface`
|
||||
bottom bar with a top `line` (no more rounded-top elevated `Material`
|
||||
sheet) holding the primary CTA. `ShareUploadView` is pushed from outside
|
||||
`MainShellView` (a cold share-intent launch, or Files' "+" → "Upload
|
||||
file"), so it builds its own top bar rather than relying on the shell's -
|
||||
its `actions` are `[ShellAvatarButton()]` only (no `MoreTabsButton`:
|
||||
there's nowhere useful for it to go mid-upload, since jumping to another
|
||||
tab would abandon the destination picker); `ShellAvatarButton` is the same
|
||||
tap-to-Settings/swipe-to-cycle-accounts widget the shell itself uses, and a
|
||||
straight replacement for the old `ProfileAvatarButton` (now unused
|
||||
anywhere, since this was its last call site). Its bottom bar holds the
|
||||
uploading-file-name summary (single line, auto-scrolling via
|
||||
`MarqueeTitle` if it doesn't fit) above the "Upload to {folder}" CTA.
|
||||
[`MarqueeTitle`](../../lib/widgets/marquee_title.dart) (`package:marquee`) is
|
||||
shared with `FileViewerScreen`'s title - falls back to a plain ellipsized
|
||||
`Text` when the content already fits, so short text never marquees.
|
||||
@@ -259,16 +322,32 @@ no prior browsing session to preserve. The Move/Copy picker is pushed
|
||||
*while the user is actively browsing a specific Files-tab folder*, so
|
||||
reusing the shared state would strand that folder's `pathStack` under it;
|
||||
its local state means popping back always lands the user exactly where
|
||||
they were, untouched.
|
||||
`SearchView`, `AccountView` (Settings), the file-details sheet, and the
|
||||
share sheet are pushed on top via
|
||||
`Navigator`/`showModalBottomSheet`/`showGradualBottomSheet` rather than
|
||||
being tabs. `ProfileAvatarButton` (top-right on every tab) opens Settings on
|
||||
tap and cycles between saved accounts on a vertical swipe.
|
||||
they were, untouched. Unlike `ShareUploadView`, it keeps `MoreTabsButton`
|
||||
in its top bar: that button pushes a hidden tab as its own stacked screen
|
||||
rather than jumping the shell there, so it doesn't abandon this picker the
|
||||
way jumping tabs would. Its bottom bar shows a "Moving/Copying N item(s)"
|
||||
summary line (replaced by a danger-colored warning when the current folder
|
||||
is an invalid destination) above a "Move here"/"Copy here" CTA - the
|
||||
per-item conflict follow-up, if any, is
|
||||
[`MoveCopyConflictSheet`](../../lib/widgets/move_copy_conflict_sheet.dart).
|
||||
`SearchView`, `AccountView` (Settings), the file-details sheet
|
||||
(`DetailsSheet`), and the share sheet (`ShareSheet`) are pushed on top via
|
||||
`Navigator`/`showNooSheet`/`showNooDialog` rather than being tabs (the two
|
||||
per-item sheets pick between the mobile sheet and the desktop dialog via
|
||||
`NooLayout.isDesktop`, same as every other overlay in the app;
|
||||
`showGradualBottomSheet`, the custom drag-to-resize sheet they used before,
|
||||
has no remaining callers). On mobile, `ShellAvatarButton`
|
||||
(`widgets/shell/shell_common.dart`,
|
||||
shown in `AppTopBar`'s trailing actions on every tab, and reused directly by
|
||||
`ShareUploadView`) opens Settings on tap and cycles between saved accounts
|
||||
on a vertical swipe. Desktop has no avatar in the
|
||||
toolbar; `NooSidebarAccount`'s account card opens the full switcher instead
|
||||
(`showAccountSwitcher`, the same one behind the mobile drawer's chevron).
|
||||
|
||||
Files and Photos (the two tabs with multi-select) pass their selection
|
||||
toolbar into `SyncedHeaderScaffold`'s `selectionBar` param rather than
|
||||
rendering it as a second sliver app bar inside their own content: while
|
||||
non-null, it fully takes over the pinned top bar in place of the
|
||||
sync-status chip/`actions`/pull-to-reveal quota panel, so selecting reads
|
||||
as replacing the whole top chrome rather than adding a strip beneath it.
|
||||
Files and Photos (the two tabs with multi-select) render their selection
|
||||
bar as a `pinned: true` sliver at the top of their own `CustomScrollView`,
|
||||
swapped in for the controls row/type-chips row while `_isSelecting` -
|
||||
selecting reads as replacing that row in place, not adding a strip
|
||||
beneath it. (An earlier version of this routed the selection bar through
|
||||
`SyncedHeaderScaffold`'s `selectionBar` param; that's gone along with the
|
||||
scaffold itself in these two tabs.)
|
||||
|
||||
@@ -10,6 +10,7 @@ Reference files in this project:
|
||||
- `Noo Design System.dc.html` is the visual component sheet.
|
||||
- `noo-kit.js` holds the tokens (`TH`), the icon set (`SVG`/`ic`), file-type mapping (`KIND`) and mock data.
|
||||
- `Mobile Screen.dc.html` and `Desktop Screen.dc.html` are the reference builds for each screen.
|
||||
- [Noo — Missing Screens](https://claude.ai/artifact/3AGPqqMdkLSC2ypCh2CQs4) is a live design canvas (Claude Design, not a static file in this repo) covering the screens this doc originally had no recipe for: Media viewer, Search, the Upload/Move/Copy destination picker, and the Details sheet's tab switch. It has both an iOS row (built first, ready for later) and an Android row (built to match right now) - same content, chrome adapted per the platform rule above. §4 below is the written-up version of what's approved there; go back to the canvas for pixel-level layout, not just the summary.
|
||||
|
||||
---
|
||||
|
||||
@@ -295,6 +296,43 @@ Sidebar items are 38px tall with radius 12, an 18px icon and a 14/500 label. The
|
||||
2. **Share with people:** an input ("Name, email or group"), then the people with access. The owner comes first; the others each have a permission pill ("Can edit ▾").
|
||||
3. **Share link:** a toggle, then the URL field in mono with a primary "Copy link" button, then option chips (permission, expiry, password, allow download).
|
||||
4. **Send file directly:** an outline button, with a caption saying that link settings don't apply.
|
||||
- **Details sheet:** header (file tile, name, size · folder, close) as in the
|
||||
Share sheet above, then a segmented control switching Info/Versions/Activity
|
||||
in place below it - there's no separate tab-strip component, so the
|
||||
segmented control (already used for List/Grid and the Shares scope) is the
|
||||
one that does this job too. Info is a flat label/value list (Size, Type,
|
||||
Location, Modified, Created, ...). Versions and Activity reuse their own
|
||||
row/feed treatment. Same on every platform - this sheet has no iOS/Android
|
||||
split.
|
||||
- **Media viewer** (the full-screen photo/video viewer): a black stage
|
||||
regardless of theme, like a native photo/video viewer - not `bg`. A
|
||||
translucent, blurred top bar (back, filename, meta) and bottom bar float
|
||||
over the media; this is the one deliberate exception to "no blur" in
|
||||
product UI, since it's chrome over photo/video content, not over the app's
|
||||
own surfaces. Back is a plain arrow (`arrow-left`), not the iOS
|
||||
chevron+label pushed-screen pattern - platform split still to do. The
|
||||
bottom bar holds every action in one row (share, favorite, open
|
||||
externally, download, delete, details) on every platform; don't add a
|
||||
top-bar overflow menu for the same actions. Video adds a transport row
|
||||
above the action row: time · seek bar · time, then play/pause and mute
|
||||
centered below it.
|
||||
- **Search:** pushed from the shell's search entry point (`menu`/`search`
|
||||
icon in the top bar, or the inline field below an iOS large title -
|
||||
DESIGN_SYSTEM §2 "Search field" placement). The destination screen is one
|
||||
shared recipe for both platforms: back arrow + the pill `NooSearchField`
|
||||
(not the inset-panel text-field shape - a search field is always the pill,
|
||||
except the one named iOS-inline exception) taking over the top row,
|
||||
autofocus, a clear button once there's a query. Below it: a file list
|
||||
(mobile rows / desktop table, same as Files), or a centered icon + short
|
||||
sentence-case message for the empty ("Search your files") and no-results
|
||||
states.
|
||||
- **Upload / Move / Copy destination picker:** a pushed screen (outside the
|
||||
tab shell, so it carries its own complete top bar) titled "Upload to" /
|
||||
"Move to" / "Copy to". Back arrow (Android) or "Cancel" text (iOS) leading,
|
||||
no trailing action. Breadcrumb row, then a folder-only list (no files: this
|
||||
screen only browses folders) using the standard file row/tile at the
|
||||
folder kind. Bottom bar: a meta line ("Moving 3 items") above a full-width
|
||||
52px primary CTA ("Upload here" / "Move here" / "Copy here").
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -75,8 +75,47 @@ class/method already makes obvious.
|
||||
never completes) and the app stays on `_SplashView`'s indeterminate
|
||||
spinner, which makes `pumpAndSettle()` hang until its own timeout instead
|
||||
of failing fast. See `test/widget_test.dart` for the reference setup.
|
||||
- Design-system components (`lib/widgets/noo/`) are tested in
|
||||
`test/widgets/noo/`, one file per component folder. Use the helpers in
|
||||
`noo_test_utils.dart`:
|
||||
- `setUpNooTests()` turns off font fetching.
|
||||
- `testNooWidgets(...)` runs a body once per theme, light and dark, built
|
||||
through the real `AppTheme`, and hands it the `NooColors`.
|
||||
- `pumpNoo(...)` mounts the widget.
|
||||
|
||||
Build themes inside the test body, not at file level, because `AppTheme`
|
||||
touches Google Fonts before the test binding exists. `pumpNoo` centers
|
||||
the child, which loosens its constraints. To catch a widget stretching to
|
||||
fill its parent, put it in a bounded box (for example, `SizedBox` +
|
||||
`Align`).
|
||||
- Run with `flutter test`.
|
||||
|
||||
## Play Store screenshots
|
||||
|
||||
`bash tool/screenshots.sh` (Git Bash is fine on Windows) regenerates the store
|
||||
listing screenshots **without any real data**: it runs the real, unmodified app
|
||||
against a throwaway Docker Nextcloud (`tool/demo_server/`) seeded with invented
|
||||
content, on a wiped 1080x1920 emulator with a demo-mode status bar, and drives
|
||||
it with `integration_test/store_screenshots_test.dart` (host-side driver:
|
||||
`test_driver/integration_test.dart`, which saves `adb screencap` images).
|
||||
Output lands in `store_listing/screenshots/` (gitignored) after
|
||||
`tool/finalize_screenshots.py` flattens alpha and checks Play's size/aspect
|
||||
limits (each side 320-3840 px, long side at most 2x the short side - a stock
|
||||
1080x2400 phone screen is 2.22:1 and would be rejected).
|
||||
|
||||
- The test logs in by writing the demo user's account/app password into
|
||||
storage via `AccountStore` before calling `main()`, so it skips the browser
|
||||
login flow; it also fixes the theme (light, non-dynamic colour), the visible
|
||||
tabs and the first-run notification prompt so shots are reproducible.
|
||||
- It finds things by tab icon (`AppTab.icon` inside `FloatingBottomNavBar`),
|
||||
tooltips, the `ValueKey('files')`/`ValueKey('offline')` on the two
|
||||
`FilesView`s, and the fake content's names (defined in
|
||||
`tool/demo_server/seed.py`) - keep those in sync if you rename either side.
|
||||
- `flutter test` only runs `test/`, so this never runs as part of the normal
|
||||
suite; `integration_test` is a dev-only dependency.
|
||||
- Always review the images before uploading - see
|
||||
`tool/demo_server/README.md` for the known places real data could appear.
|
||||
|
||||
## Local install/deploy
|
||||
|
||||
Never use `flutter install` to push a build to a test device — it always
|
||||
@@ -99,15 +138,18 @@ on the device. `android/app/build.gradle.kts` picks a release signing key
|
||||
in this order: a local `android/key.properties` (gitignored — points at a
|
||||
gitignored keystore file, e.g. `android/app/release-keystore.jks`), then
|
||||
CI env vars (`RELEASE_KEYSTORE_PATH`/`_PASSWORD`, `RELEASE_KEY_ALIAS`/
|
||||
`_PASSWORD`, set by `.gitea/workflows/build.yml` from repo secrets), then
|
||||
falls back to the debug key if neither is configured. As long as the same
|
||||
dedicated release keystore backs both `key.properties` locally and the
|
||||
Gitea secrets, local release builds and CI-built release APKs share one
|
||||
signature, so `adb install -r` works cleanly either way. A local checkout
|
||||
with no `key.properties` set up falls back to the (per-machine, ungitted)
|
||||
debug key, which won't match a CI-signed APK — installing one over the
|
||||
other still forces a full uninstall, since there's no way around Android's
|
||||
signature check from the tooling side.
|
||||
`_PASSWORD`, set from the same repo secrets by both `.gitea/workflows/
|
||||
build.yml`, triggered by `RC*` tags and producing a sideloadable APK, and
|
||||
`.gitea/workflows/release.yml`, triggered by `Release-*` tags and producing
|
||||
the `.aab` Play Console wants), then falls back to the debug key if neither
|
||||
is configured. As long as the same dedicated release keystore backs both
|
||||
`key.properties` locally and the Gitea secrets, local release builds and
|
||||
CI-built release APKs share one signature, so `adb install -r` works
|
||||
cleanly either way. A local checkout with no `key.properties` set up falls
|
||||
back to the (per-machine, ungitted) debug key, which won't match a
|
||||
CI-signed APK — installing one over the other still forces a full
|
||||
uninstall, since there's no way around Android's signature check from the
|
||||
tooling side.
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
||||
+149
-55
@@ -1,12 +1,20 @@
|
||||
# Styling
|
||||
|
||||
**Migration in progress:** this file documents the current (pre-rework)
|
||||
Material 3 theme. The target look is
|
||||
**Migration in progress:** the target look is
|
||||
[`design-system/DESIGN_SYSTEM.md`](design-system/DESIGN_SYSTEM.md) — warm
|
||||
neutrals, one violet accent, pill controls, Schibsted Grotesk/Instrument
|
||||
Sans, no gradients/shadows. Update this file to describe the new system as
|
||||
each area gets reworked, rather than leaving stale Material 3 guidance next
|
||||
to a design system that's already superseded it.
|
||||
Sans, no gradients/shadows — built from the `widgets/noo/` component kit
|
||||
(see "Noo design-system components" below). Rebuilt so far: the app shell
|
||||
(top/bottom bars, drawer, sidebar, toolbar), Files/Offline, Photos,
|
||||
Favorites, Recent, Activity, Trash, Shares, Settings, the lock screen,
|
||||
login, `ShareUploadView`, `MoveCopyDestinationPicker`,
|
||||
`MoveCopyConflictSheet`, the shared `Breadcrumbs` widget, and the file
|
||||
details/share bottom sheets/dialogs (`DetailsSheet`/`ShareSheet`). **Still on
|
||||
the pre-rework Material 3 theme** documented in "Theme" and "Reusable
|
||||
chrome" below: `FileViewerScreen` (the media viewer), `SearchView`, and
|
||||
`LoginWebviewView`. Update this file to describe each area as it gets
|
||||
reworked, rather than leaving stale Material 3 guidance next to a design
|
||||
system that's already superseded it.
|
||||
|
||||
## Theme
|
||||
|
||||
@@ -42,62 +50,148 @@ widgets. Key points:
|
||||
and no `scrollbarTheme` override (tried once, explicitly reverted). Don't
|
||||
reintroduce one without being asked.
|
||||
|
||||
## Noo design-system components
|
||||
|
||||
The rebuilt UI is assembled from `lib/widgets/noo/`, which implements
|
||||
`DESIGN_SYSTEM.md` §2–3. Build new screens from these rather than from raw
|
||||
Material widgets or the pre-rework chrome below.
|
||||
|
||||
- **Tokens** live in [`design_tokens.dart`](../../lib/theme/design_tokens.dart):
|
||||
colors via `context.nooColors` (a `NooColors` `ThemeExtension`), plus
|
||||
`NooText`, `NooSpace`, `NooRadii`, `NooSizes`, `NooMotion` and
|
||||
`nooDialogShadow`. `NooText` styles set no color; callers add it with
|
||||
`copyWith(color: ...)`. Flutter's `TextStyle.height` is a multiple of font
|
||||
size, so the spec's CSS line-height maps to it directly (for example,
|
||||
0.9 → `height: 0.9`).
|
||||
- **Icons are Lucide** (`lucide_icons_flutter`, `LucideIcons.*`) inside
|
||||
`noo/`, never Material `Icons.*`.
|
||||
- **Components are data-agnostic.** They take strings, icons, colors and
|
||||
callbacks, not `NextcloudItem`/`AppTab`, so the screen layer maps models
|
||||
onto them.
|
||||
- **Platform differences** are chosen with a flag rather than by reading the
|
||||
platform inside the widget: `NooNavStyle` (`ios`/`android`, with
|
||||
`NooNavStyle.fromPlatform`) for nav chrome, and `iosStyle` on rows and
|
||||
cards for the ellipsis vs. vertical-ellipsis overflow icon.
|
||||
|
||||
Catalog:
|
||||
|
||||
| Folder | Components |
|
||||
|---|---|
|
||||
| `core/` | `NooButton`, `NooFab`, `NooChip`, `NooSegmentedControl`, `NooToggle`, `NooSearchField`, `NooAvatar`, `NooBadge`, `NooProgressBar` |
|
||||
| `lists/` | `NooGroupedList`, `NooSettingsRow`, `NooTabOrderRow`, `NooBanner`, `NooSummaryCard` |
|
||||
| `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` |
|
||||
| `overlays/` | `showNooSheet`, `showNooDialog`/`NooDialog`, `NooOverlayHeader`, `NooTextField`, and the share parts `NooShareSection`, `NooPersonAccessRow`, `NooPermissionPill` |
|
||||
|
||||
Gotchas:
|
||||
- `NooDrawer` can't set its own scrim. The host `Scaffold` needs
|
||||
`drawerScrimColor: context.nooColors.scrim`.
|
||||
- `NooGroupedList` draws dividers by showing `line` through 1px gaps, so each
|
||||
child must paint its own surface (`NooSettingsRow` and `NooTabOrderRow`
|
||||
do).
|
||||
- `NooSwipeAction` only reveals its action. The user has to tap the block to
|
||||
trigger it; a full swipe never deletes.
|
||||
- Window chrome (macOS traffic lights, the Windows 40px title bar) isn't
|
||||
built yet. `NooSidebar.windowControls` is the slot for it.
|
||||
|
||||
## Reusable chrome
|
||||
|
||||
- `MediaGridTile` (`lib/widgets/media_grid_tile.dart`): the full-bleed image/video
|
||||
grid card with name/size scrim, shared by Files (server preview) and Offline
|
||||
(local `FileImage`, images only - no video frame-extraction plugin, so offline
|
||||
videos use the plain icon card). `ItemThumbnail` takes an optional `localFile`
|
||||
for the same offline-image case in list tiles.
|
||||
- `FilesControlsRow` (`lib/widgets/files_controls_row.dart`): the
|
||||
sort/hidden/scope/type-filter/view-mode row, shared by the Files and
|
||||
Offline tabs (`showStorageScope: false` for Offline).
|
||||
**Rebuilt on the Noo design system** (Files/Offline, Photos, Favorites,
|
||||
Recent, Activity, Trash, Shares, Settings, the app shell, lock screen,
|
||||
login, `ShareUploadView`, `MoveCopyDestinationPicker`, `DetailsSheet` and
|
||||
`ShareSheet`): these no longer use the pieces below. Their own building
|
||||
blocks are noted where they matter:
|
||||
|
||||
- `FilesControlsRow` (`lib/widgets/files_controls_row.dart`) — now built from
|
||||
`NooChip`/`NooSegmentedControl`; still the sort/hidden/scope/type-filter/
|
||||
view-mode row shared by Files and Offline (`showStorageScope: false` for
|
||||
Offline), and reused as-is by Favorites.
|
||||
- `lib/widgets/files/file_breadcrumb_row.dart` — the noo-styled breadcrumb
|
||||
trail Files uses in place of the shared
|
||||
[`Breadcrumbs`](../../lib/widgets/breadcrumbs.dart) widget. `Breadcrumbs`
|
||||
is now also noo-styled (same tokens, private-widget-turned-shared) — it's
|
||||
used only by `ShareUploadView`/`MoveCopyDestinationPicker`, which is why
|
||||
it was safe to restyle directly instead of forking another
|
||||
`FileBreadcrumbRow`-style copy; don't move Files back onto it.
|
||||
`lib/widgets/tabs/` (`tab_state_slivers.dart`, `tab_day_groups.dart`,
|
||||
`tab_location.dart`) — the loading/error/empty-state slivers and
|
||||
day/month grouping helpers shared by Recent/Activity/Trash/Shares.
|
||||
`lib/widgets/settings/` — Settings' 8 section widgets plus
|
||||
`settings_section.dart`'s `SettingsSection`/`showSettingsPicker` and
|
||||
`settings_dialogs.dart`'s `confirmRemoveAccount`.
|
||||
`lib/widgets/shell/shell_common.dart` — account/storage formatting,
|
||||
`openSettings`/`openSearch`, `showAccountSwitcher`, `ShellAvatarButton`,
|
||||
`ShellSearchLauncher`, shared by the mobile and desktop shell chrome.
|
||||
- [`getItemIcon`/`getIconColor`/`ItemThumbnail`](../../lib/widgets/item_icon.dart)
|
||||
— the icon/color/thumbnail treatment for a file or folder, shared by any
|
||||
screen that lists `NextcloudItem`s the way the Files tab does (currently
|
||||
`files_view.dart` and `share_upload_view.dart`'s destination picker).
|
||||
Extend this rather than re-deriving per-type icons/colors at a new call
|
||||
site.
|
||||
- [`DetailsHeader`](../../lib/widgets/details/details_sheet.dart) — the
|
||||
icon-box/name/meta row every per-item bottom sheet opens on
|
||||
(`DetailsSheet`, the media viewer's collapsed peek state, and
|
||||
`ShareSheet`). Takes a `padding` override for callers whose own scroll
|
||||
view already applies horizontal insets (`ShareSheet`'s `ListView`), so it
|
||||
doesn't get doubled up. Reuse this instead of a bare title `Text` for any
|
||||
new per-item sheet - a plain title reads as under-designed next to the
|
||||
other sheets (a real instance: `ShareSheet` used to be just that).
|
||||
— `getItemIcon`/`getIconColor` are superseded by `NooFileKind` in
|
||||
rebuilt screens; `ItemThumbnail` is still reused as-is, fed into
|
||||
`NooFileTile`/`NooFileRow`/`NooFileTableRow`/`NooGridCard`'s `thumbnail`
|
||||
slot. `ShareUploadView`/`MoveCopyDestinationPicker` don't use any of the
|
||||
three any more - both destination pickers now list folders only (see
|
||||
below), and a folder never gets a real thumbnail (only image/video do),
|
||||
so a plain `NooFileTile(kind: NooFileKind.folder)` covers every row.
|
||||
- `DetailsSheet`/`ShareSheet` (`lib/widgets/details/details_sheet.dart`,
|
||||
`lib/widgets/share_sheet.dart`) are rebuilt: `showNooSheet`/`showNooDialog`
|
||||
per `NooLayout.isDesktop`, with the header built from `detailsFileTile`
|
||||
(a `NooFileTile` keyed by `NooFileKind.from`) and `detailsMetaLine` -
|
||||
shared top-level helpers in `details_sheet.dart` so both sheets open on
|
||||
the same header, replacing the old `DetailsHeader` widget. Neither sheet
|
||||
uses `showGradualBottomSheet`'s drag-to-resize any more: `DetailsSheet`
|
||||
swaps Info/Versions/Activity with a `NooSegmentedControl` (there's no
|
||||
tab-strip component in the noo kit) instead of a `TabBar`/`TabBarView`,
|
||||
and both sheets' content sits in one `Column` so `showNooSheet`'s/
|
||||
`showNooDialog`'s own `SingleChildScrollView` handles overflow - a fixed
|
||||
page per tab no longer needs a resizable sheet to see the rest.
|
||||
`showGradualBottomSheet` (`lib/widgets/gradual_bottom_sheet.dart`) has no
|
||||
remaining callers as a result.
|
||||
`ShareSheet`'s "Share with people"/"Share link"/"Send file directly"
|
||||
sections follow `DESIGN_SYSTEM.md` §4 via `NooShareSection`/
|
||||
`NooPersonAccessRow`/`NooPermissionPill`; the permission pill and the
|
||||
link's permission/expiry chips are read-only display (no
|
||||
`ItemOperations` call updates a share's permission/expiry/password/
|
||||
hide-download yet) - promote those to real controls once that exists.
|
||||
Removing a person/group/email share is reachable by tapping their
|
||||
permission pill, which opens a small "Remove access" menu.
|
||||
[`MoveCopyConflictSheet`](../../lib/widgets/move_copy_conflict_sheet.dart)
|
||||
is rebuilt: `showNooDialog`/`showNooSheet` (per `NooLayout.isDesktop`), a
|
||||
`NooGroupedList` of file-tile rows with an inline
|
||||
`NooSegmentedControl<ConflictChoice>` once "Decide per item" is picked,
|
||||
and `NooButton`s for overwrite-all/keep-both/decide-per-item/confirm. Its
|
||||
`show(BuildContext, List<MoveCopyConflict>)` API is unchanged.
|
||||
- [`FrostedGlassContainer`](../../lib/widgets/frosted_glass_container.dart) —
|
||||
the blurred/translucent pill background shared by all floating chrome
|
||||
(bottom nav bar, media-viewer top/bottom bars and video transport
|
||||
controls). Reuse this for any new floating overlay instead of building a
|
||||
new blur/shadow combo.
|
||||
- [`FloatingBottomNavBar`](../../lib/widgets/floating_bottom_bar.dart) — the
|
||||
main tab bar; opacity/blur are user-adjustable settings
|
||||
(`SettingsController.bottomBarOpacity`/`bottomBarBlur`), not constants —
|
||||
pull new adjustable visual knobs from `SettingsController` the same way
|
||||
rather than hardcoding them.
|
||||
the blurred/translucent pill background for the media viewer's top/bottom
|
||||
bars and video transport controls (its other former user, the floating
|
||||
bottom nav bar, is gone - see below). Reuse this for any new floating
|
||||
overlay instead of building a new blur/shadow combo.
|
||||
- `SettingsController.bottomBarOpacity`/`bottomBarBlur` and
|
||||
`lib/widgets/floating_bottom_bar.dart`/`media_grid_tile.dart`/
|
||||
`swipeable_item.dart`/`sync_status_badge.dart`/`selectable_thumbnail.dart`
|
||||
are gone: the bottom bar is now the flat, non-blurred `NooBottomBar` (no
|
||||
opacity/blur knob - flat surfaces per the design system), grid tiles are
|
||||
`NooGridCard`, swipe actions are `NooSwipeAction`, and per-item sync
|
||||
status is `NooFileRow`/`NooFileTableRow`'s built-in `NooStatusIcon` list
|
||||
instead of a corner badge.
|
||||
- [`SyncedHeaderScaffold`](../../lib/widgets/synced_header_scaffold.dart) —
|
||||
the pull-to-sync `CustomScrollView` header shared by all 8 tabs (see
|
||||
`architecture.md`); also where the pull-to-refresh gesture thresholds and
|
||||
the classic Material refresh spinner live. Its persistent compact chip
|
||||
(icon + "Sync off"/"Syncing…"/"Synced"/"Sync issue") reflects device-sync
|
||||
status (`SyncStatusController.syncHeaderStatus`), not the WebDAV-refresh
|
||||
loading state the pull gesture itself triggers - that has its own,
|
||||
separate floating spinner bubble, so nothing was lost by handing the
|
||||
persistent text/icon over. The expanded panel's headline is a separate,
|
||||
more detailed string (`_syncSummary` in `synced_header_scaffold.dart`) -
|
||||
counts of what's actually configured to sync ("2 folders & 1 file
|
||||
synced") rather than just repeating the chip's generic label.
|
||||
Deliberately doesn't add up the individual files inside a synced folder
|
||||
("1 folder synced", not "1 folder & 4 items synced") - once a folder's
|
||||
synced, its file count is an implementation detail, not something the
|
||||
user picked.
|
||||
- [`SyncStatusBadge`](../../lib/widgets/sync_status_badge.dart) — the small
|
||||
corner badge over a thumbnail showing per-item device-sync status
|
||||
(`cloud_done`/`sync`, nothing for not-synced/conflict); used in Files'
|
||||
list and grid tiles today. Reuse this rather than a new ad hoc badge if
|
||||
another view starts showing sync status per item.
|
||||
the pull-to-sync `CustomScrollView` header with the persistent sync-status
|
||||
chip and pull-to-refresh gesture/spinner. Every screen (including
|
||||
`ShareUploadView`/`MoveCopyDestinationPicker`, its last two users) has
|
||||
dropped it for a plain `RefreshIndicator` + `CustomScrollView`
|
||||
(device-sync status now shows per-row via `NooStatusIcon`/the Offline
|
||||
`NooSummaryCard`, not a shared header chip), so the `SyncedHeaderScaffold`
|
||||
class itself is now dead code - kept only because the same file's
|
||||
top-level `formatBytes` helper is still widely used
|
||||
(`files_view.dart`/`favorites_view.dart`/`shell_common.dart`/
|
||||
`widgets/details/*`).
|
||||
- [`SegmentedIconGroup`/`ToggleIconButton`](../../lib/widgets/segmented_icon_toggle.dart)
|
||||
and [`SortMenuButton`](../../lib/widgets/sort_menu_button.dart) — the
|
||||
Material sort/filter-chip pieces `ShareUploadView`/
|
||||
`MoveCopyDestinationPicker` used to mirror Files' old controls row with.
|
||||
Both destination pickers dropped that whole row (they only ever browse
|
||||
folders, so sort/hidden/scope/type-filter/grid controls don't apply), so
|
||||
these two files are now dead code too - `Breadcrumbs` is the only shared
|
||||
widget promoted to noo styling instead of removed, since Files' own
|
||||
`FileBreadcrumbRow` proved the same trail is still wanted elsewhere.
|
||||
- [`SeekBarPainter`/`SeekBarPreview`](../../lib/widgets/seek_bar_painter.dart)
|
||||
— the four `MediaProgressBarStyle` presets (Default/Wavy/Slim/Squiggly)
|
||||
for the video player's seek bar, plus a perpetually-animated
|
||||
|
||||
Reference in New Issue
Block a user