2023-02-01 18:59:34 +02:00
|
|
|
import 'dart:io';
|
|
|
|
|
2022-08-03 22:36:12 +02:00
|
|
|
import 'package:auto_route/auto_route.dart';
|
2023-02-01 18:59:34 +02:00
|
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
2022-08-03 22:36:12 +02:00
|
|
|
import 'package:flutter/material.dart';
|
2022-10-14 18:26:10 +02:00
|
|
|
import 'package:flutter/services.dart';
|
2022-08-03 22:36:12 +02:00
|
|
|
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';
|
2023-01-27 23:05:08 +02:00
|
|
|
import 'package:immich_mobile/modules/album/ui/add_to_album_bottom_sheet.dart';
|
2022-08-03 22:36:12 +02:00
|
|
|
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/video_viewer_page.dart';
|
2023-02-05 05:25:11 +02:00
|
|
|
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
|
2023-02-04 22:42:42 +02:00
|
|
|
import 'package:immich_mobile/shared/services/asset.service.dart';
|
2022-12-22 22:10:21 +02:00
|
|
|
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
|
2022-08-16 01:53:30 +02:00
|
|
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
|
|
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
2023-02-01 18:59:34 +02:00
|
|
|
import 'package:immich_mobile/shared/ui/photo_view/photo_view_gallery.dart';
|
|
|
|
import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_computed_scale.dart';
|
|
|
|
import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_scale_state.dart';
|
|
|
|
import 'package:immich_mobile/shared/ui/photo_view/src/utils/photo_view_hero_attributes.dart';
|
2022-11-08 19:00:24 +02:00
|
|
|
import 'package:immich_mobile/shared/models/asset.dart';
|
2022-12-22 22:10:21 +02:00
|
|
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
2023-02-01 18:59:34 +02:00
|
|
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
|
|
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
|
|
|
import 'package:photo_manager/photo_manager.dart';
|
|
|
|
import 'package:openapi/api.dart' as api;
|
2022-08-03 22:36:12 +02:00
|
|
|
|
|
|
|
// ignore: must_be_immutable
|
|
|
|
class GalleryViewerPage extends HookConsumerWidget {
|
2022-11-08 19:00:24 +02:00
|
|
|
late List<Asset> assetList;
|
|
|
|
final Asset asset;
|
2022-08-08 02:43:09 +02:00
|
|
|
|
2022-08-03 22:36:12 +02:00
|
|
|
GalleryViewerPage({
|
|
|
|
Key? key,
|
|
|
|
required this.assetList,
|
|
|
|
required this.asset,
|
|
|
|
}) : super(key: key);
|
|
|
|
|
2022-11-08 19:00:24 +02:00
|
|
|
Asset? assetDetail;
|
2022-08-13 22:51:09 +02:00
|
|
|
|
2022-08-03 22:36:12 +02:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
|
|
final Box<dynamic> box = Hive.box(userInfoBox);
|
2022-12-02 22:55:10 +02:00
|
|
|
final settings = ref.watch(appSettingsServiceProvider);
|
|
|
|
final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue);
|
|
|
|
final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
|
2022-08-13 22:51:09 +02:00
|
|
|
final isZoomed = useState<bool>(false);
|
2023-02-03 18:26:05 +02:00
|
|
|
final showAppBar = useState<bool>(true);
|
2022-11-08 19:00:24 +02:00
|
|
|
final indexOfAsset = useState(assetList.indexOf(asset));
|
2022-11-19 07:12:54 +02:00
|
|
|
final isPlayingMotionVideo = useState(false);
|
2023-02-01 18:59:34 +02:00
|
|
|
late Offset localPosition;
|
|
|
|
final authToken = 'Bearer ${box.get(accessTokenKey)}';
|
2022-08-13 22:51:09 +02:00
|
|
|
|
|
|
|
PageController controller =
|
|
|
|
PageController(initialPage: assetList.indexOf(asset));
|
|
|
|
|
|
|
|
useEffect(
|
|
|
|
() {
|
2022-12-02 22:55:10 +02:00
|
|
|
isLoadPreview.value =
|
|
|
|
settings.getSetting<bool>(AppSettingsEnum.loadPreview);
|
|
|
|
isLoadOriginal.value =
|
|
|
|
settings.getSetting<bool>(AppSettingsEnum.loadOriginal);
|
2022-11-19 07:12:54 +02:00
|
|
|
isPlayingMotionVideo.value = false;
|
2022-08-13 22:51:09 +02:00
|
|
|
return null;
|
|
|
|
},
|
|
|
|
[],
|
|
|
|
);
|
2022-08-03 22:36:12 +02:00
|
|
|
|
2023-02-05 05:25:11 +02:00
|
|
|
void toggleFavorite(Asset asset) {
|
|
|
|
ref.watch(favoriteProvider.notifier).toggleFavorite(asset);
|
|
|
|
}
|
|
|
|
|
|
|
|
getAssetExif() async {
|
2022-11-08 19:00:24 +02:00
|
|
|
if (assetList[indexOfAsset.value].isRemote) {
|
|
|
|
assetDetail = await ref
|
|
|
|
.watch(assetServiceProvider)
|
|
|
|
.getAssetById(assetList[indexOfAsset.value].id);
|
|
|
|
} else {
|
|
|
|
// TODO local exif parsing?
|
|
|
|
assetDetail = assetList[indexOfAsset.value];
|
|
|
|
}
|
2022-08-03 22:36:12 +02:00
|
|
|
}
|
|
|
|
|
2023-02-04 22:42:42 +02:00
|
|
|
/// Thumbnail image of a remote asset. Required asset.isRemote
|
|
|
|
ImageProvider remoteThumbnailImageProvider(
|
|
|
|
Asset asset,
|
|
|
|
api.ThumbnailFormat type,
|
|
|
|
) {
|
2023-02-01 18:59:34 +02:00
|
|
|
return CachedNetworkImageProvider(
|
|
|
|
getThumbnailUrl(
|
2023-02-04 22:42:42 +02:00
|
|
|
asset,
|
2023-02-01 18:59:34 +02:00
|
|
|
type: type,
|
2023-01-11 22:54:12 +02:00
|
|
|
),
|
2023-02-01 18:59:34 +02:00
|
|
|
cacheKey: getThumbnailCacheKey(
|
2023-02-04 22:42:42 +02:00
|
|
|
asset,
|
2023-02-01 18:59:34 +02:00
|
|
|
type: type,
|
|
|
|
),
|
|
|
|
headers: {"Authorization": authToken},
|
2022-08-03 22:36:12 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-02-04 22:42:42 +02:00
|
|
|
/// Original (large) image of a remote asset. Required asset.isRemote
|
2023-02-01 18:59:34 +02:00
|
|
|
ImageProvider originalImageProvider(Asset asset) {
|
|
|
|
return CachedNetworkImageProvider(
|
2023-02-04 22:42:42 +02:00
|
|
|
getImageUrl(asset),
|
|
|
|
cacheKey: getImageCacheKey(asset),
|
2023-02-01 18:59:34 +02:00
|
|
|
headers: {"Authorization": authToken},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-02-04 22:42:42 +02:00
|
|
|
/// Thumbnail image of a local asset. Required asset.isLocal
|
2023-02-01 18:59:34 +02:00
|
|
|
ImageProvider localThumbnailImageProvider(Asset asset) {
|
|
|
|
return AssetEntityImageProvider(
|
|
|
|
asset.local!,
|
|
|
|
isOriginal: false,
|
2023-02-02 21:29:52 +02:00
|
|
|
thumbnailSize: ThumbnailSize(
|
|
|
|
MediaQuery.of(context).size.width.floor(),
|
|
|
|
MediaQuery.of(context).size.height.floor(),
|
|
|
|
),
|
2023-02-01 18:59:34 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-02-04 22:42:42 +02:00
|
|
|
/// Original (large) image of a local asset. Required asset.isLocal
|
2023-02-01 18:59:34 +02:00
|
|
|
ImageProvider localImageProvider(Asset asset) {
|
|
|
|
return AssetEntityImageProvider(asset.local!);
|
|
|
|
}
|
|
|
|
|
|
|
|
void precacheNextImage(int index) {
|
|
|
|
if (index < assetList.length && index > 0) {
|
|
|
|
final asset = assetList[index];
|
|
|
|
if (asset.isLocal) {
|
|
|
|
// Preload the local asset
|
|
|
|
precacheImage(localImageProvider(asset), context);
|
|
|
|
} else {
|
|
|
|
// Probably load WEBP either way
|
|
|
|
precacheImage(
|
|
|
|
remoteThumbnailImageProvider(
|
2023-02-04 22:42:42 +02:00
|
|
|
asset,
|
2023-02-01 18:59:34 +02:00
|
|
|
api.ThumbnailFormat.WEBP,
|
|
|
|
),
|
|
|
|
context,
|
|
|
|
);
|
|
|
|
if (isLoadPreview.value) {
|
|
|
|
// Precache the JPEG thumbnail
|
|
|
|
precacheImage(
|
|
|
|
remoteThumbnailImageProvider(
|
|
|
|
asset,
|
|
|
|
api.ThumbnailFormat.JPEG,
|
|
|
|
),
|
|
|
|
context,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (isLoadOriginal.value) {
|
|
|
|
// Preload the original asset
|
|
|
|
precacheImage(
|
|
|
|
originalImageProvider(asset),
|
|
|
|
context,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void showInfo() {
|
2023-02-04 22:42:42 +02:00
|
|
|
showModalBottomSheet(
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
borderRadius: BorderRadius.circular(15.0),
|
|
|
|
),
|
|
|
|
barrierColor: Colors.transparent,
|
|
|
|
backgroundColor: Colors.transparent,
|
|
|
|
isScrollControlled: true,
|
|
|
|
context: context,
|
|
|
|
builder: (context) {
|
|
|
|
return ExifBottomSheet(assetDetail: assetDetail!);
|
|
|
|
},
|
|
|
|
);
|
2022-08-03 22:36:12 +02:00
|
|
|
}
|
|
|
|
|
2022-12-22 22:10:21 +02:00
|
|
|
void handleDelete(Asset deleteAsset) {
|
|
|
|
showDialog(
|
|
|
|
context: context,
|
|
|
|
builder: (BuildContext _) {
|
|
|
|
return DeleteDialog(
|
|
|
|
onDelete: () {
|
|
|
|
ref.watch(assetProvider.notifier).deleteAssets({deleteAsset});
|
|
|
|
AutoRouter.of(context).pop(null);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-01-27 07:16:28 +02:00
|
|
|
void addToAlbum(Asset addToAlbumAsset) {
|
|
|
|
showModalBottomSheet(
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
borderRadius: BorderRadius.circular(15.0),
|
|
|
|
),
|
|
|
|
barrierColor: Colors.transparent,
|
|
|
|
backgroundColor: Colors.transparent,
|
|
|
|
context: context,
|
|
|
|
builder: (BuildContext _) {
|
2023-01-27 23:05:08 +02:00
|
|
|
return AddToAlbumBottomSheet(
|
|
|
|
assets: [addToAlbumAsset],
|
2023-01-27 07:16:28 +02:00
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-02-01 18:59:34 +02:00
|
|
|
void handleSwipeUpDown(DragUpdateDetails details) {
|
|
|
|
int sensitivity = 15;
|
|
|
|
int dxThreshhold = 50;
|
|
|
|
|
|
|
|
if (isZoomed.value) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for delta from initial down point
|
|
|
|
final d = details.localPosition - localPosition;
|
|
|
|
// If the magnitude of the dx swipe is large, we probably didn't mean to go down
|
|
|
|
if (d.dx.abs() > dxThreshhold) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (details.delta.dy > sensitivity) {
|
|
|
|
AutoRouter.of(context).pop();
|
|
|
|
} else if (details.delta.dy < -sensitivity) {
|
|
|
|
showInfo();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-03 18:26:05 +02:00
|
|
|
buildAppBar() {
|
|
|
|
return AnimatedOpacity(
|
|
|
|
duration: const Duration(milliseconds: 100),
|
|
|
|
opacity: (showAppBar.value || !isZoomed.value) ? 1.0 : 0.0,
|
|
|
|
child: Container(
|
|
|
|
color: Colors.black.withOpacity(0.4),
|
|
|
|
child: TopControlAppBar(
|
|
|
|
isPlayingMotionVideo: isPlayingMotionVideo.value,
|
|
|
|
asset: assetList[indexOfAsset.value],
|
2023-02-05 05:25:11 +02:00
|
|
|
isFavorite: ref.watch(favoriteProvider).contains(
|
|
|
|
assetList[indexOfAsset.value].id,
|
|
|
|
),
|
2023-02-03 18:26:05 +02:00
|
|
|
onMoreInfoPressed: () {
|
|
|
|
showInfo();
|
|
|
|
},
|
2023-02-05 05:25:11 +02:00
|
|
|
onFavorite: () {
|
|
|
|
toggleFavorite(assetList[indexOfAsset.value]);
|
|
|
|
},
|
2023-02-03 18:26:05 +02:00
|
|
|
onDownloadPressed: assetList[indexOfAsset.value].isLocal
|
|
|
|
? null
|
|
|
|
: () {
|
|
|
|
ref.watch(imageViewerStateProvider.notifier).downloadAsset(
|
2023-02-04 22:42:42 +02:00
|
|
|
assetList[indexOfAsset.value],
|
2023-02-03 18:26:05 +02:00
|
|
|
context,
|
|
|
|
);
|
|
|
|
},
|
|
|
|
onSharePressed: () {
|
|
|
|
ref
|
|
|
|
.watch(imageViewerStateProvider.notifier)
|
|
|
|
.shareAsset(assetList[indexOfAsset.value], context);
|
|
|
|
},
|
|
|
|
onToggleMotionVideo: (() {
|
|
|
|
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
|
|
|
|
}),
|
2023-02-04 22:42:42 +02:00
|
|
|
onDeletePressed: () =>
|
|
|
|
handleDelete((assetList[indexOfAsset.value])),
|
|
|
|
onAddToAlbumPressed: () =>
|
|
|
|
addToAlbum(assetList[indexOfAsset.value]),
|
2023-02-03 18:26:05 +02:00
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-08-03 22:36:12 +02:00
|
|
|
return Scaffold(
|
|
|
|
backgroundColor: Colors.black,
|
2023-02-03 18:26:05 +02:00
|
|
|
body: Stack(
|
|
|
|
children: [
|
|
|
|
PhotoViewGallery.builder(
|
2023-02-04 22:42:42 +02:00
|
|
|
scaleStateChangedCallback: (state) {
|
|
|
|
isZoomed.value = state != PhotoViewScaleState.initial;
|
|
|
|
showAppBar.value = !isZoomed.value;
|
|
|
|
},
|
|
|
|
pageController: controller,
|
|
|
|
scrollPhysics: isZoomed.value
|
|
|
|
? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in
|
|
|
|
: (Platform.isIOS
|
|
|
|
? const BouncingScrollPhysics() // Use bouncing physics for iOS
|
|
|
|
: const ClampingScrollPhysics() // Use heavy physics for Android
|
|
|
|
),
|
|
|
|
itemCount: assetList.length,
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
onPageChanged: (value) {
|
|
|
|
// Precache image
|
|
|
|
if (indexOfAsset.value < value) {
|
|
|
|
// Moving forwards, so precache the next asset
|
|
|
|
precacheNextImage(value + 1);
|
2022-11-19 07:12:54 +02:00
|
|
|
} else {
|
2023-02-04 22:42:42 +02:00
|
|
|
// Moving backwards, so precache previous asset
|
|
|
|
precacheNextImage(value - 1);
|
|
|
|
}
|
|
|
|
indexOfAsset.value = value;
|
|
|
|
HapticFeedback.selectionClick();
|
|
|
|
},
|
|
|
|
loadingBuilder: isLoadPreview.value
|
|
|
|
? (context, event) {
|
|
|
|
final asset = assetList[indexOfAsset.value];
|
|
|
|
if (!asset.isLocal) {
|
|
|
|
// Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to acheive
|
|
|
|
// Three-Stage Loading (WEBP -> JPEG -> Original)
|
|
|
|
final webPThumbnail = CachedNetworkImage(
|
|
|
|
imageUrl: getThumbnailUrl(asset),
|
|
|
|
cacheKey: getThumbnailCacheKey(asset),
|
|
|
|
httpHeaders: {'Authorization': authToken},
|
|
|
|
progressIndicatorBuilder: (_, __, ___) => const Center(
|
|
|
|
child: ImmichLoadingIndicator(),
|
|
|
|
),
|
|
|
|
fadeInDuration: const Duration(milliseconds: 0),
|
|
|
|
fit: BoxFit.contain,
|
|
|
|
);
|
|
|
|
|
|
|
|
return CachedNetworkImage(
|
|
|
|
imageUrl: getThumbnailUrl(
|
|
|
|
asset,
|
|
|
|
type: api.ThumbnailFormat.JPEG,
|
|
|
|
),
|
|
|
|
cacheKey: getThumbnailCacheKey(
|
|
|
|
asset,
|
|
|
|
type: api.ThumbnailFormat.JPEG,
|
|
|
|
),
|
|
|
|
httpHeaders: {'Authorization': authToken},
|
|
|
|
fit: BoxFit.contain,
|
|
|
|
fadeInDuration: const Duration(milliseconds: 0),
|
|
|
|
placeholder: (_, __) => webPThumbnail,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return Image(
|
|
|
|
image: localThumbnailImageProvider(asset),
|
|
|
|
fit: BoxFit.contain,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
: null,
|
|
|
|
builder: (context, index) {
|
|
|
|
getAssetExif();
|
|
|
|
if (assetList[index].isImage && !isPlayingMotionVideo.value) {
|
|
|
|
// Show photo
|
|
|
|
final ImageProvider provider;
|
|
|
|
if (assetList[index].isLocal) {
|
|
|
|
provider = localImageProvider(assetList[index]);
|
2023-02-01 18:59:34 +02:00
|
|
|
} else {
|
2023-02-04 22:42:42 +02:00
|
|
|
if (isLoadOriginal.value) {
|
|
|
|
provider = originalImageProvider(assetList[index]);
|
|
|
|
} else {
|
|
|
|
provider = remoteThumbnailImageProvider(
|
|
|
|
assetList[index],
|
|
|
|
api.ThumbnailFormat.JPEG,
|
|
|
|
);
|
|
|
|
}
|
2023-02-01 18:59:34 +02:00
|
|
|
}
|
2023-02-04 22:42:42 +02:00
|
|
|
return PhotoViewGalleryPageOptions(
|
|
|
|
onDragStart: (_, details, __) =>
|
|
|
|
localPosition = details.localPosition,
|
|
|
|
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
|
|
|
|
onTapDown: (_, __, ___) =>
|
|
|
|
showAppBar.value = !showAppBar.value,
|
|
|
|
imageProvider: provider,
|
|
|
|
heroAttributes:
|
|
|
|
PhotoViewHeroAttributes(tag: assetList[index].id),
|
|
|
|
minScale: PhotoViewComputedScale.contained,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return PhotoViewGalleryPageOptions.customChild(
|
|
|
|
onDragStart: (_, details, __) =>
|
|
|
|
localPosition = details.localPosition,
|
|
|
|
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
|
|
|
|
onTapDown: (_, __, ___) =>
|
|
|
|
showAppBar.value = !showAppBar.value,
|
|
|
|
heroAttributes:
|
|
|
|
PhotoViewHeroAttributes(tag: assetList[index].id),
|
|
|
|
maxScale: 1.0,
|
|
|
|
minScale: 1.0,
|
|
|
|
child: SafeArea(
|
|
|
|
child: VideoViewerPage(
|
|
|
|
asset: assetList[index],
|
|
|
|
isMotionVideo: isPlayingMotionVideo.value,
|
|
|
|
onVideoEnded: () {
|
|
|
|
if (isPlayingMotionVideo.value) {
|
|
|
|
isPlayingMotionVideo.value = false;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
),
|
2023-02-03 18:26:05 +02:00
|
|
|
),
|
2023-02-04 22:42:42 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
),
|
|
|
|
Positioned(
|
|
|
|
top: 0,
|
|
|
|
left: 0,
|
|
|
|
right: 0,
|
|
|
|
child: buildAppBar(),
|
|
|
|
),
|
|
|
|
],
|
2022-08-03 22:36:12 +02:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|