From 8c184dc4d4898a3259c5681d484ab6465341a07b Mon Sep 17 00:00:00 2001 From: Stevenson Chittumuri Date: Wed, 3 Aug 2022 16:36:12 -0400 Subject: [PATCH] Enable swiping between assets (#381) Enable swiping between assets (#381) Co-authored-by: Alex Co-authored-by: Malte Kiefer <59220985+MalteKiefer@users.noreply.github.com> Co-authored-by: Matthias Rupp --- mobile/.gitignore | 2 +- mobile/lib/main.dart | 1 - .../album/providers/album.provider.dart | 4 +- .../album/ui/album_viewer_thumbnail.dart | 33 ++--- .../album/views/album_viewer_page.dart | 9 +- .../asset_viewer/ui/remote_photo_view.dart | 20 ++- .../asset_viewer/views/gallery_viewer.dart | 134 ++++++++++++++++++ .../asset_viewer/views/image_viewer_page.dart | 85 +++++------ .../asset_viewer/views/video_viewer_page.dart | 94 +++--------- .../backup/providers/backup.provider.dart | 4 + mobile/lib/modules/home/ui/image_grid.dart | 16 ++- .../lib/modules/home/ui/thumbnail_image.dart | 32 ++--- mobile/lib/modules/home/views/home_page.dart | 14 +- .../search/views/search_result_page.dart | 30 ++-- mobile/lib/routing/router.dart | 3 + mobile/lib/routing/router.gr.dart | 80 ++++++++++- .../openapi/lib/model/asset_response_dto.dart | Bin 8545 -> 8740 bytes 17 files changed, 372 insertions(+), 189 deletions(-) create mode 100644 mobile/lib/modules/asset_viewer/views/gallery_viewer.dart diff --git a/mobile/.gitignore b/mobile/.gitignore index 18ca645104..6c8e3cf5a8 100644 --- a/mobile/.gitignore +++ b/mobile/.gitignore @@ -24,7 +24,7 @@ # Flutter/Dart/Pub related **/doc/api/ -**/ios/Flutter/.last_build_id +**/ios/ .dart_tool/ .flutter-plugins .flutter-plugins-dependencies diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 14d153d4ba..fdc1a43b8e 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -17,7 +17,6 @@ import 'package:immich_mobile/shared/providers/server_info.provider.dart'; import 'package:immich_mobile/shared/providers/websocket.provider.dart'; import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; import 'package:immich_mobile/shared/views/version_announcement_overlay.dart'; - import 'constants/hive_box.dart'; void main() async { diff --git a/mobile/lib/modules/album/providers/album.provider.dart b/mobile/lib/modules/album/providers/album.provider.dart index c23fa4d523..f86ffa3ee5 100644 --- a/mobile/lib/modules/album/providers/album.provider.dart +++ b/mobile/lib/modules/album/providers/album.provider.dart @@ -20,7 +20,9 @@ class AlbumNotifier extends StateNotifier> { } Future createAlbum( - String albumTitle, Set assets) async { + String albumTitle, + Set assets, + ) async { AlbumResponseDto? album = await _albumService.createAlbum(albumTitle, assets, []); diff --git a/mobile/lib/modules/album/ui/album_viewer_thumbnail.dart b/mobile/lib/modules/album/ui/album_viewer_thumbnail.dart index b959418419..17686afeb2 100644 --- a/mobile/lib/modules/album/ui/album_viewer_thumbnail.dart +++ b/mobile/lib/modules/album/ui/album_viewer_thumbnail.dart @@ -12,8 +12,13 @@ import 'package:openapi/api.dart'; class AlbumViewerThumbnail extends HookConsumerWidget { final AssetResponseDto asset; + final List assetList; - const AlbumViewerThumbnail({Key? key, required this.asset}) : super(key: key); + const AlbumViewerThumbnail({ + Key? key, + required this.asset, + required this.assetList, + }) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { @@ -28,25 +33,13 @@ class AlbumViewerThumbnail extends HookConsumerWidget { ref.watch(assetSelectionProvider).isMultiselectEnable; _viewAsset() { - if (asset.type == AssetTypeEnum.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, - asset: asset, - ), - ); - } else { - AutoRouter.of(context).push( - VideoViewerRoute( - videoUrl: - '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}', - asset: asset, - ), - ); - } + AutoRouter.of(context).push( + GalleryViewerRoute( + asset: asset, + assetList: assetList, + thumbnailRequestUrl: thumbnailRequestUrl, + ), + ); } BoxBorder drawBorderColor() { diff --git a/mobile/lib/modules/album/views/album_viewer_page.dart b/mobile/lib/modules/album/views/album_viewer_page.dart index c6fd6e7278..d75fa465ca 100644 --- a/mobile/lib/modules/album/views/album_viewer_page.dart +++ b/mobile/lib/modules/album/views/album_viewer_page.dart @@ -29,9 +29,7 @@ class AlbumViewerPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { FocusNode titleFocusNode = useFocusNode(); ScrollController scrollController = useScrollController(); - - AsyncValue albumInfo = - ref.watch(sharedAlbumDetailProvider(albumId)); + var albumInfo = ref.watch(sharedAlbumDetailProvider(albumId)); final userId = ref.watch(authenticationProvider).userId; @@ -200,7 +198,10 @@ class AlbumViewerPage extends HookConsumerWidget { ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { - return AlbumViewerThumbnail(asset: albumInfo.assets[index]); + return AlbumViewerThumbnail( + asset: albumInfo.assets[index], + assetList: albumInfo.assets, + ); }, childCount: albumInfo.assets.length, ), diff --git a/mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart b/mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart index 384eae20c4..cb930bc621 100644 --- a/mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart +++ b/mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart @@ -15,7 +15,6 @@ class _RemotePhotoViewState extends State { @override Widget build(BuildContext context) { bool allowMoving = _status == _RemoteImageStatus.full; - return PhotoView( imageProvider: _imageProvider, minScale: PhotoViewComputedScale.contained, @@ -32,8 +31,9 @@ class _RemotePhotoViewState extends State { PhotoViewControllerValue controllerValue, ) { // Disable swipe events when zoomed in - if (_zoomedIn) return; - + if (_zoomedIn) { + return; + } if (controllerValue.position.dy > swipeThreshold) { widget.onSwipeDown(); } else if (controllerValue.position.dy < -swipeThreshold) { @@ -42,7 +42,14 @@ class _RemotePhotoViewState extends State { } void _scaleStateChanged(PhotoViewScaleState state) { - _zoomedIn = state == PhotoViewScaleState.zoomedIn; + // _onScaleListener; + _zoomedIn = state != PhotoViewScaleState.initial; + if (_zoomedIn) { + widget.isZoomedListener.value = true; + } else { + widget.isZoomedListener.value = false; + } + widget.isZoomedFunction(); } CachedNetworkImageProvider _authorizedImageProvider(String url) { @@ -107,6 +114,8 @@ class RemotePhotoView extends StatefulWidget { required this.thumbnailUrl, required this.imageUrl, required this.authToken, + required this.isZoomedFunction, + required this.isZoomedListener, required this.onSwipeDown, required this.onSwipeUp, }) : super(key: key); @@ -117,6 +126,9 @@ class RemotePhotoView extends StatefulWidget { final void Function() onSwipeDown; final void Function() onSwipeUp; + final void Function() isZoomedFunction; + + final ValueNotifier isZoomedListener; @override State createState() { diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart new file mode 100644 index 0000000000..7e0ecc9357 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart @@ -0,0 +1,134 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_swipe_detector/flutter_swipe_detector.dart'; +import 'package:hive/hive.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/hive_box.dart'; +import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; +import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart'; +import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart'; +import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'; +import 'package:immich_mobile/modules/home/services/asset.service.dart'; +import 'package:openapi/api.dart'; + +// ignore: must_be_immutable +class GalleryViewerPage extends HookConsumerWidget { + late List assetList; + final AssetResponseDto asset; + final String thumbnailRequestUrl; + + GalleryViewerPage({ + Key? key, + required this.assetList, + required this.asset, + required this.thumbnailRequestUrl, + }) : super(key: key); + + AssetResponseDto? assetDetail; + @override + Widget build(BuildContext context, WidgetRef ref) { + final Box box = Hive.box(userInfoBox); + + int indexOfAsset = assetList.indexOf(asset); + + @override + void initState(int index) { + indexOfAsset = index; + } + + PageController controller = + PageController(initialPage: assetList.indexOf(asset)); + + getAssetExif() async { + assetDetail = await ref + .watch(assetServiceProvider) + .getAssetById(assetList[indexOfAsset].id); + } + + void showInfo() { + showModalBottomSheet( + backgroundColor: Colors.black, + barrierColor: Colors.transparent, + isScrollControlled: false, + context: context, + builder: (context) { + return ExifBottomSheet(assetDetail: assetDetail!); + }, + ); + } + + final isZoomed = useState(false); + ValueNotifier isZoomedListener = ValueNotifier(false); + + //make isZoomed listener call instead + void isZoomedMethod() { + if (isZoomedListener.value) { + isZoomed.value = true; + } else { + isZoomed.value = false; + } + } + + return Scaffold( + backgroundColor: Colors.black, + appBar: TopControlAppBar( + asset: assetList[indexOfAsset], + onMoreInfoPressed: () { + showInfo(); + }, + onDownloadPressed: () { + ref + .watch(imageViewerStateProvider.notifier) + .downloadAsset(assetList[indexOfAsset], context); + }, + ), + body: SafeArea( + child: PageView.builder( + controller: controller, + pageSnapping: true, + physics: isZoomed.value + ? const NeverScrollableScrollPhysics() + : const BouncingScrollPhysics(), + itemCount: assetList.length, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + initState(index); + getAssetExif(); + if (assetList[index].type == AssetTypeEnum.IMAGE) { + return ImageViewerPage( + thumbnailUrl: + '${box.get(serverEndpointKey)}/asset/thumbnail/${assetList[index].id}', + imageUrl: + '${box.get(serverEndpointKey)}/asset/file?aid=${assetList[index].deviceAssetId}&did=${assetList[index].deviceId}&isThumb=false', + authToken: 'Bearer ${box.get(accessTokenKey)}', + isZoomedFunction: isZoomedMethod, + isZoomedListener: isZoomedListener, + asset: assetList[index], + heroTag: assetList[index].id, + ); + } else { + return SwipeDetector( + onSwipeDown: (_) { + AutoRouter.of(context).pop(); + }, + onSwipeUp: (_) { + showInfo(); + }, + child: Hero( + tag: assetList[index].id, + child: VideoViewerPage( + asset: assetList[index], + videoUrl: + '${box.get(serverEndpointKey)}/asset/file?aid=${assetList[index].deviceAssetId}&did=${assetList[index].deviceId}', + ), + ), + ); + } + }, + ), + ), + ); + } +} diff --git a/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart b/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart index d88b8a9dcf..f1130cdf6c 100644 --- a/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart +++ b/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart @@ -1,15 +1,12 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hive/hive.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart'; -import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart'; import 'package:immich_mobile/modules/home/services/asset.service.dart'; import 'package:openapi/api.dart'; @@ -19,8 +16,9 @@ class ImageViewerPage extends HookConsumerWidget { final String heroTag; final String thumbnailUrl; final AssetResponseDto asset; - - AssetResponseDto? assetDetail; + final String authToken; + final ValueNotifier isZoomedListener; + final void Function() isZoomedFunction; ImageViewerPage({ Key? key, @@ -28,31 +26,22 @@ class ImageViewerPage extends HookConsumerWidget { required this.heroTag, required this.thumbnailUrl, required this.asset, + required this.authToken, + required this.isZoomedFunction, + required this.isZoomedListener, }) : super(key: key); + AssetResponseDto? assetDetail; @override Widget build(BuildContext context, WidgetRef ref) { final downloadAssetStatus = ref.watch(imageViewerStateProvider).downloadAssetStatus; - var box = Hive.box(userInfoBox); getAssetExif() async { assetDetail = await ref.watch(assetServiceProvider).getAssetById(asset.id); } - showInfo() { - showModalBottomSheet( - backgroundColor: Colors.black, - barrierColor: Colors.transparent, - isScrollControlled: false, - context: context, - builder: (context) { - return ExifBottomSheet(assetDetail: assetDetail!); - }, - ); - } - useEffect( () { getAssetExif(); @@ -61,39 +50,39 @@ class ImageViewerPage extends HookConsumerWidget { [], ); - return Scaffold( - backgroundColor: Colors.black, - appBar: TopControlAppBar( - asset: asset, - onMoreInfoPressed: showInfo, - onDownloadPressed: () { - ref - .watch(imageViewerStateProvider.notifier) - .downloadAsset(asset, context); + showInfo() { + showModalBottomSheet( + backgroundColor: Colors.black, + barrierColor: Colors.transparent, + isScrollControlled: false, + context: context, + builder: (context) { + return ExifBottomSheet(assetDetail: assetDetail ?? asset); }, - ), - body: SafeArea( - child: Stack( - children: [ - Center( - child: Hero( - tag: heroTag, - child: RemotePhotoView( - thumbnailUrl: thumbnailUrl, - imageUrl: imageUrl, - authToken: "Bearer ${box.get(accessTokenKey)}", - onSwipeDown: () => AutoRouter.of(context).pop(), - onSwipeUp: () => showInfo(), - ), - ), + ); + } + + return Stack( + children: [ + Center( + child: Hero( + tag: heroTag, + child: RemotePhotoView( + thumbnailUrl: thumbnailUrl, + imageUrl: imageUrl, + authToken: authToken, + isZoomedFunction: isZoomedFunction, + isZoomedListener: isZoomedListener, + onSwipeDown: () => AutoRouter.of(context).pop(), + onSwipeUp: () => showInfo(), ), - if (downloadAssetStatus == DownloadAssetStatus.loading) - const Center( - child: DownloadLoadingIndicator(), - ), - ], + ), ), - ), + if (downloadAssetStatus == DownloadAssetStatus.loading) + const Center( + child: DownloadLoadingIndicator(), + ), + ], ); } } diff --git a/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart b/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart index 4a2e4908ee..63d7af795a 100644 --- a/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart +++ b/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart @@ -1,7 +1,4 @@ -import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_swipe_detector/flutter_swipe_detector.dart'; import 'package:hive/hive.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/hive_box.dart'; @@ -9,9 +6,6 @@ import 'package:chewie/chewie.dart'; import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart'; -import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; -import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart'; -import 'package:immich_mobile/modules/home/services/asset.service.dart'; import 'package:openapi/api.dart'; import 'package:video_player/video_player.dart'; @@ -31,66 +25,17 @@ class VideoViewerPage extends HookConsumerWidget { String jwtToken = Hive.box(userInfoBox).get(accessTokenKey); - void showInfo() { - showModalBottomSheet( - backgroundColor: Colors.black, - barrierColor: Colors.transparent, - isScrollControlled: false, - context: context, - builder: (context) { - return ExifBottomSheet(assetDetail: assetDetail!); - }, - ); - } - - getAssetExif() async { - assetDetail = - await ref.watch(assetServiceProvider).getAssetById(asset.id); - } - - useEffect( - () { - getAssetExif(); - return null; - }, - [], - ); - - return Scaffold( - backgroundColor: Colors.black, - appBar: TopControlAppBar( - asset: asset, - onMoreInfoPressed: () { - showInfo(); - }, - onDownloadPressed: () { - ref - .watch(imageViewerStateProvider.notifier) - .downloadAsset(asset, context); - }, - ), - body: SwipeDetector( - onSwipeDown: (_) { - AutoRouter.of(context).pop(); - }, - onSwipeUp: (_) { - showInfo(); - }, - child: SafeArea( - child: Stack( - children: [ - VideoThumbnailPlayer( - url: videoUrl, - jwtToken: jwtToken, - ), - if (downloadAssetStatus == DownloadAssetStatus.loading) - const Center( - child: DownloadLoadingIndicator(), - ), - ], - ), + return Stack( + children: [ + VideoThumbnailPlayer( + url: videoUrl, + jwtToken: jwtToken, ), - ), + if (downloadAssetStatus == DownloadAssetStatus.loading) + const Center( + child: DownloadLoadingIndicator(), + ), + ], ); } } @@ -134,10 +79,13 @@ class _VideoThumbnailPlayerState extends State { _createChewieController() { chewieController = ChewieController( showOptions: true, - showControlsOnInitialize: false, + showControlsOnInitialize: true, videoPlayerController: videoPlayerController, autoPlay: true, - autoInitialize: false, + autoInitialize: true, + allowFullScreen: true, + showControls: true, + hideControlsTimer: const Duration(seconds: 5), ); } @@ -157,11 +105,13 @@ class _VideoThumbnailPlayerState extends State { controller: chewieController!, ), ) - : const SizedBox( - width: 75, - height: 75, - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, + : const Center( + child: SizedBox( + width: 75, + height: 75, + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), ), ); } diff --git a/mobile/lib/modules/backup/providers/backup.provider.dart b/mobile/lib/modules/backup/providers/backup.provider.dart index e71a8dbfd8..d6a71bf121 100644 --- a/mobile/lib/modules/backup/providers/backup.provider.dart +++ b/mobile/lib/modules/backup/providers/backup.provider.dart @@ -162,6 +162,10 @@ class BackupNotifier extends StateNotifier { onlyAll: true, type: RequestType.common, ); + + if (list.isEmpty) { + return; + } AssetPathEntity albumHasAllAssets = list.first; backupAlbumInfoBox.put( diff --git a/mobile/lib/modules/home/ui/image_grid.dart b/mobile/lib/modules/home/ui/image_grid.dart index 1a7f1b2d71..e3c31995a4 100644 --- a/mobile/lib/modules/home/ui/image_grid.dart +++ b/mobile/lib/modules/home/ui/image_grid.dart @@ -3,10 +3,18 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart'; import 'package:openapi/api.dart'; +// ignore: must_be_immutable class ImageGrid extends ConsumerWidget { final List assetGroup; + final List sortedAssetGroup; - const ImageGrid({Key? key, required this.assetGroup}) : super(key: key); + ImageGrid({ + Key? key, + required this.assetGroup, + required this.sortedAssetGroup, + }) : super(key: key); + + List imageSortedList = []; @override Widget build(BuildContext context, WidgetRef ref) { @@ -19,12 +27,14 @@ class ImageGrid extends ConsumerWidget { delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { var assetType = assetGroup[index].type; - return GestureDetector( onTap: () {}, child: Stack( children: [ - ThumbnailImage(asset: assetGroup[index]), + ThumbnailImage( + asset: assetGroup[index], + assetList: sortedAssetGroup, + ), if (assetType != AssetTypeEnum.IMAGE) Positioned( top: 5, diff --git a/mobile/lib/modules/home/ui/thumbnail_image.dart b/mobile/lib/modules/home/ui/thumbnail_image.dart index f86e76505a..c2b07c7621 100644 --- a/mobile/lib/modules/home/ui/thumbnail_image.dart +++ b/mobile/lib/modules/home/ui/thumbnail_image.dart @@ -13,8 +13,10 @@ import 'package:openapi/api.dart'; class ThumbnailImage extends HookConsumerWidget { final AssetResponseDto asset; + final List assetList; - const ThumbnailImage({Key? key, required this.asset}) : super(key: key); + const ThumbnailImage({Key? key, required this.asset, required this.assetList}) + : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { @@ -60,29 +62,17 @@ class ThumbnailImage extends HookConsumerWidget { .watch(homePageStateProvider.notifier) .addSingleSelectedItem(asset); } else { - if (asset.type == AssetTypeEnum.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, - asset: asset, - ), - ); - } else { - AutoRouter.of(context).push( - VideoViewerRoute( - videoUrl: - '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}', - asset: asset, - ), - ); - } + AutoRouter.of(context).push( + GalleryViewerRoute( + assetList: assetList, + thumbnailRequestUrl: thumbnailRequestUrl, + asset: asset, + ), + ); } }, onLongPress: () { - // Enable multi selecte function + // Enable multi select function ref.watch(homePageStateProvider.notifier).enableMultiSelect({asset}); HapticFeedback.heavyImpact(); }, diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index 66a4834b75..e63acf1abd 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -10,9 +10,11 @@ import 'package:immich_mobile/modules/home/ui/image_grid.dart'; import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart'; import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart'; import 'package:immich_mobile/modules/home/ui/profile_drawer.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/websocket.provider.dart'; +import 'package:openapi/api.dart'; class HomePage extends HookConsumerWidget { const HomePage({Key? key}) : super(key: key); @@ -25,6 +27,13 @@ class HomePage extends HookConsumerWidget { var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable; var homePageState = ref.watch(homePageStateProvider); + List sortedAssetList = []; + // set sorted List + for (var group in assetGroupByDateTime.values) { + for (var value in group) { + sortedAssetList.add(value); + } + } useEffect( () { @@ -73,7 +82,10 @@ class HomePage extends HookConsumerWidget { ); imageGridGroup.add( - ImageGrid(assetGroup: immichAssetList), + ImageGrid( + assetGroup: immichAssetList, + sortedAssetGroup: sortedAssetList, + ), ); lastMonth = currentMonth; diff --git a/mobile/lib/modules/search/views/search_result_page.dart b/mobile/lib/modules/search/views/search_result_page.dart index ffcd7c80b5..fa81613b57 100644 --- a/mobile/lib/modules/search/views/search_result_page.dart +++ b/mobile/lib/modules/search/views/search_result_page.dart @@ -11,6 +11,7 @@ import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart'; import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; import 'package:immich_mobile/modules/search/providers/search_result_page.provider.dart'; import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart'; +import 'package:openapi/api.dart'; class SearchResultPage extends HookConsumerWidget { const SearchResultPage({Key? key, required this.searchTerm}) @@ -27,7 +28,9 @@ class SearchResultPage extends HookConsumerWidget { final List imageGridGroup = []; - late FocusNode searchFocusNode; + FocusNode? searchFocusNode; + + List sortedAssetList = []; useEffect( () { @@ -37,14 +40,14 @@ class SearchResultPage extends HookConsumerWidget { Duration.zero, () => ref.read(searchResultPageProvider.notifier).search(searchTerm), ); - return () => searchFocusNode.dispose(); + return () => searchFocusNode?.dispose(); }, [], ); _onSearchSubmitted(String newSearchTerm) { debugPrint("Re-Search with $newSearchTerm"); - searchFocusNode.unfocus(); + searchFocusNode?.unfocus(); isNewSearch.value = false; currentSearchTerm.value = newSearchTerm; ref.watch(searchResultPageProvider.notifier).search(newSearchTerm); @@ -58,7 +61,7 @@ class SearchResultPage extends HookConsumerWidget { onTap: () { searchTermController.clear(); ref.watch(searchPageStateProvider.notifier).setSearchTerm(""); - searchFocusNode.requestFocus(); + searchFocusNode?.requestFocus(); }, textInputAction: TextInputAction.search, onSubmitted: (searchTerm) { @@ -131,7 +134,12 @@ class SearchResultPage extends HookConsumerWidget { if (searchResultPageState.isSuccess) { if (searchResultPageState.searchResult.isNotEmpty) { int? lastMonth; - + // set sorted List + for (var group in assetGroupByDateTime.values) { + for (var value in group) { + sortedAssetList.add(value); + } + } assetGroupByDateTime.forEach((dateGroup, immichAssetList) { DateTime parseDateGroup = DateTime.parse(dateGroup); int currentMonth = parseDateGroup.month; @@ -154,7 +162,10 @@ class SearchResultPage extends HookConsumerWidget { ); imageGridGroup.add( - ImageGrid(assetGroup: immichAssetList), + ImageGrid( + assetGroup: immichAssetList, + sortedAssetGroup: sortedAssetList, + ), ); lastMonth = currentMonth; @@ -193,7 +204,7 @@ class SearchResultPage extends HookConsumerWidget { title: GestureDetector( onTap: () { isNewSearch.value = true; - searchFocusNode.requestFocus(); + searchFocusNode?.requestFocus(); }, child: isNewSearch.value ? _buildTextField() : _buildChip(), ), @@ -201,7 +212,10 @@ class SearchResultPage extends HookConsumerWidget { ), body: GestureDetector( onTap: () { - searchFocusNode.unfocus(); + if (searchFocusNode != null) { + searchFocusNode?.unfocus(); + } + ref.watch(searchPageStateProvider.notifier).disableSearch(); }, child: Stack( diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 7680f2355e..4732e4d87d 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/album/views/library_page.dart'; +import 'package:immich_mobile/modules/asset_viewer/views/gallery_viewer.dart'; import 'package:immich_mobile/modules/backup/views/album_preview_page.dart'; import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart'; import 'package:immich_mobile/modules/backup/views/failed_backup_status_page.dart'; @@ -47,6 +48,7 @@ part 'router.gr.dart'; ], transitionsBuilder: TransitionsBuilders.fadeIn, ), + AutoRoute(page: GalleryViewerPage, guards: [AuthGuard]), AutoRoute(page: ImageViewerPage, guards: [AuthGuard]), AutoRoute(page: VideoViewerPage, guards: [AuthGuard]), AutoRoute(page: BackupControllerPage, guards: [AuthGuard]), @@ -78,6 +80,7 @@ part 'router.gr.dart'; ], ) class AppRouter extends _$AppRouter { + // ignore: unused_field final ApiService _apiService; AppRouter(this._apiService) : super(authGuard: AuthGuard(_apiService)); diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 552a7fa773..a367930af9 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -41,6 +41,16 @@ class _$AppRouter extends RootStackRouter { opaque: true, barrierDismissible: false); }, + GalleryViewerRoute.name: (routeData) { + final args = routeData.argsAs(); + return MaterialPageX( + routeData: routeData, + child: GalleryViewerPage( + key: args.key, + assetList: args.assetList, + asset: args.asset, + thumbnailRequestUrl: args.thumbnailRequestUrl)); + }, ImageViewerRoute.name: (routeData) { final args = routeData.argsAs(); return MaterialPageX( @@ -50,7 +60,10 @@ class _$AppRouter extends RootStackRouter { imageUrl: args.imageUrl, heroTag: args.heroTag, thumbnailUrl: args.thumbnailUrl, - asset: args.asset)); + asset: args.asset, + authToken: args.authToken, + isZoomedFunction: args.isZoomedFunction, + isZoomedListener: args.isZoomedListener)); }, VideoViewerRoute.name: (routeData) { final args = routeData.argsAs(); @@ -174,6 +187,8 @@ class _$AppRouter extends RootStackRouter { parent: TabControllerRoute.name, guards: [authGuard]) ]), + RouteConfig(GalleryViewerRoute.name, + path: '/gallery-viewer-page', guards: [authGuard]), RouteConfig(ImageViewerRoute.name, path: '/image-viewer-page', guards: [authGuard]), RouteConfig(VideoViewerRoute.name, @@ -237,6 +252,46 @@ class TabControllerRoute extends PageRouteInfo { static const String name = 'TabControllerRoute'; } +/// generated route for +/// [GalleryViewerPage] +class GalleryViewerRoute extends PageRouteInfo { + GalleryViewerRoute( + {Key? key, + required List assetList, + required AssetResponseDto asset, + required String thumbnailRequestUrl}) + : super(GalleryViewerRoute.name, + path: '/gallery-viewer-page', + args: GalleryViewerRouteArgs( + key: key, + assetList: assetList, + asset: asset, + thumbnailRequestUrl: thumbnailRequestUrl)); + + static const String name = 'GalleryViewerRoute'; +} + +class GalleryViewerRouteArgs { + const GalleryViewerRouteArgs( + {this.key, + required this.assetList, + required this.asset, + required this.thumbnailRequestUrl}); + + final Key? key; + + final List assetList; + + final AssetResponseDto asset; + + final String thumbnailRequestUrl; + + @override + String toString() { + return 'GalleryViewerRouteArgs{key: $key, assetList: $assetList, asset: $asset, thumbnailRequestUrl: $thumbnailRequestUrl}'; + } +} + /// generated route for /// [ImageViewerPage] class ImageViewerRoute extends PageRouteInfo { @@ -245,7 +300,10 @@ class ImageViewerRoute extends PageRouteInfo { required String imageUrl, required String heroTag, required String thumbnailUrl, - required AssetResponseDto asset}) + required AssetResponseDto asset, + required String authToken, + required void Function() isZoomedFunction, + required ValueNotifier isZoomedListener}) : super(ImageViewerRoute.name, path: '/image-viewer-page', args: ImageViewerRouteArgs( @@ -253,7 +311,10 @@ class ImageViewerRoute extends PageRouteInfo { imageUrl: imageUrl, heroTag: heroTag, thumbnailUrl: thumbnailUrl, - asset: asset)); + asset: asset, + authToken: authToken, + isZoomedFunction: isZoomedFunction, + isZoomedListener: isZoomedListener)); static const String name = 'ImageViewerRoute'; } @@ -264,7 +325,10 @@ class ImageViewerRouteArgs { required this.imageUrl, required this.heroTag, required this.thumbnailUrl, - required this.asset}); + required this.asset, + required this.authToken, + required this.isZoomedFunction, + required this.isZoomedListener}); final Key? key; @@ -276,9 +340,15 @@ class ImageViewerRouteArgs { final AssetResponseDto asset; + final String authToken; + + final void Function() isZoomedFunction; + + final ValueNotifier isZoomedListener; + @override String toString() { - return 'ImageViewerRouteArgs{key: $key, imageUrl: $imageUrl, heroTag: $heroTag, thumbnailUrl: $thumbnailUrl, asset: $asset}'; + return 'ImageViewerRouteArgs{key: $key, imageUrl: $imageUrl, heroTag: $heroTag, thumbnailUrl: $thumbnailUrl, asset: $asset, authToken: $authToken, isZoomedFunction: $isZoomedFunction, isZoomedListener: $isZoomedListener}'; } } diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index cd1e83c5f2dcbc5a712ab7507dcadc4ed115884e..9d0151b91a6afc1958d16c9276ffd789319abdd6 100644 GIT binary patch delta 478 zcmaFpw8Uk@9CkJ?1rV4#hg}NB+{Ugv`3k$jtHqxlJbT=a!j#liP$DsCe=QPLat%TGW^Pu9r;&Ge$B5sSx7*AvbDgR%{K*VSok1801gLGQ0x@dm|QDrgC@Lr zr|2UVBe2qx%Dlwf%w&bE;`}@vg=(-^Qht7pLV8htd16vds)DUTT4GLdst!lU<3e>M~E^2 delta 408 zcmZvYK`R7t7{*)F%>HMXMK+9hw~3j}SheJUEnzt-7lbr+hW%^pXm-#>E|d@OILd__ za8Pq_RIaXaM!EP1C}tg$a`@HL`@TI-&wDohJbHaBv=Xgr-hh-u7<3G9g)&c$v#iaM&IoiL+1WYfLO!E? z=w})hSQe*DZ!rAwhLg%$rTmy{J4xzhcw)_XXB|+b7QwKcEuM3bkQy;9wZoGJa3QIJ zvF_S@h2vUk!mHGVYB?P@>`l+|xm)~eHzg0?P#(p-oW_?t9n93;MpUAuwUS$$_w-7> zzDOCWh#^LV9!153(u6o