mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	Enable swiping between assets (#381)
Enable swiping between assets (#381) Co-authored-by: Alex <alex.tran1502@gmail.com> Co-authored-by: Malte Kiefer <59220985+MalteKiefer@users.noreply.github.com> Co-authored-by: Matthias Rupp <matthias.rupp@posteo.de>
This commit is contained in:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							e8d1f89a47
						
					
				
				
					commit
					8c184dc4d4
				
			@@ -20,7 +20,9 @@ class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<AlbumResponseDto?> createAlbum(
 | 
			
		||||
      String albumTitle, Set<AssetResponseDto> assets) async {
 | 
			
		||||
    String albumTitle,
 | 
			
		||||
    Set<AssetResponseDto> assets,
 | 
			
		||||
  ) async {
 | 
			
		||||
    AlbumResponseDto? album =
 | 
			
		||||
        await _albumService.createAlbum(albumTitle, assets, []);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,13 @@ import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
class AlbumViewerThumbnail extends HookConsumerWidget {
 | 
			
		||||
  final AssetResponseDto asset;
 | 
			
		||||
  final List<AssetResponseDto> 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() {
 | 
			
		||||
 
 | 
			
		||||
@@ -29,9 +29,7 @@ class AlbumViewerPage extends HookConsumerWidget {
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    FocusNode titleFocusNode = useFocusNode();
 | 
			
		||||
    ScrollController scrollController = useScrollController();
 | 
			
		||||
 | 
			
		||||
    AsyncValue<AlbumResponseDto?> 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,
 | 
			
		||||
            ),
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,6 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
 | 
			
		||||
  @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<RemotePhotoView> {
 | 
			
		||||
    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<RemotePhotoView> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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<bool> isZoomedListener;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<StatefulWidget> createState() {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										134
									
								
								mobile/lib/modules/asset_viewer/views/gallery_viewer.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								mobile/lib/modules/asset_viewer/views/gallery_viewer.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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<AssetResponseDto> 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<dynamic> 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<bool>(false);
 | 
			
		||||
    ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(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}',
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              );
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -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<bool> 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(),
 | 
			
		||||
          ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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<VideoThumbnailPlayer> {
 | 
			
		||||
  _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<VideoThumbnailPlayer> {
 | 
			
		||||
              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,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -162,6 +162,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
			
		||||
        onlyAll: true,
 | 
			
		||||
        type: RequestType.common,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (list.isEmpty) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      AssetPathEntity albumHasAllAssets = list.first;
 | 
			
		||||
 | 
			
		||||
      backupAlbumInfoBox.put(
 | 
			
		||||
 
 | 
			
		||||
@@ -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<AssetResponseDto> assetGroup;
 | 
			
		||||
  final List<AssetResponseDto> sortedAssetGroup;
 | 
			
		||||
 | 
			
		||||
  const ImageGrid({Key? key, required this.assetGroup}) : super(key: key);
 | 
			
		||||
  ImageGrid({
 | 
			
		||||
    Key? key,
 | 
			
		||||
    required this.assetGroup,
 | 
			
		||||
    required this.sortedAssetGroup,
 | 
			
		||||
  }) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  List<AssetResponseDto> 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,
 | 
			
		||||
 
 | 
			
		||||
@@ -13,8 +13,10 @@ import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
class ThumbnailImage extends HookConsumerWidget {
 | 
			
		||||
  final AssetResponseDto asset;
 | 
			
		||||
  final List<AssetResponseDto> 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();
 | 
			
		||||
      },
 | 
			
		||||
 
 | 
			
		||||
@@ -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<AssetResponseDto> 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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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<Widget> imageGridGroup = [];
 | 
			
		||||
 | 
			
		||||
    late FocusNode searchFocusNode;
 | 
			
		||||
    FocusNode? searchFocusNode;
 | 
			
		||||
 | 
			
		||||
    List<AssetResponseDto> 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(
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user