Add GET_CONTENT picker support, fix account/login bugs, restyle selection UI
Build APK / build (push) Successful in 7m40s

- Let Noo be picked as a source (Files + Photos) by other apps' native
  file/photo pickers via Android's GET_CONTENT intent, mirroring the
  existing "Share to Noo" plumbing: MainActivity.kt/PickIntentService/
  PickRequest, ServerProvider.pickRequest/confirmPick/cancelPick, and
  pick-mode tap handling in FilesView/PhotosView. While picking, the
  bottom nav and MoreTabsButton are restricted to Files/Photos only.
- Fix two account bugs: testConnection() failures no longer delete a
  still-valid stored app password (only an actual 401 does), and
  switchAccount() now retries a nominally-active-but-logged-out account
  instead of no-op'ing - both were silently locking users out of "Continue
  as" with no way back in except removing and re-adding the account
  (now exposed via login_view.dart's inline remove button).
- Fix the login WebView: clear cookies before every load so "Add Account"
  can't silently reuse an existing session, and push it as a
  non-animated route to avoid a known WebView-as-PlatformView black-screen-
  on-pop issue under Impeller.
- Redesign the Files/Photos selection toolbar to take over the top app bar
  entirely (SyncedHeaderScaffold.selectionBar) instead of sitting as a
  second bar under the controls row, with a horizontally-scrollable,
  uniformly-colored actions row and the same one-shot scroll-hint nudge
  the controls row uses.
- Redesign ShareUploadView to share the same SyncedHeaderScaffold chrome
  as every tab, with the uploading-file summary (marqueed via the new
  shared MarqueeTitle widget) and upload button grouped into one
  rounded/elevated bottom sheet.
- Restyle ShareSheet to open on the same DetailsHeader every other
  per-item sheet uses, and fix a couple of small design-language drifts
  (hardcoded text style, a non-rounded icon).
- Settings' account card: icon buttons with tooltips instead of text
  buttons for refresh/logout/remove, divider removed.
This commit is contained in:
2026-09-17 23:36:54 -04:00
parent eee2730fbe
commit 44a23c3632
24 changed files with 1298 additions and 394 deletions
+52
View File
@@ -26,6 +26,15 @@ via [`LoginFlowService`](../../lib/services/login_flow_service.dart):
Chrome-autofill (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).
`LoginWebViewView` also clears cookies (`WebViewCookieManager().
clearCookies()`) before every load, not just once - Android's WebView
`CookieManager` is a single store shared/persisted across every WebView
instance in the app process, not scoped per-controller, so without this
a second/"Add Account" login silently reuses whichever account's
Nextcloud session cookie is already there instead of prompting for
credentials (the same class of bug the Custom-Tab-reuse issue above
was, just recurring one layer down once everything moved to the owned
WebView).
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`/
@@ -119,6 +128,49 @@ rather than needing a rewrite for multi-account support.
`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.
## Being picked by other apps (photo/file picker)
Noo can also be launched *by* another app as a `GET_CONTENT` picker (e.g.
Google Drive/Instagram's "choose a file" flow), the reverse direction of
"Share to Noo" above - hand-rolled the same way, not a plugin.
- `MainActivity.kt` matches `ACTION_GET_CONTENT` (`OPENABLE`, any
mimeType - a single `*/*` filter, since Android matches it against
whatever the caller actually requested) alongside its existing
`ACTION_SEND`/`SEND_MULTIPLE` filters, and exposes the caller's requested
mimeType/multi-select flag/app label via the
`dev.ayushya.noo/pick_intent` method+event channel pair
([`PickIntentService`](../../lib/services/pick_intent_service.dart)/
[`PickRequest`](../../lib/models/pick_request.dart)) - same
cold-start-vs-already-running split as the share-intent channels.
- `ServerProvider.pickRequest`/`isPicking` drive picking mode app-wide once
`MainShellView` learns about a request at startup or via
`onNewPickRequest`. While picking, `MainShellView` restricts the visible
bottom-nav tabs to just Files and Photos (see `architecture.md`) -
`FilesView`/`PhotosView` route taps through
`itemMatchesPickFilter`/`confirmPick` instead of their normal
open/select behavior (folders still navigate; a mime-mismatched file is
rejected with a snackbar; matching files toggle-select or immediately
confirm depending on `PickRequest.allowMultiple`).
- `ServerProvider.confirmPick` downloads the selected item(s) to a
`picker/` scratch subfolder in the app's cache dir (`downloadToFile`,
same as any other download) - each item into its own `picker/<item.id>/`
subfolder, keeping the on-disk filename as plain `item.name` rather than
prefixing it with the id to dodge collisions between same-named items;
the caller reads that on-disk name back as the display name, so
prefixing it there was a real bug (Drive showing e.g. `163332_photo.jpg`
instead of `photo.jpg`) - then hands the local paths to
`PickIntentService.finishPick`, which calls back into
`MainActivity.kt.finishPick`: it wraps each file in a `content://` Uri
via this app's own `FileProvider` (`${applicationId}.picker.fileprovider`,
scoped to just that cache subfolder - see `android/app/src/main/res/xml/
file_paths.xml`) and returns it to the caller via `setResult`. Single
file uses `setDataAndType` (never `.data =`/`.type =` as two separate
calls - each one silently nulls out the other field on a plain
`Intent`); multiple files use `ClipData`. `cancelPick` mirrors this for
backing out (system back while picking, or a picked-item mismatch) with
`RESULT_CANCELED` instead.
## Multi-account storage & session persistence
[`AccountStore`](../../lib/services/account_store.dart) owns everything