Compare commits
1
Commits
3f156cbc13
..
RC2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb1688ce3a |
@@ -10,12 +10,12 @@ lib/
|
||||
# SavedAccount, AppTab, ...)
|
||||
providers/ # ServerProvider — the single app-wide ChangeNotifier
|
||||
services/ # network/IO: NextcloudService, LoginFlowService,
|
||||
# AccountStore
|
||||
# AccountStore, AppLockService
|
||||
theme/ # AppTheme (Material 3 ThemeData)
|
||||
views/ # one screen each (FilesView, PhotosView, TrashView,
|
||||
# SharesView, RecentView, ActivityView, SearchView,
|
||||
# AccountView, LoginView, FileViewerScreen,
|
||||
# ShareUploadView)
|
||||
# ShareUploadView, LockScreenView)
|
||||
widgets/ # reusable pieces shared across views
|
||||
details/ # the file-details bottom sheet and its tabs
|
||||
```
|
||||
@@ -54,9 +54,23 @@ There is exactly one `ChangeNotifier`: [`ServerProvider`](../../lib/providers/se
|
||||
- UI settings that persist across launches — split into **global** (theme
|
||||
mode, seed color, dynamic-color toggle, bottom-bar opacity/blur,
|
||||
tap-tab-to-scroll-top, seek bar style, tab order/visibility/default, swipe
|
||||
actions) and **per-account** (grid/list view, favorites-only, show-hidden,
|
||||
storage scope, Photos sort, Files' per-folder sort map, cache policy) — see
|
||||
`server.md` for exactly which is which and why
|
||||
actions, login lock — see below) and **per-account** (grid/list view,
|
||||
favorites-only, show-hidden, storage scope, Photos sort, Files'
|
||||
per-folder sort map, cache policy) — see `server.md` for exactly which is
|
||||
which and why
|
||||
- **Login lock** (`loginLockEnabled`/`lockAccountSwitching`/
|
||||
`lockHiddenFiles`/`needsUnlock`): an app-wide PIN/biometric gate via
|
||||
`AppLockService` (a thin wrapper over `local_auth` — this app never
|
||||
stores or hashes a PIN itself, it delegates entirely to whatever
|
||||
credential the OS already has configured). `needsUnlock` is
|
||||
`loginLockEnabled && !_isUnlocked`, where `_isUnlocked` is transient
|
||||
(never persisted) and reset to `false` on every backgrounding
|
||||
(`didChangeAppLifecycleState`, `AppLifecycleState.paused`) so the lock has
|
||||
real value rather than only firing once per cold start. `switchAccount`/
|
||||
`cycleToNextAccount`/`cycleToPreviousAccount` and *enabling* (not
|
||||
disabling) `showHiddenFiles`/`showHiddenPhotos` each call the shared
|
||||
`_passGate` helper, which no-ops unless both `loginLockEnabled` and the
|
||||
relevant per-feature toggle are on.
|
||||
|
||||
New **global** state belongs on `ServerProvider` as a private field + getter
|
||||
+ a method that mutates it, calls `notifyListeners()`, and persists via
|
||||
@@ -70,7 +84,7 @@ split.
|
||||
|
||||
There's no separate repository/data layer: views call `ServerProvider`
|
||||
methods directly, which call `NextcloudService`/`LoginFlowService`/
|
||||
`AccountStore`.
|
||||
`AccountStore`/`AppLockService`.
|
||||
|
||||
## Navigation / screen flow
|
||||
|
||||
@@ -85,6 +99,7 @@ state, no named routes:
|
||||
Flow v2) — `isLoggedIn` is only ever false here or after the last saved
|
||||
account is removed; switching between multiple saved accounts never
|
||||
routes through this screen (see `server.md`)
|
||||
- else `provider.needsUnlock` → `LockScreenView` (login lock — see above)
|
||||
- else `MainShellView`
|
||||
|
||||
`MainShellView` is a bottom-nav `IndexedStack` over up to 6 tabs — Files,
|
||||
|
||||
@@ -121,9 +121,53 @@ account is *currently* live (see `architecture.md`).
|
||||
its credentials and refetch everything. This is a full teardown-and-reload
|
||||
every time — there is deliberately no simultaneous multi-account state or
|
||||
background sync; only one account's content is ever live.
|
||||
- `logout()` is just `removeAccount(activeAccountId)` — with other accounts
|
||||
saved it falls back to one of them instead of ending the session;
|
||||
`isLoggedIn` only ever becomes `false` when the *last* account is removed.
|
||||
- **`logout()` vs `removeAccount()`** are deliberately different actions,
|
||||
both funneling into a shared `_deactivateSession()` helper for the
|
||||
teardown/pointer-clearing part:
|
||||
- `logout()` ends the active session but keeps the account itself fully
|
||||
intact (password, prefs, its entry in `accounts` all untouched) —
|
||||
always lands on `LoginView` even if other accounts are saved (it does
|
||||
*not* fall back to one of them the way `removeAccount` does). This
|
||||
exists so `LoginView` can offer a "Continue as ..." one-tap resume list
|
||||
(`_SavedAccountsSection` in `login_view.dart`) with no Login Flow v2
|
||||
needed - logging out must never be mistaken for forgetting an account.
|
||||
- `removeAccount(id)` deletes everything for that account (secure-storage
|
||||
password, namespaced prefs, its `accounts` entry) and, only if it was
|
||||
the active one, falls back to another saved account or - if none
|
||||
remain - calls the same `_deactivateSession()`. This is the only path
|
||||
(besides `logout()`) that can set `isLoggedIn` false, and the only one
|
||||
that's actually destructive/irreversible - UI call sites (`AccountView`)
|
||||
gate it behind a confirmation dialog; `logout()` doesn't need one.
|
||||
- Any UI code that calls either and might have ended the session should
|
||||
check `!provider.isLoggedIn` afterward and `Navigator.popUntil((r) =>
|
||||
r.isFirst)` if so — otherwise a screen pushed on top (Settings) is left
|
||||
stranded over a root route that's silently swapped to `LoginView`
|
||||
underneath it. Don't pop unconditionally — removing a *non-active*
|
||||
account, or one that fell back to another, keeps the user logged in and
|
||||
Settings should just stay open.
|
||||
|
||||
## App lock (login lock)
|
||||
|
||||
An orthogonal, app-wide security layer on top of the Nextcloud
|
||||
login/session above — not account credentials, just a gate on *using* the
|
||||
app. [`AppLockService`](../../lib/services/app_lock_service.dart) wraps
|
||||
`local_auth`; this app never implements its own PIN entry/storage/hashing —
|
||||
`authenticate()` always delegates to whatever the OS already has configured
|
||||
(biometric, or device PIN/pattern/password as fallback, via
|
||||
`biometricOnly: false`). Never build a custom in-app PIN screen for this —
|
||||
extend `AppLockService`/the `ServerProvider` gates described in
|
||||
`architecture.md` instead.
|
||||
|
||||
**Android native requirements** (both already done, keep them if you touch
|
||||
these files): `MainActivity.kt` must extend `FlutterFragmentActivity`, not
|
||||
the default `FlutterActivity` — `local_auth`'s Android implementation hosts
|
||||
its prompt via a Fragment and silently fails to build/crashes without it.
|
||||
`AndroidManifest.xml` needs `<uses-permission
|
||||
android:name="android.permission.USE_BIOMETRIC"/>` (also declared by the
|
||||
plugin's own manifest via merge, but kept explicit here too).
|
||||
`android/app/build.gradle.kts` floors `minSdk` at 24 (`local_auth_android`'s
|
||||
own requirement) via `maxOf(24, flutter.minSdkVersion)` rather than trusting
|
||||
Flutter's own default to already be high enough.
|
||||
|
||||
`isRestoringSession` still gates the splash screen until the above resolves
|
||||
— see `standards.md` for why widget tests must mock both storage channels
|
||||
|
||||
@@ -66,6 +66,32 @@ class/method already makes obvious.
|
||||
of failing fast. See `test/widget_test.dart` for the reference setup.
|
||||
- Run with `flutter test`.
|
||||
|
||||
## Local install/deploy
|
||||
|
||||
Never use `flutter install` to push a build to a test device — it always
|
||||
does a full **uninstall-then-install** (prints "Uninstalling old
|
||||
version..."), and Android deletes all app data (SharedPreferences, secure
|
||||
storage — every saved account/preference) on uninstall. This wipes the app
|
||||
clean on every single deploy, which looks like an account/settings-loss bug
|
||||
but is actually just the install method.
|
||||
|
||||
Instead, build then install with `adb`'s replace flag, which updates the
|
||||
APK in place and preserves app data:
|
||||
|
||||
```bash
|
||||
flutter build apk --release
|
||||
adb install -r build/app/outputs/flutter-apk/app-release.apk
|
||||
```
|
||||
|
||||
This only preserves data if the new APK's signature matches what's already
|
||||
on the device — a local build's release variant currently reuses the debug
|
||||
signing config (`android/app/build.gradle.kts`), so consecutive local
|
||||
builds share a key and `adb install -r` works cleanly. Installing a build
|
||||
signed with a different key (e.g. a CI-signed release APK from the Gitea
|
||||
release pipeline) over a differently-signed local build forces Android to
|
||||
require a full uninstall regardless of the install method used — there's no
|
||||
way around that from the tooling side.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Networking is deliberately split: `package:http` for simple JSON/XML
|
||||
|
||||
Reference in New Issue
Block a user