Update docs for share/upload pipeline rework and login WebView fix
Build APK / build (push) Successful in 5m10s

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 23:33:58 -04:00
co-authored by Claude Sonnet 5
parent bcc18a7060
commit be38f24f32
3 changed files with 85 additions and 16 deletions
+5 -3
View File
@@ -109,9 +109,11 @@ and rendered through `buildAppTabView` (`widgets/app_tab_view_builder.dart`).
Each tab keeps its own `ScrollController` (survives tab switches via Each tab keeps its own `ScrollController` (survives tab switches via
`IndexedStack`'s built-but-hidden trees) and tapping the already-active tab `IndexedStack`'s built-but-hidden trees) and tapping the already-active tab
scrolls it back to top (`tapTabToScrollTop` setting). `MainShellView` also scrolls it back to top (`tapTabToScrollTop` setting). `MainShellView` also
owns the app's share-intent listener (`receive_sharing_intent`): both owns the app's share-intent listener (`ShareIntentService`, backed by
`getInitialMedia()` (cold start via another app's "Share to...") and hand-rolled native handling in `MainActivity.kt` - see `server.md` for why
`getMediaStream()` (already running) push `ShareUploadView`. this isn't the `receive_sharing_intent` plugin): both `getInitialShare()`
(cold start via another app's "Share to...") and `onNewShare` (already
running) push `ShareUploadView`.
Five of the six tabs (all but Files) plus each tab's own controls share Five of the six tabs (all but Files) plus each tab's own controls share
[`SyncedHeaderScaffold`](../../lib/widgets/synced_header_scaffold.dart) — a [`SyncedHeaderScaffold`](../../lib/widgets/synced_header_scaffold.dart) — a
+67 -13
View File
@@ -9,12 +9,34 @@ via [`LoginFlowService`](../../lib/services/login_flow_service.dart):
1. `LoginFlowService.initiate(serverUrl)` POSTs to 1. `LoginFlowService.initiate(serverUrl)` POSTs to
`{server}/index.php/login/v2`, gets back a browser login URL + a poll `{server}/index.php/login/v2`, gets back a browser login URL + a poll
endpoint/token. endpoint/token.
2. The app opens the login URL in a Chrome Custom Tab (`url_launcher`, 2. The app opens the login URL for the user to authenticate/authorize.
`LaunchMode.inAppBrowserView` — real Chrome, so saved passwords/autofill Which browser depends on whether this is the first login or an
work, unlike Flutter's own embedded web view); the user authenticates and add-account flow (`addAccount`, see below):
authorizes there. There's no way to close the tab automatically on - First/only login: a Chrome Custom Tab (`url_launcher`,
success (Login Flow v2 never redirects back into the app, and a Custom `LaunchMode.inAppBrowserView`) — real Chrome, so saved
Tab belongs to Chrome's own task) — the user switches back manually. passwords/autofill work, unlike Flutter's own embedded web view.
There's no way to close the tab automatically on success (Login Flow
v2 never redirects back into the app, and a Custom Tab belongs to
Chrome's own task) — the user switches back manually.
- Adding another account: [`LoginWebViewView`](../../lib/views/login_webview_view.dart),
a normal screen this app owns, backed by `package:webview_flutter`
rather than a Custom Tab - pushed by `LoginView` the moment
`loginFlowStatus` flips to `awaitingBrowser`. Two reasons this isn't
a Custom Tab: (1) a Custom Tab shares Chrome's actual browser
profile/cookie jar - if the user is still logged into the first
account on the Nextcloud web UI in Chrome, it would silently reuse
that session and authorize the wrong account instead of prompting
fresh credentials; (2) `url_launcher`'s own `LaunchMode.inAppWebView`
(tried first) is a bare native WebView Activity with no chrome of its
own - no close button, and on at least some devices it draws
edge-to-edge and hides the status bar. Owning the screen fixes both -
isolated cookies, a normal `AppBar`/close button/safe area - and lets
it close itself automatically on success (it watches
`loginFlowStatus` itself), unlike the Custom Tab case. The cost is no
Chrome-autofill for this one flow (Android's own system Autofill
framework, e.g. a password manager, may still work in the WebView;
Chrome's own saved-password autofill specifically cannot, since
that's Chrome-only).
3. `ServerProvider` polls `LoginFlowService.poll(pollEndpoint, token)` every 3. `ServerProvider` polls `LoginFlowService.poll(pollEndpoint, token)` every
2 seconds (`Timer.periodic`, see `_pollTimer`/`_pollTimeoutTimer` in 2 seconds (`Timer.periodic`, see `_pollTimer`/`_pollTimeoutTimer` in
`server_provider.dart`) until it gets a 200 with `server`/`loginName`/ `server_provider.dart`) until it gets a 200 with `server`/`loginName`/
@@ -68,13 +90,45 @@ rather than needing a rewrite for multi-account support.
`ServerProvider` wrapper always uploads into `_currentFolderPath` — the `ServerProvider` wrapper always uploads into `_currentFolderPath` — the
share-to-upload flow (`ShareUploadView`) gets a caller-chosen destination share-to-upload flow (`ShareUploadView`) gets a caller-chosen destination
by navigating there first (`navigateToAbsoluteFolder`), then uploading. by navigating there first (`navigateToAbsoluteFolder`), then uploading.
- **Receiving a shared file from another app**: `receive_sharing_intent` - **Receiving a shared file from another app**: hand-rolled in
(Android `ACTION_SEND`/`ACTION_SEND_MULTIPLE`, `android:launchMode` `MainActivity.kt` (Android `ACTION_SEND`/`ACTION_SEND_MULTIPLE`,
`singleTask` in the manifest so a second share while running hits `android:launchMode` `singleTask` in the manifest so a second share while
`onNewIntent` instead of spawning a new instance). `MainShellView` listens running hits `onNewIntent` instead of spawning a new instance) plus
via `getInitialMedia()`/`getMediaStream()` and pushes `ShareUploadView`, [`ShareIntentService`](../../lib/services/share_intent_service.dart) on
which reuses the same `uploadFileFromPath` path after the user picks a the Dart side - not the `receive_sharing_intent` plugin, which this used
destination folder. to be. That plugin resolves a shared `content://` Uri by synchronously
copying the *entire* file into the cache dir on the main thread during
activity startup; for a large file that blocks long enough that Android
kills the newly-launched activity for failing to draw a first frame,
dropping the user straight back to the home screen with no error and no
Dart code ever running. `MainActivity.kt`'s doc comment has the full
story. The fix: `getInitialShare`/`onNewShare` only ever query cheap Uri
metadata (name/size/mime, not content) so `ShareUploadView`'s destination
picker - which mirrors the Files tab's own controls/filters/listing,
reusing the same `ServerProvider` fields and `widgets/item_icon.dart` -
always appears instantly regardless of file size.
- **Uploading a shared file**: once the user picks a destination in
`ShareUploadView`, [`UploadService`](../../lib/services/upload_service.dart)
hands the whole batch off to `ShareUploadService.kt`, an Android
foreground service, rather than uploading from Dart in that screen. This
is deliberate, not just an implementation detail: the point is that
closing the app right after confirming a destination doesn't interrupt
the upload, the same guarantee a real file-manager app's upload
notification gives you - a plain Dart `Future` (even one kept alive by a
singleton service class) stops running once the Flutter engine/Activity
are gone, only an actual Android `Service` survives that. The service
re-implements the WebDAV PUT itself in Kotlin (`HttpURLConnection`, no
new HTTP dependency) since it can't reach the Dart-side `NextcloudService`/
Dio from a separate process lifecycle - `UploadService.startUpload` passes
everything the Kotlin side needs (the pre-built `Authorization` header
from `NextcloudService.authHeaders`, not the raw password) as Intent
extras, a one-way handoff with no channel back to Dart afterward. Keep
the two upload implementations in sync manually if upload semantics
change. Progress/cancellation is entirely notification-driven (one
ongoing, updatable notification for the whole batch; its Cancel action
re-delivers an Intent to the same running service instance, which an
`AtomicBoolean` the copy/upload loops poll) - there's no plumbing back to
the Dart UI, by design, since the app may not even be running.
## Multi-account storage & session persistence ## Multi-account storage & session persistence
+13
View File
@@ -36,6 +36,12 @@ widgets. Key points:
## Reusable chrome ## Reusable 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.
- [`FrostedGlassContainer`](../../lib/widgets/frosted_glass_container.dart) — - [`FrostedGlassContainer`](../../lib/widgets/frosted_glass_container.dart) —
the blurred/translucent pill background shared by all floating chrome the blurred/translucent pill background shared by all floating chrome
(bottom nav bar, media-viewer top/bottom bars and video transport (bottom nav bar, media-viewer top/bottom bars and video transport
@@ -81,3 +87,10 @@ widgets. Key points:
exception, since those colors are content-identity cues, not theme). exception, since those colors are content-identity cues, not theme).
- Use `colorScheme.surfaceContainer*`/`onSurfaceVariant` tokens for - Use `colorScheme.surfaceContainer*`/`onSurfaceVariant` tokens for
elevation/secondary text rather than manual opacity on black/white. elevation/secondary text rather than manual opacity on black/white.
- **Anywhere the app shows its own icon in-app** (splash, lock screen,
login screen) uses `assets/icon/app_icon_monochrome.png` — a plain white
silhouette on transparent, tinted via `ColorFiltered(colorFilter:
ColorFilter.mode(colorScheme.onSurface, BlendMode.srcIn), ...)` so it
reads correctly in both light and dark mode. Never the full-color
`app_icon.png`/adaptive-icon assets for in-app UI — those are for the
launcher icon only (`flutter_launcher_icons` in `pubspec.yaml`).