1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

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 page
This commit is contained in:
Alex 2022-04-02 12:31:53 -05:00 committed by GitHub
parent 60df387459
commit 90ef64efa3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 538 additions and 257 deletions

View file

@ -9,3 +9,6 @@ dev-scale:
prod: prod:
docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans
prod-scale:
docker-compose -f ./docker/docker-compose.yml up --build -V --scale immich_server=3 --scale immich_microservices=3 --remove-orphans

View file

@ -22,6 +22,7 @@ services:
- database - database
networks: networks:
- immich_network - immich_network
restart: unless-stopped
immich_microservices: immich_microservices:
image: immich-microservices:1.4.0 image: immich-microservices:1.4.0
@ -43,7 +44,7 @@ services:
- database - database
networks: networks:
- immich_network - immich_network
restart: unless-stopped
redis: redis:
container_name: immich_redis container_name: immich_redis

View file

@ -39,6 +39,7 @@ export class ImageClassifierService {
} }
} }
tf.dispose(decodedImage);
return tags; return tags;
} }
} catch (e) { } catch (e) {

View file

@ -29,6 +29,7 @@ export class ObjectDetectionService {
} }
} }
tf.dispose(decodedImage);
return [...tags]; return [...tags];
} }
} catch (e) { } catch (e) {

View file

@ -51,7 +51,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "app.alextran.immich" applicationId "app.alextran.immich"
minSdkVersion 20 minSdkVersion 21
targetSdkVersion flutter.targetSdkVersion targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName

View file

@ -20,4 +20,7 @@
</application> </application>
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
</manifest> </manifest>

View file

@ -13,7 +13,7 @@ PODS:
- Flutter - Flutter
- path_provider_ios (0.0.1): - path_provider_ios (0.0.1):
- Flutter - Flutter
- photo_manager (1.0.0): - photo_manager (2.0.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- SAMKeychain (1.5.3) - SAMKeychain (1.5.3)
@ -70,7 +70,7 @@ SPEC CHECKSUMS:
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5 path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
photo_manager: 84fa94fbeb82e607333ea9a13c43b58e0903a463 photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
@ -23,20 +23,26 @@
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>2</string> <string>2</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true />
<key>MGLMapboxMetricsEnabledSettingShownInApp</key> <key>MGLMapboxMetricsEnabledSettingShownInApp</key>
<true/> <true />
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
<true/> <true />
</dict> </dict>
<key>NSLocationAlwaysUsageDescription</key> <key>NSLocationAlwaysUsageDescription</key>
<string>Enable location setting to show position of assets on map</string> <string>Enable location setting to show position of assets on map</string>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string>Enable location setting to show position of assets on map</string> <string>Enable location setting to show position of assets on map</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>We need to manage backup your photos album</string> <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> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
@ -57,10 +63,10 @@
<key>UIUserInterfaceStyle</key> <key>UIUserInterfaceStyle</key>
<string>Light</string> <string>Light</string>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<true/> <true />
<key>io.flutter.embedded_views_preview</key> <key>io.flutter.embedded_views_preview</key>
<true/> <true />
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false />
</dict> </dict>
</plist> </plist>

View file

@ -97,6 +97,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
textTheme: GoogleFonts.workSansTextTheme( textTheme: GoogleFonts.workSansTextTheme(
Theme.of(context).textTheme.apply(fontSizeFactor: 1.0), Theme.of(context).textTheme.apply(fontSizeFactor: 1.0),
), ),
snackBarTheme: SnackBarThemeData(contentTextStyle: TextStyle(fontFamily: GoogleFonts.workSans().fontFamily)),
scaffoldBackgroundColor: const Color(0xFFf6f8fe), scaffoldBackgroundColor: const Color(0xFFf6f8fe),
appBarTheme: const AppBarTheme( appBarTheme: const AppBarTheme(
backgroundColor: Colors.white, backgroundColor: Colors.white,

View file

@ -1,28 +1,34 @@
import 'dart:convert'; import 'dart:convert';
enum DownloadAssetStatus { idle, loading, success, error }
class ImageViewerPageState { class ImageViewerPageState {
final bool isBottomSheetEnable; // enum
final DownloadAssetStatus downloadAssetStatus;
ImageViewerPageState({ ImageViewerPageState({
required this.isBottomSheetEnable, required this.downloadAssetStatus,
}); });
ImageViewerPageState copyWith({ ImageViewerPageState copyWith({
bool? isBottomSheetEnable, DownloadAssetStatus? downloadAssetStatus,
}) { }) {
return ImageViewerPageState( return ImageViewerPageState(
isBottomSheetEnable: isBottomSheetEnable ?? this.isBottomSheetEnable, downloadAssetStatus: downloadAssetStatus ?? this.downloadAssetStatus,
); );
} }
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { final result = <String, dynamic>{};
'isBottomSheetEnable': isBottomSheetEnable,
}; result.addAll({'downloadAssetStatus': downloadAssetStatus.index});
return result;
} }
factory ImageViewerPageState.fromMap(Map<String, dynamic> map) { factory ImageViewerPageState.fromMap(Map<String, dynamic> map) {
return ImageViewerPageState( return ImageViewerPageState(
isBottomSheetEnable: map['isBottomSheetEnable'] ?? false, downloadAssetStatus: DownloadAssetStatus.values[map['downloadAssetStatus'] ?? 0],
); );
} }
@ -31,15 +37,15 @@ class ImageViewerPageState {
factory ImageViewerPageState.fromJson(String source) => ImageViewerPageState.fromMap(json.decode(source)); factory ImageViewerPageState.fromJson(String source) => ImageViewerPageState.fromMap(json.decode(source));
@override @override
String toString() => 'ImageViewerPageState(isBottomSheetEnable: $isBottomSheetEnable)'; String toString() => 'ImageViewerPageState(downloadAssetStatus: $downloadAssetStatus)';
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
return other is ImageViewerPageState && other.isBottomSheetEnable == isBottomSheetEnable; return other is ImageViewerPageState && other.downloadAssetStatus == downloadAssetStatus;
} }
@override @override
int get hashCode => isBottomSheetEnable.hashCode; int get hashCode => downloadAssetStatus.hashCode;
} }

View file

@ -0,0 +1,6 @@
class RequestDownloadAssetInfo {
final String assetId;
final String deviceId;
RequestDownloadAssetInfo(this.assetId, this.deviceId);
}

View file

@ -1,21 +1,43 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/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/models/immich_asset.model.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
class ImageViewerPageStateNotifier extends StateNotifier<ImageViewerPageState> { class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
ImageViewerPageStateNotifier() : super(ImageViewerPageState(isBottomSheetEnable: false)); final ImageViewerService _imageViewerService = ImageViewerService();
void toggleBottomSheet() { ImageViewerStateNotifier() : super(ImageViewerPageState(downloadAssetStatus: DownloadAssetStatus.idle));
bool isBottomSheetEnable = state.isBottomSheetEnable;
if (isBottomSheetEnable) { void downloadAsset(ImmichAsset asset, BuildContext context) async {
state.copyWith(isBottomSheetEnable: false); 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 { } 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>( final imageViewerStateProvider =
((ref) => ImageViewerPageStateNotifier())); StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(((ref) => ImageViewerStateNotifier()));

View file

@ -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;
}
}

View file

@ -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,
),
);
}
}

View file

@ -1,14 +1,19 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart'; import 'package:immich_mobile/shared/models/immich_asset.model.dart';
class TopControlAppBar extends StatelessWidget with PreferredSizeWidget { class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
const TopControlAppBar({Key? key, required this.asset, required this.onMoreInfoPressed}) : super(key: key); const TopControlAppBar(
{Key? key, required this.asset, required this.onMoreInfoPressed, required this.onDownloadPressed})
: super(key: key);
final ImmichAsset asset; final ImmichAsset asset;
final Function onMoreInfoPressed; final Function onMoreInfoPressed;
final Function onDownloadPressed;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
double iconSize = 18.0; double iconSize = 18.0;
return AppBar( return AppBar(
@ -29,7 +34,7 @@ class TopControlAppBar extends StatelessWidget with PreferredSizeWidget {
iconSize: iconSize, iconSize: iconSize,
splashRadius: iconSize, splashRadius: iconSize,
onPressed: () { onPressed: () {
print("download"); onDownloadPressed();
}, },
icon: const Icon(Icons.cloud_download_rounded), icon: const Icon(Icons.cloud_download_rounded),
), ),

View file

@ -4,6 +4,9 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.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/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/home/services/asset.service.dart'; import 'package:immich_mobile/modules/home/services/asset.service.dart';
@ -25,6 +28,7 @@ class ImageViewerPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final downloadAssetStatus = ref.watch(imageViewerStateProvider).downloadAssetStatus;
var box = Hive.box(userInfoBox); var box = Hive.box(userInfoBox);
getAssetExif() async { getAssetExif() async {
@ -48,11 +52,17 @@ class ImageViewerPage extends HookConsumerWidget {
context: context, context: context,
builder: (context) { builder: (context) {
return ExifBottomSheet(assetDetail: assetDetail!); return ExifBottomSheet(assetDetail: assetDetail!);
}); },
);
},
onDownloadPressed: () {
ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context);
}, },
), ),
body: SafeArea( body: SafeArea(
child: Center( child: Stack(
children: [
Center(
child: Hero( child: Hero(
tag: heroTag, tag: heroTag,
child: CachedNetworkImage( child: CachedNetworkImage(
@ -102,6 +112,12 @@ class ImageViewerPage extends HookConsumerWidget {
), ),
), ),
), ),
if (downloadAssetStatus == DownloadAssetStatus.loading)
const Center(
child: DownloadLoadingIndicator(),
),
],
),
), ),
); );
} }

View file

@ -1,36 +1,75 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.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:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/constants/hive_box.dart';
import 'package:chewie/chewie.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'; import 'package:video_player/video_player.dart';
class VideoViewerPage extends StatelessWidget { // ignore: must_be_immutable
class VideoViewerPage extends HookConsumerWidget {
final String videoUrl; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final downloadAssetStatus = ref.watch(imageViewerStateProvider).downloadAssetStatus;
String jwtToken = Hive.box(userInfoBox).get(accessTokenKey); String jwtToken = Hive.box(userInfoBox).get(accessTokenKey);
getAssetExif() async {
assetDetail = await _assetService.getAssetById(asset.id);
}
useEffect(() {
getAssetExif();
return null;
}, []);
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.black,
appBar: AppBar( appBar: TopControlAppBar(
systemOverlayStyle: SystemUiOverlayStyle.light, asset: asset,
onMoreInfoPressed: () {
showModalBottomSheet(
backgroundColor: Colors.black, backgroundColor: Colors.black,
leading: IconButton( barrierColor: Colors.transparent,
onPressed: () { isScrollControlled: false,
AutoRouter.of(context).pop(); context: context,
builder: (context) {
return ExifBottomSheet(assetDetail: assetDetail!);
},
);
},
onDownloadPressed: () {
ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context);
}, },
icon: const Icon(Icons.arrow_back_ios)),
), ),
body: SafeArea( body: SafeArea(
child: VideoThumbnailPlayer( child: Stack(
children: [
VideoThumbnailPlayer(
url: videoUrl, url: videoUrl,
jwtToken: jwtToken, jwtToken: jwtToken,
), ),
if (downloadAssetStatus == DownloadAssetStatus.loading)
const Center(
child: DownloadLoadingIndicator(),
),
],
),
), ),
); );
} }

View file

@ -66,7 +66,7 @@ class ThumbnailImage extends HookConsumerWidget {
AutoRouter.of(context).push( AutoRouter.of(context).push(
VideoViewerRoute( VideoViewerRoute(
videoUrl: '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}', videoUrl: '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
), asset: asset),
); );
} }
} }

View file

@ -130,7 +130,8 @@ class LoginButton extends ConsumerWidget {
ImmichToast.show( ImmichToast.show(
context: context, context: context,
msg: "Error logging you in, check server url, email and password!", msg: "Error logging you in, check server url, email and password!",
toastType: ToastType.error); toastType: ToastType.error,
);
} }
}, },
child: const Text("Login")); child: const Text("Login"));

View file

@ -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,
),
),
),
),
],
),
),
),
);
}
}

View file

@ -1,5 +1,4 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
@ -10,6 +9,7 @@ import 'package:immich_mobile/modules/search/models/curated_object.model.dart';
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
import 'package:immich_mobile/modules/search/ui/search_bar.dart'; import 'package:immich_mobile/modules/search/ui/search_bar.dart';
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart'; import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/capitalize_first_letter.dart'; import 'package:immich_mobile/utils/capitalize_first_letter.dart';
@ -40,12 +40,12 @@ class SearchPage extends HookConsumerWidget {
_buildPlaces() { _buildPlaces() {
return curatedLocation.when( return curatedLocation.when(
loading: () => const CircularProgressIndicator(), loading: () => const SizedBox(width: 60, height: 60, child: CircularProgressIndicator.adaptive()),
error: (err, stack) => Text('Error: $err'), error: (err, stack) => Text('Error: $err'),
data: (curatedLocations) { data: (curatedLocations) {
return curatedLocations.isNotEmpty return curatedLocations.isNotEmpty
? SizedBox( ? SizedBox(
height: MediaQuery.of(context).size.width / 3, height: MediaQuery.of(context).size.width / 2,
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.only(left: 16), padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@ -66,7 +66,7 @@ class SearchPage extends HookConsumerWidget {
), ),
) )
: SizedBox( : SizedBox(
height: MediaQuery.of(context).size.width / 3, height: MediaQuery.of(context).size.width / 2,
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.only(left: 16), padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@ -87,12 +87,12 @@ class SearchPage extends HookConsumerWidget {
_buildThings() { _buildThings() {
return curatedObjects.when( return curatedObjects.when(
loading: () => const CircularProgressIndicator(), loading: () => const SizedBox(width: 60, height: 60, child: CircularProgressIndicator.adaptive()),
error: (err, stack) => Text('Error: $err'), error: (err, stack) => Text('Error: $err'),
data: (objects) { data: (objects) {
return objects.isNotEmpty return objects.isNotEmpty
? SizedBox( ? SizedBox(
height: MediaQuery.of(context).size.width / 3, height: MediaQuery.of(context).size.width / 2,
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.only(left: 16), padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@ -114,7 +114,7 @@ class SearchPage extends HookConsumerWidget {
), ),
) )
: SizedBox( : SizedBox(
height: MediaQuery.of(context).size.width / 3, height: MediaQuery.of(context).size.width / 2,
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.only(left: 16), padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@ -172,66 +172,3 @@ class SearchPage extends HookConsumerWidget {
); );
} }
} }
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 / 3,
height: MediaQuery.of(context).size.width / 3,
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: 150,
height: 150,
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: 12,
),
),
),
),
],
),
),
),
);
}
}

View file

@ -1,6 +1,7 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/daily_title_text.dart'; import 'package:immich_mobile/modules/home/ui/daily_title_text.dart';
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart'; import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
@ -107,7 +108,10 @@ class SearchResultPage extends HookConsumerWidget {
} }
if (searchResultPageState.isLoading) { if (searchResultPageState.isLoading) {
return const CircularProgressIndicator.adaptive(); return Center(
child: SpinKitDancingSquare(
color: Theme.of(context).primaryColor,
));
} }
if (searchResultPageState.isSuccess) { if (searchResultPageState.isSuccess) {

View file

@ -9,7 +9,7 @@ import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:immich_mobile/shared/views/backup_controller_page.dart'; import 'package:immich_mobile/shared/views/backup_controller_page.dart';
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart'; import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
import 'package:immich_mobile/shared/views/tab_controller_page.dart'; import 'package:immich_mobile/shared/views/tab_controller_page.dart';
import 'package:immich_mobile/shared/views/video_viewer_page.dart'; import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
part 'router.gr.dart'; part 'router.gr.dart';

View file

@ -44,7 +44,8 @@ class _$AppRouter extends RootStackRouter {
final args = routeData.argsAs<VideoViewerRouteArgs>(); final args = routeData.argsAs<VideoViewerRouteArgs>();
return MaterialPageX<dynamic>( return MaterialPageX<dynamic>(
routeData: routeData, routeData: routeData,
child: VideoViewerPage(key: args.key, videoUrl: args.videoUrl)); child: VideoViewerPage(
key: args.key, videoUrl: args.videoUrl, asset: args.asset));
}, },
BackupControllerRoute.name: (routeData) { BackupControllerRoute.name: (routeData) {
return MaterialPageX<dynamic>( return MaterialPageX<dynamic>(
@ -163,24 +164,29 @@ class ImageViewerRouteArgs {
/// generated route for /// generated route for
/// [VideoViewerPage] /// [VideoViewerPage]
class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> { class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
VideoViewerRoute({Key? key, required String videoUrl}) VideoViewerRoute(
{Key? key, required String videoUrl, required ImmichAsset asset})
: super(VideoViewerRoute.name, : super(VideoViewerRoute.name,
path: '/video-viewer-page', path: '/video-viewer-page',
args: VideoViewerRouteArgs(key: key, videoUrl: videoUrl)); args: VideoViewerRouteArgs(
key: key, videoUrl: videoUrl, asset: asset));
static const String name = 'VideoViewerRoute'; static const String name = 'VideoViewerRoute';
} }
class VideoViewerRouteArgs { class VideoViewerRouteArgs {
const VideoViewerRouteArgs({this.key, required this.videoUrl}); const VideoViewerRouteArgs(
{this.key, required this.videoUrl, required this.asset});
final Key? key; final Key? key;
final String videoUrl; final String videoUrl;
final ImmichAsset asset;
@override @override
String toString() { String toString() {
return 'VideoViewerRouteArgs{key: $key, videoUrl: $videoUrl}'; return 'VideoViewerRouteArgs{key: $key, videoUrl: $videoUrl, asset: $asset}';
} }
} }

View file

@ -10,6 +10,8 @@ class ImmichAsset {
final String modifiedAt; final String modifiedAt;
final bool isFavorite; final bool isFavorite;
final String? duration; final String? duration;
final String originalPath;
final String resizePath;
ImmichAsset({ ImmichAsset({
required this.id, required this.id,
@ -21,6 +23,8 @@ class ImmichAsset {
required this.modifiedAt, required this.modifiedAt,
required this.isFavorite, required this.isFavorite,
this.duration, this.duration,
required this.originalPath,
required this.resizePath,
}); });
ImmichAsset copyWith({ ImmichAsset copyWith({
@ -33,6 +37,8 @@ class ImmichAsset {
String? modifiedAt, String? modifiedAt,
bool? isFavorite, bool? isFavorite,
String? duration, String? duration,
String? originalPath,
String? resizePath,
}) { }) {
return ImmichAsset( return ImmichAsset(
id: id ?? this.id, id: id ?? this.id,
@ -44,6 +50,8 @@ class ImmichAsset {
modifiedAt: modifiedAt ?? this.modifiedAt, modifiedAt: modifiedAt ?? this.modifiedAt,
isFavorite: isFavorite ?? this.isFavorite, isFavorite: isFavorite ?? this.isFavorite,
duration: duration ?? this.duration, duration: duration ?? this.duration,
originalPath: originalPath ?? this.originalPath,
resizePath: resizePath ?? this.resizePath,
); );
} }
@ -58,6 +66,8 @@ class ImmichAsset {
'modifiedAt': modifiedAt, 'modifiedAt': modifiedAt,
'isFavorite': isFavorite, 'isFavorite': isFavorite,
'duration': duration, 'duration': duration,
'originalPath': originalPath,
'resizePath': resizePath,
}; };
} }
@ -72,6 +82,8 @@ class ImmichAsset {
modifiedAt: map['modifiedAt'] ?? '', modifiedAt: map['modifiedAt'] ?? '',
isFavorite: map['isFavorite'] ?? false, isFavorite: map['isFavorite'] ?? false,
duration: map['duration'], duration: map['duration'],
originalPath: map['originalPath'] ?? '',
resizePath: map['resizePath'] ?? '',
); );
} }
@ -81,7 +93,7 @@ class ImmichAsset {
@override @override
String toString() { String toString() {
return 'ImmichAsset(id: $id, deviceAssetId: $deviceAssetId, userId: $userId, deviceId: $deviceId, type: $type, createdAt: $createdAt, modifiedAt: $modifiedAt, isFavorite: $isFavorite, duration: $duration)'; return 'ImmichAsset(id: $id, deviceAssetId: $deviceAssetId, userId: $userId, deviceId: $deviceId, type: $type, createdAt: $createdAt, modifiedAt: $modifiedAt, isFavorite: $isFavorite, duration: $duration, originalPath: $originalPath, resizePath: $resizePath)';
} }
@override @override
@ -97,7 +109,9 @@ class ImmichAsset {
other.createdAt == createdAt && other.createdAt == createdAt &&
other.modifiedAt == modifiedAt && other.modifiedAt == modifiedAt &&
other.isFavorite == isFavorite && other.isFavorite == isFavorite &&
other.duration == duration; other.duration == duration &&
other.originalPath == originalPath &&
other.resizePath == resizePath;
} }
@override @override
@ -110,6 +124,8 @@ class ImmichAsset {
createdAt.hashCode ^ createdAt.hashCode ^
modifiedAt.hashCode ^ modifiedAt.hashCode ^
isFavorite.hashCode ^ isFavorite.hashCode ^
duration.hashCode; duration.hashCode ^
originalPath.hashCode ^
resizePath.hashCode;
} }
} }

View file

@ -73,7 +73,7 @@ class BackupService {
}); });
// Build thumbnail multipart data // Build thumbnail multipart data
var thumbnailData = await entity.thumbDataWithSize(1280, 720); var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(720, 1280));
if (thumbnailData != null) { if (thumbnailData != null) {
thumbnailUploadData = MultipartFile.fromBytes( thumbnailUploadData = MultipartFile.fromBytes(
List.from(thumbnailData), List.from(thumbnailData),

View file

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
@ -25,17 +26,37 @@ class NetworkService {
} }
} }
Future<dynamic> getRequest({required String url}) async { Future<dynamic> getRequest({required String url, bool isByteResponse = false, bool isStreamReponse = false}) async {
try { try {
var dio = Dio(); var dio = Dio();
dio.interceptors.add(AuthenticatedRequestInterceptor()); dio.interceptors.add(AuthenticatedRequestInterceptor());
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey); var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
Response res = await dio.get('$savedEndpoint/$url');
if (isByteResponse) {
Response<List<int>> res = await dio.get<List<int>>(
'$savedEndpoint/$url',
options: Options(responseType: ResponseType.bytes),
);
if (res.statusCode == 200) { if (res.statusCode == 200) {
return res; return res;
} }
} else if (isStreamReponse) {
Response<ResponseBody> res = await dio.get<ResponseBody>(
'$savedEndpoint/$url',
options: Options(responseType: ResponseType.stream),
);
if (res.statusCode == 200) {
return res;
}
} else {
Response res = await dio.get('$savedEndpoint/$url');
if (res.statusCode == 200) {
return res;
}
}
} on DioError catch (e) { } on DioError catch (e) {
debugPrint("DioError: ${e.response}"); debugPrint("DioError: ${e.response}");
} catch (e) { } catch (e) {

View file

@ -8,12 +8,24 @@ class ImmichToast {
required BuildContext context, required BuildContext context,
required String msg, required String msg,
ToastType toastType = ToastType.info, ToastType toastType = ToastType.info,
ToastGravity gravity = ToastGravity.TOP,
}) { }) {
FToast fToast; FToast fToast;
fToast = FToast(); fToast = FToast();
fToast.init(context); fToast.init(context);
_getColor(ToastType type, BuildContext context) {
switch (type) {
case ToastType.info:
return Theme.of(context).primaryColor;
case ToastType.success:
return const Color.fromARGB(255, 78, 140, 124);
case ToastType.error:
return const Color.fromARGB(255, 220, 48, 85);
}
}
fToast.showToast( fToast.showToast(
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
@ -36,8 +48,8 @@ class ImmichToast {
: Container(), : Container(),
(toastType == ToastType.success) (toastType == ToastType.success)
? const Icon( ? const Icon(
Icons.check, Icons.check_circle_rounded,
color: Color.fromARGB(255, 104, 248, 140), color: Color.fromARGB(255, 78, 140, 124),
) )
: Container(), : Container(),
(toastType == ToastType.error) (toastType == ToastType.error)
@ -53,7 +65,7 @@ class ImmichToast {
child: Text( child: Text(
msg, msg,
style: TextStyle( style: TextStyle(
color: Theme.of(context).primaryColor, color: _getColor(toastType, context),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 15, fontSize: 15,
), ),
@ -62,7 +74,7 @@ class ImmichToast {
], ],
), ),
), ),
gravity: ToastGravity.TOP, gravity: gravity,
toastDuration: const Duration(seconds: 2), toastDuration: const Duration(seconds: 2),
); );
} }

View file

@ -328,6 +328,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0-dev.0" version: "2.0.0-dev.0"
flutter_spinkit:
dependency: "direct main"
description:
name: flutter_spinkit
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.0"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -680,7 +687,7 @@ packages:
name: photo_manager name: photo_manager
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.10" version: "2.0.6"
photo_view: photo_view:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -11,7 +11,7 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2
photo_manager: ^1.3.10 photo_manager: ^2.0.6
flutter_hooks: ^0.18.0 flutter_hooks: ^0.18.0
hooks_riverpod: ^2.0.0-dev.0 hooks_riverpod: ^2.0.0-dev.0
hive: hive:
@ -33,10 +33,10 @@ dependencies:
badges: ^2.0.2 badges: ^2.0.2
photo_view: ^0.13.0 photo_view: ^0.13.0
socket_io_client: ^2.0.0-beta.4-nullsafety.0 socket_io_client: ^2.0.0-beta.4-nullsafety.0
# mapbox_gl: ^0.15.0
flutter_map: ^0.14.0 flutter_map: ^0.14.0
flutter_udid: ^2.0.0 flutter_udid: ^2.0.0
package_info_plus: ^1.4.0 package_info_plus: ^1.4.0
flutter_spinkit: ^5.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -76,6 +76,15 @@ export class AssetController {
return 'ok'; return 'ok';
} }
@Get('/download')
async downloadFile(
@GetAuthUser() authUser: AuthUserDto,
@Response({ passthrough: true }) res: Res,
@Query(ValidationPipe) query: ServeFileDto,
) {
return this.assetService.downloadFile(authUser, query, res);
}
@Get('/file') @Get('/file')
async serveFile( async serveFile(
@Headers() headers, @Headers() headers,

View file

@ -13,6 +13,7 @@ import { Response as Res } from 'express';
import { promisify } from 'util'; import { promisify } from 'util';
import { DeleteAssetDto } from './dto/delete-asset.dto'; import { DeleteAssetDto } from './dto/delete-asset.dto';
import { SearchAssetDto } from './dto/search-asset.dto'; import { SearchAssetDto } from './dto/search-asset.dto';
import path from 'path';
const fileInfo = promisify(stat); const fileInfo = promisify(stat);
@ -146,10 +147,26 @@ export class AssetService {
}); });
} }
public async downloadFile(authUser: AuthUserDto, query: ServeFileDto, res: Res) {
let file = null;
const asset = await this.findOne(authUser, query.did, query.aid);
if (query.isThumb === 'false' || !query.isThumb) {
file = createReadStream(asset.originalPath);
} else {
file = createReadStream(asset.resizePath);
}
return new StreamableFile(file);
}
public async serveFile(authUser: AuthUserDto, query: ServeFileDto, res: Res, headers: any) { public async serveFile(authUser: AuthUserDto, query: ServeFileDto, res: Res, headers: any) {
let file = null; let file = null;
const asset = await this.findOne(authUser, query.did, query.aid); const asset = await this.findOne(authUser, query.did, query.aid);
if (!asset) {
throw new BadRequestException('Asset does not exist');
}
// Handle Sending Images // Handle Sending Images
if (asset.type == AssetType.IMAGE || query.isThumb == 'true') { if (asset.type == AssetType.IMAGE || query.isThumb == 'true') {
res.set({ res.set({