mirror of
https://github.com/immich-app/immich.git
synced 2024-11-28 09:33:27 +02:00
feat(mobile): unify asset grid multiselect actions (#5407)
* feat(mobile): unify asset grid multiselect actions * add favorite & archive page * show edit date&place on main photos screen * Reposition exit button * Sort favorite with the same order as other view --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
b9a9a3956c
commit
c25556bb08
@ -31,7 +31,6 @@
|
|||||||
"app_bar_signout_dialog_content": "Are you sure you want to sign out?",
|
"app_bar_signout_dialog_content": "Are you sure you want to sign out?",
|
||||||
"app_bar_signout_dialog_ok": "Yes",
|
"app_bar_signout_dialog_ok": "Yes",
|
||||||
"app_bar_signout_dialog_title": "Sign out",
|
"app_bar_signout_dialog_title": "Sign out",
|
||||||
"archive_page_no_archived_assets": "No archived assets found",
|
|
||||||
"archive_page_title": "Archive ({})",
|
"archive_page_title": "Archive ({})",
|
||||||
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
|
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
|
||||||
"asset_list_layout_settings_group_automatically": "Automatic",
|
"asset_list_layout_settings_group_automatically": "Automatic",
|
||||||
@ -139,6 +138,7 @@
|
|||||||
"control_bottom_app_bar_create_new_album": "Create new album",
|
"control_bottom_app_bar_create_new_album": "Create new album",
|
||||||
"control_bottom_app_bar_delete": "Delete",
|
"control_bottom_app_bar_delete": "Delete",
|
||||||
"control_bottom_app_bar_favorite": "Favorite",
|
"control_bottom_app_bar_favorite": "Favorite",
|
||||||
|
"control_bottom_app_bar_unfavorite": "Unfavorite",
|
||||||
"control_bottom_app_bar_share": "Share",
|
"control_bottom_app_bar_share": "Share",
|
||||||
"control_bottom_app_bar_share_to": "Share To",
|
"control_bottom_app_bar_share_to": "Share To",
|
||||||
"control_bottom_app_bar_stack": "Stack",
|
"control_bottom_app_bar_stack": "Stack",
|
||||||
@ -172,7 +172,6 @@
|
|||||||
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
||||||
"experimental_settings_subtitle": "Use at your own risk!",
|
"experimental_settings_subtitle": "Use at your own risk!",
|
||||||
"experimental_settings_title": "Experimental",
|
"experimental_settings_title": "Experimental",
|
||||||
"favorites_page_no_favorites": "No favorite assets found",
|
|
||||||
"favorites_page_title": "Favorites",
|
"favorites_page_title": "Favorites",
|
||||||
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
|
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
|
||||||
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
|
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
|
||||||
|
@ -2,11 +2,13 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
import 'package:immich_mobile/shared/models/store.dart';
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.dart';
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/utils/renderlist_generator.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
class AlbumNotifier extends StateNotifier<List<Album>> {
|
class AlbumNotifier extends StateNotifier<List<Album>> {
|
||||||
@ -49,3 +51,24 @@ final albumProvider =
|
|||||||
ref.watch(dbProvider),
|
ref.watch(dbProvider),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final albumWatcher =
|
||||||
|
StreamProvider.autoDispose.family<Album, int>((ref, albumId) async* {
|
||||||
|
final db = ref.watch(dbProvider);
|
||||||
|
final a = await db.albums.get(albumId);
|
||||||
|
if (a != null) yield a;
|
||||||
|
await for (final a in db.albums.watchObject(albumId, fireImmediately: true)) {
|
||||||
|
if (a != null) yield a;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final albumRenderlistProvider =
|
||||||
|
StreamProvider.autoDispose.family<RenderList, int>((ref, albumId) {
|
||||||
|
final album = ref.watch(albumWatcher(albumId)).value;
|
||||||
|
if (album != null) {
|
||||||
|
final query =
|
||||||
|
album.assets.filter().isTrashedEqualTo(false).sortByFileCreatedAtDesc();
|
||||||
|
return renderListGeneratorWithGroupBy(query, GroupAssetsBy.none);
|
||||||
|
}
|
||||||
|
return const Stream.empty();
|
||||||
|
});
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
|
||||||
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
|
||||||
|
|
||||||
final albumDetailProvider =
|
|
||||||
StreamProvider.family<Album, int>((ref, albumId) async* {
|
|
||||||
final user = ref.watch(currentUserProvider);
|
|
||||||
if (user == null) return;
|
|
||||||
final AlbumService service = ref.watch(albumServiceProvider);
|
|
||||||
|
|
||||||
await for (final a in service.watchAlbum(albumId)) {
|
|
||||||
if (a == null) {
|
|
||||||
throw Exception("Album with ID=$albumId does not exist anymore!");
|
|
||||||
}
|
|
||||||
await for (final _ in a.watchRenderList(GroupAssetsBy.none)) {
|
|
||||||
yield a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@ -0,0 +1,6 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
|
|
||||||
|
final currentAlbumProvider = StateProvider<Album?>((ref) {
|
||||||
|
return null;
|
||||||
|
});
|
@ -2,7 +2,6 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
|
|
||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
@ -11,7 +10,7 @@ import 'package:immich_mobile/shared/providers/db.provider.dart';
|
|||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
||||||
SharedAlbumNotifier(this._albumService, Isar db, this._ref) : super([]) {
|
SharedAlbumNotifier(this._albumService, Isar db) : super([]) {
|
||||||
final query = db.albums.filter().sharedEqualTo(true).sortByCreatedAtDesc();
|
final query = db.albums.filter().sharedEqualTo(true).sortByCreatedAtDesc();
|
||||||
query.findAll().then((value) => state = value);
|
query.findAll().then((value) => state = value);
|
||||||
_streamSub = query.watch().listen((data) => state = data);
|
_streamSub = query.watch().listen((data) => state = data);
|
||||||
@ -19,7 +18,6 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
|||||||
|
|
||||||
final AlbumService _albumService;
|
final AlbumService _albumService;
|
||||||
late final StreamSubscription<List<Album>> _streamSub;
|
late final StreamSubscription<List<Album>> _streamSub;
|
||||||
final Ref _ref;
|
|
||||||
|
|
||||||
Future<Album?> createSharedAlbum(
|
Future<Album?> createSharedAlbum(
|
||||||
String albumName,
|
String albumName,
|
||||||
@ -68,15 +66,8 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> setActivityEnabled(Album album, bool activityEnabled) async {
|
Future<bool> setActivityEnabled(Album album, bool activityEnabled) {
|
||||||
final result =
|
return _albumService.setActivityEnabled(album, activityEnabled);
|
||||||
await _albumService.setActivityEnabled(album, activityEnabled);
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
_ref.invalidate(albumDetailProvider(album.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -91,6 +82,5 @@ final sharedAlbumProvider =
|
|||||||
return SharedAlbumNotifier(
|
return SharedAlbumNotifier(
|
||||||
ref.watch(albumServiceProvider),
|
ref.watch(albumServiceProvider),
|
||||||
ref.watch(dbProvider),
|
ref.watch(dbProvider),
|
||||||
ref,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -219,11 +219,6 @@ class AlbumService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<Album?> watchAlbum(int albumId) async* {
|
|
||||||
yield await _db.albums.get(albumId);
|
|
||||||
yield* _db.albums.watchObject(albumId);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<AddAssetsResponse?> addAdditionalAssetToAlbum(
|
Future<AddAssetsResponse?> addAdditionalAssetToAlbum(
|
||||||
Iterable<Asset> assets,
|
Iterable<Asset> assets,
|
||||||
Album album,
|
Album album,
|
||||||
@ -248,8 +243,12 @@ class AlbumService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
album.assets.addAll(successAssets);
|
await _db.writeTxn(() async {
|
||||||
await _db.writeTxn(() => album.assets.save());
|
await album.assets.update(link: successAssets);
|
||||||
|
final a = await _db.albums.get(album.id);
|
||||||
|
// trigger watcher
|
||||||
|
await _db.albums.put(a!);
|
||||||
|
});
|
||||||
|
|
||||||
return AddAssetsResponse(
|
return AddAssetsResponse(
|
||||||
alreadyInAlbum: duplicatedAssets,
|
alreadyInAlbum: duplicatedAssets,
|
||||||
@ -359,8 +358,12 @@ class AlbumService {
|
|||||||
ids: assets.map((asset) => asset.remoteId!).toList(),
|
ids: assets.map((asset) => asset.remoteId!).toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
album.assets.removeAll(assets);
|
await _db.writeTxn(() async {
|
||||||
await _db.writeTxn(() => album.assets.update(unlink: assets));
|
await album.assets.update(unlink: assets);
|
||||||
|
final a = await _db.albums.get(album.id);
|
||||||
|
// trigger watcher
|
||||||
|
await _db.albums.put(a!);
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -380,7 +383,12 @@ class AlbumService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
album.sharedUsers.remove(user);
|
album.sharedUsers.remove(user);
|
||||||
await _db.writeTxn(() => album.sharedUsers.update(unlink: [user]));
|
await _db.writeTxn(() async {
|
||||||
|
await album.sharedUsers.update(unlink: [user]);
|
||||||
|
final a = await _db.albums.get(album.id);
|
||||||
|
// trigger watcher
|
||||||
|
await _db.albums.put(a!);
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -4,7 +4,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
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/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
|
import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
|
||||||
@ -63,8 +62,6 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.invalidate(albumDetailProvider(album.id));
|
|
||||||
context.pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,14 +5,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
|
import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
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/album_viewer.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/shared/ui/share_dialog.dart';
|
|
||||||
import 'package:immich_mobile/shared/services/share.service.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
|
|
||||||
@ -22,8 +18,6 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
Key? key,
|
Key? key,
|
||||||
required this.album,
|
required this.album,
|
||||||
required this.userId,
|
required this.userId,
|
||||||
required this.selected,
|
|
||||||
required this.selectionDisabled,
|
|
||||||
required this.titleFocusNode,
|
required this.titleFocusNode,
|
||||||
this.onAddPhotos,
|
this.onAddPhotos,
|
||||||
this.onAddUsers,
|
this.onAddUsers,
|
||||||
@ -32,8 +26,6 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
|
|
||||||
final Album album;
|
final Album album;
|
||||||
final String userId;
|
final String userId;
|
||||||
final Set<Asset> selected;
|
|
||||||
final void Function() selectionDisabled;
|
|
||||||
final FocusNode titleFocusNode;
|
final FocusNode titleFocusNode;
|
||||||
final Function(Album album)? onAddPhotos;
|
final Function(Album album)? onAddPhotos;
|
||||||
final Function(Album album)? onAddUsers;
|
final Function(Album album)? onAddUsers;
|
||||||
@ -144,109 +136,27 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
isProcessing.value = false;
|
isProcessing.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void onRemoveFromAlbumPressed() async {
|
|
||||||
isProcessing.value = true;
|
|
||||||
|
|
||||||
bool isSuccess =
|
|
||||||
await ref.watch(sharedAlbumProvider.notifier).removeAssetFromAlbum(
|
|
||||||
album,
|
|
||||||
selected,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isSuccess) {
|
|
||||||
context.pop();
|
|
||||||
selectionDisabled();
|
|
||||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
|
||||||
ref.invalidate(albumDetailProvider(album.id));
|
|
||||||
} else {
|
|
||||||
context.pop();
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: "album_viewer_appbar_share_err_remove".tr(),
|
|
||||||
toastType: ToastType.error,
|
|
||||||
gravity: ToastGravity.BOTTOM,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
isProcessing.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleShareAssets(
|
|
||||||
WidgetRef ref,
|
|
||||||
BuildContext context,
|
|
||||||
Set<Asset> selection,
|
|
||||||
) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext buildContext) {
|
|
||||||
ref.watch(shareServiceProvider).shareAssets(selection.toList()).then(
|
|
||||||
(bool status) {
|
|
||||||
if (!status) {
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: 'image_viewer_page_state_provider_share_error'.tr(),
|
|
||||||
toastType: ToastType.error,
|
|
||||||
gravity: ToastGravity.BOTTOM,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
buildContext.pop();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return const ShareDialog();
|
|
||||||
},
|
|
||||||
barrierDismissible: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onShareAssetsTo() async {
|
|
||||||
isProcessing.value = true;
|
|
||||||
handleShareAssets(ref, context, selected);
|
|
||||||
isProcessing.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
buildBottomSheetActions() {
|
buildBottomSheetActions() {
|
||||||
if (selected.isNotEmpty) {
|
return [
|
||||||
return [
|
album.ownerId == userId
|
||||||
ListTile(
|
? ListTile(
|
||||||
leading: const Icon(Icons.ios_share_rounded),
|
leading: const Icon(Icons.delete_forever_rounded),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'album_viewer_appbar_share_to',
|
'album_viewer_appbar_share_delete',
|
||||||
style: TextStyle(fontWeight: FontWeight.w500),
|
style: TextStyle(fontWeight: FontWeight.w500),
|
||||||
).tr(),
|
).tr(),
|
||||||
onTap: () => onShareAssetsTo(),
|
onTap: () => onDeleteAlbumPressed(),
|
||||||
),
|
)
|
||||||
album.ownerId == userId
|
: ListTile(
|
||||||
? ListTile(
|
leading: const Icon(Icons.person_remove_rounded),
|
||||||
leading: const Icon(Icons.delete_sweep_rounded),
|
title: const Text(
|
||||||
title: const Text(
|
'album_viewer_appbar_share_leave',
|
||||||
'album_viewer_appbar_share_remove',
|
style: TextStyle(fontWeight: FontWeight.w500),
|
||||||
style: TextStyle(fontWeight: FontWeight.w500),
|
).tr(),
|
||||||
).tr(),
|
onTap: () => onLeaveAlbumPressed(),
|
||||||
onTap: () => onRemoveFromAlbumPressed(),
|
),
|
||||||
)
|
];
|
||||||
: const SizedBox(),
|
// }
|
||||||
];
|
|
||||||
} else {
|
|
||||||
return [
|
|
||||||
album.ownerId == userId
|
|
||||||
? ListTile(
|
|
||||||
leading: const Icon(Icons.delete_forever_rounded),
|
|
||||||
title: const Text(
|
|
||||||
'album_viewer_appbar_share_delete',
|
|
||||||
style: TextStyle(fontWeight: FontWeight.w500),
|
|
||||||
).tr(),
|
|
||||||
onTap: () => onDeleteAlbumPressed(),
|
|
||||||
)
|
|
||||||
: ListTile(
|
|
||||||
leading: const Icon(Icons.person_remove_rounded),
|
|
||||||
title: const Text(
|
|
||||||
'album_viewer_appbar_share_leave',
|
|
||||||
style: TextStyle(fontWeight: FontWeight.w500),
|
|
||||||
).tr(),
|
|
||||||
onTap: () => onLeaveAlbumPressed(),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void buildBottomSheet() {
|
void buildBottomSheet() {
|
||||||
@ -308,10 +218,8 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
...buildBottomSheetActions(),
|
...buildBottomSheetActions(),
|
||||||
if (selected.isEmpty && onAddPhotos != null) ...commonActions,
|
if (onAddPhotos != null) ...commonActions,
|
||||||
if (selected.isEmpty &&
|
if (onAddPhotos != null && userId == album.ownerId)
|
||||||
onAddPhotos != null &&
|
|
||||||
userId == album.ownerId)
|
|
||||||
...ownerActions,
|
...ownerActions,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -349,13 +257,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildLeadingButton() {
|
buildLeadingButton() {
|
||||||
if (selected.isNotEmpty) {
|
if (isEditAlbum) {
|
||||||
return IconButton(
|
|
||||||
onPressed: selectionDisabled,
|
|
||||||
icon: const Icon(Icons.close_rounded),
|
|
||||||
splashRadius: 25,
|
|
||||||
);
|
|
||||||
} else if (isEditAlbum) {
|
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
bool isSuccess = await ref
|
bool isSuccess = await ref
|
||||||
@ -388,7 +290,6 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
return AppBar(
|
return AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: buildLeadingButton(),
|
leading: buildLeadingButton(),
|
||||||
title: selected.isNotEmpty ? Text('${selected.length}') : null,
|
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
actions: [
|
actions: [
|
||||||
if (album.shared && (album.activityEnabled || comments != 0))
|
if (album.shared && (album.activityEnabled || comments != 0))
|
||||||
|
@ -1,23 +1,26 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/providers/current_album.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/album/services/album.service.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
|
import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/album_viewer_editable_title.dart';
|
import 'package:immich_mobile/modules/album/ui/album_viewer_editable_title.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart';
|
import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
|
|
||||||
@ -29,39 +32,30 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
FocusNode titleFocusNode = useFocusNode();
|
FocusNode titleFocusNode = useFocusNode();
|
||||||
final album = ref.watch(albumDetailProvider(albumId));
|
final album = ref.watch(albumWatcher(albumId));
|
||||||
|
album.whenData(
|
||||||
|
(value) =>
|
||||||
|
Future((() => ref.read(currentAlbumProvider.notifier).state = value)),
|
||||||
|
);
|
||||||
final userId = ref.watch(authenticationProvider).userId;
|
final userId = ref.watch(authenticationProvider).userId;
|
||||||
final selection = useState<Set<Asset>>({});
|
|
||||||
final multiSelectEnabled = useState(false);
|
|
||||||
final isProcessing = useProcessingOverlay();
|
final isProcessing = useProcessingOverlay();
|
||||||
|
|
||||||
useEffect(
|
Future<bool> onRemoveFromAlbumPressed(Iterable<Asset> assets) async {
|
||||||
() {
|
final a = album.valueOrNull;
|
||||||
// Fetch album updates, e.g., cover image
|
final bool isSuccess = a != null &&
|
||||||
ref.invalidate(albumDetailProvider(albumId));
|
await ref
|
||||||
return null;
|
.read(sharedAlbumProvider.notifier)
|
||||||
},
|
.removeAssetFromAlbum(a, assets);
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
Future<bool> onWillPop() async {
|
if (!isSuccess) {
|
||||||
if (multiSelectEnabled.value) {
|
ImmichToast.show(
|
||||||
selection.value = {};
|
context: context,
|
||||||
multiSelectEnabled.value = false;
|
msg: "album_viewer_appbar_share_err_remove".tr(),
|
||||||
return false;
|
toastType: ToastType.error,
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
return isSuccess;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void selectionListener(bool active, Set<Asset> selected) {
|
|
||||||
selection.value = selected;
|
|
||||||
multiSelectEnabled.value = selected.isNotEmpty;
|
|
||||||
}
|
|
||||||
|
|
||||||
void disableSelection() {
|
|
||||||
selection.value = {};
|
|
||||||
multiSelectEnabled.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find out if the assets in album exist on the device
|
/// Find out if the assets in album exist on the device
|
||||||
@ -80,15 +74,10 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
// Check if there is new assets add
|
// Check if there is new assets add
|
||||||
isProcessing.value = true;
|
isProcessing.value = true;
|
||||||
|
|
||||||
var addAssetsResult =
|
await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum(
|
||||||
await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum(
|
returnPayload.selectedAssets,
|
||||||
returnPayload.selectedAssets,
|
albumInfo,
|
||||||
albumInfo,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
if (addAssetsResult != null && addAssetsResult.successfullyAdded > 0) {
|
|
||||||
ref.invalidate(albumDetailProvider(albumId));
|
|
||||||
}
|
|
||||||
|
|
||||||
isProcessing.value = false;
|
isProcessing.value = false;
|
||||||
}
|
}
|
||||||
@ -102,14 +91,10 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
if (sharedUserIds != null) {
|
if (sharedUserIds != null) {
|
||||||
isProcessing.value = true;
|
isProcessing.value = true;
|
||||||
|
|
||||||
var isSuccess = await ref
|
await ref
|
||||||
.watch(albumServiceProvider)
|
.watch(albumServiceProvider)
|
||||||
.addAdditionalUserToAlbum(sharedUserIds, album);
|
.addAdditionalUserToAlbum(sharedUserIds, album);
|
||||||
|
|
||||||
if (isSuccess) {
|
|
||||||
ref.invalidate(albumDetailProvider(album.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
isProcessing.value = false;
|
isProcessing.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -193,10 +178,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
Widget buildSharedUserIconsRow(Album album) {
|
Widget buildSharedUserIconsRow(Album album) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () async {
|
onTap: () => context.autoPush(AlbumOptionsRoute(album: album)),
|
||||||
await context.autoPush(AlbumOptionsRoute(album: album));
|
|
||||||
ref.invalidate(albumDetailProvider(album.id));
|
|
||||||
},
|
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 50,
|
height: 50,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
@ -244,42 +226,32 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: album.when(
|
appBar: ref.watch(multiselectProvider)
|
||||||
data: (data) => AlbumViewerAppbar(
|
? null
|
||||||
titleFocusNode: titleFocusNode,
|
: album.when(
|
||||||
album: data,
|
data: (data) => AlbumViewerAppbar(
|
||||||
userId: userId,
|
titleFocusNode: titleFocusNode,
|
||||||
selected: selection.value,
|
album: data,
|
||||||
selectionDisabled: disableSelection,
|
userId: userId,
|
||||||
onAddPhotos: onAddPhotosPressed,
|
onAddPhotos: onAddPhotosPressed,
|
||||||
onAddUsers: onAddUsersPressed,
|
onAddUsers: onAddUsersPressed,
|
||||||
onActivities: onActivitiesPressed,
|
onActivities: onActivitiesPressed,
|
||||||
),
|
|
||||||
error: (error, stackTrace) => AppBar(title: const Text("Error")),
|
|
||||||
loading: () => AppBar(),
|
|
||||||
),
|
|
||||||
body: album.widgetWhen(
|
|
||||||
onData: (data) => WillPopScope(
|
|
||||||
onWillPop: onWillPop,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => titleFocusNode.unfocus(),
|
|
||||||
child: ImmichAssetGrid(
|
|
||||||
renderList: data.renderList,
|
|
||||||
listener: selectionListener,
|
|
||||||
selectionActive: multiSelectEnabled.value,
|
|
||||||
showMultiSelectIndicator: false,
|
|
||||||
topWidget: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
buildHeader(data),
|
|
||||||
if (data.isRemote) buildControlButton(data),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
isOwner: userId == data.ownerId,
|
error: (error, stackTrace) => AppBar(title: const Text("Error")),
|
||||||
sharedAlbumId:
|
loading: () => AppBar(),
|
||||||
data.shared && data.activityEnabled ? data.remoteId : null,
|
|
||||||
),
|
),
|
||||||
|
body: album.widgetWhen(
|
||||||
|
onData: (data) => MultiselectGrid(
|
||||||
|
renderListProvider: albumRenderlistProvider(albumId),
|
||||||
|
topWidget: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
buildHeader(data),
|
||||||
|
if (data.isRemote) buildControlButton(data),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
onRemoveFromAlbum: onRemoveFromAlbumPressed,
|
||||||
|
editEnabled: data.ownerId == userId,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,34 +1,19 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/archive/providers/archive_asset_provider.dart';
|
import 'package:immich_mobile/modules/archive/providers/archive_asset_provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
|
||||||
import 'package:immich_mobile/utils/selection_handlers.dart';
|
|
||||||
|
|
||||||
class ArchivePage extends HookConsumerWidget {
|
class ArchivePage extends HookConsumerWidget {
|
||||||
const ArchivePage({super.key});
|
const ArchivePage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final archivedAssets = ref.watch(archiveProvider);
|
AppBar buildAppBar() {
|
||||||
final selectionEnabledHook = useState(false);
|
final archivedAssets = ref.watch(archiveProvider);
|
||||||
final selection = useState(<Asset>{});
|
final count = archivedAssets.value?.totalAssets.toString() ?? "?";
|
||||||
final processing = useState(false);
|
|
||||||
|
|
||||||
void selectionListener(
|
|
||||||
bool multiselect,
|
|
||||||
Set<Asset> selectedAssets,
|
|
||||||
) {
|
|
||||||
selectionEnabledHook.value = multiselect;
|
|
||||||
selection.value = selectedAssets;
|
|
||||||
}
|
|
||||||
|
|
||||||
AppBar buildAppBar(String count) {
|
|
||||||
return AppBar(
|
return AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () => context.autoPop(),
|
onPressed: () => context.autoPop(),
|
||||||
@ -42,69 +27,14 @@ class ArchivePage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildBottomBar() {
|
|
||||||
return SafeArea(
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: SizedBox(
|
|
||||||
height: 64,
|
|
||||||
child: Card(
|
|
||||||
child: ListTile(
|
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
|
||||||
),
|
|
||||||
leading: const Icon(
|
|
||||||
Icons.unarchive_rounded,
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
'control_bottom_app_bar_unarchive'.tr(),
|
|
||||||
style: const TextStyle(fontSize: 14),
|
|
||||||
),
|
|
||||||
onTap: processing.value
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
processing.value = true;
|
|
||||||
try {
|
|
||||||
await handleArchiveAssets(
|
|
||||||
ref,
|
|
||||||
context,
|
|
||||||
selection.value.toList(),
|
|
||||||
shouldArchive: false,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
processing.value = false;
|
|
||||||
selectionEnabledHook.value = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: archivedAssets.maybeWhen(
|
appBar: ref.watch(multiselectProvider) ? null : buildAppBar(),
|
||||||
data: (data) => buildAppBar(data.totalAssets.toString()),
|
body: MultiselectGrid(
|
||||||
orElse: () => buildAppBar("?"),
|
renderListProvider: archiveProvider,
|
||||||
),
|
unarchive: true,
|
||||||
body: archivedAssets.widgetWhen(
|
archiveEnabled: true,
|
||||||
onData: (data) => data.isEmpty
|
deleteEnabled: true,
|
||||||
? Center(
|
editEnabled: true,
|
||||||
child: Text('archive_page_no_archived_assets'.tr()),
|
|
||||||
)
|
|
||||||
: Stack(
|
|
||||||
children: [
|
|
||||||
ImmichAssetGrid(
|
|
||||||
renderList: data,
|
|
||||||
listener: selectionListener,
|
|
||||||
selectionActive: selectionEnabledHook.value,
|
|
||||||
),
|
|
||||||
if (selectionEnabledHook.value) buildBottomBar(),
|
|
||||||
if (processing.value)
|
|
||||||
const Center(child: ImmichLoadingIndicator()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
|
import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/providers/current_album.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
|
|
||||||
@ -17,8 +18,8 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
required this.onFavorite,
|
required this.onFavorite,
|
||||||
required this.onUploadPressed,
|
required this.onUploadPressed,
|
||||||
required this.isOwner,
|
required this.isOwner,
|
||||||
required this.shareAlbumId,
|
|
||||||
required this.onActivitiesPressed,
|
required this.onActivitiesPressed,
|
||||||
|
required this.isPartner,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final Asset asset;
|
final Asset asset;
|
||||||
@ -31,16 +32,17 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
final Function(Asset) onFavorite;
|
final Function(Asset) onFavorite;
|
||||||
final bool isPlayingMotionVideo;
|
final bool isPlayingMotionVideo;
|
||||||
final bool isOwner;
|
final bool isOwner;
|
||||||
final String? shareAlbumId;
|
final bool isPartner;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
const double iconSize = 22.0;
|
const double iconSize = 22.0;
|
||||||
final a = ref.watch(assetWatcher(asset)).value ?? asset;
|
final a = ref.watch(assetWatcher(asset)).value ?? asset;
|
||||||
final comments = shareAlbumId != null
|
final album = ref.watch(currentAlbumProvider);
|
||||||
|
final comments = album != null && album.remoteId != null
|
||||||
? ref.watch(
|
? ref.watch(
|
||||||
activityStatisticsStateProvider(
|
activityStatisticsStateProvider(
|
||||||
(albumId: shareAlbumId!, assetId: asset.remoteId),
|
(albumId: album.remoteId!, assetId: asset.remoteId),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: 0;
|
: 0;
|
||||||
@ -169,8 +171,8 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
if (asset.livePhotoVideoId != null) buildLivePhotoButton(),
|
if (asset.livePhotoVideoId != null) buildLivePhotoButton(),
|
||||||
if (asset.isLocal && !asset.isRemote) buildUploadButton(),
|
if (asset.isLocal && !asset.isRemote) buildUploadButton(),
|
||||||
if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(),
|
if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(),
|
||||||
if (asset.isRemote && isOwner) buildAddToAlbumButtom(),
|
if (asset.isRemote && (isOwner || isPartner)) buildAddToAlbumButtom(),
|
||||||
if (shareAlbumId != null) buildActivitiesButton(),
|
if (album != null && album.shared) buildActivitiesButton(),
|
||||||
buildMoreInfoButton(),
|
buildMoreInfoButton(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -9,6 +9,7 @@ import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
|||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/providers/current_album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart';
|
||||||
@ -22,6 +23,7 @@ import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
|||||||
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
||||||
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
|
import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
|
||||||
|
import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
|
||||||
import 'package:immich_mobile/shared/cache/original_image_provider.dart';
|
import 'package:immich_mobile/shared/cache/original_image_provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/store.dart';
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
@ -29,6 +31,7 @@ import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
|
|||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
import 'package:immich_mobile/shared/ui/photo_view/photo_view_gallery.dart';
|
import 'package:immich_mobile/shared/ui/photo_view/photo_view_gallery.dart';
|
||||||
@ -49,8 +52,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
final int initialIndex;
|
final int initialIndex;
|
||||||
final int heroOffset;
|
final int heroOffset;
|
||||||
final bool showStack;
|
final bool showStack;
|
||||||
final bool isOwner;
|
|
||||||
final String? sharedAlbumId;
|
|
||||||
|
|
||||||
GalleryViewerPage({
|
GalleryViewerPage({
|
||||||
super.key,
|
super.key,
|
||||||
@ -59,8 +60,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
required this.totalAssets,
|
required this.totalAssets,
|
||||||
this.heroOffset = 0,
|
this.heroOffset = 0,
|
||||||
this.showStack = false,
|
this.showStack = false,
|
||||||
this.isOwner = true,
|
|
||||||
this.sharedAlbumId,
|
|
||||||
}) : controller = PageController(initialPage: initialIndex);
|
}) : controller = PageController(initialPage: initialIndex);
|
||||||
|
|
||||||
final PageController controller;
|
final PageController controller;
|
||||||
@ -94,10 +93,16 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
final stackElements = showStack ? [currentAsset, ...stack] : <Asset>[];
|
final stackElements = showStack ? [currentAsset, ...stack] : <Asset>[];
|
||||||
// Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id
|
// Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id
|
||||||
final isFromDto = currentAsset.id == Isar.autoIncrement;
|
final isFromDto = currentAsset.id == Isar.autoIncrement;
|
||||||
|
final album = ref.watch(currentAlbumProvider);
|
||||||
|
|
||||||
Asset asset() => stackIndex.value == -1
|
Asset asset() => stackIndex.value == -1
|
||||||
? currentAsset
|
? currentAsset
|
||||||
: stackElements.elementAt(stackIndex.value);
|
: stackElements.elementAt(stackIndex.value);
|
||||||
|
final isOwner = asset().ownerId == ref.watch(currentUserProvider)?.isarId;
|
||||||
|
final isPartner = ref
|
||||||
|
.watch(partnerSharedWithProvider)
|
||||||
|
.map((e) => e.isarId)
|
||||||
|
.contains(asset().ownerId);
|
||||||
|
|
||||||
bool isParent = stackIndex.value == -1 || stackIndex.value == 0;
|
bool isParent = stackIndex.value == -1 || stackIndex.value == 0;
|
||||||
|
|
||||||
@ -113,9 +118,8 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
void toggleFavorite(Asset asset) => ref
|
void toggleFavorite(Asset asset) =>
|
||||||
.watch(assetProvider.notifier)
|
ref.read(assetProvider.notifier).toggleFavorite([asset]);
|
||||||
.toggleFavorite([asset], !asset.isFavorite);
|
|
||||||
|
|
||||||
/// Original (large) image of a remote asset. Required asset.isRemote
|
/// Original (large) image of a remote asset. Required asset.isRemote
|
||||||
ImageProvider remoteOriginalProvider(Asset asset) =>
|
ImageProvider remoteOriginalProvider(Asset asset) =>
|
||||||
@ -305,9 +309,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleArchive(Asset asset) {
|
handleArchive(Asset asset) {
|
||||||
ref
|
ref.watch(assetProvider.notifier).toggleArchive([asset]);
|
||||||
.watch(assetProvider.notifier)
|
|
||||||
.toggleArchive([asset], !asset.isArchived);
|
|
||||||
if (isParent) {
|
if (isParent) {
|
||||||
context.autoPop();
|
context.autoPop();
|
||||||
return;
|
return;
|
||||||
@ -331,10 +333,10 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleActivities() {
|
handleActivities() {
|
||||||
if (sharedAlbumId != null) {
|
if (album != null && album.shared && album.remoteId != null) {
|
||||||
context.autoPush(
|
context.autoPush(
|
||||||
ActivitiesRoute(
|
ActivitiesRoute(
|
||||||
albumId: sharedAlbumId!,
|
albumId: album.remoteId!,
|
||||||
assetId: asset().remoteId,
|
assetId: asset().remoteId,
|
||||||
withAssetThumbs: false,
|
withAssetThumbs: false,
|
||||||
isOwner: isOwner,
|
isOwner: isOwner,
|
||||||
@ -353,6 +355,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
color: Colors.black.withOpacity(0.4),
|
color: Colors.black.withOpacity(0.4),
|
||||||
child: TopControlAppBar(
|
child: TopControlAppBar(
|
||||||
isOwner: isOwner,
|
isOwner: isOwner,
|
||||||
|
isPartner: isPartner,
|
||||||
isPlayingMotionVideo: isPlayingMotionVideo.value,
|
isPlayingMotionVideo: isPlayingMotionVideo.value,
|
||||||
asset: asset(),
|
asset: asset(),
|
||||||
onMoreInfoPressed: showInfo,
|
onMoreInfoPressed: showInfo,
|
||||||
@ -371,7 +374,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
|
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
|
||||||
}),
|
}),
|
||||||
onAddToAlbumPressed: () => addToAlbum(asset()),
|
onAddToAlbumPressed: () => addToAlbum(asset()),
|
||||||
shareAlbumId: sharedAlbumId,
|
|
||||||
onActivitiesPressed: handleActivities,
|
onActivitiesPressed: handleActivities,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -17,6 +17,6 @@ final favoriteAssetsProvider = StreamProvider<RenderList>((ref) {
|
|||||||
.filter()
|
.filter()
|
||||||
.isFavoriteEqualTo(true)
|
.isFavoriteEqualTo(true)
|
||||||
.isTrashedEqualTo(false)
|
.isTrashedEqualTo(false)
|
||||||
.sortByFileCreatedAt();
|
.sortByFileCreatedAtDesc();
|
||||||
return renderListGenerator(query, ref);
|
return renderListGenerator(query, ref);
|
||||||
});
|
});
|
||||||
|
@ -1,31 +1,16 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
|
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart';
|
||||||
import 'package:immich_mobile/utils/selection_handlers.dart';
|
|
||||||
|
|
||||||
class FavoritesPage extends HookConsumerWidget {
|
class FavoritesPage extends HookConsumerWidget {
|
||||||
const FavoritesPage({Key? key}) : super(key: key);
|
const FavoritesPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final selectionEnabledHook = useState(false);
|
|
||||||
final selection = useState(<Asset>{});
|
|
||||||
final processing = useState(false);
|
|
||||||
|
|
||||||
void selectionListener(
|
|
||||||
bool multiselect,
|
|
||||||
Set<Asset> selectedAssets,
|
|
||||||
) {
|
|
||||||
selectionEnabledHook.value = multiselect;
|
|
||||||
selection.value = selectedAssets;
|
|
||||||
}
|
|
||||||
|
|
||||||
AppBar buildAppBar() {
|
AppBar buildAppBar() {
|
||||||
return AppBar(
|
return AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
@ -40,66 +25,14 @@ class FavoritesPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void unfavorite() async {
|
|
||||||
try {
|
|
||||||
if (selection.value.isNotEmpty) {
|
|
||||||
await handleFavoriteAssets(
|
|
||||||
ref,
|
|
||||||
context,
|
|
||||||
selection.value.toList(),
|
|
||||||
shouldFavorite: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
processing.value = false;
|
|
||||||
selectionEnabledHook.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildBottomBar() {
|
|
||||||
return SafeArea(
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: SizedBox(
|
|
||||||
height: 64,
|
|
||||||
child: Card(
|
|
||||||
child: ListTile(
|
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
|
||||||
),
|
|
||||||
leading: const Icon(
|
|
||||||
Icons.star_border,
|
|
||||||
),
|
|
||||||
title: const Text(
|
|
||||||
"Unfavorite",
|
|
||||||
style: TextStyle(fontSize: 14),
|
|
||||||
),
|
|
||||||
onTap: processing.value ? null : unfavorite,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: buildAppBar(),
|
appBar: ref.watch(multiselectProvider) ? null : buildAppBar(),
|
||||||
body: ref.watch(favoriteAssetsProvider).widgetWhen(
|
body: MultiselectGrid(
|
||||||
onData: (data) => data.isEmpty
|
renderListProvider: favoriteAssetsProvider,
|
||||||
? Center(
|
favoriteEnabled: true,
|
||||||
child: Text('favorites_page_no_favorites'.tr()),
|
editEnabled: true,
|
||||||
)
|
unfavorite: true,
|
||||||
: Stack(
|
),
|
||||||
children: [
|
|
||||||
ImmichAssetGrid(
|
|
||||||
renderList: data,
|
|
||||||
selectionActive: selectionEnabledHook.value,
|
|
||||||
listener: selectionListener,
|
|
||||||
),
|
|
||||||
if (selectionEnabledHook.value) buildBottomBar(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
|
||||||
class DisableMultiSelectButton extends ConsumerWidget {
|
class DisableMultiSelectButton extends ConsumerWidget {
|
||||||
const DisableMultiSelectButton({
|
const DisableMultiSelectButton({
|
||||||
@ -13,24 +14,25 @@ class DisableMultiSelectButton extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Padding(
|
return Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
|
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () => onPressed(),
|
||||||
onPressed();
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.close_rounded),
|
icon: const Icon(Icons.close_rounded),
|
||||||
label: Text(
|
label: Text(
|
||||||
'$selectedItemCount',
|
'$selectedItemCount',
|
||||||
style: const TextStyle(
|
style: context.textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
height: 2.5,
|
||||||
fontSize: 18,
|
color: context.isDarkTheme ? Colors.black : Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,8 +33,6 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
|||||||
final bool shrinkWrap;
|
final bool shrinkWrap;
|
||||||
final bool showDragScroll;
|
final bool showDragScroll;
|
||||||
final bool showStack;
|
final bool showStack;
|
||||||
final bool isOwner;
|
|
||||||
final String? sharedAlbumId;
|
|
||||||
|
|
||||||
const ImmichAssetGrid({
|
const ImmichAssetGrid({
|
||||||
super.key,
|
super.key,
|
||||||
@ -55,8 +53,6 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
|||||||
this.shrinkWrap = false,
|
this.shrinkWrap = false,
|
||||||
this.showDragScroll = true,
|
this.showDragScroll = true,
|
||||||
this.showStack = false,
|
this.showStack = false,
|
||||||
this.isOwner = true,
|
|
||||||
this.sharedAlbumId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -121,8 +117,6 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
|||||||
shrinkWrap: shrinkWrap,
|
shrinkWrap: shrinkWrap,
|
||||||
showDragScroll: showDragScroll,
|
showDragScroll: showDragScroll,
|
||||||
showStack: showStack,
|
showStack: showStack,
|
||||||
isOwner: isOwner,
|
|
||||||
sharedAlbumId: sharedAlbumId,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -39,8 +39,6 @@ class ImmichAssetGridView extends StatefulWidget {
|
|||||||
final bool shrinkWrap;
|
final bool shrinkWrap;
|
||||||
final bool showDragScroll;
|
final bool showDragScroll;
|
||||||
final bool showStack;
|
final bool showStack;
|
||||||
final bool isOwner;
|
|
||||||
final String? sharedAlbumId;
|
|
||||||
|
|
||||||
const ImmichAssetGridView({
|
const ImmichAssetGridView({
|
||||||
super.key,
|
super.key,
|
||||||
@ -61,8 +59,6 @@ class ImmichAssetGridView extends StatefulWidget {
|
|||||||
this.shrinkWrap = false,
|
this.shrinkWrap = false,
|
||||||
this.showDragScroll = true,
|
this.showDragScroll = true,
|
||||||
this.showStack = false,
|
this.showStack = false,
|
||||||
this.isOwner = true,
|
|
||||||
this.sharedAlbumId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -143,8 +139,6 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||||||
showStorageIndicator: widget.showStorageIndicator,
|
showStorageIndicator: widget.showStorageIndicator,
|
||||||
heroOffset: widget.heroOffset,
|
heroOffset: widget.heroOffset,
|
||||||
showStack: widget.showStack,
|
showStack: widget.showStack,
|
||||||
isOwner: widget.isOwner,
|
|
||||||
sharedAlbumId: widget.sharedAlbumId,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,14 +14,12 @@ class ThumbnailImage extends StatelessWidget {
|
|||||||
final int totalAssets;
|
final int totalAssets;
|
||||||
final bool showStorageIndicator;
|
final bool showStorageIndicator;
|
||||||
final bool showStack;
|
final bool showStack;
|
||||||
final bool isOwner;
|
|
||||||
final bool useGrayBoxPlaceholder;
|
final bool useGrayBoxPlaceholder;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
final bool multiselectEnabled;
|
final bool multiselectEnabled;
|
||||||
final Function? onSelect;
|
final Function? onSelect;
|
||||||
final Function? onDeselect;
|
final Function? onDeselect;
|
||||||
final int heroOffset;
|
final int heroOffset;
|
||||||
final String? sharedAlbumId;
|
|
||||||
|
|
||||||
const ThumbnailImage({
|
const ThumbnailImage({
|
||||||
Key? key,
|
Key? key,
|
||||||
@ -31,8 +29,6 @@ class ThumbnailImage extends StatelessWidget {
|
|||||||
required this.totalAssets,
|
required this.totalAssets,
|
||||||
this.showStorageIndicator = true,
|
this.showStorageIndicator = true,
|
||||||
this.showStack = false,
|
this.showStack = false,
|
||||||
this.isOwner = true,
|
|
||||||
this.sharedAlbumId,
|
|
||||||
this.useGrayBoxPlaceholder = false,
|
this.useGrayBoxPlaceholder = false,
|
||||||
this.isSelected = false,
|
this.isSelected = false,
|
||||||
this.multiselectEnabled = false,
|
this.multiselectEnabled = false,
|
||||||
@ -185,8 +181,6 @@ class ThumbnailImage extends StatelessWidget {
|
|||||||
totalAssets: totalAssets,
|
totalAssets: totalAssets,
|
||||||
heroOffset: heroOffset,
|
heroOffset: heroOffset,
|
||||||
showStack: showStack,
|
showStack: showStack,
|
||||||
isOwner: isOwner,
|
|
||||||
sharedAlbumId: sharedAlbumId,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
|
import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
|
||||||
import 'package:immich_mobile/modules/home/models/selection_state.dart';
|
import 'package:immich_mobile/modules/home/models/selection_state.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
|
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
|
||||||
@ -12,37 +14,39 @@ import 'package:immich_mobile/shared/models/album.dart';
|
|||||||
|
|
||||||
class ControlBottomAppBar extends ConsumerWidget {
|
class ControlBottomAppBar extends ConsumerWidget {
|
||||||
final void Function(bool shareLocal) onShare;
|
final void Function(bool shareLocal) onShare;
|
||||||
final void Function() onFavorite;
|
final void Function()? onFavorite;
|
||||||
final void Function() onArchive;
|
final void Function()? onArchive;
|
||||||
final void Function() onDelete;
|
final void Function()? onDelete;
|
||||||
final Function(Album album) onAddToAlbum;
|
final Function(Album album) onAddToAlbum;
|
||||||
final void Function() onCreateNewAlbum;
|
final void Function() onCreateNewAlbum;
|
||||||
final void Function() onUpload;
|
final void Function() onUpload;
|
||||||
final void Function() onStack;
|
final void Function()? onStack;
|
||||||
final void Function() onEditTime;
|
final void Function()? onEditTime;
|
||||||
final void Function() onEditLocation;
|
final void Function()? onEditLocation;
|
||||||
|
final void Function()? onRemoveFromAlbum;
|
||||||
|
|
||||||
final List<Album> albums;
|
|
||||||
final List<Album> sharedAlbums;
|
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
|
final bool unfavorite;
|
||||||
|
final bool unarchive;
|
||||||
final SelectionAssetState selectionAssetState;
|
final SelectionAssetState selectionAssetState;
|
||||||
|
|
||||||
const ControlBottomAppBar({
|
const ControlBottomAppBar({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.onShare,
|
required this.onShare,
|
||||||
required this.onFavorite,
|
this.onFavorite,
|
||||||
required this.onArchive,
|
this.onArchive,
|
||||||
required this.onDelete,
|
this.onDelete,
|
||||||
required this.sharedAlbums,
|
|
||||||
required this.albums,
|
|
||||||
required this.onAddToAlbum,
|
required this.onAddToAlbum,
|
||||||
required this.onCreateNewAlbum,
|
required this.onCreateNewAlbum,
|
||||||
required this.onUpload,
|
required this.onUpload,
|
||||||
required this.onStack,
|
this.onStack,
|
||||||
required this.onEditTime,
|
this.onEditTime,
|
||||||
required this.onEditLocation,
|
this.onEditLocation,
|
||||||
|
this.onRemoveFromAlbum,
|
||||||
this.selectionAssetState = const SelectionAssetState(),
|
this.selectionAssetState = const SelectionAssetState(),
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
|
this.unarchive = false,
|
||||||
|
this.unfavorite = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -52,6 +56,8 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||||||
var hasLocal = selectionAssetState.hasLocal;
|
var hasLocal = selectionAssetState.hasLocal;
|
||||||
final trashEnabled =
|
final trashEnabled =
|
||||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
||||||
|
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
||||||
|
final sharedAlbums = ref.watch(sharedAlbumProvider);
|
||||||
|
|
||||||
List<Widget> renderActionButtons() {
|
List<Widget> renderActionButtons() {
|
||||||
return [
|
return [
|
||||||
@ -66,56 +72,73 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||||||
label: "control_bottom_app_bar_share_to".tr(),
|
label: "control_bottom_app_bar_share_to".tr(),
|
||||||
onPressed: enabled ? () => onShare(true) : null,
|
onPressed: enabled ? () => onShare(true) : null,
|
||||||
),
|
),
|
||||||
if (hasRemote)
|
if (hasRemote && onArchive != null)
|
||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
iconData: Icons.archive,
|
iconData: unarchive ? Icons.unarchive : Icons.archive,
|
||||||
label: "control_bottom_app_bar_archive".tr(),
|
label: (unarchive
|
||||||
|
? "control_bottom_app_bar_unarchive"
|
||||||
|
: "control_bottom_app_bar_archive")
|
||||||
|
.tr(),
|
||||||
onPressed: enabled ? onArchive : null,
|
onPressed: enabled ? onArchive : null,
|
||||||
),
|
),
|
||||||
if (hasRemote)
|
if (hasRemote && onFavorite != null)
|
||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
iconData: Icons.favorite_border_rounded,
|
iconData: unfavorite
|
||||||
label: "control_bottom_app_bar_favorite".tr(),
|
? Icons.favorite_border_rounded
|
||||||
|
: Icons.favorite_rounded,
|
||||||
|
label: (unfavorite
|
||||||
|
? "control_bottom_app_bar_unfavorite"
|
||||||
|
: "control_bottom_app_bar_favorite")
|
||||||
|
.tr(),
|
||||||
onPressed: enabled ? onFavorite : null,
|
onPressed: enabled ? onFavorite : null,
|
||||||
),
|
),
|
||||||
if (hasRemote)
|
if (hasRemote && onEditTime != null)
|
||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
iconData: Icons.edit_calendar_outlined,
|
iconData: Icons.edit_calendar_outlined,
|
||||||
label: "control_bottom_app_bar_edit_time".tr(),
|
label: "control_bottom_app_bar_edit_time".tr(),
|
||||||
onPressed: enabled ? onEditTime : null,
|
onPressed: enabled ? onEditTime : null,
|
||||||
),
|
),
|
||||||
if (hasRemote)
|
if (hasRemote && onEditLocation != null)
|
||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
iconData: Icons.edit_location_alt_outlined,
|
iconData: Icons.edit_location_alt_outlined,
|
||||||
label: "control_bottom_app_bar_edit_location".tr(),
|
label: "control_bottom_app_bar_edit_location".tr(),
|
||||||
onPressed: enabled ? onEditLocation : null,
|
onPressed: enabled ? onEditLocation : null,
|
||||||
),
|
),
|
||||||
ControlBoxButton(
|
if (onDelete != null)
|
||||||
iconData: Icons.delete_outline_rounded,
|
ControlBoxButton(
|
||||||
label: "control_bottom_app_bar_delete".tr(),
|
iconData: Icons.delete_outline_rounded,
|
||||||
onPressed: enabled
|
label: "control_bottom_app_bar_delete".tr(),
|
||||||
? () {
|
onPressed: enabled
|
||||||
if (!trashEnabled) {
|
? () {
|
||||||
showDialog(
|
if (!trashEnabled) {
|
||||||
context: context,
|
showDialog(
|
||||||
builder: (BuildContext context) {
|
context: context,
|
||||||
return DeleteDialog(
|
builder: (BuildContext context) {
|
||||||
onDelete: onDelete,
|
return DeleteDialog(
|
||||||
);
|
onDelete: onDelete!,
|
||||||
},
|
);
|
||||||
);
|
},
|
||||||
} else {
|
);
|
||||||
onDelete();
|
} else {
|
||||||
|
onDelete!();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
: null,
|
||||||
: null,
|
),
|
||||||
),
|
if (!hasLocal &&
|
||||||
if (!hasLocal && selectionAssetState.selectedCount > 1)
|
selectionAssetState.selectedCount > 1 &&
|
||||||
|
onStack != null)
|
||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
iconData: Icons.filter_none_rounded,
|
iconData: Icons.filter_none_rounded,
|
||||||
label: "control_bottom_app_bar_stack".tr(),
|
label: "control_bottom_app_bar_stack".tr(),
|
||||||
onPressed: enabled ? onStack : null,
|
onPressed: enabled ? onStack : null,
|
||||||
),
|
),
|
||||||
|
if (onRemoveFromAlbum != null)
|
||||||
|
ControlBoxButton(
|
||||||
|
iconData: Icons.delete_sweep_rounded,
|
||||||
|
label: 'album_viewer_appbar_share_remove'.tr(),
|
||||||
|
onPressed: enabled ? onRemoveFromAlbum : null,
|
||||||
|
),
|
||||||
if (hasLocal)
|
if (hasLocal)
|
||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
iconData: Icons.backup_outlined,
|
iconData: Icons.backup_outlined,
|
||||||
|
@ -1,57 +1,30 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
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/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
|
||||||
import 'package:immich_mobile/modules/asset_viewer/services/asset_stack.service.dart';
|
|
||||||
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/models/selection_state.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/providers/multiselect.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';
|
|
||||||
import 'package:immich_mobile/modules/memories/ui/memory_lane.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_app_bar.dart';
|
import 'package:immich_mobile/shared/ui/immich_app_bar.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
|
||||||
import 'package:immich_mobile/utils/selection_handlers.dart';
|
|
||||||
|
|
||||||
class HomePage extends HookConsumerWidget {
|
class HomePage extends HookConsumerWidget {
|
||||||
const HomePage({Key? key}) : super(key: key);
|
const HomePage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final multiselectEnabled = ref.watch(multiselectProvider.notifier);
|
|
||||||
final selectionEnabledHook = useState(false);
|
|
||||||
final selectionAssetState = useState(const SelectionAssetState());
|
|
||||||
|
|
||||||
final selection = useState(<Asset>{});
|
|
||||||
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
|
||||||
final sharedAlbums = ref.watch(sharedAlbumProvider);
|
|
||||||
final albumService = ref.watch(albumServiceProvider);
|
|
||||||
final currentUser = ref.watch(currentUserProvider);
|
final currentUser = ref.watch(currentUserProvider);
|
||||||
final timelineUsers = ref.watch(timelineUsersIdsProvider);
|
final timelineUsers = ref.watch(timelineUsersIdsProvider);
|
||||||
final trashEnabled =
|
|
||||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
|
||||||
|
|
||||||
final tipOneOpacity = useState(0.0);
|
final tipOneOpacity = useState(0.0);
|
||||||
final refreshCount = useState(0);
|
final refreshCount = useState(0);
|
||||||
final processing = useProcessingOverlay();
|
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
@ -61,394 +34,82 @@ class HomePage extends HookConsumerWidget {
|
|||||||
ref.read(albumProvider.notifier).getAllAlbums();
|
ref.read(albumProvider.notifier).getAllAlbums();
|
||||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||||
ref.read(serverInfoProvider.notifier).getServerInfo();
|
ref.read(serverInfoProvider.notifier).getServerInfo();
|
||||||
|
return;
|
||||||
selectionEnabledHook.addListener(() {
|
|
||||||
multiselectEnabled.state = selectionEnabledHook.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return () {
|
|
||||||
// This does not work in tests
|
|
||||||
if (kReleaseMode) {
|
|
||||||
selectionEnabledHook.dispose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
Widget buildLoadingIndicator() {
|
||||||
|
Timer(const Duration(seconds: 2), () => tipOneOpacity.value = 1);
|
||||||
|
|
||||||
Widget buildBody() {
|
return Center(
|
||||||
void selectionListener(
|
child: Column(
|
||||||
bool multiselect,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
Set<Asset> selectedAssets,
|
|
||||||
) {
|
|
||||||
selectionEnabledHook.value = multiselect;
|
|
||||||
selection.value = selectedAssets;
|
|
||||||
selectionAssetState.value =
|
|
||||||
SelectionAssetState.fromSelection(selectedAssets);
|
|
||||||
}
|
|
||||||
|
|
||||||
errorBuilder(String? msg) => msg != null && msg.isNotEmpty
|
|
||||||
? () => ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: msg,
|
|
||||||
gravity: ToastGravity.BOTTOM,
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
Iterable<Asset> remoteOnly(
|
|
||||||
Iterable<Asset> assets, {
|
|
||||||
void Function()? errorCallback,
|
|
||||||
}) {
|
|
||||||
final bool onlyRemote = assets.every((e) => e.isRemote);
|
|
||||||
if (!onlyRemote) {
|
|
||||||
if (errorCallback != null) errorCallback();
|
|
||||||
return assets.where((a) => a.isRemote);
|
|
||||||
}
|
|
||||||
return assets;
|
|
||||||
}
|
|
||||||
|
|
||||||
Iterable<Asset> ownedOnly(
|
|
||||||
Iterable<Asset> assets, {
|
|
||||||
void Function()? errorCallback,
|
|
||||||
}) {
|
|
||||||
if (currentUser == null) return [];
|
|
||||||
final userId = currentUser.isarId;
|
|
||||||
final bool onlyOwned = assets.every((e) => e.ownerId == userId);
|
|
||||||
if (!onlyOwned) {
|
|
||||||
if (errorCallback != null) errorCallback();
|
|
||||||
return assets.where((a) => a.ownerId == userId);
|
|
||||||
}
|
|
||||||
return assets;
|
|
||||||
}
|
|
||||||
|
|
||||||
Iterable<Asset> ownedRemoteSelection({
|
|
||||||
String? localErrorMessage,
|
|
||||||
String? ownerErrorMessage,
|
|
||||||
}) {
|
|
||||||
final assets = selection.value;
|
|
||||||
return remoteOnly(
|
|
||||||
ownedOnly(assets, errorCallback: errorBuilder(ownerErrorMessage)),
|
|
||||||
errorCallback: errorBuilder(localErrorMessage),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Iterable<Asset> remoteSelection({String? errorMessage}) => remoteOnly(
|
|
||||||
selection.value,
|
|
||||||
errorCallback: errorBuilder(errorMessage),
|
|
||||||
);
|
|
||||||
|
|
||||||
void onShareAssets(bool shareLocal) {
|
|
||||||
processing.value = true;
|
|
||||||
if (shareLocal) {
|
|
||||||
handleShareAssets(ref, context, selection.value.toList());
|
|
||||||
} else {
|
|
||||||
final ids =
|
|
||||||
remoteSelection(errorMessage: "home_page_share_err_local".tr())
|
|
||||||
.map((e) => e.remoteId!);
|
|
||||||
context.autoPush(SharedLinkEditRoute(assetsList: ids.toList()));
|
|
||||||
}
|
|
||||||
processing.value = false;
|
|
||||||
selectionEnabledHook.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void onFavoriteAssets() async {
|
|
||||||
processing.value = true;
|
|
||||||
try {
|
|
||||||
final remoteAssets = ownedRemoteSelection(
|
|
||||||
localErrorMessage: 'home_page_favorite_err_local'.tr(),
|
|
||||||
ownerErrorMessage: 'home_page_favorite_err_partner'.tr(),
|
|
||||||
);
|
|
||||||
if (remoteAssets.isNotEmpty) {
|
|
||||||
await handleFavoriteAssets(ref, context, remoteAssets.toList());
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
processing.value = false;
|
|
||||||
selectionEnabledHook.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onArchiveAsset() async {
|
|
||||||
processing.value = true;
|
|
||||||
try {
|
|
||||||
final remoteAssets = ownedRemoteSelection(
|
|
||||||
localErrorMessage: 'home_page_archive_err_local'.tr(),
|
|
||||||
ownerErrorMessage: 'home_page_archive_err_partner'.tr(),
|
|
||||||
);
|
|
||||||
await handleArchiveAssets(ref, context, remoteAssets.toList());
|
|
||||||
} finally {
|
|
||||||
processing.value = false;
|
|
||||||
selectionEnabledHook.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onDelete() async {
|
|
||||||
processing.value = true;
|
|
||||||
try {
|
|
||||||
final toDelete = ownedOnly(
|
|
||||||
selection.value,
|
|
||||||
errorCallback: errorBuilder('home_page_delete_err_partner'.tr()),
|
|
||||||
).toList();
|
|
||||||
await ref
|
|
||||||
.read(assetProvider.notifier)
|
|
||||||
.deleteAssets(toDelete, force: !trashEnabled);
|
|
||||||
|
|
||||||
final hasRemote = toDelete.any((a) => a.isRemote);
|
|
||||||
final assetOrAssets = toDelete.length > 1 ? 'assets' : 'asset';
|
|
||||||
final trashOrRemoved =
|
|
||||||
!trashEnabled ? 'deleted permanently' : 'trashed';
|
|
||||||
if (hasRemote) {
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: '${selection.value.length} $assetOrAssets $trashOrRemoved',
|
|
||||||
gravity: ToastGravity.BOTTOM,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
selectionEnabledHook.value = false;
|
|
||||||
} finally {
|
|
||||||
processing.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onUpload() {
|
|
||||||
processing.value = true;
|
|
||||||
selectionEnabledHook.value = false;
|
|
||||||
try {
|
|
||||||
ref.read(manualUploadProvider.notifier).uploadAssets(
|
|
||||||
context,
|
|
||||||
selection.value.where((a) => a.storage == AssetState.local),
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
processing.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onAddToAlbum(Album album) async {
|
|
||||||
processing.value = true;
|
|
||||||
try {
|
|
||||||
final Iterable<Asset> assets = remoteSelection(
|
|
||||||
errorMessage: "home_page_add_to_album_err_local".tr(),
|
|
||||||
);
|
|
||||||
if (assets.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final result = await albumService.addAdditionalAssetToAlbum(
|
|
||||||
assets,
|
|
||||||
album,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result != null) {
|
|
||||||
if (result.alreadyInAlbum.isNotEmpty) {
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: "home_page_add_to_album_conflicts".tr(
|
|
||||||
namedArgs: {
|
|
||||||
"album": album.name,
|
|
||||||
"added": result.successfullyAdded.toString(),
|
|
||||||
"failed": result.alreadyInAlbum.length.toString(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: "home_page_add_to_album_success".tr(
|
|
||||||
namedArgs: {
|
|
||||||
"album": album.name,
|
|
||||||
"added": result.successfullyAdded.toString(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
toastType: ToastType.success,
|
|
||||||
);
|
|
||||||
|
|
||||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
|
||||||
ref.invalidate(albumDetailProvider(album.id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
processing.value = false;
|
|
||||||
selectionEnabledHook.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onCreateNewAlbum() async {
|
|
||||||
processing.value = true;
|
|
||||||
try {
|
|
||||||
final Iterable<Asset> assets = remoteSelection(
|
|
||||||
errorMessage: "home_page_add_to_album_err_local".tr(),
|
|
||||||
);
|
|
||||||
if (assets.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final result =
|
|
||||||
await albumService.createAlbumWithGeneratedName(assets);
|
|
||||||
|
|
||||||
if (result != null) {
|
|
||||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
|
||||||
ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
|
||||||
selectionEnabledHook.value = false;
|
|
||||||
|
|
||||||
context.autoPush(AlbumViewerRoute(albumId: result.id));
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
processing.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onStack() async {
|
|
||||||
try {
|
|
||||||
processing.value = true;
|
|
||||||
if (!selectionEnabledHook.value || selection.value.length < 2) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final parent = selection.value.elementAt(0);
|
|
||||||
selection.value.remove(parent);
|
|
||||||
await ref.read(assetStackServiceProvider).updateStack(
|
|
||||||
parent,
|
|
||||||
childrenToAdd: selection.value.toList(),
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
processing.value = false;
|
|
||||||
selectionEnabledHook.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onEditTime() async {
|
|
||||||
try {
|
|
||||||
final remoteAssets = ownedRemoteSelection(
|
|
||||||
localErrorMessage: 'home_page_favorite_err_local'.tr(),
|
|
||||||
ownerErrorMessage: 'home_page_favorite_err_partner'.tr(),
|
|
||||||
);
|
|
||||||
if (remoteAssets.isNotEmpty) {
|
|
||||||
handleEditDateTime(ref, context, remoteAssets.toList());
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
selectionEnabledHook.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onEditLocation() async {
|
|
||||||
try {
|
|
||||||
final remoteAssets = ownedRemoteSelection(
|
|
||||||
localErrorMessage: 'home_page_favorite_err_local'.tr(),
|
|
||||||
ownerErrorMessage: 'home_page_favorite_err_partner'.tr(),
|
|
||||||
);
|
|
||||||
if (remoteAssets.isNotEmpty) {
|
|
||||||
handleEditLocation(ref, context, remoteAssets.toList());
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
selectionEnabledHook.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> refreshAssets() async {
|
|
||||||
final fullRefresh = refreshCount.value > 0;
|
|
||||||
await ref.read(assetProvider.notifier).getAllAsset(clear: fullRefresh);
|
|
||||||
if (timelineUsers.length > 1) {
|
|
||||||
await ref.read(assetProvider.notifier).getPartnerAssets();
|
|
||||||
}
|
|
||||||
if (fullRefresh) {
|
|
||||||
// refresh was forced: user requested another refresh within 2 seconds
|
|
||||||
refreshCount.value = 0;
|
|
||||||
} else {
|
|
||||||
refreshCount.value++;
|
|
||||||
// set counter back to 0 if user does not request refresh again
|
|
||||||
Timer(const Duration(seconds: 4), () => refreshCount.value = 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildLoadingIndicator() {
|
|
||||||
Timer(const Duration(seconds: 2), () => tipOneOpacity.value = 1);
|
|
||||||
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const ImmichLoadingIndicator(),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 16.0),
|
|
||||||
child: Text(
|
|
||||||
'home_page_building_timeline',
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 16,
|
|
||||||
color: context.primaryColor,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
AnimatedOpacity(
|
|
||||||
duration: const Duration(milliseconds: 500),
|
|
||||||
opacity: tipOneOpacity.value,
|
|
||||||
child: SizedBox(
|
|
||||||
width: 250,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
|
||||||
child: const Text(
|
|
||||||
'home_page_first_time_notice',
|
|
||||||
textAlign: TextAlign.justify,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SafeArea(
|
|
||||||
top: true,
|
|
||||||
bottom: false,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
children: [
|
||||||
ref
|
const ImmichLoadingIndicator(),
|
||||||
.watch(
|
Padding(
|
||||||
timelineUsers.length > 1
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
? multiUserAssetsProvider(timelineUsers)
|
child: Text(
|
||||||
: assetsProvider(currentUser?.isarId),
|
'home_page_building_timeline',
|
||||||
)
|
style: TextStyle(
|
||||||
.when(
|
fontWeight: FontWeight.w600,
|
||||||
data: (data) => data.isEmpty
|
fontSize: 16,
|
||||||
? buildLoadingIndicator()
|
color: context.primaryColor,
|
||||||
: ImmichAssetGrid(
|
),
|
||||||
renderList: data,
|
).tr(),
|
||||||
listener: selectionListener,
|
),
|
||||||
selectionActive: selectionEnabledHook.value,
|
AnimatedOpacity(
|
||||||
onRefresh: refreshAssets,
|
duration: const Duration(milliseconds: 500),
|
||||||
topWidget:
|
opacity: tipOneOpacity.value,
|
||||||
(currentUser != null && currentUser.memoryEnabled)
|
child: SizedBox(
|
||||||
? const MemoryLane()
|
width: 250,
|
||||||
: const SizedBox(),
|
child: Padding(
|
||||||
showStack: true,
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
),
|
child: const Text(
|
||||||
error: (error, _) => Center(child: Text(error.toString())),
|
'home_page_first_time_notice',
|
||||||
loading: buildLoadingIndicator,
|
textAlign: TextAlign.justify,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
),
|
),
|
||||||
if (selectionEnabledHook.value)
|
|
||||||
ControlBottomAppBar(
|
|
||||||
onShare: onShareAssets,
|
|
||||||
onFavorite: onFavoriteAssets,
|
|
||||||
onArchive: onArchiveAsset,
|
|
||||||
onDelete: onDelete,
|
|
||||||
onAddToAlbum: onAddToAlbum,
|
|
||||||
albums: albums,
|
|
||||||
sharedAlbums: sharedAlbums,
|
|
||||||
onCreateNewAlbum: onCreateNewAlbum,
|
|
||||||
onUpload: onUpload,
|
|
||||||
enabled: !processing.value,
|
|
||||||
selectionAssetState: selectionAssetState.value,
|
|
||||||
onStack: onStack,
|
|
||||||
onEditTime: onEditTime,
|
|
||||||
onEditLocation: onEditLocation,
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> refreshAssets() async {
|
||||||
|
final fullRefresh = refreshCount.value > 0;
|
||||||
|
await ref.read(assetProvider.notifier).getAllAsset(clear: fullRefresh);
|
||||||
|
if (timelineUsers.length > 1) {
|
||||||
|
await ref.read(assetProvider.notifier).getPartnerAssets();
|
||||||
|
}
|
||||||
|
if (fullRefresh) {
|
||||||
|
// refresh was forced: user requested another refresh within 2 seconds
|
||||||
|
refreshCount.value = 0;
|
||||||
|
} else {
|
||||||
|
refreshCount.value++;
|
||||||
|
// set counter back to 0 if user does not request refresh again
|
||||||
|
Timer(const Duration(seconds: 4), () => refreshCount.value = 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildBody() {
|
||||||
|
return MultiselectGrid(
|
||||||
|
renderListProvider: timelineUsers.length > 1
|
||||||
|
? multiUserAssetsProvider(timelineUsers)
|
||||||
|
: assetsProvider(currentUser?.isarId),
|
||||||
|
buildLoadingIndicator: buildLoadingIndicator,
|
||||||
|
onRefresh: refreshAssets,
|
||||||
|
stackEnabled: true,
|
||||||
|
archiveEnabled: true,
|
||||||
|
editEnabled: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: !selectionEnabledHook.value ? const ImmichAppBar() : null,
|
appBar: ref.watch(multiselectProvider) ? null : const ImmichAppBar(),
|
||||||
body: buildBody(),
|
body: buildBody(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.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/partner/providers/partner.provider.dart';
|
import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.dart';
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
|
|
||||||
class PartnerDetailPage extends HookConsumerWidget {
|
class PartnerDetailPage extends HookConsumerWidget {
|
||||||
@ -15,7 +15,6 @@ class PartnerDetailPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final assets = ref.watch(assetsProvider(partner.isarId));
|
|
||||||
final inTimeline = useState(partner.inTimeline);
|
final inTimeline = useState(partner.inTimeline);
|
||||||
bool toggleInProcess = false;
|
bool toggleInProcess = false;
|
||||||
|
|
||||||
@ -57,33 +56,30 @@ class PartnerDetailPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: ref.watch(multiselectProvider)
|
||||||
title: Text(partner.name),
|
? null
|
||||||
elevation: 0,
|
: AppBar(
|
||||||
centerTitle: false,
|
title: Text(partner.name),
|
||||||
actions: [
|
elevation: 0,
|
||||||
IconButton(
|
centerTitle: false,
|
||||||
onPressed: toggleInTimeline,
|
actions: [
|
||||||
icon: Icon(
|
IconButton(
|
||||||
inTimeline.value ? Icons.collections : Icons.collections_outlined,
|
onPressed: toggleInTimeline,
|
||||||
|
icon: Icon(
|
||||||
|
inTimeline.value
|
||||||
|
? Icons.collections
|
||||||
|
: Icons.collections_outlined,
|
||||||
|
),
|
||||||
|
tooltip: "Show/hide photos on your main timeline",
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
tooltip: "Show/hide photos on your main timeline",
|
body: MultiselectGrid(
|
||||||
),
|
renderListProvider: assetsProvider(partner.isarId),
|
||||||
],
|
onRefresh: () =>
|
||||||
),
|
ref.read(assetProvider.notifier).getPartnerAssets(partner),
|
||||||
body: assets.widgetWhen(
|
deleteEnabled: false,
|
||||||
onData: (renderList) => renderList.isEmpty
|
favoriteEnabled: false,
|
||||||
? Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Text(
|
|
||||||
"It seems ${partner.name} does not have any photos...\n"
|
|
||||||
"Or your server version does not match the app version."),
|
|
||||||
)
|
|
||||||
: ImmichAssetGrid(
|
|
||||||
renderList: renderList,
|
|
||||||
onRefresh: () =>
|
|
||||||
ref.read(assetProvider.notifier).getPartnerAssets(partner),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -72,8 +72,6 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
totalAssets: args.totalAssets,
|
totalAssets: args.totalAssets,
|
||||||
heroOffset: args.heroOffset,
|
heroOffset: args.heroOffset,
|
||||||
showStack: args.showStack,
|
showStack: args.showStack,
|
||||||
isOwner: args.isOwner,
|
|
||||||
sharedAlbumId: args.sharedAlbumId,
|
|
||||||
),
|
),
|
||||||
transitionsBuilder: CustomTransitionsBuilders.zoomedPage,
|
transitionsBuilder: CustomTransitionsBuilders.zoomedPage,
|
||||||
opaque: true,
|
opaque: true,
|
||||||
@ -799,8 +797,6 @@ class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
|
|||||||
required int totalAssets,
|
required int totalAssets,
|
||||||
int heroOffset = 0,
|
int heroOffset = 0,
|
||||||
bool showStack = false,
|
bool showStack = false,
|
||||||
bool isOwner = true,
|
|
||||||
String? sharedAlbumId,
|
|
||||||
}) : super(
|
}) : super(
|
||||||
GalleryViewerRoute.name,
|
GalleryViewerRoute.name,
|
||||||
path: '/gallery-viewer-page',
|
path: '/gallery-viewer-page',
|
||||||
@ -811,8 +807,6 @@ class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
|
|||||||
totalAssets: totalAssets,
|
totalAssets: totalAssets,
|
||||||
heroOffset: heroOffset,
|
heroOffset: heroOffset,
|
||||||
showStack: showStack,
|
showStack: showStack,
|
||||||
isOwner: isOwner,
|
|
||||||
sharedAlbumId: sharedAlbumId,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -827,8 +821,6 @@ class GalleryViewerRouteArgs {
|
|||||||
required this.totalAssets,
|
required this.totalAssets,
|
||||||
this.heroOffset = 0,
|
this.heroOffset = 0,
|
||||||
this.showStack = false,
|
this.showStack = false,
|
||||||
this.isOwner = true,
|
|
||||||
this.sharedAlbumId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
@ -843,13 +835,9 @@ class GalleryViewerRouteArgs {
|
|||||||
|
|
||||||
final bool showStack;
|
final bool showStack;
|
||||||
|
|
||||||
final bool isOwner;
|
|
||||||
|
|
||||||
final String? sharedAlbumId;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'GalleryViewerRouteArgs{key: $key, initialIndex: $initialIndex, loadAsset: $loadAsset, totalAssets: $totalAssets, heroOffset: $heroOffset, showStack: $showStack, isOwner: $isOwner, sharedAlbumId: $sharedAlbumId}';
|
return 'GalleryViewerRouteArgs{key: $key, initialIndex: $initialIndex, loadAsset: $loadAsset, totalAssets: $totalAssets, heroOffset: $heroOffset, showStack: $showStack}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/models/store.dart';
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.dart';
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
@ -43,11 +42,6 @@ class Album {
|
|||||||
final IsarLinks<User> sharedUsers = IsarLinks<User>();
|
final IsarLinks<User> sharedUsers = IsarLinks<User>();
|
||||||
final IsarLinks<Asset> assets = IsarLinks<Asset>();
|
final IsarLinks<Asset> assets = IsarLinks<Asset>();
|
||||||
|
|
||||||
RenderList _renderList = RenderList.empty();
|
|
||||||
|
|
||||||
@ignore
|
|
||||||
RenderList get renderList => _renderList;
|
|
||||||
|
|
||||||
@ignore
|
@ignore
|
||||||
bool get isRemote => remoteId != null;
|
bool get isRemote => remoteId != null;
|
||||||
|
|
||||||
@ -75,17 +69,6 @@ class Album {
|
|||||||
return name.join(' ');
|
return name.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<void> watchRenderList(GroupAssetsBy groupAssetsBy) async* {
|
|
||||||
final query =
|
|
||||||
assets.filter().isTrashedEqualTo(false).sortByFileCreatedAtDesc();
|
|
||||||
_renderList = await RenderList.fromQuery(query, groupAssetsBy);
|
|
||||||
yield _renderList;
|
|
||||||
await for (final _ in query.watchLazy()) {
|
|
||||||
_renderList = await RenderList.fromQuery(query, groupAssetsBy);
|
|
||||||
yield _renderList;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(other) {
|
bool operator ==(other) {
|
||||||
if (other is! Album) return false;
|
if (other is! Album) return false;
|
||||||
|
@ -202,7 +202,8 @@ class AssetNotifier extends StateNotifier<bool> {
|
|||||||
return isSuccess ? remote : [];
|
return isSuccess ? remote : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggleFavorite(List<Asset> assets, bool status) async {
|
Future<void> toggleFavorite(List<Asset> assets, [bool? status]) async {
|
||||||
|
status ??= !assets.every((a) => a.isFavorite);
|
||||||
final newAssets = await _assetService.changeFavoriteStatus(assets, status);
|
final newAssets = await _assetService.changeFavoriteStatus(assets, status);
|
||||||
for (Asset? newAsset in newAssets) {
|
for (Asset? newAsset in newAssets) {
|
||||||
if (newAsset == null) {
|
if (newAsset == null) {
|
||||||
@ -212,7 +213,8 @@ class AssetNotifier extends StateNotifier<bool> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggleArchive(List<Asset> assets, bool status) async {
|
Future<void> toggleArchive(List<Asset> assets, [bool? status]) async {
|
||||||
|
status ??= assets.every((a) => a.isArchived);
|
||||||
final newAssets = await _assetService.changeArchiveStatus(assets, status);
|
final newAssets = await _assetService.changeArchiveStatus(assets, status);
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (Asset oldAsset in assets) {
|
for (Asset oldAsset in assets) {
|
||||||
|
419
mobile/lib/shared/ui/asset_grid/multiselect_grid.dart
Normal file
419
mobile/lib/shared/ui/asset_grid/multiselect_grid.dart
Normal file
@ -0,0 +1,419 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/providers/album.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/asset_viewer/services/asset_stack.service.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/models/selection_state.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.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';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
|
import 'package:immich_mobile/utils/selection_handlers.dart';
|
||||||
|
|
||||||
|
class MultiselectGrid extends HookConsumerWidget {
|
||||||
|
const MultiselectGrid({
|
||||||
|
Key? key,
|
||||||
|
required this.renderListProvider,
|
||||||
|
this.onRefresh,
|
||||||
|
this.buildLoadingIndicator,
|
||||||
|
this.onRemoveFromAlbum,
|
||||||
|
this.topWidget,
|
||||||
|
this.stackEnabled = false,
|
||||||
|
this.archiveEnabled = false,
|
||||||
|
this.deleteEnabled = true,
|
||||||
|
this.favoriteEnabled = true,
|
||||||
|
this.editEnabled = false,
|
||||||
|
this.unarchive = false,
|
||||||
|
this.unfavorite = false,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final ProviderListenable<AsyncValue<RenderList>> renderListProvider;
|
||||||
|
final Future<void> Function()? onRefresh;
|
||||||
|
final Widget Function()? buildLoadingIndicator;
|
||||||
|
final Future<bool> Function(Iterable<Asset>)? onRemoveFromAlbum;
|
||||||
|
final Widget? topWidget;
|
||||||
|
final bool stackEnabled;
|
||||||
|
final bool archiveEnabled;
|
||||||
|
final bool unarchive;
|
||||||
|
final bool deleteEnabled;
|
||||||
|
final bool favoriteEnabled;
|
||||||
|
final bool unfavorite;
|
||||||
|
final bool editEnabled;
|
||||||
|
|
||||||
|
Widget buildDefaultLoadingIndicator() =>
|
||||||
|
const Center(child: ImmichLoadingIndicator());
|
||||||
|
|
||||||
|
Widget buildEmptyIndicator() =>
|
||||||
|
const Center(child: Text("No assets to show"));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final multiselectEnabled = ref.watch(multiselectProvider.notifier);
|
||||||
|
final selectionEnabledHook = useState(false);
|
||||||
|
final selectionAssetState = useState(const SelectionAssetState());
|
||||||
|
|
||||||
|
final selection = useState(<Asset>{});
|
||||||
|
final currentUser = ref.watch(currentUserProvider);
|
||||||
|
final processing = useProcessingOverlay();
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
selectionEnabledHook.addListener(() {
|
||||||
|
multiselectEnabled.state = selectionEnabledHook.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return () {
|
||||||
|
// This does not work in tests
|
||||||
|
if (kReleaseMode) {
|
||||||
|
selectionEnabledHook.dispose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
void selectionListener(
|
||||||
|
bool multiselect,
|
||||||
|
Set<Asset> selectedAssets,
|
||||||
|
) {
|
||||||
|
selectionEnabledHook.value = multiselect;
|
||||||
|
selection.value = selectedAssets;
|
||||||
|
selectionAssetState.value =
|
||||||
|
SelectionAssetState.fromSelection(selectedAssets);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorBuilder(String? msg) => msg != null && msg.isNotEmpty
|
||||||
|
? () => ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: msg,
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
Iterable<Asset> remoteOnly(
|
||||||
|
Iterable<Asset> assets, {
|
||||||
|
void Function()? errorCallback,
|
||||||
|
}) {
|
||||||
|
final bool onlyRemote = assets.every((e) => e.isRemote);
|
||||||
|
if (!onlyRemote) {
|
||||||
|
if (errorCallback != null) errorCallback();
|
||||||
|
return assets.where((a) => a.isRemote);
|
||||||
|
}
|
||||||
|
return assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<Asset> ownedOnly(
|
||||||
|
Iterable<Asset> assets, {
|
||||||
|
void Function()? errorCallback,
|
||||||
|
}) {
|
||||||
|
if (currentUser == null) return [];
|
||||||
|
final userId = currentUser.isarId;
|
||||||
|
final bool onlyOwned = assets.every((e) => e.ownerId == userId);
|
||||||
|
if (!onlyOwned) {
|
||||||
|
if (errorCallback != null) errorCallback();
|
||||||
|
return assets.where((a) => a.ownerId == userId);
|
||||||
|
}
|
||||||
|
return assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<Asset> ownedRemoteSelection({
|
||||||
|
String? localErrorMessage,
|
||||||
|
String? ownerErrorMessage,
|
||||||
|
}) {
|
||||||
|
final assets = selection.value;
|
||||||
|
return remoteOnly(
|
||||||
|
ownedOnly(assets, errorCallback: errorBuilder(ownerErrorMessage)),
|
||||||
|
errorCallback: errorBuilder(localErrorMessage),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<Asset> remoteSelection({String? errorMessage}) => remoteOnly(
|
||||||
|
selection.value,
|
||||||
|
errorCallback: errorBuilder(errorMessage),
|
||||||
|
);
|
||||||
|
|
||||||
|
void onShareAssets(bool shareLocal) {
|
||||||
|
processing.value = true;
|
||||||
|
if (shareLocal) {
|
||||||
|
handleShareAssets(ref, context, selection.value.toList());
|
||||||
|
} else {
|
||||||
|
final ids =
|
||||||
|
remoteSelection(errorMessage: "home_page_share_err_local".tr())
|
||||||
|
.map((e) => e.remoteId!);
|
||||||
|
context.autoPush(SharedLinkEditRoute(assetsList: ids.toList()));
|
||||||
|
}
|
||||||
|
processing.value = false;
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onFavoriteAssets() async {
|
||||||
|
processing.value = true;
|
||||||
|
try {
|
||||||
|
final remoteAssets = ownedRemoteSelection(
|
||||||
|
localErrorMessage: 'home_page_favorite_err_local'.tr(),
|
||||||
|
ownerErrorMessage: 'home_page_favorite_err_partner'.tr(),
|
||||||
|
);
|
||||||
|
if (remoteAssets.isNotEmpty) {
|
||||||
|
await handleFavoriteAssets(ref, context, remoteAssets.toList());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
processing.value = false;
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onArchiveAsset() async {
|
||||||
|
processing.value = true;
|
||||||
|
try {
|
||||||
|
final remoteAssets = ownedRemoteSelection(
|
||||||
|
localErrorMessage: 'home_page_archive_err_local'.tr(),
|
||||||
|
ownerErrorMessage: 'home_page_archive_err_partner'.tr(),
|
||||||
|
);
|
||||||
|
await handleArchiveAssets(ref, context, remoteAssets.toList());
|
||||||
|
} finally {
|
||||||
|
processing.value = false;
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDelete() async {
|
||||||
|
processing.value = true;
|
||||||
|
try {
|
||||||
|
final trashEnabled =
|
||||||
|
ref.read(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
||||||
|
final toDelete = ownedOnly(
|
||||||
|
selection.value,
|
||||||
|
errorCallback: errorBuilder('home_page_delete_err_partner'.tr()),
|
||||||
|
).toList();
|
||||||
|
await ref
|
||||||
|
.read(assetProvider.notifier)
|
||||||
|
.deleteAssets(toDelete, force: !trashEnabled);
|
||||||
|
|
||||||
|
final hasRemote = toDelete.any((a) => a.isRemote);
|
||||||
|
final assetOrAssets = toDelete.length > 1 ? 'assets' : 'asset';
|
||||||
|
final trashOrRemoved =
|
||||||
|
!trashEnabled ? 'deleted permanently' : 'trashed';
|
||||||
|
if (hasRemote) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: '${selection.value.length} $assetOrAssets $trashOrRemoved',
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
} finally {
|
||||||
|
processing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onUpload() {
|
||||||
|
processing.value = true;
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
try {
|
||||||
|
ref.read(manualUploadProvider.notifier).uploadAssets(
|
||||||
|
context,
|
||||||
|
selection.value.where((a) => a.storage == AssetState.local),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
processing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onAddToAlbum(Album album) async {
|
||||||
|
processing.value = true;
|
||||||
|
try {
|
||||||
|
final Iterable<Asset> assets = remoteSelection(
|
||||||
|
errorMessage: "home_page_add_to_album_err_local".tr(),
|
||||||
|
);
|
||||||
|
if (assets.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final result =
|
||||||
|
await ref.read(albumServiceProvider).addAdditionalAssetToAlbum(
|
||||||
|
assets,
|
||||||
|
album,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
if (result.alreadyInAlbum.isNotEmpty) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "home_page_add_to_album_conflicts".tr(
|
||||||
|
namedArgs: {
|
||||||
|
"album": album.name,
|
||||||
|
"added": result.successfullyAdded.toString(),
|
||||||
|
"failed": result.alreadyInAlbum.length.toString(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "home_page_add_to_album_success".tr(
|
||||||
|
namedArgs: {
|
||||||
|
"album": album.name,
|
||||||
|
"added": result.successfullyAdded.toString(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
toastType: ToastType.success,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
processing.value = false;
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onCreateNewAlbum() async {
|
||||||
|
processing.value = true;
|
||||||
|
try {
|
||||||
|
final Iterable<Asset> assets = remoteSelection(
|
||||||
|
errorMessage: "home_page_add_to_album_err_local".tr(),
|
||||||
|
);
|
||||||
|
if (assets.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final result = await ref
|
||||||
|
.read(albumServiceProvider)
|
||||||
|
.createAlbumWithGeneratedName(assets);
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
ref.watch(albumProvider.notifier).getAllAlbums();
|
||||||
|
ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
|
||||||
|
context.autoPush(AlbumViewerRoute(albumId: result.id));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
processing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onStack() async {
|
||||||
|
try {
|
||||||
|
processing.value = true;
|
||||||
|
if (!selectionEnabledHook.value || selection.value.length < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final parent = selection.value.elementAt(0);
|
||||||
|
selection.value.remove(parent);
|
||||||
|
await ref.read(assetStackServiceProvider).updateStack(
|
||||||
|
parent,
|
||||||
|
childrenToAdd: selection.value.toList(),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
processing.value = false;
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onEditTime() async {
|
||||||
|
try {
|
||||||
|
final remoteAssets = ownedRemoteSelection(
|
||||||
|
localErrorMessage: 'home_page_favorite_err_local'.tr(),
|
||||||
|
ownerErrorMessage: 'home_page_favorite_err_partner'.tr(),
|
||||||
|
);
|
||||||
|
if (remoteAssets.isNotEmpty) {
|
||||||
|
handleEditDateTime(ref, context, remoteAssets.toList());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onEditLocation() async {
|
||||||
|
try {
|
||||||
|
final remoteAssets = ownedRemoteSelection(
|
||||||
|
localErrorMessage: 'home_page_favorite_err_local'.tr(),
|
||||||
|
ownerErrorMessage: 'home_page_favorite_err_partner'.tr(),
|
||||||
|
);
|
||||||
|
if (remoteAssets.isNotEmpty) {
|
||||||
|
handleEditLocation(ref, context, remoteAssets.toList());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T> Function() wrapLongRunningFun<T>(Future<T> Function() fun) =>
|
||||||
|
() async {
|
||||||
|
processing.value = true;
|
||||||
|
try {
|
||||||
|
final result = await fun();
|
||||||
|
if (result.runtimeType != bool || result == true) {
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
processing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return SafeArea(
|
||||||
|
top: true,
|
||||||
|
bottom: false,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
ref.watch(renderListProvider).when(
|
||||||
|
data: (data) => data.isEmpty &&
|
||||||
|
(buildLoadingIndicator != null || topWidget == null)
|
||||||
|
? (buildLoadingIndicator ?? buildEmptyIndicator)()
|
||||||
|
: ImmichAssetGrid(
|
||||||
|
renderList: data,
|
||||||
|
listener: selectionListener,
|
||||||
|
selectionActive: selectionEnabledHook.value,
|
||||||
|
onRefresh: onRefresh == null
|
||||||
|
? null
|
||||||
|
: wrapLongRunningFun(onRefresh!),
|
||||||
|
topWidget: topWidget,
|
||||||
|
showStack: stackEnabled,
|
||||||
|
),
|
||||||
|
error: (error, _) => Center(child: Text(error.toString())),
|
||||||
|
loading: buildLoadingIndicator ?? buildDefaultLoadingIndicator,
|
||||||
|
),
|
||||||
|
if (selectionEnabledHook.value)
|
||||||
|
ControlBottomAppBar(
|
||||||
|
onShare: onShareAssets,
|
||||||
|
onFavorite: favoriteEnabled ? onFavoriteAssets : null,
|
||||||
|
onArchive: archiveEnabled ? onArchiveAsset : null,
|
||||||
|
onDelete: deleteEnabled ? onDelete : null,
|
||||||
|
onAddToAlbum: onAddToAlbum,
|
||||||
|
onCreateNewAlbum: onCreateNewAlbum,
|
||||||
|
onUpload: onUpload,
|
||||||
|
enabled: !processing.value,
|
||||||
|
selectionAssetState: selectionAssetState.value,
|
||||||
|
onStack: stackEnabled ? onStack : null,
|
||||||
|
onEditTime: editEnabled ? onEditTime : null,
|
||||||
|
onEditLocation: editEnabled ? onEditLocation : null,
|
||||||
|
unfavorite: unfavorite,
|
||||||
|
unarchive: unarchive,
|
||||||
|
onRemoveFromAlbum: onRemoveFromAlbum != null
|
||||||
|
? wrapLongRunningFun(
|
||||||
|
() => onRemoveFromAlbum!(selection.value),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -45,10 +45,11 @@ Future<void> handleArchiveAssets(
|
|||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
List<Asset> selection, {
|
List<Asset> selection, {
|
||||||
bool shouldArchive = true,
|
bool? shouldArchive,
|
||||||
ToastGravity toastGravity = ToastGravity.BOTTOM,
|
ToastGravity toastGravity = ToastGravity.BOTTOM,
|
||||||
}) async {
|
}) async {
|
||||||
if (selection.isNotEmpty) {
|
if (selection.isNotEmpty) {
|
||||||
|
shouldArchive ??= !selection.every((a) => a.isArchived);
|
||||||
await ref
|
await ref
|
||||||
.read(assetProvider.notifier)
|
.read(assetProvider.notifier)
|
||||||
.toggleArchive(selection, shouldArchive);
|
.toggleArchive(selection, shouldArchive);
|
||||||
@ -69,10 +70,11 @@ Future<void> handleFavoriteAssets(
|
|||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
List<Asset> selection, {
|
List<Asset> selection, {
|
||||||
bool shouldFavorite = true,
|
bool? shouldFavorite,
|
||||||
ToastGravity toastGravity = ToastGravity.BOTTOM,
|
ToastGravity toastGravity = ToastGravity.BOTTOM,
|
||||||
}) async {
|
}) async {
|
||||||
if (selection.isNotEmpty) {
|
if (selection.isNotEmpty) {
|
||||||
|
shouldFavorite ??= !selection.every((a) => a.isFavorite);
|
||||||
await ref
|
await ref
|
||||||
.watch(assetProvider.notifier)
|
.watch(assetProvider.notifier)
|
||||||
.toggleFavorite(selection, shouldFavorite);
|
.toggleFavorite(selection, shouldFavorite);
|
||||||
|
Loading…
Reference in New Issue
Block a user