mirror of
https://github.com/immich-app/immich.git
synced 2025-01-11 06:10:28 +02:00
Get thumbnail from app (#68)
* Renamed multipart filed name 'files' to 'assetData'. * Added an additional field name of 'thumbnailData' to multipart form. * Implemented upload mechanism for thumbnail directly from the mobile client. * Removed dead code * Implemented a version checking mechanism.
This commit is contained in:
parent
be72df70fe
commit
e407a4fa13
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.DS_Store
|
13
PR_CHECKLIST.md
Normal file
13
PR_CHECKLIST.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Deployment checklist for iOS/Android/Server
|
||||||
|
|
||||||
|
[] Up version in [mobile/pubspec.yml](/mobile/pubspec.yaml)
|
||||||
|
|
||||||
|
[] Up version in [docker/docker-compose.yml](/docker/docker-compose.yml) for `immich_server` service
|
||||||
|
|
||||||
|
[] Up version in [docker/docker-compose.gpu.yml](/docker/docker-compose.gpu.yml) for `immich_server` service
|
||||||
|
|
||||||
|
[] Up version in [server/src/constants/server_version.constant.ts](/server/src/constants/server_version.constant.ts)
|
||||||
|
|
||||||
|
[] Up version in iOS Fastlane [/mobile/ios/fastlane/Fastfile](/mobile/ios/fastlane/Fastfile)
|
||||||
|
|
||||||
|
All of the version should be the same.
|
@ -47,8 +47,8 @@ This project is under heavy development, there will be continous functions, feat
|
|||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
- Upload assets(videos/images).
|
- Upload and view assets(videos/images).
|
||||||
- View assets.
|
- Multi-user supported.
|
||||||
- Quick navigation with drag scroll bar.
|
- Quick navigation with drag scroll bar.
|
||||||
- Auto Backup.
|
- Auto Backup.
|
||||||
- Support HEIC/HEIF Backup.
|
- Support HEIC/HEIF Backup.
|
||||||
@ -59,6 +59,7 @@ This project is under heavy development, there will be continous functions, feat
|
|||||||
- Upload assets from your local computer/server using [immich cli tools](https://www.npmjs.com/package/immich)
|
- Upload assets from your local computer/server using [immich cli tools](https://www.npmjs.com/package/immich)
|
||||||
- [Optional] Reserve geocoding using Mapbox (Generous free-tier of 100,000 search/month)
|
- [Optional] Reserve geocoding using Mapbox (Generous free-tier of 100,000 search/month)
|
||||||
- Show asset's location information on map (OpenStreetMap).
|
- Show asset's location information on map (OpenStreetMap).
|
||||||
|
- Show curated places on the search page
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
immich_server:
|
immich_server:
|
||||||
image: immich-server-dev:1.0.0
|
image: immich-server-dev:1.3.0
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
target: development
|
target: development
|
||||||
|
@ -2,7 +2,7 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
immich_server:
|
immich_server:
|
||||||
image: immich-server-dev:1.0.0
|
image: immich-server-dev:1.3.0
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
target: development
|
target: development
|
||||||
|
@ -9,6 +9,8 @@ PODS:
|
|||||||
- FMDB (2.7.5):
|
- FMDB (2.7.5):
|
||||||
- FMDB/standard (= 2.7.5)
|
- FMDB/standard (= 2.7.5)
|
||||||
- FMDB/standard (2.7.5)
|
- FMDB/standard (2.7.5)
|
||||||
|
- package_info_plus (0.4.5):
|
||||||
|
- Flutter
|
||||||
- path_provider_ios (0.0.1):
|
- path_provider_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- photo_manager (1.0.0):
|
- photo_manager (1.0.0):
|
||||||
@ -28,6 +30,7 @@ DEPENDENCIES:
|
|||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||||
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||||
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
||||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||||
@ -47,6 +50,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_udid/ios"
|
:path: ".symlinks/plugins/flutter_udid/ios"
|
||||||
fluttertoast:
|
fluttertoast:
|
||||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||||
|
package_info_plus:
|
||||||
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
path_provider_ios:
|
path_provider_ios:
|
||||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||||
photo_manager:
|
photo_manager:
|
||||||
@ -63,6 +68,7 @@ SPEC CHECKSUMS:
|
|||||||
flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c
|
flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c
|
||||||
fluttertoast: 6122fa75143e992b1d3470f61000f591a798cc58
|
fluttertoast: 6122fa75143e992b1d3470f61000f591a798cc58
|
||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||||
|
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
||||||
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
|
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
|
||||||
photo_manager: 84fa94fbeb82e607333ea9a13c43b58e0903a463
|
photo_manager: 84fa94fbeb82e607333ea9a13c43b58e0903a463
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
|
@ -18,6 +18,9 @@ default_platform(:ios)
|
|||||||
platform :ios do
|
platform :ios do
|
||||||
desc "iOS deployment"
|
desc "iOS deployment"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
|
increment_version_number(
|
||||||
|
version_number: "1.3.0" # Set a specific version number
|
||||||
|
)
|
||||||
increment_build_number({
|
increment_build_number({
|
||||||
build_number: latest_testflight_build_number + 1
|
build_number: latest_testflight_build_number + 1
|
||||||
})
|
})
|
||||||
|
@ -7,6 +7,7 @@ import 'package:immich_mobile/routing/router.dart';
|
|||||||
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
||||||
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
|
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
import 'constants/hive_box.dart';
|
import 'constants/hive_box.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
@ -43,7 +44,10 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
|
|||||||
ref.watch(backupProvider.notifier).resumeBackup();
|
ref.watch(backupProvider.notifier).resumeBackup();
|
||||||
ref.watch(websocketProvider.notifier).connect();
|
ref.watch(websocketProvider.notifier).connect();
|
||||||
ref.watch(assetProvider.notifier).getAllAsset();
|
ref.watch(assetProvider.notifier).getAllAsset();
|
||||||
|
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AppLifecycleState.inactive:
|
case AppLifecycleState.inactive:
|
||||||
debugPrint("[APP STATE] inactive");
|
debugPrint("[APP STATE] inactive");
|
||||||
ref.watch(appStateProvider.notifier).state = AppStateEnum.inactive;
|
ref.watch(appStateProvider.notifier).state = AppStateEnum.inactive;
|
||||||
@ -51,10 +55,12 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
|
|||||||
ref.watch(backupProvider.notifier).cancelBackup();
|
ref.watch(backupProvider.notifier).cancelBackup();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AppLifecycleState.paused:
|
case AppLifecycleState.paused:
|
||||||
debugPrint("[APP STATE] paused");
|
debugPrint("[APP STATE] paused");
|
||||||
ref.watch(appStateProvider.notifier).state = AppStateEnum.paused;
|
ref.watch(appStateProvider.notifier).state = AppStateEnum.paused;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AppLifecycleState.detached:
|
case AppLifecycleState.detached:
|
||||||
debugPrint("[APP STATE] detached");
|
debugPrint("[APP STATE] detached");
|
||||||
ref.watch(appStateProvider.notifier).state = AppStateEnum.detached;
|
ref.watch(appStateProvider.notifier).state = AppStateEnum.detached;
|
||||||
|
@ -7,7 +7,9 @@ import 'package:immich_mobile/modules/login/providers/authentication.provider.da
|
|||||||
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/backup_state.model.dart';
|
import 'package:immich_mobile/shared/models/backup_state.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/server_info_state.model.dart';
|
||||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
|
|
||||||
class ImmichSliverAppBar extends ConsumerWidget {
|
class ImmichSliverAppBar extends ConsumerWidget {
|
||||||
const ImmichSliverAppBar({
|
const ImmichSliverAppBar({
|
||||||
@ -21,6 +23,8 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final BackUpState _backupState = ref.watch(backupProvider);
|
final BackUpState _backupState = ref.watch(backupProvider);
|
||||||
bool _isEnableAutoBackup = ref.watch(authenticationProvider).deviceInfo.isAutoBackup;
|
bool _isEnableAutoBackup = ref.watch(authenticationProvider).deviceInfo.isAutoBackup;
|
||||||
|
final ServerInfoState _serverInfoState = ref.watch(serverInfoProvider);
|
||||||
|
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
floating: true,
|
floating: true,
|
||||||
@ -30,12 +34,46 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||||
leading: Builder(
|
leading: Builder(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return IconButton(
|
return Stack(
|
||||||
icon: const Icon(Icons.account_circle_rounded),
|
children: [
|
||||||
onPressed: () {
|
Positioned(
|
||||||
Scaffold.of(context).openDrawer();
|
top: 5,
|
||||||
},
|
child: IconButton(
|
||||||
tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
|
splashRadius: 25,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.account_circle_rounded,
|
||||||
|
size: 30,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
Scaffold.of(context).openDrawer();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_serverInfoState.isVersionMismatch
|
||||||
|
? Positioned(
|
||||||
|
bottom: 12,
|
||||||
|
right: 12,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => Scaffold.of(context).openDrawer(),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
elevation: 1,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(50.0),
|
||||||
|
),
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.all(2.0),
|
||||||
|
child: Icon(
|
||||||
|
Icons.info,
|
||||||
|
color: Color.fromARGB(255, 243, 188, 106),
|
||||||
|
size: 15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -5,7 +5,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/server_info_state.model.dart';
|
||||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
|
||||||
@ -15,6 +17,8 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
AuthenticationState _authState = ref.watch(authenticationProvider);
|
AuthenticationState _authState = ref.watch(authenticationProvider);
|
||||||
|
ServerInfoState _serverInfoState = ref.watch(serverInfoProvider);
|
||||||
|
|
||||||
final appInfo = useState({});
|
final appInfo = useState({});
|
||||||
|
|
||||||
_getPackageInfo() async {
|
_getPackageInfo() async {
|
||||||
@ -92,13 +96,70 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: Card(
|
||||||
"Version V${appInfo.value["version"]}+${appInfo.value["buildNumber"]}",
|
color: Colors.grey[100],
|
||||||
style: TextStyle(
|
child: Padding(
|
||||||
fontSize: 12,
|
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
|
||||||
color: Colors.grey[400],
|
child: Column(
|
||||||
fontWeight: FontWeight.bold,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
fontStyle: FontStyle.italic,
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
_serverInfoState.isVersionMismatch
|
||||||
|
? _serverInfoState.versionMismatchErrorMessage
|
||||||
|
: "Client and Server are up-to-date",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style:
|
||||||
|
TextStyle(fontSize: 11, color: Theme.of(context).primaryColor, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"App Version",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.grey[500],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.grey[500],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Server Version",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.grey[500],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"${_serverInfoState.serverVersion.major}.${_serverInfoState.serverVersion.minor}.${_serverInfoState.serverVersion.patch}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.grey[500],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -11,6 +11,7 @@ import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
|
|||||||
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
|
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/profile_drawer.dart';
|
import 'package:immich_mobile/modules/home/ui/profile_drawer.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
import 'package:sliver_tools/sliver_tools.dart';
|
import 'package:sliver_tools/sliver_tools.dart';
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ class HomePage extends HookConsumerWidget {
|
|||||||
useEffect(() {
|
useEffect(() {
|
||||||
ref.read(websocketProvider.notifier).connect();
|
ref.read(websocketProvider.notifier).connect();
|
||||||
ref.read(assetProvider.notifier).getAllAsset();
|
ref.read(assetProvider.notifier).getAllAsset();
|
||||||
|
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
||||||
return null;
|
return null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.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/shared/providers/server_info.provider.dart';
|
||||||
|
|
||||||
class TabNavigationObserver extends AutoRouterObserver {
|
class TabNavigationObserver extends AutoRouterObserver {
|
||||||
/// Riverpod Instance
|
/// Riverpod Instance
|
||||||
@ -26,5 +27,7 @@ class TabNavigationObserver extends AutoRouterObserver {
|
|||||||
// Refresh Location State
|
// Refresh Location State
|
||||||
ref.refresh(getCuratedLocationProvider);
|
ref.refresh(getCuratedLocationProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
78
mobile/lib/shared/models/server_info_state.model.dart
Normal file
78
mobile/lib/shared/models/server_info_state.model.dart
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/shared/models/mapbox_info.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/server_version.model.dart';
|
||||||
|
|
||||||
|
class ServerInfoState {
|
||||||
|
final MapboxInfo mapboxInfo;
|
||||||
|
final ServerVersion serverVersion;
|
||||||
|
final bool isVersionMismatch;
|
||||||
|
final String versionMismatchErrorMessage;
|
||||||
|
|
||||||
|
ServerInfoState({
|
||||||
|
required this.mapboxInfo,
|
||||||
|
required this.serverVersion,
|
||||||
|
required this.isVersionMismatch,
|
||||||
|
required this.versionMismatchErrorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
ServerInfoState copyWith({
|
||||||
|
MapboxInfo? mapboxInfo,
|
||||||
|
ServerVersion? serverVersion,
|
||||||
|
bool? isVersionMismatch,
|
||||||
|
String? versionMismatchErrorMessage,
|
||||||
|
}) {
|
||||||
|
return ServerInfoState(
|
||||||
|
mapboxInfo: mapboxInfo ?? this.mapboxInfo,
|
||||||
|
serverVersion: serverVersion ?? this.serverVersion,
|
||||||
|
isVersionMismatch: isVersionMismatch ?? this.isVersionMismatch,
|
||||||
|
versionMismatchErrorMessage: versionMismatchErrorMessage ?? this.versionMismatchErrorMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'mapboxInfo': mapboxInfo.toMap(),
|
||||||
|
'serverVersion': serverVersion.toMap(),
|
||||||
|
'isVersionMismatch': isVersionMismatch,
|
||||||
|
'versionMismatchErrorMessage': versionMismatchErrorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ServerInfoState.fromMap(Map<String, dynamic> map) {
|
||||||
|
return ServerInfoState(
|
||||||
|
mapboxInfo: MapboxInfo.fromMap(map['mapboxInfo']),
|
||||||
|
serverVersion: ServerVersion.fromMap(map['serverVersion']),
|
||||||
|
isVersionMismatch: map['isVersionMismatch'] ?? false,
|
||||||
|
versionMismatchErrorMessage: map['versionMismatchErrorMessage'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory ServerInfoState.fromJson(String source) => ServerInfoState.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ServerInfoState(mapboxInfo: $mapboxInfo, serverVersion: $serverVersion, isVersionMismatch: $isVersionMismatch, versionMismatchErrorMessage: $versionMismatchErrorMessage)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is ServerInfoState &&
|
||||||
|
other.mapboxInfo == mapboxInfo &&
|
||||||
|
other.serverVersion == serverVersion &&
|
||||||
|
other.isVersionMismatch == isVersionMismatch &&
|
||||||
|
other.versionMismatchErrorMessage == versionMismatchErrorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return mapboxInfo.hashCode ^
|
||||||
|
serverVersion.hashCode ^
|
||||||
|
isVersionMismatch.hashCode ^
|
||||||
|
versionMismatchErrorMessage.hashCode;
|
||||||
|
}
|
||||||
|
}
|
72
mobile/lib/shared/models/server_version.model.dart
Normal file
72
mobile/lib/shared/models/server_version.model.dart
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class ServerVersion {
|
||||||
|
final int major;
|
||||||
|
final int minor;
|
||||||
|
final int patch;
|
||||||
|
final int build;
|
||||||
|
|
||||||
|
ServerVersion({
|
||||||
|
required this.major,
|
||||||
|
required this.minor,
|
||||||
|
required this.patch,
|
||||||
|
required this.build,
|
||||||
|
});
|
||||||
|
|
||||||
|
ServerVersion copyWith({
|
||||||
|
int? major,
|
||||||
|
int? minor,
|
||||||
|
int? patch,
|
||||||
|
int? build,
|
||||||
|
}) {
|
||||||
|
return ServerVersion(
|
||||||
|
major: major ?? this.major,
|
||||||
|
minor: minor ?? this.minor,
|
||||||
|
patch: patch ?? this.patch,
|
||||||
|
build: build ?? this.build,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'major': major,
|
||||||
|
'minor': minor,
|
||||||
|
'patch': patch,
|
||||||
|
'build': build,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ServerVersion.fromMap(Map<String, dynamic> map) {
|
||||||
|
return ServerVersion(
|
||||||
|
major: map['major']?.toInt() ?? 0,
|
||||||
|
minor: map['minor']?.toInt() ?? 0,
|
||||||
|
patch: map['patch']?.toInt() ?? 0,
|
||||||
|
build: map['build']?.toInt() ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory ServerVersion.fromJson(String source) => ServerVersion.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ServerVersion(major: $major, minor: $minor, patch: $patch, build: $build)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is ServerVersion &&
|
||||||
|
other.major == major &&
|
||||||
|
other.minor == minor &&
|
||||||
|
other.patch == patch &&
|
||||||
|
other.build == build;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return major.hashCode ^ minor.hashCode ^ patch.hashCode ^ build.hashCode;
|
||||||
|
}
|
||||||
|
}
|
@ -1,59 +1,19 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/mapbox_info.model.dart';
|
import 'package:immich_mobile/shared/models/mapbox_info.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/server_info_state.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/server_version.model.dart';
|
||||||
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
class ServerInfoState {
|
|
||||||
final MapboxInfo mapboxInfo;
|
|
||||||
ServerInfoState({
|
|
||||||
required this.mapboxInfo,
|
|
||||||
});
|
|
||||||
|
|
||||||
ServerInfoState copyWith({
|
|
||||||
MapboxInfo? mapboxInfo,
|
|
||||||
}) {
|
|
||||||
return ServerInfoState(
|
|
||||||
mapboxInfo: mapboxInfo ?? this.mapboxInfo,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'mapboxInfo': mapboxInfo.toMap(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory ServerInfoState.fromMap(Map<String, dynamic> map) {
|
|
||||||
return ServerInfoState(
|
|
||||||
mapboxInfo: MapboxInfo.fromMap(map['mapboxInfo']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory ServerInfoState.fromJson(String source) => ServerInfoState.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'ServerInfoState(mapboxInfo: $mapboxInfo)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is ServerInfoState && other.mapboxInfo == mapboxInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => mapboxInfo.hashCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
|
class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
|
||||||
ServerInfoNotifier()
|
ServerInfoNotifier()
|
||||||
: super(
|
: super(
|
||||||
ServerInfoState(
|
ServerInfoState(
|
||||||
mapboxInfo: MapboxInfo(isEnable: false, mapboxSecret: ""),
|
mapboxInfo: MapboxInfo(isEnable: false, mapboxSecret: ""),
|
||||||
|
serverVersion: ServerVersion(major: 0, patch: 0, minor: 0, build: 0),
|
||||||
|
isVersionMismatch: false,
|
||||||
|
versionMismatchErrorMessage: "",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -61,9 +21,63 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
|
|||||||
|
|
||||||
getMapboxInfo() async {
|
getMapboxInfo() async {
|
||||||
MapboxInfo mapboxInfoRes = await _serverInfoService.getMapboxInfo();
|
MapboxInfo mapboxInfoRes = await _serverInfoService.getMapboxInfo();
|
||||||
print(mapboxInfoRes);
|
|
||||||
state = state.copyWith(mapboxInfo: mapboxInfoRes);
|
state = state.copyWith(mapboxInfo: mapboxInfoRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getServerVersion() async {
|
||||||
|
ServerVersion? serverVersion = await _serverInfoService.getServerVersion();
|
||||||
|
|
||||||
|
if (serverVersion == null) {
|
||||||
|
state = state.copyWith(
|
||||||
|
isVersionMismatch: true,
|
||||||
|
versionMismatchErrorMessage:
|
||||||
|
"Server is out of date. Some functionalities might not working correctly. Download and rebuild server",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.copyWith(serverVersion: serverVersion);
|
||||||
|
|
||||||
|
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
|
|
||||||
|
Map<String, int> appVersion = _getDetailVersion(packageInfo.version);
|
||||||
|
|
||||||
|
if (appVersion["major"]! > serverVersion.major) {
|
||||||
|
state = state.copyWith(
|
||||||
|
isVersionMismatch: true,
|
||||||
|
versionMismatchErrorMessage:
|
||||||
|
"Server is out of date in major version. Some functionalities might not work correctly. Download and rebuild server",
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appVersion["minor"]! > serverVersion.minor) {
|
||||||
|
state = state.copyWith(
|
||||||
|
isVersionMismatch: true,
|
||||||
|
versionMismatchErrorMessage:
|
||||||
|
"Server is out of date in minor version. Some functionalities might not work correctly. Consider download and rebuild server",
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.copyWith(isVersionMismatch: false, versionMismatchErrorMessage: "");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, int> _getDetailVersion(String version) {
|
||||||
|
List<String> detail = version.split(".");
|
||||||
|
|
||||||
|
var major = detail[0];
|
||||||
|
var minor = detail[1];
|
||||||
|
var patch = detail[2];
|
||||||
|
|
||||||
|
return {
|
||||||
|
"major": int.parse(major),
|
||||||
|
"minor": int.parse(minor),
|
||||||
|
"patch": int.parse(patch),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final serverInfoProvider = StateNotifierProvider<ServerInfoNotifier, ServerInfoState>((ref) {
|
final serverInfoProvider = StateNotifierProvider<ServerInfoNotifier, ServerInfoState>((ref) {
|
||||||
|
@ -30,10 +30,14 @@ class BackupService {
|
|||||||
Function(int, int) uploadProgress) async {
|
Function(int, int) uploadProgress) async {
|
||||||
var dio = Dio();
|
var dio = Dio();
|
||||||
dio.interceptors.add(AuthenticatedRequestInterceptor());
|
dio.interceptors.add(AuthenticatedRequestInterceptor());
|
||||||
|
|
||||||
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
||||||
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
File? file;
|
File? file;
|
||||||
|
|
||||||
|
MultipartFile assetRawUploadData;
|
||||||
|
MultipartFile thumbnailUploadData;
|
||||||
|
|
||||||
for (var entity in assetList) {
|
for (var entity in assetList) {
|
||||||
try {
|
try {
|
||||||
if (entity.type == AssetType.video) {
|
if (entity.type == AssetType.video) {
|
||||||
@ -43,12 +47,20 @@ class BackupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
|
FormData formData;
|
||||||
String originalFileName = await entity.titleAsync;
|
String originalFileName = await entity.titleAsync;
|
||||||
String fileNameWithoutPath = originalFileName.toString().split(".")[0];
|
String fileNameWithoutPath = originalFileName.toString().split(".")[0];
|
||||||
var fileExtension = p.extension(file.path);
|
var fileExtension = p.extension(file.path);
|
||||||
var mimeType = FileHelper.getMimeType(file.path);
|
var mimeType = FileHelper.getMimeType(file.path);
|
||||||
|
assetRawUploadData = await MultipartFile.fromFile(
|
||||||
var formData = FormData.fromMap({
|
file.path,
|
||||||
|
filename: fileNameWithoutPath,
|
||||||
|
contentType: MediaType(
|
||||||
|
mimeType["type"],
|
||||||
|
mimeType["subType"],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
formData = FormData.fromMap({
|
||||||
'deviceAssetId': entity.id,
|
'deviceAssetId': entity.id,
|
||||||
'deviceId': deviceId,
|
'deviceId': deviceId,
|
||||||
'assetType': _getAssetType(entity.type),
|
'assetType': _getAssetType(entity.type),
|
||||||
@ -57,18 +69,36 @@ class BackupService {
|
|||||||
'isFavorite': entity.isFavorite,
|
'isFavorite': entity.isFavorite,
|
||||||
'fileExtension': fileExtension,
|
'fileExtension': fileExtension,
|
||||||
'duration': entity.videoDuration,
|
'duration': entity.videoDuration,
|
||||||
'files': [
|
'assetData': [assetRawUploadData]
|
||||||
await MultipartFile.fromFile(
|
|
||||||
file.path,
|
|
||||||
filename: fileNameWithoutPath,
|
|
||||||
contentType: MediaType(
|
|
||||||
mimeType["type"],
|
|
||||||
mimeType["subType"],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Build thumbnail multipart data
|
||||||
|
var thumbnailData = await entity.thumbDataWithSize(1280, 720);
|
||||||
|
if (thumbnailData != null) {
|
||||||
|
thumbnailUploadData = MultipartFile.fromBytes(
|
||||||
|
List.from(thumbnailData),
|
||||||
|
filename: fileNameWithoutPath,
|
||||||
|
contentType: MediaType(
|
||||||
|
"image",
|
||||||
|
"jpeg",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send thumbnail data if it is exist
|
||||||
|
formData = FormData.fromMap({
|
||||||
|
'deviceAssetId': entity.id,
|
||||||
|
'deviceId': deviceId,
|
||||||
|
'assetType': _getAssetType(entity.type),
|
||||||
|
'createdAt': entity.createDateTime.toIso8601String(),
|
||||||
|
'modifiedAt': entity.modifiedDateTime.toIso8601String(),
|
||||||
|
'isFavorite': entity.isFavorite,
|
||||||
|
'fileExtension': fileExtension,
|
||||||
|
'duration': entity.videoDuration,
|
||||||
|
'thumbnailData': [thumbnailUploadData],
|
||||||
|
'assetData': [assetRawUploadData]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Response res = await dio.post(
|
Response res = await dio.post(
|
||||||
'$savedEndpoint/asset/upload',
|
'$savedEndpoint/asset/upload',
|
||||||
data: formData,
|
data: formData,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:immich_mobile/shared/models/mapbox_info.model.dart';
|
import 'package:immich_mobile/shared/models/mapbox_info.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/server_version.model.dart';
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
import 'package:immich_mobile/shared/services/network.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
||||||
|
|
||||||
@ -17,4 +18,10 @@ class ServerInfoService {
|
|||||||
|
|
||||||
return MapboxInfo.fromJson(response.toString());
|
return MapboxInfo.fromJson(response.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<ServerVersion?> getServerVersion() async {
|
||||||
|
Response response = await _networkService.getRequest(url: 'server-info/version');
|
||||||
|
|
||||||
|
return ServerVersion.fromJson(response.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: A new Flutter project.
|
description: A new Flutter project.
|
||||||
|
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.1.0+1
|
version: 1.3.0+0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.15.1 <3.0.0"
|
sdk: ">=2.15.1 <3.0.0"
|
||||||
|
@ -9,7 +9,7 @@ WORKDIR /usr/src/app
|
|||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
RUN apk add --update-cache build-base python3 libheif vips-dev vips ffmpeg
|
# RUN apk add --update-cache build-base python3 libheif vips-dev vips ffmpeg
|
||||||
|
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ WORKDIR /usr/src/app
|
|||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
RUN apk add --update-cache build-base python3 libheif vips-dev vips ffmpeg
|
# RUN apk add --update-cache build-base python3 libheif vips-dev vips ffmpeg
|
||||||
|
|
||||||
RUN npm install --only=production
|
RUN npm install --only=production
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||||
import { AssetService } from './asset.service';
|
import { AssetService } from './asset.service';
|
||||||
import { FilesInterceptor } from '@nestjs/platform-express';
|
import { FileFieldsInterceptor, FilesInterceptor } from '@nestjs/platform-express';
|
||||||
import { multerOption } from '../../config/multer-option.config';
|
import { multerOption } from '../../config/multer-option.config';
|
||||||
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||||
@ -29,34 +29,43 @@ import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto';
|
|||||||
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
||||||
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 { CommunicationGateway } from '../communication/communication.gateway';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('asset')
|
@Controller('asset')
|
||||||
export class AssetController {
|
export class AssetController {
|
||||||
constructor(
|
constructor(
|
||||||
|
private wsCommunicateionGateway: CommunicationGateway,
|
||||||
private assetService: AssetService,
|
private assetService: AssetService,
|
||||||
private assetOptimizeService: AssetOptimizeService,
|
|
||||||
private backgroundTaskService: BackgroundTaskService,
|
private backgroundTaskService: BackgroundTaskService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('upload')
|
@Post('upload')
|
||||||
@UseInterceptors(FilesInterceptor('files', 30, multerOption))
|
@UseInterceptors(
|
||||||
|
FileFieldsInterceptor(
|
||||||
|
[
|
||||||
|
{ name: 'assetData', maxCount: 1 },
|
||||||
|
{ name: 'thumbnailData', maxCount: 1 },
|
||||||
|
],
|
||||||
|
multerOption,
|
||||||
|
),
|
||||||
|
)
|
||||||
async uploadFile(
|
async uploadFile(
|
||||||
@GetAuthUser() authUser,
|
@GetAuthUser() authUser,
|
||||||
@UploadedFiles() files: Express.Multer.File[],
|
@UploadedFiles() uploadFiles: { assetData: Express.Multer.File[]; thumbnailData?: Express.Multer.File[] },
|
||||||
@Body(ValidationPipe) assetInfo: CreateAssetDto,
|
@Body(ValidationPipe) assetInfo: CreateAssetDto,
|
||||||
) {
|
) {
|
||||||
files.forEach(async (file) => {
|
uploadFiles.assetData.forEach(async (file) => {
|
||||||
const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype);
|
const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype);
|
||||||
|
|
||||||
if (savedAsset && savedAsset.type == AssetType.IMAGE) {
|
if (uploadFiles.thumbnailData != null) {
|
||||||
await this.assetOptimizeService.resizeImage(savedAsset);
|
await this.assetService.updateThumbnailInfo(savedAsset.id, uploadFiles.thumbnailData[0].path);
|
||||||
await this.backgroundTaskService.extractExif(savedAsset, file.originalname, file.size);
|
await this.backgroundTaskService.tagImage(uploadFiles.thumbnailData[0].path, savedAsset);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (savedAsset && savedAsset.type == AssetType.VIDEO) {
|
await this.backgroundTaskService.extractExif(savedAsset, file.originalname, file.size);
|
||||||
await this.assetOptimizeService.getVideoThumbnail(savedAsset, file.originalname);
|
|
||||||
}
|
this.wsCommunicateionGateway.server.to(savedAsset.userId).emit('on_upload_success', JSON.stringify(savedAsset));
|
||||||
});
|
});
|
||||||
|
|
||||||
return 'ok';
|
return 'ok';
|
||||||
|
@ -8,9 +8,12 @@ import { AssetOptimizeService } from '../../modules/image-optimize/image-optimiz
|
|||||||
import { BullModule } from '@nestjs/bull';
|
import { BullModule } from '@nestjs/bull';
|
||||||
import { BackgroundTaskModule } from '../../modules/background-task/background-task.module';
|
import { BackgroundTaskModule } from '../../modules/background-task/background-task.module';
|
||||||
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
||||||
|
import { CommunicationModule } from '../communication/communication.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
CommunicationModule,
|
||||||
|
|
||||||
BullModule.registerQueue({
|
BullModule.registerQueue({
|
||||||
name: 'optimize',
|
name: 'optimize',
|
||||||
defaultJobOptions: {
|
defaultJobOptions: {
|
||||||
|
@ -24,6 +24,12 @@ export class AssetService {
|
|||||||
private assetRepository: Repository<AssetEntity>,
|
private assetRepository: Repository<AssetEntity>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public async updateThumbnailInfo(assetId: string, path: string) {
|
||||||
|
return await this.assetRepository.update(assetId, {
|
||||||
|
resizePath: path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async createUserAsset(authUser: AuthUserDto, assetInfo: CreateAssetDto, path: string, mimeType: string) {
|
public async createUserAsset(authUser: AuthUserDto, assetInfo: CreateAssetDto, path: string, mimeType: string) {
|
||||||
const asset = new AssetEntity();
|
const asset = new AssetEntity();
|
||||||
asset.deviceAssetId = assetInfo.deviceAssetId;
|
asset.deviceAssetId = assetInfo.deviceAssetId;
|
||||||
|
@ -5,6 +5,7 @@ import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
|||||||
import { ServerInfoService } from './server-info.service';
|
import { ServerInfoService } from './server-info.service';
|
||||||
import mapboxGeocoding, { GeocodeService } from '@mapbox/mapbox-sdk/services/geocoding';
|
import mapboxGeocoding, { GeocodeService } from '@mapbox/mapbox-sdk/services/geocoding';
|
||||||
import { MapiResponse } from '@mapbox/mapbox-sdk/lib/classes/mapi-response';
|
import { MapiResponse } from '@mapbox/mapbox-sdk/lib/classes/mapi-response';
|
||||||
|
import { serverVersion } from '../../constants/server_version.constant';
|
||||||
|
|
||||||
@Controller('server-info')
|
@Controller('server-info')
|
||||||
export class ServerInfoController {
|
export class ServerInfoController {
|
||||||
@ -30,4 +31,9 @@ export class ServerInfoController {
|
|||||||
mapboxSecret: this.configService.get('MAPBOX_KEY'),
|
mapboxSecret: this.configService.get('MAPBOX_KEY'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/version')
|
||||||
|
async getServerVersion() {
|
||||||
|
return serverVersion;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,17 +23,33 @@ export const multerOption: MulterOptions = {
|
|||||||
destination: (req: Request, file: Express.Multer.File, cb: any) => {
|
destination: (req: Request, file: Express.Multer.File, cb: any) => {
|
||||||
const uploadPath = multerConfig.dest;
|
const uploadPath = multerConfig.dest;
|
||||||
|
|
||||||
const userPath = `${uploadPath}/${req.user['id']}/original/${req.body['deviceId']}`;
|
if (file.fieldname == 'assetData') {
|
||||||
|
const originalUploadFolder = `${uploadPath}/${req.user['id']}/original/${req.body['deviceId']}`;
|
||||||
|
|
||||||
if (!existsSync(userPath)) {
|
if (!existsSync(originalUploadFolder)) {
|
||||||
mkdirSync(userPath, { recursive: true });
|
mkdirSync(originalUploadFolder, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(null, originalUploadFolder);
|
||||||
|
} else if (file.fieldname == 'thumbnailData') {
|
||||||
|
const thumbnailUploadFolder = `${uploadPath}/${req.user['id']}/thumb/${req.body['deviceId']}`;
|
||||||
|
|
||||||
|
if (!existsSync(thumbnailUploadFolder)) {
|
||||||
|
mkdirSync(thumbnailUploadFolder, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(null, thumbnailUploadFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
cb(null, userPath);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
filename: (req: Request, file: Express.Multer.File, cb: any) => {
|
filename: (req: Request, file: Express.Multer.File, cb: any) => {
|
||||||
cb(null, `${file.originalname.split('.')[0]}${req.body['fileExtension']}`);
|
// console.log(req, file);
|
||||||
|
|
||||||
|
if (file.fieldname == 'assetData') {
|
||||||
|
cb(null, `${file.originalname.split('.')[0]}${req.body['fileExtension']}`);
|
||||||
|
} else if (file.fieldname == 'thumbnailData') {
|
||||||
|
cb(null, `${file.originalname.split('.')[0]}.jpeg`);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
9
server/src/constants/server_version.constant.ts
Normal file
9
server/src/constants/server_version.constant.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// major.minor.patch+build
|
||||||
|
// check mobile/pubspec.yml for current release version
|
||||||
|
|
||||||
|
export const serverVersion = {
|
||||||
|
major: 1,
|
||||||
|
minor: 3,
|
||||||
|
patch: 0,
|
||||||
|
build: 0,
|
||||||
|
};
|
@ -41,7 +41,6 @@ export class BackgroundTaskProcessor {
|
|||||||
async extractExif(job: Job) {
|
async extractExif(job: Job) {
|
||||||
const { savedAsset, fileName, fileSize }: { savedAsset: AssetEntity; fileName: string; fileSize: number } =
|
const { savedAsset, fileName, fileSize }: { savedAsset: AssetEntity; fileName: string; fileSize: number } =
|
||||||
job.data;
|
job.data;
|
||||||
|
|
||||||
const fileBuffer = await readFile(savedAsset.originalPath);
|
const fileBuffer = await readFile(savedAsset.originalPath);
|
||||||
|
|
||||||
const exifData = await exifr.parse(fileBuffer);
|
const exifData = await exifr.parse(fileBuffer);
|
||||||
|
@ -22,122 +22,4 @@ export class ImageOptimizeProcessor {
|
|||||||
|
|
||||||
private backgroundTaskService: BackgroundTaskService,
|
private backgroundTaskService: BackgroundTaskService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Process('resize-image')
|
|
||||||
async resizeUploadedImage(job: Job) {
|
|
||||||
const { savedAsset }: { savedAsset: AssetEntity } = job.data;
|
|
||||||
|
|
||||||
const basePath = APP_UPLOAD_LOCATION;
|
|
||||||
const resizePath = savedAsset.originalPath.replace('/original/', '/thumb/');
|
|
||||||
|
|
||||||
// Create folder for thumb image if not exist
|
|
||||||
|
|
||||||
const resizeDir = `${basePath}/${savedAsset.userId}/thumb/${savedAsset.deviceId}`;
|
|
||||||
|
|
||||||
if (!existsSync(resizeDir)) {
|
|
||||||
mkdirSync(resizeDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
readFile(savedAsset.originalPath, async (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error Reading File');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special Assets Type - ios
|
|
||||||
if (
|
|
||||||
savedAsset.mimeType == 'image/heic' ||
|
|
||||||
savedAsset.mimeType == 'image/heif' ||
|
|
||||||
savedAsset.mimeType == 'image/dng'
|
|
||||||
) {
|
|
||||||
let desitnation = '';
|
|
||||||
if (savedAsset.mimeType == 'image/heic') {
|
|
||||||
desitnation = resizePath.replace('.HEIC', '.jpeg');
|
|
||||||
} else if (savedAsset.mimeType == 'image/heif') {
|
|
||||||
desitnation = resizePath.replace('.HEIF', '.jpeg');
|
|
||||||
} else if (savedAsset.mimeType == 'image/dng') {
|
|
||||||
desitnation = resizePath.replace('.DNG', '.jpeg');
|
|
||||||
}
|
|
||||||
|
|
||||||
sharp(data)
|
|
||||||
.toFormat('jpeg')
|
|
||||||
.resize(512, 512, { fit: 'outside' })
|
|
||||||
.toFile(desitnation, async (err, info) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error resizing file ', err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await this.assetRepository.update(savedAsset, { resizePath: desitnation });
|
|
||||||
|
|
||||||
if (res.affected) {
|
|
||||||
this.wsCommunicateionGateway.server
|
|
||||||
.to(savedAsset.userId)
|
|
||||||
.emit('on_upload_success', JSON.stringify(savedAsset));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tag Image
|
|
||||||
this.backgroundTaskService.tagImage(desitnation, savedAsset);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
sharp(data)
|
|
||||||
.resize(512, 512, { fit: 'outside' })
|
|
||||||
.toFile(resizePath, async (err, info) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error resizing file ', err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await this.assetRepository.update(savedAsset, { resizePath: resizePath });
|
|
||||||
if (res.affected) {
|
|
||||||
this.wsCommunicateionGateway.server
|
|
||||||
.to(savedAsset.userId)
|
|
||||||
.emit('on_upload_success', JSON.stringify(savedAsset));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tag Image
|
|
||||||
this.backgroundTaskService.tagImage(resizePath, savedAsset);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return 'ok';
|
|
||||||
}
|
|
||||||
|
|
||||||
@Process('get-video-thumbnail')
|
|
||||||
async resizeUploadedVideo(job: Job) {
|
|
||||||
const { savedAsset, filename }: { savedAsset: AssetEntity; filename: String } = job.data;
|
|
||||||
|
|
||||||
const basePath = APP_UPLOAD_LOCATION;
|
|
||||||
// const resizePath = savedAsset.originalPath.replace('/original/', '/thumb/');
|
|
||||||
// Create folder for thumb image if not exist
|
|
||||||
const resizeDir = `${basePath}/${savedAsset.userId}/thumb/${savedAsset.deviceId}`;
|
|
||||||
|
|
||||||
if (!existsSync(resizeDir)) {
|
|
||||||
mkdirSync(resizeDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
ffmpeg(savedAsset.originalPath)
|
|
||||||
.thumbnail({
|
|
||||||
count: 1,
|
|
||||||
timestamps: [1],
|
|
||||||
folder: resizeDir,
|
|
||||||
filename: `${filename}.png`,
|
|
||||||
})
|
|
||||||
.on('end', async (a) => {
|
|
||||||
const thumbnailPath = `${resizeDir}/${filename}.png`;
|
|
||||||
|
|
||||||
const res = await this.assetRepository.update(savedAsset, { resizePath: `${resizeDir}/${filename}.png` });
|
|
||||||
|
|
||||||
if (res.affected) {
|
|
||||||
this.wsCommunicateionGateway.server
|
|
||||||
.to(savedAsset.userId)
|
|
||||||
.emit('on_upload_success', JSON.stringify(savedAsset));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tag Image
|
|
||||||
this.backgroundTaskService.tagImage(thumbnailPath, savedAsset);
|
|
||||||
});
|
|
||||||
|
|
||||||
return 'ok';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -7,33 +7,4 @@ import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class AssetOptimizeService {
|
export class AssetOptimizeService {
|
||||||
constructor(@InjectQueue('optimize') private optimizeQueue: Queue) {}
|
constructor(@InjectQueue('optimize') private optimizeQueue: Queue) {}
|
||||||
|
|
||||||
public async resizeImage(savedAsset: AssetEntity) {
|
|
||||||
const job = await this.optimizeQueue.add(
|
|
||||||
'resize-image',
|
|
||||||
{
|
|
||||||
savedAsset,
|
|
||||||
},
|
|
||||||
{ jobId: randomUUID() },
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
jobId: job.id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getVideoThumbnail(savedAsset: AssetEntity, filename: string) {
|
|
||||||
const job = await this.optimizeQueue.add(
|
|
||||||
'get-video-thumbnail',
|
|
||||||
{
|
|
||||||
savedAsset,
|
|
||||||
filename,
|
|
||||||
},
|
|
||||||
{ jobId: randomUUID() },
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
jobId: job.id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user