Update docs for share/upload pipeline rework and login WebView fix
Build APK / build (push) Successful in 5m10s
Build APK / build (push) Successful in 5m10s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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`).
|
||||||
|
|||||||
Reference in New Issue
Block a user