Add in-app copy/move, background downloads, and a Favorites tab
Build APK / build (push) Successful in 5m18s
Build APK / build (push) Successful in 5m18s
- WebDAV MOVE/COPY-backed copy/move for files and folders, with a destination picker (independent nav state so it doesn't disturb the Files tab's browsing position) and a conflict-resolution sheet (overwrite all / keep both / decide per item). - Downloads now use the same foreground-service + notification approach as uploads, surviving app closure. - Favorites is now its own tab (account-wide, via a WebDAV SEARCH query) instead of a Files-tab filter toggle, which couldn't represent favorited items outside the currently browsed folder correctly. - Share sheet restyled to match the app's design language, plus a "share file directly" action via the OS share sheet; details sheet tab icons resized to match the bottom nav. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -102,10 +102,15 @@ state, no named routes:
|
||||
- else `provider.needsUnlock` → `LockScreenView` (login lock — see above)
|
||||
- else `MainShellView`
|
||||
|
||||
`MainShellView` is a bottom-nav `IndexedStack` over up to 6 tabs — Files,
|
||||
Photos, Activity, Trash, Shares, Recent — user-configurable (order,
|
||||
visibility up to `maxVisibleTabs`, default tab) via `AppTab`/`ServerProvider`
|
||||
and rendered through `buildAppTabView` (`widgets/app_tab_view_builder.dart`).
|
||||
`MainShellView` is a bottom-nav `IndexedStack` over up to 7 tabs — Files,
|
||||
Photos, Favorites, Activity, Trash, Shares, Recent — user-configurable
|
||||
(order, visibility up to `maxVisibleTabs`, default tab) via
|
||||
`AppTab`/`ServerProvider` and rendered through `buildAppTabView`
|
||||
(`widgets/app_tab_view_builder.dart`). `maxVisibleTabs` (5) is less than the
|
||||
total tab count, and `ServerProvider._enforceMaxVisibleTabs` already
|
||||
auto-hides overflow on load (fresh install, or - as when Favorites was
|
||||
added - an existing saved tab order from before a new tab existed), so
|
||||
adding a tab to the `AppTab` enum needs no extra migration.
|
||||
Each tab keeps its own `ScrollController` (survives tab switches via
|
||||
`IndexedStack`'s built-but-hidden trees) and tapping the already-active tab
|
||||
scrolls it back to top (`tapTabToScrollTop` setting). `MainShellView` also
|
||||
@@ -140,6 +145,23 @@ sheet peeking up from the bottom edge rather than a plain flat bar.
|
||||
[`MarqueeTitle`](../../lib/widgets/marquee_title.dart) (`package:marquee`) is
|
||||
shared with `FileViewerScreen`'s title - falls back to a plain ellipsized
|
||||
`Text` when the content already fits, so short text never marquees.
|
||||
|
||||
The Move/Copy destination picker
|
||||
([`MoveCopyDestinationPicker`](../../lib/views/move_copy_destination_picker.dart),
|
||||
pushed from Files/Photos' selection toolbar - see `server.md` for the
|
||||
backend side) visually mirrors `ShareUploadView`'s browser the same way,
|
||||
but is a deliberately different case for state: it does **not** reuse
|
||||
`ServerProvider`'s shared `currentFolderPath`/`pathStack`/`items`, and
|
||||
owns its own local navigation state instead, fetching through the
|
||||
stateless `ServerProvider.fetchFolderListing`/`applyFilesDisplayPrefs`
|
||||
pair. `ShareUploadView` can get away with hijacking the shared state
|
||||
because it always resets to root on entry and pops all the way to the
|
||||
app's root route on completion - fine for a cold share-intent launch with
|
||||
no prior browsing session to preserve. The Move/Copy picker is pushed
|
||||
*while the user is actively browsing a specific Files-tab folder*, so
|
||||
reusing the shared state would strand that folder's `pathStack` under it;
|
||||
its local state means popping back always lands the user exactly where
|
||||
they were, untouched.
|
||||
`SearchView`, `AccountView` (Settings), the file-details sheet, and the
|
||||
share sheet are pushed on top via
|
||||
`Navigator`/`showModalBottomSheet`/`showGradualBottomSheet` rather than
|
||||
|
||||
@@ -76,12 +76,87 @@ rather than needing a rewrite for multi-account support.
|
||||
- **Everything else** (shares, activity, trash, favorites, quota, user info,
|
||||
file versions) goes through Nextcloud's OCS APIs (`/ocs/v2.php/...`), JSON
|
||||
in, with the `OCS-APIRequest: true` header required on every OCS call.
|
||||
*Toggling* a favorite is OCS; *listing* every favorite (`fetchFavorites`,
|
||||
for the Favorites tab) is WebDAV instead - the same `SEARCH` mechanism
|
||||
`fetchAllMedia`/`fetchRecentFiles` use, filtered by `oc:favorite` instead
|
||||
of mimetype/date. Favorites is a real tab
|
||||
(`views/favorites_view.dart`/`ServerProvider.favoriteItems`/
|
||||
`fetchAllFavorites`/`_allFavorites`), not a filter toggle scoped to
|
||||
whatever folder the Files tab happens to be browsing (that's what it
|
||||
used to be - see the note below on why that didn't work). It shares
|
||||
Files' own sort/hidden/storage-scope/grid-list display prefs
|
||||
(`applyFilesDisplayPrefs`, also used by the Move/Copy destination
|
||||
picker) rather than a separate parallel settings dimension. Tapping a
|
||||
favorited folder switches to the Files tab, navigated there
|
||||
(`navigateToAbsoluteFolder` + `requestTab`); a favorited file opens
|
||||
directly from the Favorites tab itself. `deleteItem`/`renameItem`/move/
|
||||
copy all re-sync `_allFavorites` afterward via `_syncFavoritesIfLoaded`
|
||||
(only once Favorites has actually been opened this session, tracked by
|
||||
`_favoritesEverFetched`, so those actions don't pay for an extra request
|
||||
on every edit for an account that's never visited the tab) since none of
|
||||
them know how to patch `_allFavorites` in place the way
|
||||
`toggleItemFavorite` does (added/removed/updated by id, right inline).
|
||||
**Note**: this used to be a "favorites-only" filter toggle on the Files
|
||||
tab's controls row instead of its own tab, filtering the currently
|
||||
browsed folder's `_items`. That had two real bugs in sequence: first,
|
||||
the filter dropped every non-favorited item *including folders*, so a
|
||||
non-favorited folder (containing a favorited item nested inside)
|
||||
vanished from the listing entirely, with no way to navigate into it;
|
||||
fixing that by exempting folders from the filter was itself wrong,
|
||||
because the actual intent was for favorites-only to show every
|
||||
favorited item *account-wide*, not just direct children of whatever
|
||||
folder was open - a strict filter over the wrong scope. Converting it to
|
||||
a real tab, backed by an account-wide fetch, was the actual fix; keep
|
||||
favorites account-wide rather than reintroducing a current-folder-scoped
|
||||
filter.
|
||||
- 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`.
|
||||
- `NextcloudService.downloadToFile` (`Dio`, progress callbacks) is still
|
||||
used for small in-app-only downloads: text/PDF previews (`fetchBytes` via
|
||||
`package:http`) and "open externally" (`FileViewerScreen._downloadToTemp`
|
||||
streams to the app's own cache dir so `open_file` can hand it to another
|
||||
app). Explicitly saving a file **to the device** - the "Download" action
|
||||
in Files/Photos' selection toolbar and the media viewer's download button
|
||||
- instead hands off to `DownloadService.kt`, an Android foreground
|
||||
service, the same way "Share to Noo" hands its upload off to
|
||||
`ShareUploadService.kt` (see that section below) rather than downloading
|
||||
in Dart and prompting `file_saver` per file: a real Service survives the
|
||||
app being closed mid-download, with one cancellable notification for the
|
||||
whole batch. `DownloadService.kt` re-implements a plain WebDAV GET in
|
||||
Kotlin for the same reason `ShareUploadService.kt`'s PUT does - keep
|
||||
`NextcloudService.downloadToFile` in sync manually if download semantics
|
||||
change - and writes straight into the device's public Downloads
|
||||
collection via `MediaStore.Downloads` (API 29+; a background Service
|
||||
can't prompt `file_saver`'s SAF picker the way the Flutter/Activity side
|
||||
can, so this is the direct equivalent) with a legacy
|
||||
`Environment.DIRECTORY_DOWNLOADS` file-write fallback pre-Android-10.
|
||||
Folders are filtered out client-side before handing off (no recursive/
|
||||
zip download support); `DetailsVersionsTab`'s version-restore download
|
||||
and `ShareSheet`'s "Share file directly" (native OS share, not saved to
|
||||
Downloads) keep using `downloadToFile` directly instead, since both need
|
||||
the bytes in-app rather than saved to Downloads.
|
||||
- **Move/Copy** (`moveItem`/`copyItem(itemPath, destFolderPath, {overwrite,
|
||||
newName})`) share one private `_moveOrCopy` helper with `renameItem`
|
||||
(itself just a same-folder MOVE) - WebDAV `MOVE`/`COPY` are the same
|
||||
request shape, just a different verb, and both are recursive by default
|
||||
for a folder ("collection"), so no extra `Depth` header is needed. They
|
||||
return the raw HTTP status rather than a bool: `412 Precondition Failed`
|
||||
is WebDAV's standard signal for "something's already there" when
|
||||
`Overwrite: F`, which is exactly the conflict `ServerProvider.moveItems`/
|
||||
`copyItems` need to detect without a separate existence-check request
|
||||
per item. `ServerProvider` attempts every item in the batch first,
|
||||
collects conflicts into `MoveCopyResult.conflicts`
|
||||
(`models/move_copy_result.dart`), and only then shows one summary
|
||||
(`MoveCopyConflictSheet`) instead of prompting per conflict as they're
|
||||
hit; resolving picks overwrite/keep-both (auto-renamed via
|
||||
`_nextAvailableName` against a fresh listing of the destination, fetched
|
||||
once up front, not per item)/skip per item via `resolveConflicts`. The
|
||||
destination-picker screen behind this
|
||||
(`views/move_copy_destination_picker.dart`) is covered in
|
||||
`architecture.md`, including why it can't reuse the Files tab's shared
|
||||
navigation state the way `ShareUploadView` does.
|
||||
- **Uploads** (`uploadFileFromPath(folderPath, fileName, localFilePath,
|
||||
{onProgress})`) stream the local file via `Dio().put()` with an explicit
|
||||
`Content-Length` and `onSendProgress`, mirroring the download path. The
|
||||
|
||||
Reference in New Issue
Block a user