diff --git a/Makefile b/Makefile
index 767b9e2fee..8cc41062bb 100644
--- a/Makefile
+++ b/Makefile
@@ -8,4 +8,7 @@ dev-scale:
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich_server=3 --remove-orphans
prod:
- docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans
\ No newline at end of file
+ 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
\ No newline at end of file
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 6a0016f59c..5bc9de64a6 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -22,6 +22,7 @@ services:
- database
networks:
- immich_network
+ restart: unless-stopped
immich_microservices:
image: immich-microservices:1.4.0
@@ -43,7 +44,7 @@ services:
- database
networks:
- immich_network
-
+ restart: unless-stopped
redis:
container_name: immich_redis
diff --git a/microservices/src/image-classifier/image-classifier.service.ts b/microservices/src/image-classifier/image-classifier.service.ts
index d65abb8a61..5c61918b3f 100644
--- a/microservices/src/image-classifier/image-classifier.service.ts
+++ b/microservices/src/image-classifier/image-classifier.service.ts
@@ -39,6 +39,7 @@ export class ImageClassifierService {
}
}
+ tf.dispose(decodedImage);
return tags;
}
} catch (e) {
diff --git a/microservices/src/object-detection/object-detection.service.ts b/microservices/src/object-detection/object-detection.service.ts
index 532133f3ca..b93b68beb4 100644
--- a/microservices/src/object-detection/object-detection.service.ts
+++ b/microservices/src/object-detection/object-detection.service.ts
@@ -29,6 +29,7 @@ export class ObjectDetectionService {
}
}
+ tf.dispose(decodedImage);
return [...tags];
}
} catch (e) {
diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle
index f2faf08058..2f7ca021ff 100644
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -51,7 +51,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "app.alextran.immich"
- minSdkVersion 20
+ minSdkVersion 21
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml
index f9fd6c9379..a0be4dff2c 100644
--- a/mobile/android/app/src/main/AndroidManifest.xml
+++ b/mobile/android/app/src/main/AndroidManifest.xml
@@ -20,4 +20,7 @@
+
+
+
\ No newline at end of file
diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock
index 6c2db2cba5..fa4fd0b51d 100644
--- a/mobile/ios/Podfile.lock
+++ b/mobile/ios/Podfile.lock
@@ -13,7 +13,7 @@ PODS:
- Flutter
- path_provider_ios (0.0.1):
- Flutter
- - photo_manager (1.0.0):
+ - photo_manager (2.0.0):
- Flutter
- FlutterMacOS
- SAMKeychain (1.5.3)
@@ -70,7 +70,7 @@ SPEC CHECKSUMS:
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
- photo_manager: 84fa94fbeb82e607333ea9a13c43b58e0903a463
+ photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist
index ebd0a876e5..47b9d604ff 100644
--- a/mobile/ios/Runner/Info.plist
+++ b/mobile/ios/Runner/Info.plist
@@ -1,66 +1,72 @@
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleDisplayName
- Immich
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- immich_mobile
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
- CFBundleSignature
- ????
- CFBundleVersion
- 2
- LSRequiresIPhoneOS
-
- MGLMapboxMetricsEnabledSettingShownInApp
-
- NSAppTransportSecurity
-
- NSAllowsArbitraryLoads
-
-
- NSLocationAlwaysUsageDescription
- Enable location setting to show position of assets on map
- NSLocationWhenInUseUsageDescription
- Enable location setting to show position of assets on map
- NSPhotoLibraryUsageDescription
- We need to manage backup your photos album
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UIUserInterfaceStyle
- Light
- UIViewControllerBasedStatusBarAppearance
-
- io.flutter.embedded_views_preview
-
- ITSAppUsesNonExemptEncryption
-
-
-
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Immich
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ immich_mobile
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 2
+ LSRequiresIPhoneOS
+
+ MGLMapboxMetricsEnabledSettingShownInApp
+
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
+ NSLocationAlwaysUsageDescription
+ Enable location setting to show position of assets on map
+
+ NSLocationWhenInUseUsageDescription
+ Enable location setting to show position of assets on map
+
+ NSPhotoLibraryUsageDescription
+ We need to manage backup your photos album
+
+ NSPhotoLibraryAddUsageDescription
+ We need to manage backup your photos album
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIUserInterfaceStyle
+ Light
+ UIViewControllerBasedStatusBarAppearance
+
+ io.flutter.embedded_views_preview
+
+ ITSAppUsesNonExemptEncryption
+
+
+
\ No newline at end of file
diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart
index 194cd42a96..cb847f8e61 100644
--- a/mobile/lib/main.dart
+++ b/mobile/lib/main.dart
@@ -97,6 +97,7 @@ class _ImmichAppState extends ConsumerState with WidgetsBindingObserv
textTheme: GoogleFonts.workSansTextTheme(
Theme.of(context).textTheme.apply(fontSizeFactor: 1.0),
),
+ snackBarTheme: SnackBarThemeData(contentTextStyle: TextStyle(fontFamily: GoogleFonts.workSans().fontFamily)),
scaffoldBackgroundColor: const Color(0xFFf6f8fe),
appBarTheme: const AppBarTheme(
backgroundColor: Colors.white,
diff --git a/mobile/lib/modules/asset_viewer/models/image_viewer_page_state.model.dart b/mobile/lib/modules/asset_viewer/models/image_viewer_page_state.model.dart
index 63280ff69d..8590ce6890 100644
--- a/mobile/lib/modules/asset_viewer/models/image_viewer_page_state.model.dart
+++ b/mobile/lib/modules/asset_viewer/models/image_viewer_page_state.model.dart
@@ -1,28 +1,34 @@
import 'dart:convert';
+enum DownloadAssetStatus { idle, loading, success, error }
+
class ImageViewerPageState {
- final bool isBottomSheetEnable;
+ // enum
+ final DownloadAssetStatus downloadAssetStatus;
+
ImageViewerPageState({
- required this.isBottomSheetEnable,
+ required this.downloadAssetStatus,
});
ImageViewerPageState copyWith({
- bool? isBottomSheetEnable,
+ DownloadAssetStatus? downloadAssetStatus,
}) {
return ImageViewerPageState(
- isBottomSheetEnable: isBottomSheetEnable ?? this.isBottomSheetEnable,
+ downloadAssetStatus: downloadAssetStatus ?? this.downloadAssetStatus,
);
}
Map toMap() {
- return {
- 'isBottomSheetEnable': isBottomSheetEnable,
- };
+ final result = {};
+
+ result.addAll({'downloadAssetStatus': downloadAssetStatus.index});
+
+ return result;
}
factory ImageViewerPageState.fromMap(Map map) {
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));
@override
- String toString() => 'ImageViewerPageState(isBottomSheetEnable: $isBottomSheetEnable)';
+ String toString() => 'ImageViewerPageState(downloadAssetStatus: $downloadAssetStatus)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
- return other is ImageViewerPageState && other.isBottomSheetEnable == isBottomSheetEnable;
+ return other is ImageViewerPageState && other.downloadAssetStatus == downloadAssetStatus;
}
@override
- int get hashCode => isBottomSheetEnable.hashCode;
+ int get hashCode => downloadAssetStatus.hashCode;
}
diff --git a/mobile/lib/modules/asset_viewer/models/request_download_asset_info.model.dart b/mobile/lib/modules/asset_viewer/models/request_download_asset_info.model.dart
new file mode 100644
index 0000000000..80a99bfe9c
--- /dev/null
+++ b/mobile/lib/modules/asset_viewer/models/request_download_asset_info.model.dart
@@ -0,0 +1,6 @@
+class RequestDownloadAssetInfo {
+ final String assetId;
+ final String deviceId;
+
+ RequestDownloadAssetInfo(this.assetId, this.deviceId);
+}
diff --git a/mobile/lib/modules/asset_viewer/models/store_model_here.txt b/mobile/lib/modules/asset_viewer/models/store_model_here.txt
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart b/mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart
index c7854a5316..1ba0029388 100644
--- a/mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart
+++ b/mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart
@@ -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 {
- ImageViewerPageStateNotifier() : super(ImageViewerPageState(isBottomSheetEnable: false));
+class ImageViewerStateNotifier extends StateNotifier {
+ 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(
- ((ref) => ImageViewerPageStateNotifier()));
+final imageViewerStateProvider =
+ StateNotifierProvider(((ref) => ImageViewerStateNotifier()));
diff --git a/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart b/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart
new file mode 100644
index 0000000000..08e766f4a9
--- /dev/null
+++ b/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart
@@ -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 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;
+ }
+}
diff --git a/mobile/lib/modules/asset_viewer/services/store_services_here.txt b/mobile/lib/modules/asset_viewer/services/store_services_here.txt
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/mobile/lib/modules/asset_viewer/ui/download_loading_indicator.dart b/mobile/lib/modules/asset_viewer/ui/download_loading_indicator.dart
new file mode 100644
index 0000000000..f53d9692f4
--- /dev/null
+++ b/mobile/lib/modules/asset_viewer/ui/download_loading_indicator.dart
@@ -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,
+ ),
+ );
+ }
+}
diff --git a/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart b/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart
index c8b570c5ad..75a08c25b8 100644
--- a/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart
+++ b/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart
@@ -1,14 +1,19 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
-class TopControlAppBar extends StatelessWidget with PreferredSizeWidget {
- const TopControlAppBar({Key? key, required this.asset, required this.onMoreInfoPressed}) : super(key: key);
+class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
+ const TopControlAppBar(
+ {Key? key, required this.asset, required this.onMoreInfoPressed, required this.onDownloadPressed})
+ : super(key: key);
final ImmichAsset asset;
final Function onMoreInfoPressed;
+ final Function onDownloadPressed;
+
@override
- Widget build(BuildContext context) {
+ Widget build(BuildContext context, WidgetRef ref) {
double iconSize = 18.0;
return AppBar(
@@ -29,7 +34,7 @@ class TopControlAppBar extends StatelessWidget with PreferredSizeWidget {
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
- print("download");
+ onDownloadPressed();
},
icon: const Icon(Icons.cloud_download_rounded),
),
diff --git a/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart b/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart
index 4f3585e3c0..856e4c3764 100644
--- a/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart
+++ b/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart
@@ -4,6 +4,9 @@ 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: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';
@@ -25,6 +28,7 @@ class ImageViewerPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final downloadAssetStatus = ref.watch(imageViewerStateProvider).downloadAssetStatus;
var box = Hive.box(userInfoBox);
getAssetExif() async {
@@ -42,65 +46,77 @@ class ImageViewerPage extends HookConsumerWidget {
asset: asset,
onMoreInfoPressed: () {
showModalBottomSheet(
- backgroundColor: Colors.black,
- barrierColor: Colors.transparent,
- isScrollControlled: false,
- context: context,
- builder: (context) {
- return ExifBottomSheet(assetDetail: assetDetail!);
- });
+ backgroundColor: Colors.black,
+ barrierColor: Colors.transparent,
+ isScrollControlled: false,
+ context: context,
+ builder: (context) {
+ return ExifBottomSheet(assetDetail: assetDetail!);
+ },
+ );
+ },
+ onDownloadPressed: () {
+ ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context);
},
),
body: SafeArea(
- child: Center(
- child: Hero(
- tag: heroTag,
- child: CachedNetworkImage(
- fit: BoxFit.cover,
- imageUrl: imageUrl,
- httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
- fadeInDuration: const Duration(milliseconds: 250),
- errorWidget: (context, url, error) => ConstrainedBox(
- constraints: const BoxConstraints(maxWidth: 300),
- child: Wrap(
- spacing: 32,
- runSpacing: 32,
- alignment: WrapAlignment.center,
- children: [
- const Text(
- "Failed To Render Image - Possibly Corrupted Data",
- textAlign: TextAlign.center,
- style: TextStyle(fontSize: 16, color: Colors.white),
+ child: Stack(
+ children: [
+ Center(
+ child: Hero(
+ tag: heroTag,
+ child: CachedNetworkImage(
+ fit: BoxFit.cover,
+ imageUrl: imageUrl,
+ httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
+ fadeInDuration: const Duration(milliseconds: 250),
+ errorWidget: (context, url, error) => ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 300),
+ child: Wrap(
+ spacing: 32,
+ runSpacing: 32,
+ alignment: WrapAlignment.center,
+ children: [
+ const Text(
+ "Failed To Render Image - Possibly Corrupted Data",
+ textAlign: TextAlign.center,
+ style: TextStyle(fontSize: 16, color: Colors.white),
+ ),
+ SingleChildScrollView(
+ child: Text(
+ error.toString(),
+ textAlign: TextAlign.center,
+ style: TextStyle(fontSize: 12, color: Colors.grey[400]),
+ ),
+ ),
+ ],
),
- SingleChildScrollView(
- child: Text(
- error.toString(),
- textAlign: TextAlign.center,
- style: TextStyle(fontSize: 12, color: Colors.grey[400]),
+ ),
+ placeholder: (context, url) {
+ return CachedNetworkImage(
+ cacheKey: thumbnailUrl,
+ fit: BoxFit.cover,
+ imageUrl: thumbnailUrl,
+ httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
+ placeholderFadeInDuration: const Duration(milliseconds: 0),
+ progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
+ scale: 0.2,
+ child: CircularProgressIndicator(value: downloadProgress.progress),
),
- ),
- ],
+ errorWidget: (context, url, error) => Icon(
+ Icons.error,
+ color: Colors.grey[300],
+ ),
+ );
+ },
),
),
- placeholder: (context, url) {
- return CachedNetworkImage(
- cacheKey: thumbnailUrl,
- fit: BoxFit.cover,
- imageUrl: thumbnailUrl,
- httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
- placeholderFadeInDuration: const Duration(milliseconds: 0),
- progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
- scale: 0.2,
- child: CircularProgressIndicator(value: downloadProgress.progress),
- ),
- errorWidget: (context, url, error) => Icon(
- Icons.error,
- color: Colors.grey[300],
- ),
- );
- },
),
- ),
+ if (downloadAssetStatus == DownloadAssetStatus.loading)
+ const Center(
+ child: DownloadLoadingIndicator(),
+ ),
+ ],
),
),
);
diff --git a/mobile/lib/shared/views/video_viewer_page.dart b/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart
similarity index 50%
rename from mobile/lib/shared/views/video_viewer_page.dart
rename to mobile/lib/modules/asset_viewer/views/video_viewer_page.dart
index c80393d233..891128313f 100644
--- a/mobile/lib/shared/views/video_viewer_page.dart
+++ b/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart
@@ -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(),
+ ),
+ ],
),
),
);
diff --git a/mobile/lib/modules/home/ui/thumbnail_image.dart b/mobile/lib/modules/home/ui/thumbnail_image.dart
index d0dcf13cf3..023e12236d 100644
--- a/mobile/lib/modules/home/ui/thumbnail_image.dart
+++ b/mobile/lib/modules/home/ui/thumbnail_image.dart
@@ -65,8 +65,8 @@ class ThumbnailImage extends HookConsumerWidget {
} else {
AutoRouter.of(context).push(
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),
);
}
}
diff --git a/mobile/lib/modules/login/ui/login_form.dart b/mobile/lib/modules/login/ui/login_form.dart
index 76e6f5bcb7..28e086f221 100644
--- a/mobile/lib/modules/login/ui/login_form.dart
+++ b/mobile/lib/modules/login/ui/login_form.dart
@@ -128,9 +128,10 @@ class LoginButton extends ConsumerWidget {
AutoRouter.of(context).pushNamed("/tab-controller-page");
} else {
ImmichToast.show(
- context: context,
- msg: "Error logging you in, check server url, email and password!",
- toastType: ToastType.error);
+ context: context,
+ msg: "Error logging you in, check server url, email and password!",
+ toastType: ToastType.error,
+ );
}
},
child: const Text("Login"));
diff --git a/mobile/lib/modules/search/ui/thumbnail_with_info.dart b/mobile/lib/modules/search/ui/thumbnail_with_info.dart
new file mode 100644
index 0000000000..fe6912526b
--- /dev/null
+++ b/mobile/lib/modules/search/ui/thumbnail_with_info.dart
@@ -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,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/mobile/lib/modules/search/views/search_page.dart b/mobile/lib/modules/search/views/search_page.dart
index ba8bb6259b..9e8b3cd78e 100644
--- a/mobile/lib/modules/search/views/search_page.dart
+++ b/mobile/lib/modules/search/views/search_page.dart
@@ -1,5 +1,4 @@
import 'package:auto_route/auto_route.dart';
-import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/ui/search_bar.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/utils/capitalize_first_letter.dart';
@@ -40,12 +40,12 @@ class SearchPage extends HookConsumerWidget {
_buildPlaces() {
return curatedLocation.when(
- loading: () => const CircularProgressIndicator(),
+ loading: () => const SizedBox(width: 60, height: 60, child: CircularProgressIndicator.adaptive()),
error: (err, stack) => Text('Error: $err'),
data: (curatedLocations) {
return curatedLocations.isNotEmpty
? SizedBox(
- height: MediaQuery.of(context).size.width / 3,
+ height: MediaQuery.of(context).size.width / 2,
child: ListView.builder(
padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal,
@@ -66,7 +66,7 @@ class SearchPage extends HookConsumerWidget {
),
)
: SizedBox(
- height: MediaQuery.of(context).size.width / 3,
+ height: MediaQuery.of(context).size.width / 2,
child: ListView.builder(
padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal,
@@ -87,12 +87,12 @@ class SearchPage extends HookConsumerWidget {
_buildThings() {
return curatedObjects.when(
- loading: () => const CircularProgressIndicator(),
+ loading: () => const SizedBox(width: 60, height: 60, child: CircularProgressIndicator.adaptive()),
error: (err, stack) => Text('Error: $err'),
data: (objects) {
return objects.isNotEmpty
? SizedBox(
- height: MediaQuery.of(context).size.width / 3,
+ height: MediaQuery.of(context).size.width / 2,
child: ListView.builder(
padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal,
@@ -114,7 +114,7 @@ class SearchPage extends HookConsumerWidget {
),
)
: SizedBox(
- height: MediaQuery.of(context).size.width / 3,
+ height: MediaQuery.of(context).size.width / 2,
child: ListView.builder(
padding: const EdgeInsets.only(left: 16),
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,
- ),
- ),
- ),
- ),
- ],
- ),
- ),
- ),
- );
- }
-}
diff --git a/mobile/lib/modules/search/views/search_result_page.dart b/mobile/lib/modules/search/views/search_result_page.dart
index 6f4841861d..456db879ef 100644
--- a/mobile/lib/modules/search/views/search_result_page.dart
+++ b/mobile/lib/modules/search/views/search_result_page.dart
@@ -1,6 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_spinkit/flutter_spinkit.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/draggable_scrollbar.dart';
@@ -107,7 +108,10 @@ class SearchResultPage extends HookConsumerWidget {
}
if (searchResultPageState.isLoading) {
- return const CircularProgressIndicator.adaptive();
+ return Center(
+ child: SpinKitDancingSquare(
+ color: Theme.of(context).primaryColor,
+ ));
}
if (searchResultPageState.isSuccess) {
diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart
index 92dc4ae8e3..ee38bff71b 100644
--- a/mobile/lib/routing/router.dart
+++ b/mobile/lib/routing/router.dart
@@ -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/modules/asset_viewer/views/image_viewer_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';
diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart
index 040cd58299..06c0c599b0 100644
--- a/mobile/lib/routing/router.gr.dart
+++ b/mobile/lib/routing/router.gr.dart
@@ -44,7 +44,8 @@ class _$AppRouter extends RootStackRouter {
final args = routeData.argsAs();
return MaterialPageX(
routeData: routeData,
- child: VideoViewerPage(key: args.key, videoUrl: args.videoUrl));
+ child: VideoViewerPage(
+ key: args.key, videoUrl: args.videoUrl, asset: args.asset));
},
BackupControllerRoute.name: (routeData) {
return MaterialPageX(
@@ -163,24 +164,29 @@ class ImageViewerRouteArgs {
/// generated route for
/// [VideoViewerPage]
class VideoViewerRoute extends PageRouteInfo {
- VideoViewerRoute({Key? key, required String videoUrl})
+ VideoViewerRoute(
+ {Key? key, required String videoUrl, required ImmichAsset asset})
: super(VideoViewerRoute.name,
path: '/video-viewer-page',
- args: VideoViewerRouteArgs(key: key, videoUrl: videoUrl));
+ args: VideoViewerRouteArgs(
+ key: key, videoUrl: videoUrl, asset: asset));
static const String name = 'VideoViewerRoute';
}
class VideoViewerRouteArgs {
- const VideoViewerRouteArgs({this.key, required this.videoUrl});
+ const VideoViewerRouteArgs(
+ {this.key, required this.videoUrl, required this.asset});
final Key? key;
final String videoUrl;
+ final ImmichAsset asset;
+
@override
String toString() {
- return 'VideoViewerRouteArgs{key: $key, videoUrl: $videoUrl}';
+ return 'VideoViewerRouteArgs{key: $key, videoUrl: $videoUrl, asset: $asset}';
}
}
diff --git a/mobile/lib/shared/models/immich_asset.model.dart b/mobile/lib/shared/models/immich_asset.model.dart
index 0073dfccc7..5097a36829 100644
--- a/mobile/lib/shared/models/immich_asset.model.dart
+++ b/mobile/lib/shared/models/immich_asset.model.dart
@@ -10,6 +10,8 @@ class ImmichAsset {
final String modifiedAt;
final bool isFavorite;
final String? duration;
+ final String originalPath;
+ final String resizePath;
ImmichAsset({
required this.id,
@@ -21,6 +23,8 @@ class ImmichAsset {
required this.modifiedAt,
required this.isFavorite,
this.duration,
+ required this.originalPath,
+ required this.resizePath,
});
ImmichAsset copyWith({
@@ -33,6 +37,8 @@ class ImmichAsset {
String? modifiedAt,
bool? isFavorite,
String? duration,
+ String? originalPath,
+ String? resizePath,
}) {
return ImmichAsset(
id: id ?? this.id,
@@ -44,6 +50,8 @@ class ImmichAsset {
modifiedAt: modifiedAt ?? this.modifiedAt,
isFavorite: isFavorite ?? this.isFavorite,
duration: duration ?? this.duration,
+ originalPath: originalPath ?? this.originalPath,
+ resizePath: resizePath ?? this.resizePath,
);
}
@@ -58,6 +66,8 @@ class ImmichAsset {
'modifiedAt': modifiedAt,
'isFavorite': isFavorite,
'duration': duration,
+ 'originalPath': originalPath,
+ 'resizePath': resizePath,
};
}
@@ -72,6 +82,8 @@ class ImmichAsset {
modifiedAt: map['modifiedAt'] ?? '',
isFavorite: map['isFavorite'] ?? false,
duration: map['duration'],
+ originalPath: map['originalPath'] ?? '',
+ resizePath: map['resizePath'] ?? '',
);
}
@@ -81,7 +93,7 @@ class ImmichAsset {
@override
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
@@ -97,7 +109,9 @@ class ImmichAsset {
other.createdAt == createdAt &&
other.modifiedAt == modifiedAt &&
other.isFavorite == isFavorite &&
- other.duration == duration;
+ other.duration == duration &&
+ other.originalPath == originalPath &&
+ other.resizePath == resizePath;
}
@override
@@ -110,6 +124,8 @@ class ImmichAsset {
createdAt.hashCode ^
modifiedAt.hashCode ^
isFavorite.hashCode ^
- duration.hashCode;
+ duration.hashCode ^
+ originalPath.hashCode ^
+ resizePath.hashCode;
}
}
diff --git a/mobile/lib/shared/services/backup.service.dart b/mobile/lib/shared/services/backup.service.dart
index c2e0a3552e..d4af18959d 100644
--- a/mobile/lib/shared/services/backup.service.dart
+++ b/mobile/lib/shared/services/backup.service.dart
@@ -73,7 +73,7 @@ class BackupService {
});
// Build thumbnail multipart data
- var thumbnailData = await entity.thumbDataWithSize(1280, 720);
+ var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(720, 1280));
if (thumbnailData != null) {
thumbnailUploadData = MultipartFile.fromBytes(
List.from(thumbnailData),
diff --git a/mobile/lib/shared/services/network.service.dart b/mobile/lib/shared/services/network.service.dart
index e9f1fc2245..45d740e4fc 100644
--- a/mobile/lib/shared/services/network.service.dart
+++ b/mobile/lib/shared/services/network.service.dart
@@ -1,3 +1,4 @@
+import 'dart:async';
import 'dart:convert';
import 'package:dio/dio.dart';
@@ -25,16 +26,36 @@ class NetworkService {
}
}
- Future getRequest({required String url}) async {
+ Future getRequest({required String url, bool isByteResponse = false, bool isStreamReponse = false}) async {
try {
var dio = Dio();
dio.interceptors.add(AuthenticatedRequestInterceptor());
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
- Response res = await dio.get('$savedEndpoint/$url');
- if (res.statusCode == 200) {
- return res;
+ if (isByteResponse) {
+ Response> res = await dio.get>(
+ '$savedEndpoint/$url',
+ options: Options(responseType: ResponseType.bytes),
+ );
+
+ if (res.statusCode == 200) {
+ return res;
+ }
+ } else if (isStreamReponse) {
+ Response res = await dio.get(
+ '$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) {
debugPrint("DioError: ${e.response}");
diff --git a/mobile/lib/shared/ui/immich_toast.dart b/mobile/lib/shared/ui/immich_toast.dart
index 89781ccb48..b28ef1619c 100644
--- a/mobile/lib/shared/ui/immich_toast.dart
+++ b/mobile/lib/shared/ui/immich_toast.dart
@@ -8,12 +8,24 @@ class ImmichToast {
required BuildContext context,
required String msg,
ToastType toastType = ToastType.info,
+ ToastGravity gravity = ToastGravity.TOP,
}) {
FToast fToast;
fToast = FToast();
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(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
@@ -36,8 +48,8 @@ class ImmichToast {
: Container(),
(toastType == ToastType.success)
? const Icon(
- Icons.check,
- color: Color.fromARGB(255, 104, 248, 140),
+ Icons.check_circle_rounded,
+ color: Color.fromARGB(255, 78, 140, 124),
)
: Container(),
(toastType == ToastType.error)
@@ -53,7 +65,7 @@ class ImmichToast {
child: Text(
msg,
style: TextStyle(
- color: Theme.of(context).primaryColor,
+ color: _getColor(toastType, context),
fontWeight: FontWeight.bold,
fontSize: 15,
),
@@ -62,7 +74,7 @@ class ImmichToast {
],
),
),
- gravity: ToastGravity.TOP,
+ gravity: gravity,
toastDuration: const Duration(seconds: 2),
);
}
diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock
index 0e4b2479e8..d395fb34cd 100644
--- a/mobile/pubspec.lock
+++ b/mobile/pubspec.lock
@@ -328,6 +328,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: "direct dev"
description: flutter
@@ -680,7 +687,7 @@ packages:
name: photo_manager
url: "https://pub.dartlang.org"
source: hosted
- version: "1.3.10"
+ version: "2.0.6"
photo_view:
dependency: "direct main"
description:
diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml
index 6ba29316fc..dea871b0bb 100644
--- a/mobile/pubspec.yaml
+++ b/mobile/pubspec.yaml
@@ -11,7 +11,7 @@ dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
- photo_manager: ^1.3.10
+ photo_manager: ^2.0.6
flutter_hooks: ^0.18.0
hooks_riverpod: ^2.0.0-dev.0
hive:
@@ -33,11 +33,11 @@ dependencies:
badges: ^2.0.2
photo_view: ^0.13.0
socket_io_client: ^2.0.0-beta.4-nullsafety.0
- # mapbox_gl: ^0.15.0
flutter_map: ^0.14.0
flutter_udid: ^2.0.0
package_info_plus: ^1.4.0
-
+ flutter_spinkit: ^5.1.0
+
dev_dependencies:
flutter_test:
sdk: flutter
diff --git a/server/src/api-v1/asset/asset.controller.ts b/server/src/api-v1/asset/asset.controller.ts
index 4af564caf6..5bf9fd6764 100644
--- a/server/src/api-v1/asset/asset.controller.ts
+++ b/server/src/api-v1/asset/asset.controller.ts
@@ -76,6 +76,15 @@ export class AssetController {
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')
async serveFile(
@Headers() headers,
diff --git a/server/src/api-v1/asset/asset.service.ts b/server/src/api-v1/asset/asset.service.ts
index 487e06571d..a217681366 100644
--- a/server/src/api-v1/asset/asset.service.ts
+++ b/server/src/api-v1/asset/asset.service.ts
@@ -13,6 +13,7 @@ import { Response as Res } from 'express';
import { promisify } from 'util';
import { DeleteAssetDto } from './dto/delete-asset.dto';
import { SearchAssetDto } from './dto/search-asset.dto';
+import path from 'path';
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) {
let file = null;
const asset = await this.findOne(authUser, query.did, query.aid);
+ if (!asset) {
+ throw new BadRequestException('Asset does not exist');
+ }
// Handle Sending Images
if (asset.type == AssetType.IMAGE || query.isThumb == 'true') {
res.set({