feat(mobile): Manual asset upload (#3445)

* fix: exclude albums filter in backup provider

* refactor: Separate builder methods for Top Control App Bar buttons

* fix: Show download button only for Remote only assets

* fix(mobile): Force Refresh duration is too low to trigger it consistently

* feat(mobile): Make Buttons dynamic in Home Selection DraggableScrollableSheet

* feat(mobile): Manual Asset upload

* refactor(mobile): Replace _showToast with ImmichToast calls

* refactor(mobile): home_page selectionAssetState handling

* chore(mobile): min and initial size of DraggableScrollState increased

This is to prevent the buttons in the bottom sheet getting clipped behind the 3 way navigation buttons
in the default density of Android devices

* feat(mobile): notifications for manual upload progress

* wording

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
shalong-tanwen 2023-08-06 02:40:50 +00:00 committed by GitHub
parent f1b92718d5
commit deaf81e2a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 887 additions and 163 deletions

View file

@ -5,6 +5,8 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
import 'package:immich_mobile/shared/models/album.dart';
@ -15,10 +17,12 @@ class ControlBottomAppBar extends ConsumerWidget {
final void Function() onDelete;
final Function(Album album) onAddToAlbum;
final void Function() onCreateNewAlbum;
final void Function() onUpload;
final List<Album> albums;
final List<Album> sharedAlbums;
final bool enabled;
final AssetState selectionAssetState;
const ControlBottomAppBar({
Key? key,
@ -30,12 +34,15 @@ class ControlBottomAppBar extends ConsumerWidget {
required this.albums,
required this.onAddToAlbum,
required this.onCreateNewAlbum,
required this.onUpload,
this.selectionAssetState = AssetState.remote,
this.enabled = true,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
var hasRemote = selectionAssetState == AssetState.remote;
Widget renderActionButtons() {
return Row(
@ -47,11 +54,12 @@ class ControlBottomAppBar extends ConsumerWidget {
label: "control_bottom_app_bar_share".tr(),
onPressed: enabled ? onShare : null,
),
ControlBoxButton(
iconData: Icons.favorite_border_rounded,
label: "control_bottom_app_bar_favorite".tr(),
onPressed: enabled ? onFavorite : null,
),
if (hasRemote)
ControlBoxButton(
iconData: Icons.favorite_border_rounded,
label: "control_bottom_app_bar_favorite".tr(),
onPressed: enabled ? onFavorite : null,
),
ControlBoxButton(
iconData: Icons.delete_outline_rounded,
label: "control_bottom_app_bar_delete".tr(),
@ -66,19 +74,35 @@ class ControlBottomAppBar extends ConsumerWidget {
)
: null,
),
ControlBoxButton(
iconData: Icons.archive,
label: "control_bottom_app_bar_archive".tr(),
onPressed: enabled ? onArchive : null,
),
if (!hasRemote)
ControlBoxButton(
iconData: Icons.backup_outlined,
label: "Upload",
onPressed: enabled
? () => showDialog(
context: context,
builder: (BuildContext context) {
return UploadDialog(
onUpload: onUpload,
);
},
)
: null,
),
if (hasRemote)
ControlBoxButton(
iconData: Icons.archive,
label: "control_bottom_app_bar_archive".tr(),
onPressed: enabled ? onArchive : null,
),
],
);
}
return DraggableScrollableSheet(
initialChildSize: 0.30,
minChildSize: 0.15,
maxChildSize: 0.57,
initialChildSize: hasRemote ? 0.30 : 0.18,
minChildSize: 0.18,
maxChildSize: hasRemote ? 0.57 : 0.18,
snap: true,
builder: (
BuildContext context,
@ -105,29 +129,33 @@ class ControlBottomAppBar extends ConsumerWidget {
const CustomDraggingHandle(),
const SizedBox(height: 12),
renderActionButtons(),
const Divider(
indent: 16,
endIndent: 16,
thickness: 1,
),
AddToAlbumTitleRow(
onCreateNewAlbum: enabled ? onCreateNewAlbum : null,
),
if (hasRemote)
const Divider(
indent: 16,
endIndent: 16,
thickness: 1,
),
if (hasRemote)
AddToAlbumTitleRow(
onCreateNewAlbum: enabled ? onCreateNewAlbum : null,
),
],
),
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: AddToAlbumSliverList(
albums: albums,
sharedAlbums: sharedAlbums,
onAddToAlbum: onAddToAlbum,
enabled: enabled,
if (hasRemote)
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: AddToAlbumSliverList(
albums: albums,
sharedAlbums: sharedAlbums,
onAddToAlbum: onAddToAlbum,
enabled: enabled,
),
),
),
const SliverToBoxAdapter(
child: SizedBox(height: 200),
)
if (hasRemote)
const SliverToBoxAdapter(
child: SizedBox(height: 200),
)
],
),
);

View file

@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer_header.dart';
import 'package:immich_mobile/modules/home/ui/profile_drawer/server_info_box.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
@ -35,6 +36,7 @@ class ProfileDrawer extends HookConsumerWidget {
onTap: () async {
await ref.watch(authenticationProvider.notifier).logout();
ref.read(manualUploadProvider.notifier).cancelBackup();
ref.watch(backupProvider.notifier).cancelBackup();
ref.watch(assetProvider.notifier).clearAllAsset();
ref.watch(websocketProvider.notifier).disconnect();

View file

@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
class UploadDialog extends ConfirmDialog {
final Function onUpload;
const UploadDialog({Key? key, required this.onUpload})
: super(
key: key,
title: 'upload_dialog_title',
content: 'upload_dialog_info',
cancel: 'upload_dialog_cancel',
ok: 'upload_dialog_ok',
onOk: onUpload,
);
}

View file

@ -11,6 +11,7 @@ import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
@ -36,6 +37,7 @@ class HomePage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final multiselectEnabled = ref.watch(multiselectProvider.notifier);
final selectionEnabledHook = useState(false);
final selectionAssetState = useState(AssetState.remote);
final selection = useState(<Asset>{});
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
@ -80,6 +82,9 @@ class HomePage extends HookConsumerWidget {
) {
selectionEnabledHook.value = multiselect;
selection.value = selectedAssets;
selectionAssetState.value = selectedAssets.any((e) => e.isRemote)
? AssetState.remote
: AssetState.local;
}
void onShareAssets() {
@ -172,6 +177,28 @@ class HomePage extends HookConsumerWidget {
}
}
void onUpload() async {
processing.value = true;
try {
final Set<Asset> assets = selection.value;
if (assets.length > 30) {
ImmichToast.show(
context: context,
msg: 'home_page_upload_err_limit'.tr(),
gravity: ToastGravity.BOTTOM,
);
} else {
processing.value = false;
selectionEnabledHook.value = false;
await ref
.read(manualUploadProvider.notifier)
.uploadAssets(context, assets);
}
} finally {
processing.value = false;
}
}
void onAddToAlbum(Album album) async {
processing.value = true;
try {
@ -253,7 +280,7 @@ class HomePage extends HookConsumerWidget {
} else {
refreshCount.value++;
// set counter back to 0 if user does not request refresh again
Timer(const Duration(seconds: 2), () {
Timer(const Duration(seconds: 4), () {
refreshCount.value = 0;
});
}
@ -330,7 +357,9 @@ class HomePage extends HookConsumerWidget {
albums: albums,
sharedAlbums: sharedAlbums,
onCreateNewAlbum: onCreateNewAlbum,
onUpload: onUpload,
enabled: !processing.value,
selectionAssetState: selectionAssetState.value,
),
if (processing.value) const Center(child: ImmichLoadingIndicator())
],