|
@@ -12,9 +12,7 @@ import 'package:immich_mobile/modules/asset_viewer/ui/advanced_bottom_sheet.dart
|
|
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.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/ui/top_control_app_bar.dart';
|
|
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
|
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
|
-import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
|
|
|
|
import 'package:immich_mobile/shared/models/store.dart';
|
|
import 'package:immich_mobile/shared/models/store.dart';
|
|
-import 'package:immich_mobile/shared/services/asset.service.dart';
|
|
|
|
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
|
|
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
|
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
|
@@ -32,16 +30,16 @@ import 'package:openapi/api.dart' as api;
|
|
|
|
|
|
// ignore: must_be_immutable
|
|
// ignore: must_be_immutable
|
|
class GalleryViewerPage extends HookConsumerWidget {
|
|
class GalleryViewerPage extends HookConsumerWidget {
|
|
- final List<Asset> assetList;
|
|
|
|
- final Asset asset;
|
|
|
|
|
|
+ final Asset Function(int index) loadAsset;
|
|
|
|
+ final int totalAssets;
|
|
|
|
+ final int initialIndex;
|
|
|
|
|
|
GalleryViewerPage({
|
|
GalleryViewerPage({
|
|
super.key,
|
|
super.key,
|
|
- required this.assetList,
|
|
|
|
- required this.asset,
|
|
|
|
- }) : controller = PageController(initialPage: assetList.indexOf(asset));
|
|
|
|
-
|
|
|
|
- Asset? assetDetail;
|
|
|
|
|
|
+ required this.initialIndex,
|
|
|
|
+ required this.loadAsset,
|
|
|
|
+ required this.totalAssets,
|
|
|
|
+ }) : controller = PageController(initialPage: initialIndex);
|
|
|
|
|
|
final PageController controller;
|
|
final PageController controller;
|
|
|
|
|
|
@@ -52,11 +50,15 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
|
|
final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
|
|
final isZoomed = useState<bool>(false);
|
|
final isZoomed = useState<bool>(false);
|
|
final showAppBar = useState<bool>(true);
|
|
final showAppBar = useState<bool>(true);
|
|
- final indexOfAsset = useState(assetList.indexOf(asset));
|
|
|
|
final isPlayingMotionVideo = useState(false);
|
|
final isPlayingMotionVideo = useState(false);
|
|
final isPlayingVideo = useState(false);
|
|
final isPlayingVideo = useState(false);
|
|
late Offset localPosition;
|
|
late Offset localPosition;
|
|
final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}';
|
|
final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}';
|
|
|
|
+ final currentIndex = useState(initialIndex);
|
|
|
|
+ final currentAsset = loadAsset(currentIndex.value);
|
|
|
|
+ final watchedAsset = ref.watch(assetDetailProvider(currentAsset));
|
|
|
|
+
|
|
|
|
+ Asset asset() => watchedAsset.value ?? currentAsset;
|
|
|
|
|
|
showAppBar.addListener(() {
|
|
showAppBar.addListener(() {
|
|
// Change to and from immersive mode, hiding navigation and app bar
|
|
// Change to and from immersive mode, hiding navigation and app bar
|
|
@@ -79,16 +81,9 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
[],
|
|
[],
|
|
);
|
|
);
|
|
|
|
|
|
- void toggleFavorite(Asset asset) {
|
|
|
|
- ref.watch(favoriteProvider.notifier).toggleFavorite(asset);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- void getAssetExif() async {
|
|
|
|
- assetDetail = assetList[indexOfAsset.value];
|
|
|
|
- assetDetail = await ref
|
|
|
|
- .watch(assetServiceProvider)
|
|
|
|
- .loadExif(assetList[indexOfAsset.value]);
|
|
|
|
- }
|
|
|
|
|
|
+ void toggleFavorite(Asset asset) => ref
|
|
|
|
+ .watch(assetProvider.notifier)
|
|
|
|
+ .toggleFavorite([asset], !asset.isFavorite);
|
|
|
|
|
|
/// Thumbnail image of a remote asset. Required asset.isRemote
|
|
/// Thumbnail image of a remote asset. Required asset.isRemote
|
|
ImageProvider remoteThumbnailImageProvider(
|
|
ImageProvider remoteThumbnailImageProvider(
|
|
@@ -138,8 +133,8 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
}
|
|
}
|
|
|
|
|
|
void precacheNextImage(int index) {
|
|
void precacheNextImage(int index) {
|
|
- if (index < assetList.length && index >= 0) {
|
|
|
|
- final asset = assetList[index];
|
|
|
|
|
|
+ if (index < totalAssets && index >= 0) {
|
|
|
|
+ final asset = loadAsset(index);
|
|
|
|
|
|
if (asset.isLocal) {
|
|
if (asset.isLocal) {
|
|
// Preload the local asset
|
|
// Preload the local asset
|
|
@@ -193,13 +188,13 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
if (ref
|
|
if (ref
|
|
.watch(appSettingsServiceProvider)
|
|
.watch(appSettingsServiceProvider)
|
|
.getSetting<bool>(AppSettingsEnum.advancedTroubleshooting)) {
|
|
.getSetting<bool>(AppSettingsEnum.advancedTroubleshooting)) {
|
|
- return AdvancedBottomSheet(assetDetail: assetDetail!);
|
|
|
|
|
|
+ return AdvancedBottomSheet(assetDetail: asset());
|
|
}
|
|
}
|
|
return Padding(
|
|
return Padding(
|
|
padding: EdgeInsets.only(
|
|
padding: EdgeInsets.only(
|
|
bottom: MediaQuery.of(context).viewInsets.bottom,
|
|
bottom: MediaQuery.of(context).viewInsets.bottom,
|
|
),
|
|
),
|
|
- child: ExifBottomSheet(asset: assetDetail!),
|
|
|
|
|
|
+ child: ExifBottomSheet(asset: asset()),
|
|
);
|
|
);
|
|
},
|
|
},
|
|
);
|
|
);
|
|
@@ -211,7 +206,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
builder: (BuildContext _) {
|
|
builder: (BuildContext _) {
|
|
return DeleteDialog(
|
|
return DeleteDialog(
|
|
onDelete: () {
|
|
onDelete: () {
|
|
- if (assetList.length == 1) {
|
|
|
|
|
|
+ if (totalAssets == 1) {
|
|
// Handle only one asset
|
|
// Handle only one asset
|
|
AutoRouter.of(context).pop();
|
|
AutoRouter.of(context).pop();
|
|
} else {
|
|
} else {
|
|
@@ -221,7 +216,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
curve: Curves.fastLinearToSlowEaseIn,
|
|
curve: Curves.fastLinearToSlowEaseIn,
|
|
);
|
|
);
|
|
}
|
|
}
|
|
- assetList.remove(deleteAsset);
|
|
|
|
ref.watch(assetProvider.notifier).deleteAssets({deleteAsset});
|
|
ref.watch(assetProvider.notifier).deleteAssets({deleteAsset});
|
|
},
|
|
},
|
|
);
|
|
);
|
|
@@ -267,9 +261,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
}
|
|
}
|
|
|
|
|
|
shareAsset() {
|
|
shareAsset() {
|
|
- ref
|
|
|
|
- .watch(imageViewerStateProvider.notifier)
|
|
|
|
- .shareAsset(assetList[indexOfAsset.value], context);
|
|
|
|
|
|
+ ref.watch(imageViewerStateProvider.notifier).shareAsset(asset(), context);
|
|
}
|
|
}
|
|
|
|
|
|
handleArchive(Asset asset) {
|
|
handleArchive(Asset asset) {
|
|
@@ -291,30 +283,21 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
color: Colors.black.withOpacity(0.4),
|
|
color: Colors.black.withOpacity(0.4),
|
|
child: TopControlAppBar(
|
|
child: TopControlAppBar(
|
|
isPlayingMotionVideo: isPlayingMotionVideo.value,
|
|
isPlayingMotionVideo: isPlayingMotionVideo.value,
|
|
- asset: assetList[indexOfAsset.value],
|
|
|
|
- isFavorite: ref.watch(favoriteProvider).contains(
|
|
|
|
- assetList[indexOfAsset.value].id,
|
|
|
|
- ),
|
|
|
|
- onMoreInfoPressed: () {
|
|
|
|
- showInfo();
|
|
|
|
- },
|
|
|
|
- onFavorite: () {
|
|
|
|
- toggleFavorite(assetList[indexOfAsset.value]);
|
|
|
|
- },
|
|
|
|
- onDownloadPressed: assetList[indexOfAsset.value].storage ==
|
|
|
|
- AssetState.local
|
|
|
|
|
|
+ asset: asset(),
|
|
|
|
+ isFavorite: asset().isFavorite,
|
|
|
|
+ onMoreInfoPressed: showInfo,
|
|
|
|
+ onFavorite: asset().isRemote ? () => toggleFavorite(asset()) : null,
|
|
|
|
+ onDownloadPressed: asset().storage == AssetState.local
|
|
? null
|
|
? null
|
|
- : () {
|
|
|
|
|
|
+ : () =>
|
|
ref.watch(imageViewerStateProvider.notifier).downloadAsset(
|
|
ref.watch(imageViewerStateProvider.notifier).downloadAsset(
|
|
- assetList[indexOfAsset.value],
|
|
|
|
|
|
+ asset(),
|
|
context,
|
|
context,
|
|
- );
|
|
|
|
- },
|
|
|
|
|
|
+ ),
|
|
onToggleMotionVideo: (() {
|
|
onToggleMotionVideo: (() {
|
|
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
|
|
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
|
|
}),
|
|
}),
|
|
- onAddToAlbumPressed: () =>
|
|
|
|
- addToAlbum(assetList[indexOfAsset.value]),
|
|
|
|
|
|
+ onAddToAlbumPressed: () => addToAlbum(asset()),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
);
|
|
@@ -324,8 +307,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
final show = (showAppBar.value || // onTap has the final say
|
|
final show = (showAppBar.value || // onTap has the final say
|
|
(showAppBar.value && !isZoomed.value)) &&
|
|
(showAppBar.value && !isZoomed.value)) &&
|
|
!isPlayingVideo.value;
|
|
!isPlayingVideo.value;
|
|
- final currentAsset = assetList[indexOfAsset.value];
|
|
|
|
-
|
|
|
|
return AnimatedOpacity(
|
|
return AnimatedOpacity(
|
|
duration: const Duration(milliseconds: 100),
|
|
duration: const Duration(milliseconds: 100),
|
|
opacity: show ? 1.0 : 0.0,
|
|
opacity: show ? 1.0 : 0.0,
|
|
@@ -343,7 +324,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
label: 'control_bottom_app_bar_share'.tr(),
|
|
label: 'control_bottom_app_bar_share'.tr(),
|
|
tooltip: 'control_bottom_app_bar_share'.tr(),
|
|
tooltip: 'control_bottom_app_bar_share'.tr(),
|
|
),
|
|
),
|
|
- currentAsset.isArchived
|
|
|
|
|
|
+ asset().isArchived
|
|
? BottomNavigationBarItem(
|
|
? BottomNavigationBarItem(
|
|
icon: const Icon(Icons.unarchive_rounded),
|
|
icon: const Icon(Icons.unarchive_rounded),
|
|
label: 'control_bottom_app_bar_unarchive'.tr(),
|
|
label: 'control_bottom_app_bar_unarchive'.tr(),
|
|
@@ -366,10 +347,10 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
shareAsset();
|
|
shareAsset();
|
|
break;
|
|
break;
|
|
case 1:
|
|
case 1:
|
|
- handleArchive(assetList[indexOfAsset.value]);
|
|
|
|
|
|
+ handleArchive(asset());
|
|
break;
|
|
break;
|
|
case 2:
|
|
case 2:
|
|
- handleDelete(assetList[indexOfAsset.value]);
|
|
|
|
|
|
+ handleDelete(asset());
|
|
break;
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
},
|
|
@@ -399,33 +380,33 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
? const ScrollPhysics() // Use bouncing physics for iOS
|
|
? const ScrollPhysics() // Use bouncing physics for iOS
|
|
: const ClampingScrollPhysics() // Use heavy physics for Android
|
|
: const ClampingScrollPhysics() // Use heavy physics for Android
|
|
),
|
|
),
|
|
- itemCount: assetList.length,
|
|
|
|
|
|
+ itemCount: totalAssets,
|
|
scrollDirection: Axis.horizontal,
|
|
scrollDirection: Axis.horizontal,
|
|
onPageChanged: (value) {
|
|
onPageChanged: (value) {
|
|
// Precache image
|
|
// Precache image
|
|
- if (indexOfAsset.value < value) {
|
|
|
|
|
|
+ if (currentIndex.value < value) {
|
|
// Moving forwards, so precache the next asset
|
|
// Moving forwards, so precache the next asset
|
|
precacheNextImage(value + 1);
|
|
precacheNextImage(value + 1);
|
|
} else {
|
|
} else {
|
|
// Moving backwards, so precache previous asset
|
|
// Moving backwards, so precache previous asset
|
|
precacheNextImage(value - 1);
|
|
precacheNextImage(value - 1);
|
|
}
|
|
}
|
|
- indexOfAsset.value = value;
|
|
|
|
|
|
+ currentIndex.value = value;
|
|
HapticFeedback.selectionClick();
|
|
HapticFeedback.selectionClick();
|
|
},
|
|
},
|
|
loadingBuilder: isLoadPreview.value
|
|
loadingBuilder: isLoadPreview.value
|
|
? (context, event) {
|
|
? (context, event) {
|
|
- final asset = assetList[indexOfAsset.value];
|
|
|
|
- if (!asset.isLocal) {
|
|
|
|
|
|
+ final a = asset();
|
|
|
|
+ if (!a.isLocal) {
|
|
// Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to achieve
|
|
// Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to achieve
|
|
// Three-Stage Loading (WEBP -> JPEG -> Original)
|
|
// Three-Stage Loading (WEBP -> JPEG -> Original)
|
|
final webPThumbnail = CachedNetworkImage(
|
|
final webPThumbnail = CachedNetworkImage(
|
|
imageUrl: getThumbnailUrl(
|
|
imageUrl: getThumbnailUrl(
|
|
- asset,
|
|
|
|
|
|
+ a,
|
|
type: api.ThumbnailFormat.WEBP,
|
|
type: api.ThumbnailFormat.WEBP,
|
|
),
|
|
),
|
|
cacheKey: getThumbnailCacheKey(
|
|
cacheKey: getThumbnailCacheKey(
|
|
- asset,
|
|
|
|
|
|
+ a,
|
|
type: api.ThumbnailFormat.WEBP,
|
|
type: api.ThumbnailFormat.WEBP,
|
|
),
|
|
),
|
|
httpHeaders: {'Authorization': authToken},
|
|
httpHeaders: {'Authorization': authToken},
|
|
@@ -444,11 +425,11 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
// makes sense if the original is loaded in the builder
|
|
// makes sense if the original is loaded in the builder
|
|
return CachedNetworkImage(
|
|
return CachedNetworkImage(
|
|
imageUrl: getThumbnailUrl(
|
|
imageUrl: getThumbnailUrl(
|
|
- asset,
|
|
|
|
|
|
+ a,
|
|
type: api.ThumbnailFormat.JPEG,
|
|
type: api.ThumbnailFormat.JPEG,
|
|
),
|
|
),
|
|
cacheKey: getThumbnailCacheKey(
|
|
cacheKey: getThumbnailCacheKey(
|
|
- asset,
|
|
|
|
|
|
+ a,
|
|
type: api.ThumbnailFormat.JPEG,
|
|
type: api.ThumbnailFormat.JPEG,
|
|
),
|
|
),
|
|
httpHeaders: {'Authorization': authToken},
|
|
httpHeaders: {'Authorization': authToken},
|
|
@@ -462,30 +443,30 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
}
|
|
}
|
|
} else {
|
|
} else {
|
|
return Image(
|
|
return Image(
|
|
- image: localThumbnailImageProvider(asset),
|
|
|
|
|
|
+ image: localThumbnailImageProvider(a),
|
|
fit: BoxFit.contain,
|
|
fit: BoxFit.contain,
|
|
);
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
: null,
|
|
: null,
|
|
builder: (context, index) {
|
|
builder: (context, index) {
|
|
- getAssetExif();
|
|
|
|
- if (assetList[index].isImage && !isPlayingMotionVideo.value) {
|
|
|
|
|
|
+ final asset = loadAsset(index);
|
|
|
|
+ if (asset.isImage && !isPlayingMotionVideo.value) {
|
|
// Show photo
|
|
// Show photo
|
|
final ImageProvider provider;
|
|
final ImageProvider provider;
|
|
- if (assetList[index].isLocal) {
|
|
|
|
- provider = localImageProvider(assetList[index]);
|
|
|
|
|
|
+ if (asset.isLocal) {
|
|
|
|
+ provider = localImageProvider(asset);
|
|
} else {
|
|
} else {
|
|
if (isLoadOriginal.value) {
|
|
if (isLoadOriginal.value) {
|
|
- provider = originalImageProvider(assetList[index]);
|
|
|
|
|
|
+ provider = originalImageProvider(asset);
|
|
} else if (isLoadPreview.value) {
|
|
} else if (isLoadPreview.value) {
|
|
provider = remoteThumbnailImageProvider(
|
|
provider = remoteThumbnailImageProvider(
|
|
- assetList[index],
|
|
|
|
|
|
+ asset,
|
|
api.ThumbnailFormat.JPEG,
|
|
api.ThumbnailFormat.JPEG,
|
|
);
|
|
);
|
|
} else {
|
|
} else {
|
|
provider = remoteThumbnailImageProvider(
|
|
provider = remoteThumbnailImageProvider(
|
|
- assetList[index],
|
|
|
|
|
|
+ asset,
|
|
api.ThumbnailFormat.WEBP,
|
|
api.ThumbnailFormat.WEBP,
|
|
);
|
|
);
|
|
}
|
|
}
|
|
@@ -499,13 +480,13 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
showAppBar.value = !showAppBar.value,
|
|
showAppBar.value = !showAppBar.value,
|
|
imageProvider: provider,
|
|
imageProvider: provider,
|
|
heroAttributes: PhotoViewHeroAttributes(
|
|
heroAttributes: PhotoViewHeroAttributes(
|
|
- tag: assetList[index].id,
|
|
|
|
|
|
+ tag: asset.id,
|
|
),
|
|
),
|
|
filterQuality: FilterQuality.high,
|
|
filterQuality: FilterQuality.high,
|
|
tightMode: true,
|
|
tightMode: true,
|
|
minScale: PhotoViewComputedScale.contained,
|
|
minScale: PhotoViewComputedScale.contained,
|
|
errorBuilder: (context, error, stackTrace) => ImmichImage(
|
|
errorBuilder: (context, error, stackTrace) => ImmichImage(
|
|
- assetList[indexOfAsset.value],
|
|
|
|
|
|
+ asset,
|
|
fit: BoxFit.contain,
|
|
fit: BoxFit.contain,
|
|
),
|
|
),
|
|
);
|
|
);
|
|
@@ -516,7 +497,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
onDragUpdate: (_, details, __) =>
|
|
onDragUpdate: (_, details, __) =>
|
|
handleSwipeUpDown(details),
|
|
handleSwipeUpDown(details),
|
|
heroAttributes: PhotoViewHeroAttributes(
|
|
heroAttributes: PhotoViewHeroAttributes(
|
|
- tag: assetList[index].id,
|
|
|
|
|
|
+ tag: asset.id,
|
|
),
|
|
),
|
|
filterQuality: FilterQuality.high,
|
|
filterQuality: FilterQuality.high,
|
|
maxScale: 1.0,
|
|
maxScale: 1.0,
|
|
@@ -526,7 +507,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|
child: VideoViewerPage(
|
|
child: VideoViewerPage(
|
|
onPlaying: () => isPlayingVideo.value = true,
|
|
onPlaying: () => isPlayingVideo.value = true,
|
|
onPaused: () => isPlayingVideo.value = false,
|
|
onPaused: () => isPlayingVideo.value = false,
|
|
- asset: assetList[index],
|
|
|
|
|
|
+ asset: asset,
|
|
isMotionVideo: isPlayingMotionVideo.value,
|
|
isMotionVideo: isPlayingMotionVideo.value,
|
|
onVideoEnded: () {
|
|
onVideoEnded: () {
|
|
if (isPlayingMotionVideo.value) {
|
|
if (isPlayingMotionVideo.value) {
|