From d3466d013dce3884a9702de72c5c7fc3f4d8babd Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Tue, 8 Feb 2022 11:20:22 -0600 Subject: [PATCH] Implemented interaction to toggle multiselect for group and individual items --- README.md | 3 + .../home/models/home_page_state.model.dart | 23 ++-- .../providers/home_page_state.provider.dart | 27 +++- .../lib/modules/home/ui/daily_title_text.dart | 28 +++- .../lib/modules/home/ui/thumbnail_image.dart | 127 +++++++++++++----- 5 files changed, 152 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 873beb4bab..51378c86d8 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,11 @@ This project is under heavy development, there will be continous functions, feat # Features [x] Upload assets(videos/images) + [x] View assets + [x] Quick navigation with drag scroll bar + [x] Auto Backup # Development diff --git a/mobile/lib/modules/home/models/home_page_state.model.dart b/mobile/lib/modules/home/models/home_page_state.model.dart index caea70cbe5..6a094930c3 100644 --- a/mobile/lib/modules/home/models/home_page_state.model.dart +++ b/mobile/lib/modules/home/models/home_page_state.model.dart @@ -6,20 +6,23 @@ import 'package:immich_mobile/shared/models/immich_asset.model.dart'; class HomePageState { final bool isMultiSelectEnable; - final List selectedItems; - + final Set selectedItems; + final Set selectedDateGroup; HomePageState({ required this.isMultiSelectEnable, required this.selectedItems, + required this.selectedDateGroup, }); HomePageState copyWith({ bool? isMultiSelectEnable, - List? selectedItems, + Set? selectedItems, + Set? selectedDateGroup, }) { return HomePageState( isMultiSelectEnable: isMultiSelectEnable ?? this.isMultiSelectEnable, selectedItems: selectedItems ?? this.selectedItems, + selectedDateGroup: selectedDateGroup ?? this.selectedDateGroup, ); } @@ -27,13 +30,15 @@ class HomePageState { return { 'isMultiSelectEnable': isMultiSelectEnable, 'selectedItems': selectedItems.map((x) => x.toMap()).toList(), + 'selectedDateGroup': selectedDateGroup.toList(), }; } factory HomePageState.fromMap(Map map) { return HomePageState( isMultiSelectEnable: map['isMultiSelectEnable'] ?? false, - selectedItems: List.from(map['selectedItems']?.map((x) => ImmichAsset.fromMap(x))), + selectedItems: Set.from(map['selectedItems']?.map((x) => ImmichAsset.fromMap(x))), + selectedDateGroup: Set.from(map['selectedDateGroup']), ); } @@ -42,18 +47,20 @@ class HomePageState { factory HomePageState.fromJson(String source) => HomePageState.fromMap(json.decode(source)); @override - String toString() => 'HomePageState(isMultiSelectEnable: $isMultiSelectEnable, selectedItems: $selectedItems)'; + String toString() => + 'HomePageState(isMultiSelectEnable: $isMultiSelectEnable, selectedItems: $selectedItems, selectedDateGroup: $selectedDateGroup)'; @override bool operator ==(Object other) { if (identical(this, other)) return true; - final listEquals = const DeepCollectionEquality().equals; + final setEquals = const DeepCollectionEquality().equals; return other is HomePageState && other.isMultiSelectEnable == isMultiSelectEnable && - listEquals(other.selectedItems, selectedItems); + setEquals(other.selectedItems, selectedItems) && + setEquals(other.selectedDateGroup, selectedDateGroup); } @override - int get hashCode => isMultiSelectEnable.hashCode ^ selectedItems.hashCode; + int get hashCode => isMultiSelectEnable.hashCode ^ selectedItems.hashCode ^ selectedDateGroup.hashCode; } diff --git a/mobile/lib/modules/home/providers/home_page_state.provider.dart b/mobile/lib/modules/home/providers/home_page_state.provider.dart index 854001ce14..940737e9cd 100644 --- a/mobile/lib/modules/home/providers/home_page_state.provider.dart +++ b/mobile/lib/modules/home/providers/home_page_state.provider.dart @@ -7,28 +7,41 @@ class HomePageStateNotifier extends StateNotifier { : super( HomePageState( isMultiSelectEnable: false, - selectedItems: [], + selectedItems: {}, + selectedDateGroup: {}, ), ); - void enableMultiSelect(List selectedItems) { + void addSelectedDateGroup(String dateGroupTitle) { + state = state.copyWith(selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle}); + } + + void removeSelectedDateGroup(String dateGroupTitle) { + var currentDateGroup = state.selectedDateGroup; + + currentDateGroup.removeWhere((e) => e == dateGroupTitle); + + state = state.copyWith(selectedDateGroup: currentDateGroup); + } + + void enableMultiSelect(Set selectedItems) { state = state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems); } void disableMultiSelect() { - state = state.copyWith(isMultiSelectEnable: false, selectedItems: []); + state = state.copyWith(isMultiSelectEnable: false, selectedItems: {}, selectedDateGroup: {}); } void addSingleSelectedItem(ImmichAsset asset) { - state = state.copyWith(selectedItems: [...state.selectedItems, asset]); + state = state.copyWith(selectedItems: {...state.selectedItems, asset}); } void addMultipleSelectedItems(List assets) { - state = state.copyWith(selectedItems: [...state.selectedItems, ...assets]); + state = state.copyWith(selectedItems: {...state.selectedItems, ...assets}); } void removeSingleSelectedItem(ImmichAsset asset) { - List currentList = state.selectedItems; + Set currentList = state.selectedItems; currentList.removeWhere((e) => e.id == asset.id); @@ -36,7 +49,7 @@ class HomePageStateNotifier extends StateNotifier { } void removeMultipleSelectedItem(List assets) { - List currentList = state.selectedItems; + Set currentList = state.selectedItems; for (ImmichAsset asset in assets) { currentList.removeWhere((e) => e.id == asset.id); diff --git a/mobile/lib/modules/home/ui/daily_title_text.dart b/mobile/lib/modules/home/ui/daily_title_text.dart index 1bcd4c9f3f..add168c70a 100644 --- a/mobile/lib/modules/home/ui/daily_title_text.dart +++ b/mobile/lib/modules/home/ui/daily_title_text.dart @@ -21,6 +21,8 @@ class DailyTitleText extends ConsumerWidget { var formatDateTemplate = currentYear == groupYear ? 'E, MMM dd' : 'E, MMM dd, yyyy'; var dateText = DateFormat(formatDateTemplate).format(DateTime.parse(isoDate)); var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable; + var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup; + var selectedItems = ref.watch(homePageStateProvider).selectedItems; return SliverToBoxAdapter( child: Padding( @@ -38,14 +40,30 @@ class DailyTitleText extends ConsumerWidget { const Spacer(), GestureDetector( onTap: () { - if (!isMultiSelectEnable) { - ref.watch(homePageStateProvider.notifier).enableMultiSelect(assetGroup); - } else { + if (isMultiSelectEnable && + selectedDateGroup.contains(dateText) && + selectedDateGroup.length == 1 && + selectedItems.length == assetGroup.length) { ref.watch(homePageStateProvider.notifier).disableMultiSelect(); + } else if (isMultiSelectEnable && + selectedDateGroup.contains(dateText) && + selectedItems.length != assetGroup.length) { + ref.watch(homePageStateProvider.notifier).removeSelectedDateGroup(dateText); + ref.watch(homePageStateProvider.notifier).removeMultipleSelectedItem(assetGroup); + } else if (isMultiSelectEnable && + selectedDateGroup.contains(dateText) && + selectedDateGroup.length > 1) { + ref.watch(homePageStateProvider.notifier).removeSelectedDateGroup(dateText); + ref.watch(homePageStateProvider.notifier).removeMultipleSelectedItem(assetGroup); + } else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) { + ref.watch(homePageStateProvider.notifier).addSelectedDateGroup(dateText); + ref.watch(homePageStateProvider.notifier).addMultipleSelectedItems(assetGroup); + } else { + ref.watch(homePageStateProvider.notifier).enableMultiSelect(assetGroup.toSet()); + ref.watch(homePageStateProvider.notifier).addSelectedDateGroup(dateText); } - print(ref.watch(homePageStateProvider).selectedItems.length); }, - child: isMultiSelectEnable + child: isMultiSelectEnable && selectedDateGroup.contains(dateText) ? const Icon(Icons.check_circle_rounded) : const Icon(Icons.check_circle_outline_rounded), ) diff --git a/mobile/lib/modules/home/ui/thumbnail_image.dart b/mobile/lib/modules/home/ui/thumbnail_image.dart index 64857cc81e..6d35749933 100644 --- a/mobile/lib/modules/home/ui/thumbnail_image.dart +++ b/mobile/lib/modules/home/ui/thumbnail_image.dart @@ -1,66 +1,121 @@ import 'package:auto_route/auto_route.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/hive_box.dart'; +import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; import 'package:immich_mobile/shared/models/immich_asset.model.dart'; import 'package:immich_mobile/routing/router.dart'; -class ThumbnailImage extends HookWidget { +class ThumbnailImage extends HookConsumerWidget { final ImmichAsset asset; const ThumbnailImage({Key? key, required this.asset}) : super(key: key); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final cacheKey = useState(1); var box = Hive.box(userInfoBox); var thumbnailRequestUrl = '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true'; + + var selectedAsset = ref.watch(homePageStateProvider).selectedItems; + var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable; + + Widget _buildSelectionIcon(ImmichAsset asset) { + if (selectedAsset.contains(asset)) { + return Icon( + Icons.check_circle, + color: Theme.of(context).primaryColor, + ); + } else { + return const Icon( + Icons.circle_outlined, + color: Colors.white, + ); + } + } + return GestureDetector( onTap: () { - if (asset.type == 'IMAGE') { - AutoRouter.of(context).push( - ImageViewerRoute( - imageUrl: - '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false', - heroTag: asset.id, - thumbnailUrl: thumbnailRequestUrl, - ), - ); + if (isMultiSelectEnable && selectedAsset.contains(asset) && selectedAsset.length == 1) { + ref.watch(homePageStateProvider.notifier).disableMultiSelect(); + } else if (isMultiSelectEnable && selectedAsset.contains(asset) && selectedAsset.length > 1) { + ref.watch(homePageStateProvider.notifier).removeSingleSelectedItem(asset); + } else if (isMultiSelectEnable && !selectedAsset.contains(asset)) { + ref.watch(homePageStateProvider.notifier).addSingleSelectedItem(asset); } else { - debugPrint("Navigate to video player"); + if (asset.type == 'IMAGE') { + AutoRouter.of(context).push( + ImageViewerRoute( + imageUrl: + '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false', + heroTag: asset.id, + thumbnailUrl: thumbnailRequestUrl, + ), + ); + } else { + debugPrint("Navigate to video player"); - AutoRouter.of(context).push( - VideoViewerRoute( - videoUrl: '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}', - ), - ); + AutoRouter.of(context).push( + VideoViewerRoute( + videoUrl: '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}', + ), + ); + } } }, - onLongPress: () {}, + onLongPress: () { + // Enable multi selecte function + ref.watch(homePageStateProvider.notifier).enableMultiSelect({asset}); + HapticFeedback.heavyImpact(); + }, child: Hero( tag: asset.id, - child: CachedNetworkImage( - cacheKey: "${asset.id}-${cacheKey.value}", - width: 300, - height: 300, - memCacheHeight: asset.type == 'IMAGE' ? 250 : 400, - fit: BoxFit.cover, - imageUrl: thumbnailRequestUrl, - httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, - fadeInDuration: const Duration(milliseconds: 250), - progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale( - scale: 0.2, - child: CircularProgressIndicator(value: downloadProgress.progress), - ), - errorWidget: (context, url, error) { - debugPrint("Error Loading Thumbnail Widget $error"); - cacheKey.value += 1; - return const Icon(Icons.error); - }, + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + border: isMultiSelectEnable && selectedAsset.contains(asset) + ? Border.all(color: Theme.of(context).primaryColorLight, width: 10) + : const Border(), + ), + child: CachedNetworkImage( + cacheKey: "${asset.id}-${cacheKey.value}", + width: 300, + height: 300, + memCacheHeight: asset.type == 'IMAGE' ? 250 : 400, + fit: BoxFit.cover, + imageUrl: thumbnailRequestUrl, + httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, + fadeInDuration: const Duration(milliseconds: 250), + progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale( + scale: 0.2, + child: CircularProgressIndicator(value: downloadProgress.progress), + ), + errorWidget: (context, url, error) { + debugPrint("Error Loading Thumbnail Widget $error"); + cacheKey.value += 1; + return const Icon(Icons.error); + }, + ), + ), + Container( + child: isMultiSelectEnable + ? Padding( + padding: const EdgeInsets.all(3.0), + child: Align( + alignment: Alignment.topLeft, + child: _buildSelectionIcon(asset), + ), + ) + : Container(), + ), + ], ), ), );