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