diff --git a/mobile/.gitignore b/mobile/.gitignore index 18ca64510..6c8e3cf5a 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 14d153d4b..fdc1a43b8 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 c23fa4d52..f86ffa3ee 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 b95941841..17686afeb 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 c6fd6e727..d75fa465c 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 384eae20c..cb930bc62 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 000000000..7e0ecc935 --- /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 d88b8a9dc..f1130cdf6 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 4a2e4908e..63d7af795 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 e71a8dbfd..d6a71bf12 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 1a7f1b2d7..e3c31995a 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 f86e76505..c2b07c762 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 66a4834b7..e63acf1ab 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 ffcd7c80b..fa81613b5 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 7680f2355..4732e4d87 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 552a7fa77..a367930af 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 cd1e83c5f..9d0151b91 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -76,69 +76,72 @@ class AssetResponseDto { SmartInfoResponseDto? smartInfo; @override - bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto && - other.type == type && - other.id == id && - other.deviceAssetId == deviceAssetId && - other.ownerId == ownerId && - other.deviceId == deviceId && - other.originalPath == originalPath && - other.resizePath == resizePath && - other.createdAt == createdAt && - other.modifiedAt == modifiedAt && - other.isFavorite == isFavorite && - other.mimeType == mimeType && - other.duration == duration && - other.webpPath == webpPath && - other.encodedVideoPath == encodedVideoPath && - other.exifInfo == exifInfo && - other.smartInfo == smartInfo; + bool operator ==(Object other) => + identical(this, other) || + other is AssetResponseDto && + other.type == type && + other.id == id && + other.deviceAssetId == deviceAssetId && + other.ownerId == ownerId && + other.deviceId == deviceId && + other.originalPath == originalPath && + other.resizePath == resizePath && + other.createdAt == createdAt && + other.modifiedAt == modifiedAt && + other.isFavorite == isFavorite && + other.mimeType == mimeType && + other.duration == duration && + other.webpPath == webpPath && + other.encodedVideoPath == encodedVideoPath && + other.exifInfo == exifInfo && + other.smartInfo == smartInfo; @override int get hashCode => - // ignore: unnecessary_parenthesis - (type.hashCode) + - (id.hashCode) + - (deviceAssetId.hashCode) + - (ownerId.hashCode) + - (deviceId.hashCode) + - (originalPath.hashCode) + - (resizePath == null ? 0 : resizePath!.hashCode) + - (createdAt.hashCode) + - (modifiedAt.hashCode) + - (isFavorite.hashCode) + - (mimeType == null ? 0 : mimeType!.hashCode) + - (duration.hashCode) + - (webpPath == null ? 0 : webpPath!.hashCode) + - (encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) + - (exifInfo == null ? 0 : exifInfo!.hashCode) + - (smartInfo == null ? 0 : smartInfo!.hashCode); + // ignore: unnecessary_parenthesis + (type.hashCode) + + (id.hashCode) + + (deviceAssetId.hashCode) + + (ownerId.hashCode) + + (deviceId.hashCode) + + (originalPath.hashCode) + + (resizePath == null ? 0 : resizePath!.hashCode) + + (createdAt.hashCode) + + (modifiedAt.hashCode) + + (isFavorite.hashCode) + + (mimeType == null ? 0 : mimeType!.hashCode) + + (duration.hashCode) + + (webpPath == null ? 0 : webpPath!.hashCode) + + (encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) + + (exifInfo == null ? 0 : exifInfo!.hashCode) + + (smartInfo == null ? 0 : smartInfo!.hashCode); @override - String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo]'; + String toString() => + 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo]'; Map toJson() { final _json = {}; - _json[r'type'] = type; - _json[r'id'] = id; - _json[r'deviceAssetId'] = deviceAssetId; - _json[r'ownerId'] = ownerId; - _json[r'deviceId'] = deviceId; - _json[r'originalPath'] = originalPath; + _json[r'type'] = type; + _json[r'id'] = id; + _json[r'deviceAssetId'] = deviceAssetId; + _json[r'ownerId'] = ownerId; + _json[r'deviceId'] = deviceId; + _json[r'originalPath'] = originalPath; if (resizePath != null) { _json[r'resizePath'] = resizePath; } else { _json[r'resizePath'] = null; } - _json[r'createdAt'] = createdAt; - _json[r'modifiedAt'] = modifiedAt; - _json[r'isFavorite'] = isFavorite; + _json[r'createdAt'] = createdAt; + _json[r'modifiedAt'] = modifiedAt; + _json[r'isFavorite'] = isFavorite; if (mimeType != null) { _json[r'mimeType'] = mimeType; } else { _json[r'mimeType'] = null; } - _json[r'duration'] = duration; + _json[r'duration'] = duration; if (webpPath != null) { _json[r'webpPath'] = webpPath; } else { @@ -174,8 +177,10 @@ class AssetResponseDto { // Note 2: this code is stripped in release mode! assert(() { requiredKeys.forEach((key) { - assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.'); - assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.'); + assert(json.containsKey(key), + 'Required key "AssetResponseDto[$key]" is missing from JSON.'); + assert(json[key] != null, + 'Required key "AssetResponseDto[$key]" has a null value in JSON.'); }); return true; }()); @@ -202,7 +207,10 @@ class AssetResponseDto { return null; } - static List? listFromJson(dynamic json, {bool growable = false,}) { + static List? listFromJson( + dynamic json, { + bool growable = false, + }) { final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { @@ -230,12 +238,18 @@ class AssetResponseDto { } // maps a json object with a list of AssetResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + static Map> mapListFromJson( + dynamic json, { + bool growable = false, + }) { final map = >{}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = AssetResponseDto.listFromJson(entry.value, growable: growable,); + final value = AssetResponseDto.listFromJson( + entry.value, + growable: growable, + ); if (value != null) { map[entry.key] = value; } @@ -262,4 +276,3 @@ class AssetResponseDto { 'encodedVideoPath', }; } -