From 0c877e619cbf41679ce1b5ab25c88642bd6a854f Mon Sep 17 00:00:00 2001 From: ayushya Date: Wed, 16 Sep 2026 00:25:53 -0400 Subject: [PATCH] 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 --- .claude/context/architecture.md | 59 ++++++++++++++++++++++++++++++ .claude/context/server.md | 63 +++++++++++++++++++++++++++++++++ .claude/context/standards.md | 55 ++++++++++++++++++++++++++++ .claude/context/styling.md | 55 ++++++++++++++++++++++++++++ CLAUDE.md | 24 +++++++++++++ 5 files changed, 256 insertions(+) create mode 100644 .claude/context/architecture.md create mode 100644 .claude/context/server.md create mode 100644 .claude/context/standards.md create mode 100644 .claude/context/styling.md create mode 100644 CLAUDE.md diff --git a/.claude/context/architecture.md b/.claude/context/architecture.md new file mode 100644 index 0000000..9be043a --- /dev/null +++ b/.claude/context/architecture.md @@ -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. diff --git a/.claude/context/server.md b/.claude/context/server.md new file mode 100644 index 0000000..de9d130 --- /dev/null +++ b/.claude/context/server.md @@ -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. diff --git a/.claude/context/standards.md b/.claude/context/standards.md new file mode 100644 index 0000000..57c00dc --- /dev/null +++ b/.claude/context/standards.md @@ -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()` in `build`, + and `context.read()` 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. diff --git a/.claude/context/styling.md b/.claude/context/styling.md new file mode 100644 index 0000000..6812968 --- /dev/null +++ b/.claude/context/styling.md @@ -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. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c655a78 --- /dev/null +++ b/CLAUDE.md @@ -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 +```