mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feat(mobile): show local assets (#905)
* introduce Asset as composition of AssetResponseDTO and AssetEntity * filter out duplicate assets (that are both local and remote, take only remote for now) * only allow remote images to be added to albums * introduce ImmichImage to render Asset using local or remote data * optimized deletion of local assets * local video file playback * allow multiple methods to wait on background service finished * skip local assets when adding to album from home screen * fix and optimize delete * show gray box placeholder for local assets * add comments * fix bug: duplicate assets in state after onNewAssetUploaded
This commit is contained in:
committed by
GitHub
parent
99da181cfc
commit
1633af7af6
@@ -14,12 +14,12 @@ import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'
|
||||
import 'package:immich_mobile/modules/home/services/asset.service.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:openapi/api.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class GalleryViewerPage extends HookConsumerWidget {
|
||||
late List<AssetResponseDto> assetList;
|
||||
final AssetResponseDto asset;
|
||||
late List<Asset> assetList;
|
||||
final Asset asset;
|
||||
|
||||
GalleryViewerPage({
|
||||
Key? key,
|
||||
@@ -27,7 +27,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
required this.asset,
|
||||
}) : super(key: key);
|
||||
|
||||
AssetResponseDto? assetDetail;
|
||||
Asset? assetDetail;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -37,8 +37,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
final loading = useState(false);
|
||||
final isZoomed = useState<bool>(false);
|
||||
ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(false);
|
||||
|
||||
int indexOfAsset = assetList.indexOf(asset);
|
||||
final indexOfAsset = useState(assetList.indexOf(asset));
|
||||
|
||||
PageController controller =
|
||||
PageController(initialPage: assetList.indexOf(asset));
|
||||
@@ -52,15 +51,15 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
[],
|
||||
);
|
||||
|
||||
@override
|
||||
initState(int index) {
|
||||
indexOfAsset = index;
|
||||
}
|
||||
|
||||
getAssetExif() async {
|
||||
assetDetail = await ref
|
||||
.watch(assetServiceProvider)
|
||||
.getAssetById(assetList[indexOfAsset].id);
|
||||
if (assetList[indexOfAsset.value].isRemote) {
|
||||
assetDetail = await ref
|
||||
.watch(assetServiceProvider)
|
||||
.getAssetById(assetList[indexOfAsset.value].id);
|
||||
} else {
|
||||
// TODO local exif parsing?
|
||||
assetDetail = assetList[indexOfAsset.value];
|
||||
}
|
||||
}
|
||||
|
||||
void showInfo() {
|
||||
@@ -88,19 +87,20 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
backgroundColor: Colors.black,
|
||||
appBar: TopControlAppBar(
|
||||
loading: loading.value,
|
||||
asset: assetList[indexOfAsset],
|
||||
asset: assetList[indexOfAsset.value],
|
||||
onMoreInfoPressed: () {
|
||||
showInfo();
|
||||
},
|
||||
onDownloadPressed: () {
|
||||
ref
|
||||
.watch(imageViewerStateProvider.notifier)
|
||||
.downloadAsset(assetList[indexOfAsset], context);
|
||||
},
|
||||
onDownloadPressed: assetList[indexOfAsset.value].isLocal
|
||||
? null
|
||||
: () {
|
||||
ref.watch(imageViewerStateProvider.notifier).downloadAsset(
|
||||
assetList[indexOfAsset.value].remote!, context);
|
||||
},
|
||||
onSharePressed: () {
|
||||
ref
|
||||
.watch(imageViewerStateProvider.notifier)
|
||||
.shareAsset(assetList[indexOfAsset], context);
|
||||
.shareAsset(assetList[indexOfAsset.value], context);
|
||||
},
|
||||
),
|
||||
body: SafeArea(
|
||||
@@ -113,14 +113,13 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
itemCount: assetList.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
onPageChanged: (value) {
|
||||
indexOfAsset.value = value;
|
||||
HapticFeedback.selectionClick();
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
initState(index);
|
||||
|
||||
getAssetExif();
|
||||
|
||||
if (assetList[index].type == AssetTypeEnum.IMAGE) {
|
||||
if (assetList[index].isImage) {
|
||||
return ImageViewerPage(
|
||||
authToken: 'Bearer ${box.get(accessTokenKey)}',
|
||||
isZoomedFunction: isZoomedMethod,
|
||||
@@ -139,11 +138,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
},
|
||||
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}',
|
||||
),
|
||||
child: VideoViewerPage(asset: assetList[index]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,13 +8,12 @@ import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator
|
||||
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/home/services/asset.service.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class ImageViewerPage extends HookConsumerWidget {
|
||||
final String heroTag;
|
||||
final AssetResponseDto asset;
|
||||
final Asset asset;
|
||||
final String authToken;
|
||||
final ValueNotifier<bool> isZoomedListener;
|
||||
final void Function() isZoomedFunction;
|
||||
@@ -30,7 +29,7 @@ class ImageViewerPage extends HookConsumerWidget {
|
||||
required this.threeStageLoading,
|
||||
}) : super(key: key);
|
||||
|
||||
AssetResponseDto? assetDetail;
|
||||
Asset? assetDetail;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -38,8 +37,13 @@ class ImageViewerPage extends HookConsumerWidget {
|
||||
ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
||||
|
||||
getAssetExif() async {
|
||||
assetDetail =
|
||||
await ref.watch(assetServiceProvider).getAssetById(asset.id);
|
||||
if (asset.isRemote) {
|
||||
assetDetail =
|
||||
await ref.watch(assetServiceProvider).getAssetById(asset.id);
|
||||
} else {
|
||||
// TODO local exif parsing?
|
||||
assetDetail = asset;
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(
|
||||
@@ -68,17 +72,13 @@ class ImageViewerPage extends HookConsumerWidget {
|
||||
child: Hero(
|
||||
tag: heroTag,
|
||||
child: RemotePhotoView(
|
||||
thumbnailUrl: getThumbnailUrl(asset),
|
||||
cacheKey: asset.id,
|
||||
imageUrl: getImageUrl(asset),
|
||||
previewUrl: threeStageLoading
|
||||
? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG)
|
||||
: null,
|
||||
asset: asset,
|
||||
authToken: authToken,
|
||||
threeStageLoading: threeStageLoading,
|
||||
isZoomedFunction: isZoomedFunction,
|
||||
isZoomedListener: isZoomedListener,
|
||||
onSwipeDown: () => AutoRouter.of(context).pop(),
|
||||
onSwipeUp: () => showInfo(),
|
||||
onSwipeUp: asset.isRemote ? showInfo : () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -6,24 +8,41 @@ 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:openapi/api.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class VideoViewerPage extends HookConsumerWidget {
|
||||
final String videoUrl;
|
||||
final AssetResponseDto asset;
|
||||
AssetResponseDto? assetDetail;
|
||||
final Asset asset;
|
||||
|
||||
VideoViewerPage({Key? key, required this.videoUrl, required this.asset})
|
||||
: super(key: key);
|
||||
const VideoViewerPage({Key? key, required this.asset}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
if (asset.isLocal) {
|
||||
final AsyncValue<File> videoFile = ref.watch(_fileFamily(asset.local!));
|
||||
return videoFile.when(
|
||||
data: (data) => VideoThumbnailPlayer(file: data),
|
||||
error: (error, stackTrace) => Icon(
|
||||
Icons.image_not_supported_outlined,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
loading: () => const Center(
|
||||
child: SizedBox(
|
||||
width: 75,
|
||||
height: 75,
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final downloadAssetStatus =
|
||||
ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
||||
|
||||
String jwtToken = Hive.box(userInfoBox).get(accessTokenKey);
|
||||
final box = Hive.box(userInfoBox);
|
||||
final String jwtToken = box.get(accessTokenKey);
|
||||
final String videoUrl =
|
||||
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}';
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
@@ -40,11 +59,21 @@ class VideoViewerPage extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class VideoThumbnailPlayer extends StatefulWidget {
|
||||
final String url;
|
||||
final String? jwtToken;
|
||||
final _fileFamily =
|
||||
FutureProvider.family<File, AssetEntity>((ref, entity) async {
|
||||
final file = await entity.file;
|
||||
if (file == null) {
|
||||
throw Exception();
|
||||
}
|
||||
return file;
|
||||
});
|
||||
|
||||
const VideoThumbnailPlayer({Key? key, required this.url, this.jwtToken})
|
||||
class VideoThumbnailPlayer extends StatefulWidget {
|
||||
final String? url;
|
||||
final String? jwtToken;
|
||||
final File? file;
|
||||
|
||||
const VideoThumbnailPlayer({Key? key, this.url, this.jwtToken, this.file})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@@ -63,10 +92,12 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
|
||||
|
||||
Future<void> initializePlayer() async {
|
||||
try {
|
||||
videoPlayerController = VideoPlayerController.network(
|
||||
widget.url,
|
||||
httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"},
|
||||
);
|
||||
videoPlayerController = widget.file == null
|
||||
? VideoPlayerController.network(
|
||||
widget.url!,
|
||||
httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"},
|
||||
)
|
||||
: VideoPlayerController.file(widget.file!);
|
||||
|
||||
await videoPlayerController.initialize();
|
||||
_createChewieController();
|
||||
|
||||
Reference in New Issue
Block a user