Add CLAUDE.md with topic-specific context docs
Split project context into .claude/context/ (architecture, server, styling, standards) instead of one large file, referenced from a minimal root CLAUDE.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
lib/
|
||||||
|
main.dart # app root, theme wiring, top-level navigation switch
|
||||||
|
models/ # plain data classes (NextcloudItem, NextcloudShare, ...)
|
||||||
|
providers/ # ServerProvider — the single app-wide ChangeNotifier
|
||||||
|
services/ # network/IO: NextcloudService, LoginFlowService
|
||||||
|
theme/ # AppTheme (Material 3 ThemeData)
|
||||||
|
views/ # one screen each (FilesView, PhotosView, ...)
|
||||||
|
widgets/ # reusable pieces shared across views
|
||||||
|
details/ # the file-details bottom sheet and its tabs
|
||||||
|
```
|
||||||
|
|
||||||
|
`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 it from the `ServerProvider` passed down or
|
||||||
|
read via `context.watch`/`context.read`.
|
||||||
|
|
||||||
|
## State management
|
||||||
|
|
||||||
|
There is exactly one `ChangeNotifier`: [`ServerProvider`](../../lib/providers/server_provider.dart).
|
||||||
|
It is created once in `main()` and provided at the root with `provider`'s
|
||||||
|
`ChangeNotifierProvider`. It owns:
|
||||||
|
|
||||||
|
- auth/session state (`isLoggedIn`, `isRestoringSession`, login-flow status)
|
||||||
|
- the active `NextcloudService` instance (null until logged in)
|
||||||
|
- all fetched data (`items`, `quota`, `activities`)
|
||||||
|
- navigation-within-files state (`currentFolderPath`, `pathStack`)
|
||||||
|
- UI settings that persist across launches (theme mode, seed color, dynamic
|
||||||
|
color toggle, bottom-bar opacity/blur, sort/filter/view-mode prefs)
|
||||||
|
|
||||||
|
New app-wide state belongs on `ServerProvider` as a private field + getter +
|
||||||
|
a method that mutates it and calls `notifyListeners()`. Screen-local state
|
||||||
|
(e.g. a `TextEditingController`, an expanded/collapsed flag) stays in that
|
||||||
|
view's own `State` class — see `standards.md` for the split.
|
||||||
|
|
||||||
|
There's no separate repository/data layer: views call `ServerProvider`
|
||||||
|
methods directly, which call `NextcloudService`/`LoginFlowService`.
|
||||||
|
|
||||||
|
## Navigation / screen flow
|
||||||
|
|
||||||
|
`main.dart`'s `NextcloudApp` picks the app's `home` screen from provider
|
||||||
|
state, no named routes:
|
||||||
|
|
||||||
|
- `provider.isRestoringSession` → `_SplashView` (spinner while
|
||||||
|
`flutter_secure_storage`/`shared_preferences` are read on startup)
|
||||||
|
- else `!provider.isLoggedIn` → `LoginView` (server-address entry + Login
|
||||||
|
Flow v2)
|
||||||
|
- else `MainShellView`
|
||||||
|
|
||||||
|
`MainShellView` is a bottom-nav `IndexedStack` with three persistent tabs —
|
||||||
|
Files, Photos, Activity — each keeping its own `ScrollController` so state
|
||||||
|
(scroll position, `IndexedStack`'s built-but-hidden trees) survives tab
|
||||||
|
switches. `SearchView` and the file-details sheet are pushed on top via
|
||||||
|
`Navigator`/`showModalBottomSheet` rather than being tabs.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Server integration
|
||||||
|
|
||||||
|
## Auth: Login Flow v2
|
||||||
|
|
||||||
|
The app **never** collects a Nextcloud password directly. It implements
|
||||||
|
[Login Flow v2](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2)
|
||||||
|
via [`LoginFlowService`](../../lib/services/login_flow_service.dart):
|
||||||
|
|
||||||
|
1. `LoginFlowService.initiate(serverUrl)` POSTs to
|
||||||
|
`{server}/index.php/login/v2`, gets back a browser login URL + a poll
|
||||||
|
endpoint/token.
|
||||||
|
2. The app opens the login URL in the system browser (`url_launcher`); the
|
||||||
|
user authenticates and authorizes there.
|
||||||
|
3. `ServerProvider` polls `LoginFlowService.poll(pollEndpoint, token)` every
|
||||||
|
2 seconds (`Timer.periodic`, see `_pollTimer`/`_pollTimeoutTimer` in
|
||||||
|
`server_provider.dart`) until it gets a 200 with `server`/`loginName`/
|
||||||
|
`appPassword`, a non-404 error, or a 10-minute timeout.
|
||||||
|
4. The returned **app password** (scoped, revocable) is what gets stored and
|
||||||
|
used for every subsequent request — real user passwords are never in
|
||||||
|
memory or on disk.
|
||||||
|
|
||||||
|
`LoginFlowStatus` (`idle` → `initiating` → `awaitingBrowser` → `error`)
|
||||||
|
drives `LoginView`'s UI; see `LoginFlowService`'s doc comment for the full
|
||||||
|
flow rationale before changing it.
|
||||||
|
|
||||||
|
## Talking to the server
|
||||||
|
|
||||||
|
[`NextcloudService`](../../lib/services/nextcloud_service.dart) is the
|
||||||
|
client for an authenticated session — constructed with `serverUrl` +
|
||||||
|
`username` + the app password, one instance per login (held as
|
||||||
|
`ServerProvider.service`, recreated on login/logout).
|
||||||
|
|
||||||
|
- **Files**: WebDAV (`PROPFIND`/`MKCOL`/`DELETE`/`MOVE` etc. against
|
||||||
|
`/remote.php/dav/files/{username}/...`) via raw `http`/`dio` calls with a
|
||||||
|
hand-rolled XML request body and `package:xml` for parsing responses —
|
||||||
|
there is no WebDAV client dependency. `_parseDavDate`/`_davPath` in this
|
||||||
|
file exist because WebDAV responses use RFC 1123 dates and either bare
|
||||||
|
paths or full URLs for `href`; reuse them rather than re-deriving.
|
||||||
|
- **Everything else** (shares, activity, trash, favorites, quota, user info)
|
||||||
|
goes through Nextcloud's OCS APIs (`/ocs/v2.php/...`), JSON in, with the
|
||||||
|
`OCS-APIRequest: true` header required on every OCS call.
|
||||||
|
- Auth header is HTTP Basic (`username:appPassword`, base64), built in
|
||||||
|
`_headers`/exposed as `authHeaders` for widgets that need to hit URLs
|
||||||
|
directly (e.g. `Image.network(url, headers: service.authHeaders)` for
|
||||||
|
thumbnails/previews).
|
||||||
|
- Downloads stream through `Dio` (`downloadToFile`) for progress callbacks;
|
||||||
|
small in-app previews (text/PDF) use `fetchBytes` via `package:http`.
|
||||||
|
|
||||||
|
## Session persistence
|
||||||
|
|
||||||
|
- **Credentials** (`server`, `loginName`, `appPassword`) live in
|
||||||
|
`flutter_secure_storage` — OS keychain/keystore-backed, never
|
||||||
|
`shared_preferences`.
|
||||||
|
- **UI/app preferences** (theme mode, seed color, dynamic-color toggle,
|
||||||
|
bottom-bar opacity/blur, grid vs. list, sort field, hidden-files toggle,
|
||||||
|
etc.) live in `shared_preferences` — see the `_pref*` key constants at the
|
||||||
|
top of `server_provider.dart`.
|
||||||
|
- On startup, `ServerProvider._restoreSession()` reads the secure-storage
|
||||||
|
keys and, if all three are present, rebuilds a `NextcloudService` without
|
||||||
|
re-hitting the login flow (`_applyCredentials(..., persist: false)`).
|
||||||
|
`isRestoringSession` gates the splash screen until this resolves — see
|
||||||
|
`standards.md` for why widget tests must mock both storage channels
|
||||||
|
rather than relying on this async path throwing naturally.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Code standards
|
||||||
|
|
||||||
|
## Linting
|
||||||
|
|
||||||
|
`analysis_options.yaml` includes `package:flutter_lints/flutter.yaml` with
|
||||||
|
no rules added/relaxed. Run `flutter analyze` before considering a change
|
||||||
|
done; it should stay clean.
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
Comments are used sparingly and only for non-obvious *why* — a hidden
|
||||||
|
constraint, a workaround, or the rationale for overriding a framework
|
||||||
|
default. See `AppTheme._pageTransitionsTheme`/`_sliderTheme`,
|
||||||
|
`NextcloudService._parseDavDate`/`_davPath`, and `LoginFlowService`'s class
|
||||||
|
doc comment for the house style: one short doc comment on the
|
||||||
|
class/function explaining *why* it exists, not what each line does. Don't
|
||||||
|
add comments that restate the code or describe what a well-named
|
||||||
|
class/method already makes obvious.
|
||||||
|
|
||||||
|
## Widget structure
|
||||||
|
|
||||||
|
- Screens (`views/`) are typically `StatefulWidget` when they own
|
||||||
|
controllers/local UI state (e.g. `LoginView`'s form key + text
|
||||||
|
controller); presentation is frequently split into small private
|
||||||
|
`StatelessWidget`s in the same file (`_ServerForm`, `_WaitingForBrowser`
|
||||||
|
in `login_view.dart`) rather than inlined in one large `build`. Follow
|
||||||
|
this split for any view complex enough to have more than one visual
|
||||||
|
"mode".
|
||||||
|
- Private helpers/widgets are prefixed with `_` and live in the same file as
|
||||||
|
their one caller; promote to `widgets/` only once something is reused
|
||||||
|
across files.
|
||||||
|
- Read provider state with `context.watch<ServerProvider>()` in `build`,
|
||||||
|
and `context.read<ServerProvider>()` for one-off calls from callbacks
|
||||||
|
(matches `LoginView._handleContinue`).
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Widget tests must mock platform channels that the app touches on startup
|
||||||
|
— `SharedPreferences.setMockInitialValues({})` and a mock
|
||||||
|
`MethodChannel('plugins.it_nomads.com/flutter_secure_storage')` handler —
|
||||||
|
and disable Google Fonts network fetching
|
||||||
|
(`GoogleFonts.config.allowRuntimeFetching = false`) in `setUpAll`. Without
|
||||||
|
these, `ServerProvider`'s session restore never resolves in the test
|
||||||
|
sandbox (no plugin implementation is registered, so the read future just
|
||||||
|
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.
|
||||||
|
- Run with `flutter test`.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
Networking is deliberately split: `package:http` for simple JSON/XML
|
||||||
|
request-response calls, `package:dio` only where streaming/progress is
|
||||||
|
needed (downloads). Don't introduce a third HTTP client — extend the
|
||||||
|
existing split instead.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Styling
|
||||||
|
|
||||||
|
## Theme
|
||||||
|
|
||||||
|
All theming goes through [`AppTheme`](../../lib/theme/app_theme.dart)
|
||||||
|
(`AppTheme.light`/`AppTheme.dark`) — don't set colors/fonts ad hoc in
|
||||||
|
widgets. Key points:
|
||||||
|
|
||||||
|
- **Material 3**, seed-color based. `AppTheme.seedColors` is the picker list
|
||||||
|
users choose from; `defaultNextcloudBlue` (`#0082C9`) is the fallback.
|
||||||
|
- **Dynamic color** (Android 12+ Material You / desktop accent color) is
|
||||||
|
supported via `package:dynamic_color`'s `DynamicColorBuilder` wrapping the
|
||||||
|
whole app in `main.dart`. When available and `useDynamicColor` is on, the
|
||||||
|
OS-provided `ColorScheme` wins over the seed color — always thread both
|
||||||
|
`dynamicScheme` and `useDynamicColor` through when adding a theme knob.
|
||||||
|
- **Font**: Inter via `google_fonts`, applied through
|
||||||
|
`GoogleFonts.interTextTheme(...)`. In tests, set
|
||||||
|
`GoogleFonts.config.allowRuntimeFetching = false` in `setUpAll` — without
|
||||||
|
it, the font-fetch call to Google's CDN can stall `pumpAndSettle`
|
||||||
|
indefinitely (see `standards.md`).
|
||||||
|
- **Cards**: flat (`elevation: 0`), 20px rounded corners,
|
||||||
|
`surfaceContainerLow`.
|
||||||
|
- **App bars**: flat, not centered, `surface` background.
|
||||||
|
- Two Flutter defaults are deliberately overridden app-wide rather than
|
||||||
|
per-widget, each with a comment explaining why in `app_theme.dart`:
|
||||||
|
Android predictive-back page transitions, and the non-2023 `SliderTheme`.
|
||||||
|
Follow that pattern (a themed default + a comment) instead of overriding
|
||||||
|
per-instance if you need the same behavior elsewhere.
|
||||||
|
- Dark theme supports an `amoled` flag that flattens every surface tone to
|
||||||
|
pure black — extend `colorScheme.copyWith(...)` there if a new surface
|
||||||
|
role needs the same treatment, don't hardcode `Colors.black` at call sites.
|
||||||
|
|
||||||
|
## Reusable chrome
|
||||||
|
|
||||||
|
- [`FrostedGlassContainer`](../../lib/widgets/frosted_glass_container.dart) —
|
||||||
|
the blurred/translucent pill background shared by all floating chrome
|
||||||
|
(bottom nav bar, media-viewer action bar). 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
|
||||||
|
(`ServerProvider.bottomBarOpacity`/`bottomBarBlur`), not constants — pull
|
||||||
|
new adjustable visual knobs from the provider the same way rather than
|
||||||
|
hardcoding them.
|
||||||
|
- Icons: prefer `Icons.*_rounded` (matches the rest of the app) or
|
||||||
|
`material_symbols_icons` where Material Symbols are already in use; avoid
|
||||||
|
mixing in the sharp/outlined default set.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- No hardcoded colors for anything themeable — pull from
|
||||||
|
`Theme.of(context).colorScheme`, not `Colors.blue` etc. (per-file-type
|
||||||
|
icon tinting in `files_view.dart`'s `_getIconColor` is the one deliberate
|
||||||
|
exception, since those colors are content-identity cues, not theme).
|
||||||
|
- Use `colorScheme.surfaceContainer*`/`onSurfaceVariant` tokens for
|
||||||
|
elevation/secondary text rather than manual opacity on black/white.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Noo — Nextcloud Client
|
||||||
|
|
||||||
|
A Flutter (Material 3) client for Nextcloud: browse files, view photos, check
|
||||||
|
activity, manage shares/trash. Auth uses Nextcloud's Login Flow v2 — the app
|
||||||
|
never collects a password directly, only a server address.
|
||||||
|
|
||||||
|
Detailed, topic-specific context lives in `.claude/context/`. Read the
|
||||||
|
relevant file(s) before working in that area rather than loading all of them:
|
||||||
|
|
||||||
|
- [`architecture.md`](.claude/context/architecture.md) — folder layout, state
|
||||||
|
management, navigation/screen flow
|
||||||
|
- [`server.md`](.claude/context/server.md) — Nextcloud API/WebDAV
|
||||||
|
integration, login flow, session persistence
|
||||||
|
- [`styling.md`](.claude/context/styling.md) — theming, Material 3
|
||||||
|
conventions, fonts, reusable chrome widgets
|
||||||
|
- [`standards.md`](.claude/context/standards.md) — code style, comment
|
||||||
|
conventions, linting, testing
|
||||||
|
|
||||||
|
## Common commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter test # run tests
|
||||||
|
flutter analyze # static analysis / lints
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user