mirror of https://github.com/immich-app/immich.git
Download asset to local and error fixing (#100)
* Update photo_manager pub package * Added download endpoint for assets * Successfully save a photo to the local device's gallery * Save save a video to the local device's gallery * Fixed #97 * Added download loading indicator * Refactor and increase the font size for curated search thumbnail images * Reposition loading animation on the search result pagepull/104/head
parent
60df387459
commit
90ef64efa3
@ -1,66 +1,72 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Immich</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>immich_mobile</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSLocationAlwaysUsageDescription</key>
|
||||
<string>Enable location setting to show position of assets on map</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>Enable location setting to show position of assets on map</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>We need to manage backup your photos album</string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Light</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>io.flutter.embedded_views_preview</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Immich</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>immich_mobile</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true />
|
||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||
<true />
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true />
|
||||
</dict>
|
||||
<key>NSLocationAlwaysUsageDescription</key>
|
||||
<string>Enable location setting to show position of assets on map</string>
|
||||
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>Enable location setting to show position of assets on map</string>
|
||||
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>We need to manage backup your photos album</string>
|
||||
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>We need to manage backup your photos album</string>
|
||||
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Light</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true />
|
||||
<key>io.flutter.embedded_views_preview</key>
|
||||
<true />
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false />
|
||||
</dict>
|
||||
</plist>
|
||||
@ -0,0 +1,6 @@
|
||||
class RequestDownloadAssetInfo {
|
||||
final String assetId;
|
||||
final String deviceId;
|
||||
|
||||
RequestDownloadAssetInfo(this.assetId, this.deviceId);
|
||||
}
|
||||
@ -1,21 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
||||
import 'package:immich_mobile/modules/home/models/home_page_state.model.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
|
||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
|
||||
class ImageViewerPageStateNotifier extends StateNotifier<ImageViewerPageState> {
|
||||
ImageViewerPageStateNotifier() : super(ImageViewerPageState(isBottomSheetEnable: false));
|
||||
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
||||
final ImageViewerService _imageViewerService = ImageViewerService();
|
||||
|
||||
void toggleBottomSheet() {
|
||||
bool isBottomSheetEnable = state.isBottomSheetEnable;
|
||||
ImageViewerStateNotifier() : super(ImageViewerPageState(downloadAssetStatus: DownloadAssetStatus.idle));
|
||||
|
||||
if (isBottomSheetEnable) {
|
||||
state.copyWith(isBottomSheetEnable: false);
|
||||
void downloadAsset(ImmichAsset asset, BuildContext context) async {
|
||||
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading);
|
||||
|
||||
bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset);
|
||||
|
||||
if (isSuccess) {
|
||||
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.success);
|
||||
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: "Download Success",
|
||||
toastType: ToastType.success,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
} else {
|
||||
state.copyWith(isBottomSheetEnable: true);
|
||||
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.error);
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: "Download Error",
|
||||
toastType: ToastType.error,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle);
|
||||
}
|
||||
}
|
||||
|
||||
final homePageStateProvider = StateNotifierProvider<ImageViewerPageStateNotifier, ImageViewerPageState>(
|
||||
((ref) => ImageViewerPageStateNotifier()));
|
||||
final imageViewerStateProvider =
|
||||
StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(((ref) => ImageViewerStateNotifier()));
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:immich_mobile/constants/hive_box.dart';
|
||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class ImageViewerService {
|
||||
Future<bool> downloadAssetToDevice(ImmichAsset asset) async {
|
||||
try {
|
||||
String fileName = p.basename(asset.originalPath);
|
||||
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||
Uri filePath =
|
||||
Uri.parse("$savedEndpoint/asset/download?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false");
|
||||
|
||||
var res = await http.get(
|
||||
filePath,
|
||||
headers: {"Authorization": "Bearer ${Hive.box(userInfoBox).get(accessTokenKey)}"},
|
||||
);
|
||||
|
||||
final AssetEntity? entity;
|
||||
|
||||
if (asset.type == 'IMAGE') {
|
||||
entity = await PhotoManager.editor.saveImage(
|
||||
res.bodyBytes,
|
||||
title: p.basename(asset.originalPath),
|
||||
);
|
||||
} else {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
File tempFile = await File('${tempDir.path}/$fileName').create();
|
||||
tempFile.writeAsBytesSync(res.bodyBytes);
|
||||
entity = await PhotoManager.editor.saveVideo(tempFile, title: fileName);
|
||||
}
|
||||
|
||||
if (entity != null) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error saving file $e");
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
||||
|
||||
class DownloadLoadingIndicator extends StatelessWidget {
|
||||
const DownloadLoadingIndicator({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 60,
|
||||
width: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const SpinKitDancingSquare(
|
||||
color: Colors.white,
|
||||
size: 30.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,35 +1,74 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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: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:immich_mobile/shared/models/immich_asset.model.dart';
|
||||
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
class VideoViewerPage extends StatelessWidget {
|
||||
// ignore: must_be_immutable
|
||||
class VideoViewerPage extends HookConsumerWidget {
|
||||
final String videoUrl;
|
||||
final ImmichAsset asset;
|
||||
ImmichAssetWithExif? assetDetail;
|
||||
final AssetService _assetService = AssetService();
|
||||
|
||||
const VideoViewerPage({Key? key, required this.videoUrl}) : super(key: key);
|
||||
VideoViewerPage({Key? key, required this.videoUrl, required this.asset}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final downloadAssetStatus = ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
||||
|
||||
String jwtToken = Hive.box(userInfoBox).get(accessTokenKey);
|
||||
|
||||
getAssetExif() async {
|
||||
assetDetail = await _assetService.getAssetById(asset.id);
|
||||
}
|
||||
|
||||
useEffect(() {
|
||||
getAssetExif();
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
systemOverlayStyle: SystemUiOverlayStyle.light,
|
||||
backgroundColor: Colors.black,
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
AutoRouter.of(context).pop();
|
||||
appBar: TopControlAppBar(
|
||||
asset: asset,
|
||||
onMoreInfoPressed: () {
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.black,
|
||||
barrierColor: Colors.transparent,
|
||||
isScrollControlled: false,
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ExifBottomSheet(assetDetail: assetDetail!);
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back_ios)),
|
||||
);
|
||||
},
|
||||
onDownloadPressed: () {
|
||||
ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context);
|
||||
},
|
||||
),
|
||||
body: SafeArea(
|
||||
child: VideoThumbnailPlayer(
|
||||
url: videoUrl,
|
||||
jwtToken: jwtToken,
|
||||
child: Stack(
|
||||
children: [
|
||||
VideoThumbnailPlayer(
|
||||
url: videoUrl,
|
||||
jwtToken: jwtToken,
|
||||
),
|
||||
if (downloadAssetStatus == DownloadAssetStatus.loading)
|
||||
const Center(
|
||||
child: DownloadLoadingIndicator(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -0,0 +1,67 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:immich_mobile/constants/hive_box.dart';
|
||||
import 'package:immich_mobile/utils/capitalize_first_letter.dart';
|
||||
|
||||
class ThumbnailWithInfo extends StatelessWidget {
|
||||
const ThumbnailWithInfo({Key? key, required this.textInfo, required this.imageUrl, required this.onTap})
|
||||
: super(key: key);
|
||||
|
||||
final String textInfo;
|
||||
final String imageUrl;
|
||||
final Function onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var box = Hive.box(userInfoBox);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
onTap();
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width / 2,
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
Container(
|
||||
foregroundDecoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: Colors.black26,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: CachedNetworkImage(
|
||||
width: 250,
|
||||
height: 250,
|
||||
fit: BoxFit.cover,
|
||||
imageUrl: imageUrl,
|
||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 8,
|
||||
left: 10,
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width / 3,
|
||||
child: Text(
|
||||
textInfo.capitalizeFirstLetter(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue