mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
feat(mobile): Archive feature on mobile (#2258)
* update asset to include isArchive property * Not display archived assets on timeline * replace share button to archive button * Added archive page * Add bottom nav bar * clean up homepage * remove deadcode * improve on sync is archive * show archive asset correctly * better merge condition * Added back renderList to re-rendering don't jump around * Better way to handle showing archive assets * complete ArchiveSelectionNotifier * toggle archive * remove deadcode * fix unit tests * update assets in DB when changing assets * update asset state to reflect archived status * allow to archive assets via multi-select from timeline * fixed logic * Add options to bulk unarchive * regenerate api * Change position of toast message --------- Co-authored-by: Fynn Petersen-Frey <zoodyy@users.noreply.github.com>
This commit is contained in:
parent
635eee9e5e
commit
2e5cd986dd
@ -111,6 +111,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_archive": "Archive",
|
||||||
"control_bottom_app_bar_share": "Share",
|
"control_bottom_app_bar_share": "Share",
|
||||||
"create_album_page_untitled": "Untitled",
|
"create_album_page_untitled": "Untitled",
|
||||||
"create_shared_album_page_create": "Create",
|
"create_shared_album_page_create": "Create",
|
||||||
@ -139,6 +140,7 @@
|
|||||||
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
|
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
|
||||||
"home_page_building_timeline": "Building the timeline",
|
"home_page_building_timeline": "Building the timeline",
|
||||||
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
|
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
|
||||||
|
"home_page_archive_err_local": "Can not archive local assets yet, skipping",
|
||||||
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
|
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
|
||||||
"image_viewer_page_state_provider_download_error": "Download Error",
|
"image_viewer_page_state_provider_download_error": "Download Error",
|
||||||
"image_viewer_page_state_provider_download_success": "Download Success",
|
"image_viewer_page_state_provider_download_success": "Download Success",
|
||||||
@ -147,6 +149,7 @@
|
|||||||
"library_page_favorites": "Favorites",
|
"library_page_favorites": "Favorites",
|
||||||
"library_page_new_album": "New album",
|
"library_page_new_album": "New album",
|
||||||
"library_page_sharing": "Sharing",
|
"library_page_sharing": "Sharing",
|
||||||
|
"library_page_archive": "Archive",
|
||||||
"library_page_sort_created": "Most recently created",
|
"library_page_sort_created": "Most recently created",
|
||||||
"library_page_sort_title": "Album title",
|
"library_page_sort_title": "Album title",
|
||||||
"login_form_api_exception": "API exception. Please check the server URL and try again.",
|
"login_form_api_exception": "API exception. Please check the server URL and try again.",
|
||||||
@ -268,5 +271,6 @@
|
|||||||
"advanced_settings_troubleshooting_title": "Troubleshooting",
|
"advanced_settings_troubleshooting_title": "Troubleshooting",
|
||||||
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
|
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
|
||||||
"description_input_submit_error": "Error updating description, check the log for more details",
|
"description_input_submit_error": "Error updating description, check the log for more details",
|
||||||
"description_input_hint_text": "Add description..."
|
"description_input_hint_text": "Add description...",
|
||||||
}
|
"archive_page_title": "Archive ({})"
|
||||||
|
}
|
||||||
|
@ -192,15 +192,18 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
|
|
||||||
// Sets the navigation bar color
|
// Sets the navigation bar color
|
||||||
SystemUiOverlayStyle overlayStyle = const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent);
|
SystemUiOverlayStyle overlayStyle = const SystemUiOverlayStyle(
|
||||||
|
systemNavigationBarColor: Colors.transparent,
|
||||||
|
);
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
// Android 8 does not support transparent app bars
|
// Android 8 does not support transparent app bars
|
||||||
final info = await DeviceInfoPlugin().androidInfo;
|
final info = await DeviceInfoPlugin().androidInfo;
|
||||||
if (info.version.sdkInt <= 26) {
|
if (info.version.sdkInt <= 26) {
|
||||||
overlayStyle = MediaQuery.of(context).platformBrightness == Brightness.light
|
overlayStyle =
|
||||||
? SystemUiOverlayStyle.light
|
MediaQuery.of(context).platformBrightness == Brightness.light
|
||||||
: SystemUiOverlayStyle.dark;
|
? SystemUiOverlayStyle.light
|
||||||
}
|
: SystemUiOverlayStyle.dark;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
SystemChrome.setSystemUIOverlayStyle(overlayStyle);
|
SystemChrome.setSystemUIOverlayStyle(overlayStyle);
|
||||||
}
|
}
|
||||||
@ -213,9 +216,6 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
// needs to be delayed so that EasyLocalization is working
|
// needs to be delayed so that EasyLocalization is working
|
||||||
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -51,14 +51,14 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
|||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: 'add_to_album_bottom_sheet_already_exists'.tr(
|
msg: 'add_to_album_bottom_sheet_already_exists'.tr(
|
||||||
namedArgs: { "album": album.name },
|
namedArgs: {"album": album.name},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: 'add_to_album_bottom_sheet_added'.tr(
|
msg: 'add_to_album_bottom_sheet_added'.tr(
|
||||||
namedArgs: { "album": album.name },
|
namedArgs: {"album": album.name},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -71,6 +71,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
|
elevation: 0,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topLeft: Radius.circular(15),
|
topLeft: Radius.circular(15),
|
||||||
@ -99,8 +100,15 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
|||||||
style: Theme.of(context).textTheme.displayMedium,
|
style: Theme.of(context).textTheme.displayMedium,
|
||||||
),
|
),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
icon: const Icon(Icons.add),
|
icon: Icon(
|
||||||
label: Text('common_create_new_album'.tr()),
|
Icons.add,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
label: Text(
|
||||||
|
'common_create_new_album'.tr(),
|
||||||
|
style:
|
||||||
|
TextStyle(color: Theme.of(context).primaryColor),
|
||||||
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref
|
ref
|
||||||
.watch(assetSelectionProvider.notifier)
|
.watch(assetSelectionProvider.notifier)
|
||||||
|
@ -43,7 +43,8 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final selectedAlbumSortOrder = useState(settings.getSetting(AppSettingsEnum.selectedAlbumSortOrder));
|
final selectedAlbumSortOrder =
|
||||||
|
useState(settings.getSetting(AppSettingsEnum.selectedAlbumSortOrder));
|
||||||
|
|
||||||
List<Album> sortedAlbums() {
|
List<Album> sortedAlbums() {
|
||||||
if (selectedAlbumSortOrder.value == 0) {
|
if (selectedAlbumSortOrder.value == 0) {
|
||||||
@ -179,13 +180,13 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 12.0,
|
fontSize: 13.0,
|
||||||
color: isDarkMode ? Colors.white : Colors.black,
|
color: isDarkMode ? Colors.white : Colors.grey[800],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||||
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.grey[50],
|
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.grey[50],
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: isDarkMode ? Colors.grey[800]! : Colors.grey[300]!,
|
color: isDarkMode ? Colors.grey[800]! : Colors.grey[300]!,
|
||||||
@ -225,8 +226,8 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
}),
|
}),
|
||||||
const SizedBox(width: 12.0),
|
const SizedBox(width: 12.0),
|
||||||
buildLibraryNavButton(
|
buildLibraryNavButton(
|
||||||
"library_page_sharing".tr(), Icons.group_outlined, () {
|
"library_page_archive".tr(), Icons.archive_outlined, () {
|
||||||
AutoRouter.of(context).navigate(const SharingRoute());
|
AutoRouter.of(context).navigate(const ArchiveRoute());
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
|
class ArchiveSelectionNotifier extends StateNotifier<Set<int>> {
|
||||||
|
ArchiveSelectionNotifier(this.db, this.assetNotifier) : super({}) {
|
||||||
|
state = db.assets
|
||||||
|
.filter()
|
||||||
|
.isArchivedEqualTo(true)
|
||||||
|
.findAllSync()
|
||||||
|
.map((e) => e.id)
|
||||||
|
.toSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
final Isar db;
|
||||||
|
final AssetNotifier assetNotifier;
|
||||||
|
|
||||||
|
void _setArchiveForAssetId(int id, bool archive) {
|
||||||
|
if (!archive) {
|
||||||
|
state = state.difference({id});
|
||||||
|
} else {
|
||||||
|
state = state.union({id});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isArchive(int id) {
|
||||||
|
return state.contains(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> toggleArchive(Asset asset) async {
|
||||||
|
if (!asset.isRemote) return;
|
||||||
|
|
||||||
|
_setArchiveForAssetId(asset.id, !_isArchive(asset.id));
|
||||||
|
|
||||||
|
await assetNotifier.toggleArchive(
|
||||||
|
[asset],
|
||||||
|
state.contains(asset.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addToArchives(Iterable<Asset> assets) {
|
||||||
|
state = state.union(assets.map((a) => a.id).toSet());
|
||||||
|
return assetNotifier.toggleArchive(assets, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final archiveProvider =
|
||||||
|
StateNotifierProvider<ArchiveSelectionNotifier, Set<int>>((ref) {
|
||||||
|
return ArchiveSelectionNotifier(
|
||||||
|
ref.watch(dbProvider),
|
||||||
|
ref.watch(assetProvider.notifier),
|
||||||
|
);
|
||||||
|
});
|
0
mobile/lib/modules/archive/ui/store_ui_here.txt
Normal file
0
mobile/lib/modules/archive/ui/store_ui_here.txt
Normal file
124
mobile/lib/modules/archive/views/archive_page.dart
Normal file
124
mobile/lib/modules/archive/views/archive_page.dart
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
|
class ArchivePage extends HookConsumerWidget {
|
||||||
|
const ArchivePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final User me = Store.get(StoreKey.currentUser);
|
||||||
|
final query = ref
|
||||||
|
.watch(dbProvider)
|
||||||
|
.assets
|
||||||
|
.filter()
|
||||||
|
.ownerIdEqualTo(me.isarId)
|
||||||
|
.isArchivedEqualTo(true);
|
||||||
|
final stream = query.watch();
|
||||||
|
final archivedAssets = useState<List<Asset>>([]);
|
||||||
|
final selectionEnabledHook = useState(false);
|
||||||
|
final selection = useState(<Asset>{});
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
query.findAll().then((value) => archivedAssets.value = value);
|
||||||
|
final subscription = stream.listen((e) {
|
||||||
|
archivedAssets.value = e;
|
||||||
|
});
|
||||||
|
// Cancel the subscription when the widget is disposed
|
||||||
|
return subscription.cancel;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
void selectionListener(
|
||||||
|
bool multiselect,
|
||||||
|
Set<Asset> selectedAssets,
|
||||||
|
) {
|
||||||
|
selectionEnabledHook.value = multiselect;
|
||||||
|
selection.value = selectedAssets;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppBar buildAppBar() {
|
||||||
|
return AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () => AutoRouter.of(context).pop(),
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
title: const Text(
|
||||||
|
'archive_page_title',
|
||||||
|
).tr(args: [archivedAssets.value.length.toString()]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildBottomBar() {
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 64,
|
||||||
|
child: Card(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
leading: const Icon(
|
||||||
|
Icons.unarchive_rounded,
|
||||||
|
),
|
||||||
|
title:
|
||||||
|
const Text("Unarchive", style: TextStyle(fontSize: 14)),
|
||||||
|
onTap: () {
|
||||||
|
if (selection.value.isNotEmpty) {
|
||||||
|
ref
|
||||||
|
.watch(assetProvider.notifier)
|
||||||
|
.toggleArchive(selection.value, false);
|
||||||
|
|
||||||
|
final assetOrAssets =
|
||||||
|
selection.value.length > 1 ? 'assets' : 'asset';
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg:
|
||||||
|
'Moved ${selection.value.length} $assetOrAssets to library',
|
||||||
|
gravity: ToastGravity.CENTER,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: buildAppBar(),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
ImmichAssetGrid(
|
||||||
|
assets: archivedAssets.value,
|
||||||
|
listener: selectionListener,
|
||||||
|
selectionActive: selectionEnabledHook.value,
|
||||||
|
),
|
||||||
|
if (selectionEnabledHook.value) buildBottomBar()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -9,8 +9,6 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
required this.asset,
|
required this.asset,
|
||||||
required this.onMoreInfoPressed,
|
required this.onMoreInfoPressed,
|
||||||
required this.onDownloadPressed,
|
required this.onDownloadPressed,
|
||||||
required this.onSharePressed,
|
|
||||||
required this.onDeletePressed,
|
|
||||||
required this.onAddToAlbumPressed,
|
required this.onAddToAlbumPressed,
|
||||||
required this.onToggleMotionVideo,
|
required this.onToggleMotionVideo,
|
||||||
required this.isPlayingMotionVideo,
|
required this.isPlayingMotionVideo,
|
||||||
@ -22,10 +20,8 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
final Function onMoreInfoPressed;
|
final Function onMoreInfoPressed;
|
||||||
final VoidCallback? onDownloadPressed;
|
final VoidCallback? onDownloadPressed;
|
||||||
final VoidCallback onToggleMotionVideo;
|
final VoidCallback onToggleMotionVideo;
|
||||||
final VoidCallback onDeletePressed;
|
|
||||||
final VoidCallback onAddToAlbumPressed;
|
final VoidCallback onAddToAlbumPressed;
|
||||||
final VoidCallback onFavorite;
|
final VoidCallback onFavorite;
|
||||||
final Function onSharePressed;
|
|
||||||
final bool isPlayingMotionVideo;
|
final bool isPlayingMotionVideo;
|
||||||
final bool isFavorite;
|
final bool isFavorite;
|
||||||
|
|
||||||
@ -34,15 +30,15 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
const double iconSize = 18.0;
|
const double iconSize = 18.0;
|
||||||
|
|
||||||
Widget buildFavoriteButton() {
|
Widget buildFavoriteButton() {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
onFavorite();
|
onFavorite();
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
isFavorite ? Icons.star : Icons.star_border,
|
isFavorite ? Icons.star : Icons.star_border,
|
||||||
color: Colors.grey[200],
|
color: Colors.grey[200],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppBar(
|
return AppBar(
|
||||||
@ -86,15 +82,6 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
color: Colors.grey[200],
|
color: Colors.grey[200],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
onSharePressed();
|
|
||||||
},
|
|
||||||
icon: Icon(
|
|
||||||
Icons.ios_share_rounded,
|
|
||||||
color: Colors.grey[200],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (asset.isRemote)
|
if (asset.isRemote)
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -105,15 +92,6 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
color: Colors.grey[200],
|
color: Colors.grey[200],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
onDeletePressed();
|
|
||||||
},
|
|
||||||
icon: Icon(
|
|
||||||
Icons.delete_outline_rounded,
|
|
||||||
color: Colors.grey[200],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
onMoreInfoPressed();
|
onMoreInfoPressed();
|
||||||
|
@ -231,11 +231,10 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
void addToAlbum(Asset addToAlbumAsset) {
|
void addToAlbum(Asset addToAlbumAsset) {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(15.0),
|
borderRadius: BorderRadius.circular(15.0),
|
||||||
),
|
),
|
||||||
barrierColor: Colors.transparent,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext _) {
|
builder: (BuildContext _) {
|
||||||
return AddToAlbumBottomSheet(
|
return AddToAlbumBottomSheet(
|
||||||
@ -267,6 +266,19 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shareAsset() {
|
||||||
|
ref
|
||||||
|
.watch(imageViewerStateProvider.notifier)
|
||||||
|
.shareAsset(assetList[indexOfAsset.value], context);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleArchive(Asset asset) {
|
||||||
|
ref
|
||||||
|
.watch(assetProvider.notifier)
|
||||||
|
.toggleArchive([asset], !asset.isArchived);
|
||||||
|
AutoRouter.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
buildAppBar() {
|
buildAppBar() {
|
||||||
final show = (showAppBar.value || // onTap has the final say
|
final show = (showAppBar.value || // onTap has the final say
|
||||||
(showAppBar.value && !isZoomed.value)) &&
|
(showAppBar.value && !isZoomed.value)) &&
|
||||||
@ -297,16 +309,9 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onSharePressed: () {
|
|
||||||
ref
|
|
||||||
.watch(imageViewerStateProvider.notifier)
|
|
||||||
.shareAsset(assetList[indexOfAsset.value], context);
|
|
||||||
},
|
|
||||||
onToggleMotionVideo: (() {
|
onToggleMotionVideo: (() {
|
||||||
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
|
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
|
||||||
}),
|
}),
|
||||||
onDeletePressed: () =>
|
|
||||||
handleDelete((assetList[indexOfAsset.value])),
|
|
||||||
onAddToAlbumPressed: () =>
|
onAddToAlbumPressed: () =>
|
||||||
addToAlbum(assetList[indexOfAsset.value]),
|
addToAlbum(assetList[indexOfAsset.value]),
|
||||||
),
|
),
|
||||||
@ -314,6 +319,59 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildBottomBar() {
|
||||||
|
final show = (showAppBar.value || // onTap has the final say
|
||||||
|
(showAppBar.value && !isZoomed.value)) &&
|
||||||
|
!isPlayingVideo.value;
|
||||||
|
final currentAsset = assetList[indexOfAsset.value];
|
||||||
|
|
||||||
|
return AnimatedOpacity(
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
opacity: show ? 1.0 : 0.0,
|
||||||
|
child: BottomNavigationBar(
|
||||||
|
backgroundColor: Colors.black.withOpacity(0.4),
|
||||||
|
unselectedIconTheme: const IconThemeData(color: Colors.white),
|
||||||
|
selectedIconTheme: const IconThemeData(color: Colors.white),
|
||||||
|
unselectedLabelStyle: const TextStyle(color: Colors.black),
|
||||||
|
selectedLabelStyle: const TextStyle(color: Colors.black),
|
||||||
|
showSelectedLabels: false,
|
||||||
|
showUnselectedLabels: false,
|
||||||
|
items: [
|
||||||
|
const BottomNavigationBarItem(
|
||||||
|
icon: Icon(Icons.ios_share_rounded),
|
||||||
|
label: 'Share',
|
||||||
|
tooltip: 'Share',
|
||||||
|
),
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: currentAsset.isArchived
|
||||||
|
? const Icon(Icons.unarchive_rounded)
|
||||||
|
: const Icon(Icons.archive_outlined),
|
||||||
|
label: 'Archive',
|
||||||
|
tooltip: 'Archive',
|
||||||
|
),
|
||||||
|
const BottomNavigationBarItem(
|
||||||
|
icon: Icon(Icons.delete_outline),
|
||||||
|
label: 'Delete',
|
||||||
|
tooltip: 'Delete',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onTap: (index) {
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
shareAsset();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
handleArchive(assetList[indexOfAsset.value]);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
handleDelete(assetList[indexOfAsset.value]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
body: WillPopScope(
|
body: WillPopScope(
|
||||||
@ -481,6 +539,12 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
right: 0,
|
right: 0,
|
||||||
child: buildAppBar(),
|
child: buildAppBar(),
|
||||||
),
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: buildBottomBar(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -109,7 +109,7 @@ class RenderList {
|
|||||||
|
|
||||||
final groups = _groupAssets(allAssets, groupBy);
|
final groups = _groupAssets(allAssets, groupBy);
|
||||||
|
|
||||||
groups.entries.sortedBy((e) =>e.key).reversed.forEach((entry) {
|
groups.entries.sortedBy((e) => e.key).reversed.forEach((entry) {
|
||||||
final date = entry.key;
|
final date = entry.key;
|
||||||
final assets = entry.value;
|
final assets = entry.value;
|
||||||
|
|
||||||
|
@ -50,10 +50,9 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
|||||||
// Unfortunately, using the transition animation itself didn't
|
// Unfortunately, using the transition animation itself didn't
|
||||||
// seem to work reliably. So instead, wait until the duration of the
|
// seem to work reliably. So instead, wait until the duration of the
|
||||||
// animation has elapsed to re-enable the hero animations
|
// animation has elapsed to re-enable the hero animations
|
||||||
Future.delayed(transitionDuration)
|
Future.delayed(transitionDuration).then((_) {
|
||||||
.then((_) {
|
enableHeroAnimations.value = true;
|
||||||
enableHeroAnimations.value = true;
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
@ -9,6 +9,7 @@ import 'package:immich_mobile/shared/models/album.dart';
|
|||||||
class ControlBottomAppBar extends ConsumerWidget {
|
class ControlBottomAppBar extends ConsumerWidget {
|
||||||
final Function onShare;
|
final Function onShare;
|
||||||
final Function onFavorite;
|
final Function onFavorite;
|
||||||
|
final Function onArchive;
|
||||||
final Function onDelete;
|
final Function onDelete;
|
||||||
final Function(Album album) onAddToAlbum;
|
final Function(Album album) onAddToAlbum;
|
||||||
final void Function() onCreateNewAlbum;
|
final void Function() onCreateNewAlbum;
|
||||||
@ -20,6 +21,7 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||||||
Key? key,
|
Key? key,
|
||||||
required this.onShare,
|
required this.onShare,
|
||||||
required this.onFavorite,
|
required this.onFavorite,
|
||||||
|
required this.onArchive,
|
||||||
required this.onDelete,
|
required this.onDelete,
|
||||||
required this.sharedAlbums,
|
required this.sharedAlbums,
|
||||||
required this.albums,
|
required this.albums,
|
||||||
@ -62,6 +64,11 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ControlBoxButton(
|
||||||
|
iconData: Icons.archive,
|
||||||
|
label: "control_bottom_app_bar_archive".tr(),
|
||||||
|
onPressed: () => onArchive(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,6 @@ class HomePage extends HookConsumerWidget {
|
|||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
// ref.watch(shareServiceProvider).shareAssets(selection.value.toList());
|
|
||||||
selectionEnabledHook.value = false;
|
selectionEnabledHook.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,6 +131,24 @@ class HomePage extends HookConsumerWidget {
|
|||||||
selectionEnabledHook.value = false;
|
selectionEnabledHook.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onArchiveAsset() {
|
||||||
|
final remoteAssets = remoteOnlySelection(
|
||||||
|
localErrorMessage: 'home_page_archive_err_local'.tr(),
|
||||||
|
);
|
||||||
|
if (remoteAssets.isNotEmpty) {
|
||||||
|
ref.watch(assetProvider.notifier).toggleArchive(remoteAssets, true);
|
||||||
|
|
||||||
|
final assetOrAssets = remoteAssets.length > 1 ? 'assets' : 'asset';
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: 'Moved ${remoteAssets.length} $assetOrAssets to archive',
|
||||||
|
gravity: ToastGravity.CENTER,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
void onDelete() {
|
void onDelete() {
|
||||||
ref.watch(assetProvider.notifier).deleteAssets(selection.value);
|
ref.watch(assetProvider.notifier).deleteAssets(selection.value);
|
||||||
selectionEnabledHook.value = false;
|
selectionEnabledHook.value = false;
|
||||||
@ -265,7 +282,7 @@ class HomePage extends HookConsumerWidget {
|
|||||||
? buildLoadingIndicator()
|
? buildLoadingIndicator()
|
||||||
: ImmichAssetGrid(
|
: ImmichAssetGrid(
|
||||||
renderList: ref.watch(assetProvider).renderList!,
|
renderList: ref.watch(assetProvider).renderList!,
|
||||||
assets: ref.watch(assetProvider).allAssets,
|
assets: ref.read(assetProvider).allAssets,
|
||||||
assetsPerRow: appSettingService
|
assetsPerRow: appSettingService
|
||||||
.getSetting(AppSettingsEnum.tilesPerRow),
|
.getSetting(AppSettingsEnum.tilesPerRow),
|
||||||
showStorageIndicator: appSettingService
|
showStorageIndicator: appSettingService
|
||||||
@ -278,6 +295,7 @@ class HomePage extends HookConsumerWidget {
|
|||||||
ControlBottomAppBar(
|
ControlBottomAppBar(
|
||||||
onShare: onShareAssets,
|
onShare: onShareAssets,
|
||||||
onFavorite: onFavoriteAssets,
|
onFavorite: onFavoriteAssets,
|
||||||
|
onArchive: onArchiveAsset,
|
||||||
onDelete: onDelete,
|
onDelete: onDelete,
|
||||||
onAddToAlbum: onAddToAlbum,
|
onAddToAlbum: onAddToAlbum,
|
||||||
albums: albums,
|
albums: albums,
|
||||||
@ -291,9 +309,7 @@ class HomePage extends HookConsumerWidget {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: !selectionEnabledHook.value
|
appBar: !selectionEnabledHook.value
|
||||||
? HomePageAppBar(
|
? HomePageAppBar(onPopBack: reloadAllAsset)
|
||||||
onPopBack: reloadAllAsset,
|
|
||||||
)
|
|
||||||
: null,
|
: null,
|
||||||
drawer: const ProfileDrawer(),
|
drawer: const ProfileDrawer(),
|
||||||
body: buildBody(),
|
body: buildBody(),
|
||||||
|
@ -9,6 +9,7 @@ import 'package:immich_mobile/modules/album/views/library_page.dart';
|
|||||||
import 'package:immich_mobile/modules/album/views/select_additional_user_for_sharing_page.dart';
|
import 'package:immich_mobile/modules/album/views/select_additional_user_for_sharing_page.dart';
|
||||||
import 'package:immich_mobile/modules/album/views/select_user_for_sharing_page.dart';
|
import 'package:immich_mobile/modules/album/views/select_user_for_sharing_page.dart';
|
||||||
import 'package:immich_mobile/modules/album/views/sharing_page.dart';
|
import 'package:immich_mobile/modules/album/views/sharing_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/archive/views/archive_page.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/views/gallery_viewer.dart';
|
import 'package:immich_mobile/modules/asset_viewer/views/gallery_viewer.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/views/album_preview_page.dart';
|
import 'package:immich_mobile/modules/backup/views/album_preview_page.dart';
|
||||||
@ -128,6 +129,13 @@ part 'router.gr.dart';
|
|||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: AppLogDetailPage,
|
page: AppLogDetailPage,
|
||||||
),
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: ArchivePage,
|
||||||
|
guards: [
|
||||||
|
AuthGuard,
|
||||||
|
DuplicateGuard,
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class AppRouter extends _$AppRouter {
|
class AppRouter extends _$AppRouter {
|
||||||
|
@ -240,6 +240,12 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
ArchiveRoute.name: (routeData) {
|
||||||
|
return MaterialPageX<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: const ArchivePage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
HomeRoute.name: (routeData) {
|
HomeRoute.name: (routeData) {
|
||||||
return MaterialPageX<dynamic>(
|
return MaterialPageX<dynamic>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
@ -499,6 +505,14 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
AppLogDetailRoute.name,
|
AppLogDetailRoute.name,
|
||||||
path: '/app-log-detail-page',
|
path: '/app-log-detail-page',
|
||||||
),
|
),
|
||||||
|
RouteConfig(
|
||||||
|
ArchiveRoute.name,
|
||||||
|
path: '/archive-page',
|
||||||
|
guards: [
|
||||||
|
authGuard,
|
||||||
|
duplicateGuard,
|
||||||
|
],
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1022,6 +1036,18 @@ class AppLogDetailRouteArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [ArchivePage]
|
||||||
|
class ArchiveRoute extends PageRouteInfo<void> {
|
||||||
|
const ArchiveRoute()
|
||||||
|
: super(
|
||||||
|
ArchiveRoute.name,
|
||||||
|
path: '/archive-page',
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'ArchiveRoute';
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [HomePage]
|
/// [HomePage]
|
||||||
class HomeRoute extends PageRouteInfo<void> {
|
class HomeRoute extends PageRouteInfo<void> {
|
||||||
|
@ -29,7 +29,8 @@ class Asset {
|
|||||||
ownerId = fastHash(remote.ownerId),
|
ownerId = fastHash(remote.ownerId),
|
||||||
exifInfo =
|
exifInfo =
|
||||||
remote.exifInfo != null ? ExifInfo.fromDto(remote.exifInfo!) : null,
|
remote.exifInfo != null ? ExifInfo.fromDto(remote.exifInfo!) : null,
|
||||||
isFavorite = remote.isFavorite;
|
isFavorite = remote.isFavorite,
|
||||||
|
isArchived = remote.isArchived;
|
||||||
|
|
||||||
Asset.local(AssetEntity local)
|
Asset.local(AssetEntity local)
|
||||||
: localId = local.id,
|
: localId = local.id,
|
||||||
@ -44,6 +45,7 @@ class Asset {
|
|||||||
fileModifiedAt = local.modifiedDateTime,
|
fileModifiedAt = local.modifiedDateTime,
|
||||||
updatedAt = local.modifiedDateTime,
|
updatedAt = local.modifiedDateTime,
|
||||||
isFavorite = local.isFavorite,
|
isFavorite = local.isFavorite,
|
||||||
|
isArchived = false,
|
||||||
fileCreatedAt = local.createDateTime {
|
fileCreatedAt = local.createDateTime {
|
||||||
if (fileCreatedAt.year == 1970) {
|
if (fileCreatedAt.year == 1970) {
|
||||||
fileCreatedAt = fileModifiedAt;
|
fileCreatedAt = fileModifiedAt;
|
||||||
@ -70,6 +72,7 @@ class Asset {
|
|||||||
this.exifInfo,
|
this.exifInfo,
|
||||||
required this.isFavorite,
|
required this.isFavorite,
|
||||||
required this.isLocal,
|
required this.isLocal,
|
||||||
|
required this.isArchived,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ignore
|
@ignore
|
||||||
@ -132,6 +135,8 @@ class Asset {
|
|||||||
|
|
||||||
bool isLocal;
|
bool isLocal;
|
||||||
|
|
||||||
|
bool isArchived;
|
||||||
|
|
||||||
@ignore
|
@ignore
|
||||||
ExifInfo? exifInfo;
|
ExifInfo? exifInfo;
|
||||||
|
|
||||||
@ -168,7 +173,8 @@ class Asset {
|
|||||||
fileName == other.fileName &&
|
fileName == other.fileName &&
|
||||||
livePhotoVideoId == other.livePhotoVideoId &&
|
livePhotoVideoId == other.livePhotoVideoId &&
|
||||||
isFavorite == other.isFavorite &&
|
isFavorite == other.isFavorite &&
|
||||||
isLocal == other.isLocal;
|
isLocal == other.isLocal &&
|
||||||
|
isArchived == other.isArchived;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -189,7 +195,8 @@ class Asset {
|
|||||||
fileName.hashCode ^
|
fileName.hashCode ^
|
||||||
livePhotoVideoId.hashCode ^
|
livePhotoVideoId.hashCode ^
|
||||||
isFavorite.hashCode ^
|
isFavorite.hashCode ^
|
||||||
isLocal.hashCode;
|
isLocal.hashCode ^
|
||||||
|
isArchived.hashCode;
|
||||||
|
|
||||||
bool updateFromAssetEntity(AssetEntity ae) {
|
bool updateFromAssetEntity(AssetEntity ae) {
|
||||||
// TODO check more fields;
|
// TODO check more fields;
|
||||||
@ -217,6 +224,9 @@ class Asset {
|
|||||||
height ??= a.height;
|
height ??= a.height;
|
||||||
exifInfo ??= a.exifInfo;
|
exifInfo ??= a.exifInfo;
|
||||||
exifInfo?.id = id;
|
exifInfo?.id = id;
|
||||||
|
if (!isRemote) {
|
||||||
|
isArchived = a.isArchived;
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,7 +281,8 @@ class Asset {
|
|||||||
"isFavorite": $isFavorite,
|
"isFavorite": $isFavorite,
|
||||||
"isLocal": $isLocal,
|
"isLocal": $isLocal,
|
||||||
"width": ${width ?? "N/A"},
|
"width": ${width ?? "N/A"},
|
||||||
"height": ${height ?? "N/A"}
|
"height": ${height ?? "N/A"},
|
||||||
|
"isArchived": $isArchived
|
||||||
}""";
|
}""";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
@ -1,3 +1,4 @@
|
|||||||
|
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/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/exif_info.dart';
|
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||||
@ -19,6 +20,8 @@ import 'package:logging/logging.dart';
|
|||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
/// State does not contain archived assets.
|
||||||
|
/// Use database provider if you want to access the isArchived assets
|
||||||
class AssetsState {
|
class AssetsState {
|
||||||
final List<Asset> allAssets;
|
final List<Asset> allAssets;
|
||||||
final RenderList? renderList;
|
final RenderList? renderList;
|
||||||
@ -76,6 +79,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
GroupAssetsBy
|
GroupAssetsBy
|
||||||
.values[_settingsService.getSetting(AppSettingsEnum.groupAssetsBy)],
|
.values[_settingsService.getSetting(AppSettingsEnum.groupAssetsBy)],
|
||||||
);
|
);
|
||||||
|
|
||||||
state = await AssetsState.fromAssetList(newAssetList)
|
state = await AssetsState.fromAssetList(newAssetList)
|
||||||
.withRenderDataStructure(layout);
|
.withRenderDataStructure(layout);
|
||||||
}
|
}
|
||||||
@ -112,6 +116,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
}
|
}
|
||||||
final bool newRemote = await _assetService.refreshRemoteAssets();
|
final bool newRemote = await _assetService.refreshRemoteAssets();
|
||||||
final bool newLocal = await _albumService.refreshDeviceAlbums();
|
final bool newLocal = await _albumService.refreshDeviceAlbums();
|
||||||
|
debugPrint("newRemote: $newRemote, newLocal: $newLocal");
|
||||||
log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms");
|
log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms");
|
||||||
stopwatch.reset();
|
stopwatch.reset();
|
||||||
if (!newRemote &&
|
if (!newRemote &&
|
||||||
@ -139,6 +144,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
Future<List<Asset>> _getUserAssets(int userId) => _db.assets
|
Future<List<Asset>> _getUserAssets(int userId) => _db.assets
|
||||||
.filter()
|
.filter()
|
||||||
.ownerIdEqualTo(userId)
|
.ownerIdEqualTo(userId)
|
||||||
|
.isArchivedEqualTo(false)
|
||||||
.sortByFileCreatedAtDesc()
|
.sortByFileCreatedAtDesc()
|
||||||
.findAll();
|
.findAll();
|
||||||
|
|
||||||
@ -224,13 +230,46 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final index = state.allAssets.indexWhere((a) => asset.id == a.id);
|
final index = state.allAssets.indexWhere((a) => asset.id == a.id);
|
||||||
if (index > 0) {
|
if (index != -1) {
|
||||||
state.allAssets[index] = newAsset;
|
state.allAssets[index] = newAsset;
|
||||||
_updateAssetsState(state.allAssets);
|
_updateAssetsState(state.allAssets);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newAsset.isFavorite;
|
return newAsset.isFavorite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> toggleArchive(Iterable<Asset> assets, bool status) async {
|
||||||
|
final newAssets = await Future.wait(
|
||||||
|
assets.map((a) => _assetService.changeArchiveStatus(a, status)),
|
||||||
|
);
|
||||||
|
int i = 0;
|
||||||
|
bool unArchived = false;
|
||||||
|
for (Asset oldAsset in assets) {
|
||||||
|
final newAsset = newAssets[i++];
|
||||||
|
if (newAsset == null) {
|
||||||
|
log.severe("Change archive status failed for asset ${oldAsset.id}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final index = state.allAssets.indexWhere((a) => oldAsset.id == a.id);
|
||||||
|
if (newAsset.isArchived) {
|
||||||
|
// remove from state
|
||||||
|
if (index != -1) {
|
||||||
|
state.allAssets.removeAt(index);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// add to state is difficult because the list is sorted
|
||||||
|
unArchived = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (unArchived) {
|
||||||
|
final User me = Store.get(StoreKey.currentUser);
|
||||||
|
await _stateUpdateLock.run(
|
||||||
|
() async => _updateAssetsState(await _getUserAssets(me.isarId)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_updateAssetsState(state.allAssets);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final assetProvider = StateNotifierProvider<AssetNotifier, AssetsState>((ref) {
|
final assetProvider = StateNotifierProvider<AssetNotifier, AssetsState>((ref) {
|
||||||
|
@ -121,10 +121,21 @@ class AssetService {
|
|||||||
) async {
|
) async {
|
||||||
final dto =
|
final dto =
|
||||||
await _apiService.assetApi.updateAsset(asset.remoteId!, updateAssetDto);
|
await _apiService.assetApi.updateAsset(asset.remoteId!, updateAssetDto);
|
||||||
return dto == null ? null : Asset.remote(dto);
|
if (dto != null) {
|
||||||
|
final updated = Asset.remote(dto).updateFromDb(asset);
|
||||||
|
if (updated.isInDb) {
|
||||||
|
await _db.writeTxn(() => updated.put(_db));
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Asset?> changeFavoriteStatus(Asset asset, bool isFavorite) {
|
Future<Asset?> changeFavoriteStatus(Asset asset, bool isFavorite) {
|
||||||
return updateAsset(asset, UpdateAssetDto(isFavorite: isFavorite));
|
return updateAsset(asset, UpdateAssetDto(isFavorite: isFavorite));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Asset?> changeArchiveStatus(Asset asset, bool isArchive) {
|
||||||
|
return updateAsset(asset, UpdateAssetDto(isArchived: isArchive));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
build:
|
build:
|
||||||
flutter packages pub run build_runner build
|
flutter packages pub run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
watch:
|
watch:
|
||||||
flutter packages pub run build_runner watch --delete-conflicting-outputs
|
flutter packages pub run build_runner watch --delete-conflicting-outputs
|
||||||
|
@ -24,6 +24,7 @@ void main() {
|
|||||||
fileName: '',
|
fileName: '',
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
isLocal: false,
|
isLocal: false,
|
||||||
|
isArchived: false,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ Asset _getTestAsset(int id, bool favorite) {
|
|||||||
type: AssetType.image,
|
type: AssetType.image,
|
||||||
fileName: '',
|
fileName: '',
|
||||||
isFavorite: favorite,
|
isFavorite: favorite,
|
||||||
|
isArchived: false,
|
||||||
);
|
);
|
||||||
a.id = id;
|
a.id = id;
|
||||||
return a;
|
return a;
|
||||||
|
@ -32,6 +32,7 @@ void main() {
|
|||||||
fileName: localId,
|
fileName: localId,
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
isLocal: isLocal,
|
isLocal: isLocal,
|
||||||
|
isArchived: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,7 +141,7 @@ class {{{classname}}} {
|
|||||||
{{{name}}}: json[r'{{{baseName}}}'] is List
|
{{{name}}}: json[r'{{{baseName}}}'] is List
|
||||||
? (json[r'{{{baseName}}}'] as List).map((e) =>
|
? (json[r'{{{baseName}}}'] as List).map((e) =>
|
||||||
{{#items.complexType}}
|
{{#items.complexType}}
|
||||||
{{items.complexType}}.listFromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}
|
{{items.complexType}}.listFromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{#uniqueItems}}.toSet(){{/uniqueItems}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}
|
||||||
{{/items.complexType}}
|
{{/items.complexType}}
|
||||||
{{^items.complexType}}
|
{{^items.complexType}}
|
||||||
e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}>[]{{/items.isNullable}} : (e as List).cast<{{items.items.dataType}}>()
|
e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}>[]{{/items.isNullable}} : (e as List).cast<{{items.items.dataType}}>()
|
||||||
@ -150,7 +150,7 @@ class {{{classname}}} {
|
|||||||
: {{#isNullable}}null{{/isNullable}}{{^isNullable}}const []{{/isNullable}},
|
: {{#isNullable}}null{{/isNullable}}{{^isNullable}}const []{{/isNullable}},
|
||||||
{{/items.isArray}}
|
{{/items.isArray}}
|
||||||
{{^items.isArray}}
|
{{^items.isArray}}
|
||||||
{{{name}}}: {{{complexType}}}.listFromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
|
{{{name}}}: {{{complexType}}}.listFromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{#uniqueItems}}.toSet(){{/uniqueItems}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
|
||||||
{{/items.isArray}}
|
{{/items.isArray}}
|
||||||
{{/isArray}}
|
{{/isArray}}
|
||||||
{{^isArray}}
|
{{^isArray}}
|
||||||
@ -197,7 +197,7 @@ class {{{classname}}} {
|
|||||||
{{^complexType}}
|
{{^complexType}}
|
||||||
{{#isArray}}
|
{{#isArray}}
|
||||||
{{#isEnum}}
|
{{#isEnum}}
|
||||||
{{{name}}}: {{{items.datatypeWithEnum}}}.listFromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
|
{{{name}}}: {{{items.datatypeWithEnum}}}.listFromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{#uniqueItems}}.toSet(){{/uniqueItems}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
|
||||||
{{/isEnum}}
|
{{/isEnum}}
|
||||||
{{^isEnum}}
|
{{^isEnum}}
|
||||||
{{{name}}}: json[r'{{{baseName}}}'] is {{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}
|
{{{name}}}: json[r'{{{baseName}}}'] is {{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}
|
||||||
|
Loading…
Reference in New Issue
Block a user