mirror of
https://github.com/immich-app/immich.git
synced 2024-11-15 09:59:00 -07:00
Add reverse geocoding and show asset location on map in detail view (#43)
* Added reserve geocoding, location in search suggestion, and search by location * Added mapbox sdk to app * Added mapbox to image detailed view
This commit is contained in:
parent
251c92ff1e
commit
026f3c24e9
@ -38,6 +38,7 @@ This project is under heavy development, there will be continous functions, feat
|
|||||||
- Image Tagging/Classification based on ImageNet dataset
|
- Image Tagging/Classification based on ImageNet dataset
|
||||||
- Search assets based on tags and exif data (lens, make, model, orientation)
|
- Search assets based on tags and exif data (lens, make, model, orientation)
|
||||||
- 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)
|
||||||
|
- Geocoding to show asset's location information on map (required MapBox registration for their generous free tier)
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
@ -59,16 +60,12 @@ cp .env.example .env
|
|||||||
|
|
||||||
Then populate the value in there.
|
Then populate the value in there.
|
||||||
|
|
||||||
|
Notice that if set `ENABLE_MAPBOX` to `true`, you will have to provide `MAPBOX_KEY` for the server to run.
|
||||||
|
|
||||||
Pay attention to the key `UPLOAD_LOCATION`, this directory must exist and is owned by the user that run the `docker-compose` command below.
|
Pay attention to the key `UPLOAD_LOCATION`, this directory must exist and is owned by the user that run the `docker-compose` command below.
|
||||||
|
|
||||||
To start, run
|
To start, run
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose -f ./docker/docker-compose.yml up
|
|
||||||
```
|
|
||||||
|
|
||||||
To force rebuild node modules after installing new packages
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose -f ./docker/docker-compose.yml up --build -V
|
docker-compose -f ./docker/docker-compose.yml up --build -V
|
||||||
```
|
```
|
||||||
|
@ -10,4 +10,9 @@ DB_DATABASE_NAME=
|
|||||||
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
|
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
|
||||||
|
|
||||||
# JWT SECRET
|
# JWT SECRET
|
||||||
JWT_SECRET=
|
JWT_SECRET=
|
||||||
|
|
||||||
|
# MAPBOX
|
||||||
|
## ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
|
||||||
|
ENABLE_MAPBOX=
|
||||||
|
MAPBOX_KEY=
|
@ -44,7 +44,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId "com.example.immich_mobile"
|
applicationId "com.example.immich_mobile"
|
||||||
minSdkVersion flutter.minSdkVersion
|
minSdkVersion 20
|
||||||
targetSdkVersion flutter.targetSdkVersion
|
targetSdkVersion flutter.targetSdkVersion
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
|
@ -1,39 +1,23 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.immich_mobile">
|
||||||
package="com.example.immich_mobile">
|
<application android:label="Immich" android:name="${applicationName}" android:usesCleartextTraffic="true" android:icon="@mipmap/ic_launcher">
|
||||||
<application
|
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
|
||||||
android:label="Immich"
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
android:name="${applicationName}"
|
|
||||||
android:usesCleartextTraffic="true"
|
|
||||||
android:icon="@mipmap/ic_launcher">
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:exported="true"
|
|
||||||
android:launchMode="singleTop"
|
|
||||||
android:theme="@style/LaunchTheme"
|
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
|
||||||
android:hardwareAccelerated="true"
|
|
||||||
android:windowSoftInputMode="adjustResize">
|
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
|
||||||
the Android process has started. This theme is visible to the user
|
the Android process has started. This theme is visible to the user
|
||||||
while the Flutter UI initializes. After that, this theme continues
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
to determine the Window background behind the Flutter UI. -->
|
to determine the Window background behind the Flutter UI. -->
|
||||||
<meta-data
|
<meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" />
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
<intent-filter>
|
||||||
android:resource="@style/NormalTheme"
|
<action android:name="android.intent.action.MAIN" />
|
||||||
/>
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<intent-filter>
|
</intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
</activity>
|
|
||||||
<!-- Don't delete the meta-data below.
|
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
|
||||||
<meta-data
|
|
||||||
android:name="flutterEmbedding"
|
|
||||||
android:value="2"/>
|
|
||||||
|
|
||||||
|
</activity>
|
||||||
</application>
|
<!-- Don't delete the meta-data below.
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
</manifest>
|
<meta-data android:name="flutterEmbedding" android:value="2" />
|
||||||
|
|
||||||
|
|
||||||
|
</application>
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
</manifest>
|
@ -1,3 +1,3 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
BIN
mobile/assets/location-pin.png
Normal file
BIN
mobile/assets/location-pin.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
@ -34,8 +34,19 @@ target 'Runner' do
|
|||||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||||
end
|
end
|
||||||
|
|
||||||
post_install do |installer|
|
# post_install do |installer|
|
||||||
installer.pods_project.targets.each do |target|
|
# installer.pods_project.targets.each do |target|
|
||||||
flutter_additional_ios_build_settings(target)
|
# flutter_additional_ios_build_settings(target)
|
||||||
end
|
# end
|
||||||
|
# end
|
||||||
|
|
||||||
|
post_install do |installer|
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
flutter_additional_ios_build_settings(target)
|
||||||
|
target.build_configurations.each do |config|
|
||||||
|
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
|
||||||
|
config.build_settings['ENABLE_BITCODE'] = 'YES'
|
||||||
|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -8,6 +8,15 @@ 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)
|
||||||
|
- Mapbox-iOS-SDK (6.4.1):
|
||||||
|
- MapboxMobileEvents (~> 0.10.12)
|
||||||
|
- mapbox_gl (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- Mapbox-iOS-SDK (~> 6.4.0)
|
||||||
|
- MapboxAnnotationExtension (~> 0.0.1-beta.1)
|
||||||
|
- MapboxAnnotationExtension (0.0.1-beta.2):
|
||||||
|
- Mapbox-iOS-SDK (~> 6.0)
|
||||||
|
- MapboxMobileEvents (0.10.14)
|
||||||
- path_provider_ios (0.0.1):
|
- path_provider_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- photo_manager (1.0.0):
|
- photo_manager (1.0.0):
|
||||||
@ -26,6 +35,7 @@ DEPENDENCIES:
|
|||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||||
|
- mapbox_gl (from `.symlinks/plugins/mapbox_gl/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`)
|
||||||
@ -35,6 +45,9 @@ DEPENDENCIES:
|
|||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
- FMDB
|
- FMDB
|
||||||
|
- Mapbox-iOS-SDK
|
||||||
|
- MapboxAnnotationExtension
|
||||||
|
- MapboxMobileEvents
|
||||||
- Toast
|
- Toast
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
@ -44,6 +57,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter
|
:path: Flutter
|
||||||
fluttertoast:
|
fluttertoast:
|
||||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||||
|
mapbox_gl:
|
||||||
|
:path: ".symlinks/plugins/mapbox_gl/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:
|
||||||
@ -60,6 +75,10 @@ SPEC CHECKSUMS:
|
|||||||
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
||||||
fluttertoast: 6122fa75143e992b1d3470f61000f591a798cc58
|
fluttertoast: 6122fa75143e992b1d3470f61000f591a798cc58
|
||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||||
|
Mapbox-iOS-SDK: f870f83cbdc7aa4a74afcee143aafb0dae390c82
|
||||||
|
mapbox_gl: 33c5ab6306cbfa72289bb3606d2cd2e8baee9ff0
|
||||||
|
MapboxAnnotationExtension: 4eee6c26349ef6d909f1a23a7eae2d0f7ca5fa7d
|
||||||
|
MapboxMobileEvents: 5a172cc9bbf8ac0e45ba86095cbee685ede248cc
|
||||||
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
|
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
|
||||||
photo_manager: 84fa94fbeb82e607333ea9a13c43b58e0903a463
|
photo_manager: 84fa94fbeb82e607333ea9a13c43b58e0903a463
|
||||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||||
@ -67,6 +86,6 @@ SPEC CHECKSUMS:
|
|||||||
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
|
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
|
||||||
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||||
|
|
||||||
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
|
PODFILE CHECKSUM: a44d1ba6d6faf8c61ee449ab69176b941340b431
|
||||||
|
|
||||||
COCOAPODS: 1.10.1
|
COCOAPODS: 1.10.1
|
||||||
|
@ -341,6 +341,7 @@
|
|||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
NEW_SETTING = "";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
@ -419,6 +420,7 @@
|
|||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
|
NEW_SETTING = "";
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
@ -468,6 +470,7 @@
|
|||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
NEW_SETTING = "";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
@ -43,20 +43,27 @@
|
|||||||
</array>
|
</array>
|
||||||
|
|
||||||
<key>UIUserInterfaceStyle</key>
|
<key>UIUserInterfaceStyle</key>
|
||||||
<string>Light</string>
|
<string>Light</string>
|
||||||
|
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true />
|
<true />
|
||||||
|
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>We need to manage backup your photos album</string>
|
<string>We need to manage backup your photos album</string>
|
||||||
|
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true/>
|
<true />
|
||||||
</dict>
|
</dict>
|
||||||
|
|
||||||
|
<key>io.flutter.embedded_views_preview</key>
|
||||||
|
<true />
|
||||||
|
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||||
|
<true />
|
||||||
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
<string>Enable location setting to show position of assets on map</string>
|
||||||
|
<key>NSLocationAlwaysUsageDescription</key>
|
||||||
|
<string>Enable location setting to show position of assets on map</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
@ -1,7 +1,12 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
|
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:mapbox_gl/mapbox_gl.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
class ExifBottomSheet extends ConsumerWidget {
|
class ExifBottomSheet extends ConsumerWidget {
|
||||||
@ -11,6 +16,54 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
_buildMap() {
|
||||||
|
return ref.watch(serverInfoProvider).mapboxInfo.isEnable
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||||
|
child: Container(
|
||||||
|
height: 150,
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(15)),
|
||||||
|
),
|
||||||
|
child: MapboxMap(
|
||||||
|
doubleClickZoomEnabled: false,
|
||||||
|
zoomGesturesEnabled: true,
|
||||||
|
scrollGesturesEnabled: false,
|
||||||
|
accessToken: ref.watch(serverInfoProvider).mapboxInfo.mapboxSecret,
|
||||||
|
styleString: 'mapbox://styles/mapbox/streets-v11',
|
||||||
|
initialCameraPosition: CameraPosition(
|
||||||
|
zoom: 15.0,
|
||||||
|
target: LatLng(assetDetail.exifInfo!.latitude!, assetDetail.exifInfo!.longitude!),
|
||||||
|
),
|
||||||
|
onMapCreated: (MapboxMapController mapController) async {
|
||||||
|
final ByteData bytes = await rootBundle.load("assets/location-pin.png");
|
||||||
|
final Uint8List list = bytes.buffer.asUint8List();
|
||||||
|
await mapController.addImage("assetImage", list);
|
||||||
|
|
||||||
|
await mapController.addSymbol(
|
||||||
|
SymbolOptions(
|
||||||
|
geometry: LatLng(assetDetail.exifInfo!.latitude!, assetDetail.exifInfo!.longitude!),
|
||||||
|
iconImage: "assetImage",
|
||||||
|
iconSize: 0.2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildLocationText() {
|
||||||
|
return (assetDetail.exifInfo!.city != null && assetDetail.exifInfo!.state != null)
|
||||||
|
? Text(
|
||||||
|
"${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}",
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[200], fontWeight: FontWeight.bold),
|
||||||
|
)
|
||||||
|
: Container();
|
||||||
|
}
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8),
|
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
@ -53,9 +106,11 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
"LOCATION",
|
"LOCATION",
|
||||||
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
|
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
|
||||||
),
|
),
|
||||||
|
_buildMap(),
|
||||||
|
_buildLocationText(),
|
||||||
Text(
|
Text(
|
||||||
"${assetDetail.exifInfo!.latitude!.toStringAsFixed(4)}, ${assetDetail.exifInfo!.longitude!.toStringAsFixed(4)}",
|
"${assetDetail.exifInfo!.latitude!.toStringAsFixed(4)}, ${assetDetail.exifInfo!.longitude!.toStringAsFixed(4)}",
|
||||||
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
|
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -89,8 +144,10 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
"${assetDetail.exifInfo?.imageName!}${p.extension(assetDetail.originalPath)}",
|
"${assetDetail.exifInfo?.imageName!}${p.extension(assetDetail.originalPath)}",
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: assetDetail.exifInfo?.exifImageHeight != null
|
||||||
"${assetDetail.exifInfo?.exifImageHeight!} x ${assetDetail.exifInfo?.exifImageWidth!} ${assetDetail.exifInfo?.fileSizeInByte!}B "),
|
? Text(
|
||||||
|
"${assetDetail.exifInfo?.exifImageHeight} x ${assetDetail.exifInfo?.exifImageWidth} ${assetDetail.exifInfo?.fileSizeInByte!}B ")
|
||||||
|
: Container(),
|
||||||
),
|
),
|
||||||
assetDetail.exifInfo?.make != null
|
assetDetail.exifInfo?.make != null
|
||||||
? ListTile(
|
? ListTile(
|
||||||
|
@ -29,9 +29,9 @@ class TopControlAppBar extends StatelessWidget with PreferredSizeWidget {
|
|||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
splashRadius: iconSize,
|
splashRadius: iconSize,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
print("backup");
|
print("download");
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.backup_outlined),
|
icon: const Icon(Icons.cloud_download_rounded),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
|
@ -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.read(serverInfoProvider.notifier).getMapboxInfo();
|
||||||
return null;
|
return null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -19,6 +19,9 @@ class ImmichExif {
|
|||||||
final double? exposureTime;
|
final double? exposureTime;
|
||||||
final double? latitude;
|
final double? latitude;
|
||||||
final double? longitude;
|
final double? longitude;
|
||||||
|
final String? city;
|
||||||
|
final String? state;
|
||||||
|
final String? country;
|
||||||
|
|
||||||
ImmichExif({
|
ImmichExif({
|
||||||
this.id,
|
this.id,
|
||||||
@ -39,6 +42,9 @@ class ImmichExif {
|
|||||||
this.exposureTime,
|
this.exposureTime,
|
||||||
this.latitude,
|
this.latitude,
|
||||||
this.longitude,
|
this.longitude,
|
||||||
|
this.city,
|
||||||
|
this.state,
|
||||||
|
this.country,
|
||||||
});
|
});
|
||||||
|
|
||||||
ImmichExif copyWith({
|
ImmichExif copyWith({
|
||||||
@ -60,6 +66,9 @@ class ImmichExif {
|
|||||||
double? exposureTime,
|
double? exposureTime,
|
||||||
double? latitude,
|
double? latitude,
|
||||||
double? longitude,
|
double? longitude,
|
||||||
|
String? city,
|
||||||
|
String? state,
|
||||||
|
String? country,
|
||||||
}) {
|
}) {
|
||||||
return ImmichExif(
|
return ImmichExif(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@ -80,6 +89,9 @@ class ImmichExif {
|
|||||||
exposureTime: exposureTime ?? this.exposureTime,
|
exposureTime: exposureTime ?? this.exposureTime,
|
||||||
latitude: latitude ?? this.latitude,
|
latitude: latitude ?? this.latitude,
|
||||||
longitude: longitude ?? this.longitude,
|
longitude: longitude ?? this.longitude,
|
||||||
|
city: city ?? this.city,
|
||||||
|
state: state ?? this.state,
|
||||||
|
country: country ?? this.country,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,6 +115,9 @@ class ImmichExif {
|
|||||||
'exposureTime': exposureTime,
|
'exposureTime': exposureTime,
|
||||||
'latitude': latitude,
|
'latitude': latitude,
|
||||||
'longitude': longitude,
|
'longitude': longitude,
|
||||||
|
'city': city,
|
||||||
|
'state': state,
|
||||||
|
'country': country,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,6 +141,9 @@ class ImmichExif {
|
|||||||
exposureTime: map['exposureTime']?.toDouble(),
|
exposureTime: map['exposureTime']?.toDouble(),
|
||||||
latitude: map['latitude']?.toDouble(),
|
latitude: map['latitude']?.toDouble(),
|
||||||
longitude: map['longitude']?.toDouble(),
|
longitude: map['longitude']?.toDouble(),
|
||||||
|
city: map['city'],
|
||||||
|
state: map['state'],
|
||||||
|
country: map['country'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,7 +153,7 @@ class ImmichExif {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ImmichExif(id: $id, assetId: $assetId, make: $make, model: $model, imageName: $imageName, exifImageWidth: $exifImageWidth, exifImageHeight: $exifImageHeight, fileSizeInByte: $fileSizeInByte, orientation: $orientation, dateTimeOriginal: $dateTimeOriginal, modifyDate: $modifyDate, lensModel: $lensModel, fNumber: $fNumber, focalLength: $focalLength, iso: $iso, exposureTime: $exposureTime, latitude: $latitude, longitude: $longitude)';
|
return 'ImmichExif(id: $id, assetId: $assetId, make: $make, model: $model, imageName: $imageName, exifImageWidth: $exifImageWidth, exifImageHeight: $exifImageHeight, fileSizeInByte: $fileSizeInByte, orientation: $orientation, dateTimeOriginal: $dateTimeOriginal, modifyDate: $modifyDate, lensModel: $lensModel, fNumber: $fNumber, focalLength: $focalLength, iso: $iso, exposureTime: $exposureTime, latitude: $latitude, longitude: $longitude, city: $city, state: $state, country: $country)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -160,7 +178,10 @@ class ImmichExif {
|
|||||||
other.iso == iso &&
|
other.iso == iso &&
|
||||||
other.exposureTime == exposureTime &&
|
other.exposureTime == exposureTime &&
|
||||||
other.latitude == latitude &&
|
other.latitude == latitude &&
|
||||||
other.longitude == longitude;
|
other.longitude == longitude &&
|
||||||
|
other.city == city &&
|
||||||
|
other.state == state &&
|
||||||
|
other.country == country;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -182,6 +203,9 @@ class ImmichExif {
|
|||||||
iso.hashCode ^
|
iso.hashCode ^
|
||||||
exposureTime.hashCode ^
|
exposureTime.hashCode ^
|
||||||
latitude.hashCode ^
|
latitude.hashCode ^
|
||||||
longitude.hashCode;
|
longitude.hashCode ^
|
||||||
|
city.hashCode ^
|
||||||
|
state.hashCode ^
|
||||||
|
country.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
51
mobile/lib/shared/models/mapbox_info.model.dart
Normal file
51
mobile/lib/shared/models/mapbox_info.model.dart
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class MapboxInfo {
|
||||||
|
final bool isEnable;
|
||||||
|
final String mapboxSecret;
|
||||||
|
MapboxInfo({
|
||||||
|
required this.isEnable,
|
||||||
|
required this.mapboxSecret,
|
||||||
|
});
|
||||||
|
|
||||||
|
MapboxInfo copyWith({
|
||||||
|
bool? isEnable,
|
||||||
|
String? mapboxSecret,
|
||||||
|
}) {
|
||||||
|
return MapboxInfo(
|
||||||
|
isEnable: isEnable ?? this.isEnable,
|
||||||
|
mapboxSecret: mapboxSecret ?? this.mapboxSecret,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'isEnable': isEnable,
|
||||||
|
'mapboxSecret': mapboxSecret,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory MapboxInfo.fromMap(Map<String, dynamic> map) {
|
||||||
|
return MapboxInfo(
|
||||||
|
isEnable: map['isEnable'] ?? false,
|
||||||
|
mapboxSecret: map['mapboxSecret'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory MapboxInfo.fromJson(String source) => MapboxInfo.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'MapboxInfo(isEnable: $isEnable, mapboxSecret: $mapboxSecret)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is MapboxInfo && other.isEnable == isEnable && other.mapboxSecret == mapboxSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => isEnable.hashCode ^ mapboxSecret.hashCode;
|
||||||
|
}
|
71
mobile/lib/shared/providers/server_info.provider.dart
Normal file
71
mobile/lib/shared/providers/server_info.provider.dart
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/shared/models/mapbox_info.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/server_info.service.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> {
|
||||||
|
ServerInfoNotifier()
|
||||||
|
: super(
|
||||||
|
ServerInfoState(
|
||||||
|
mapboxInfo: MapboxInfo(isEnable: false, mapboxSecret: ""),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final ServerInfoService _serverInfoService = ServerInfoService();
|
||||||
|
|
||||||
|
getMapboxInfo() async {
|
||||||
|
MapboxInfo mapboxInfoRes = await _serverInfoService.getMapboxInfo();
|
||||||
|
print(mapboxInfoRes);
|
||||||
|
state = state.copyWith(mapboxInfo: mapboxInfoRes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final serverInfoProvider = StateNotifierProvider<ServerInfoNotifier, ServerInfoState>((ref) {
|
||||||
|
return ServerInfoNotifier();
|
||||||
|
});
|
@ -1,6 +1,5 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
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/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';
|
||||||
|
|
||||||
@ -12,4 +11,10 @@ class ServerInfoService {
|
|||||||
|
|
||||||
return ServerInfo.fromJson(response.toString());
|
return ServerInfo.fromJson(response.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<MapboxInfo> getMapboxInfo() async {
|
||||||
|
Response response = await _networkService.getRequest(url: 'server-info/mapbox');
|
||||||
|
|
||||||
|
return MapboxInfo.fromJson(response.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -513,6 +513,34 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
version: "1.0.2"
|
||||||
|
mapbox_gl:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: mapbox_gl
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.0"
|
||||||
|
mapbox_gl_dart:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mapbox_gl_dart
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1"
|
||||||
|
mapbox_gl_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mapbox_gl_platform_interface
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.0"
|
||||||
|
mapbox_gl_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mapbox_gl_web
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -34,7 +34,8 @@ dependencies:
|
|||||||
badges: ^2.0.2
|
badges: ^2.0.2
|
||||||
photo_view: ^0.13.0
|
photo_view: ^0.13.0
|
||||||
socket_io_client: ^2.0.0-beta.4-nullsafety.0
|
socket_io_client: ^2.0.0-beta.4-nullsafety.0
|
||||||
|
mapbox_gl: ^0.15.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
11715
server/package-lock.json
generated
11715
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -22,6 +22,7 @@
|
|||||||
"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js"
|
"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@mapbox/mapbox-sdk": "^0.13.3",
|
||||||
"@nestjs/bull": "^0.4.2",
|
"@nestjs/bull": "^0.4.2",
|
||||||
"@nestjs/common": "^8.0.0",
|
"@nestjs/common": "^8.0.0",
|
||||||
"@nestjs/config": "^1.1.6",
|
"@nestjs/config": "^1.1.6",
|
||||||
|
@ -249,7 +249,7 @@ export class AssetService {
|
|||||||
const possibleSearchTerm = new Set<String>();
|
const possibleSearchTerm = new Set<String>();
|
||||||
const rows = await this.assetRepository.query(
|
const rows = await this.assetRepository.query(
|
||||||
`
|
`
|
||||||
select distinct si.tags, e.orientation, e."lensModel", e.make, e.model , a.type
|
select distinct si.tags, e.orientation, e."lensModel", e.make, e.model , a.type, e.city, e.state, e.country
|
||||||
from assets a
|
from assets a
|
||||||
left join exif e on a.id = e."assetId"
|
left join exif e on a.id = e."assetId"
|
||||||
left join smart_info si on a.id = si."assetId"
|
left join smart_info si on a.id = si."assetId"
|
||||||
@ -274,6 +274,11 @@ export class AssetService {
|
|||||||
// Make and model
|
// Make and model
|
||||||
possibleSearchTerm.add(row['make']?.toLowerCase());
|
possibleSearchTerm.add(row['make']?.toLowerCase());
|
||||||
possibleSearchTerm.add(row['model']?.toLowerCase());
|
possibleSearchTerm.add(row['model']?.toLowerCase());
|
||||||
|
|
||||||
|
// Location
|
||||||
|
possibleSearchTerm.add(row['city']?.toLowerCase());
|
||||||
|
possibleSearchTerm.add(row['state']?.toLowerCase());
|
||||||
|
possibleSearchTerm.add(row['country']?.toLowerCase());
|
||||||
});
|
});
|
||||||
|
|
||||||
return Array.from(possibleSearchTerm).filter((x) => x != null);
|
return Array.from(possibleSearchTerm).filter((x) => x != null);
|
||||||
|
@ -61,6 +61,15 @@ export class ExifEntity {
|
|||||||
@Column({ type: 'float', nullable: true })
|
@Column({ type: 'float', nullable: true })
|
||||||
longitude: number;
|
longitude: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
city: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
state: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
country: string;
|
||||||
|
|
||||||
@OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
|
@OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
|
||||||
@JoinColumn({ name: 'assetId', referencedColumnName: 'id' })
|
@JoinColumn({ name: 'assetId', referencedColumnName: 'id' })
|
||||||
asset: ExifEntity;
|
asset: ExifEntity;
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
|
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||||
|
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 { MapiResponse } from '@mapbox/mapbox-sdk/lib/classes/mapi-response';
|
||||||
|
|
||||||
@Controller('server-info')
|
@Controller('server-info')
|
||||||
export class ServerInfoController {
|
export class ServerInfoController {
|
||||||
constructor(private readonly serverInfoService: ServerInfoService) {}
|
constructor(private readonly serverInfoService: ServerInfoService, private readonly configService: ConfigService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
async getServerInfo() {
|
async getServerInfo() {
|
||||||
@ -16,4 +21,13 @@ export class ServerInfoController {
|
|||||||
res: 'pong',
|
res: 'pong',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Get('/mapbox')
|
||||||
|
async getMapboxInfo() {
|
||||||
|
return {
|
||||||
|
isEnable: this.configService.get('ENABLE_MAPBOX'),
|
||||||
|
mapboxSecret: this.configService.get('MAPBOX_KEY'),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,29 +6,4 @@ import { UpdateUserDto } from './dto/update-user.dto';
|
|||||||
@Controller('user')
|
@Controller('user')
|
||||||
export class UserController {
|
export class UserController {
|
||||||
constructor(private readonly userService: UserService) {}
|
constructor(private readonly userService: UserService) {}
|
||||||
|
|
||||||
@Post()
|
|
||||||
create(@Body() createUserDto: CreateUserDto) {
|
|
||||||
return this.userService.create(createUserDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
findAll() {
|
|
||||||
return this.userService.findAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get(':id')
|
|
||||||
findOne(@Param('id') id: string) {
|
|
||||||
return this.userService.findOne(+id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Patch(':id')
|
|
||||||
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
|
|
||||||
return this.userService.update(+id, updateUserDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete(':id')
|
|
||||||
remove(@Param('id') id: string) {
|
|
||||||
return this.userService.remove(+id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -11,22 +11,4 @@ export class UserService {
|
|||||||
@InjectRepository(UserEntity)
|
@InjectRepository(UserEntity)
|
||||||
private userRepository: Repository<UserEntity>,
|
private userRepository: Repository<UserEntity>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
create(createUserDto: CreateUserDto) {
|
|
||||||
return 'This action adds a new user';
|
|
||||||
}
|
|
||||||
|
|
||||||
async findAll() {}
|
|
||||||
|
|
||||||
findOne(id: number) {
|
|
||||||
return `This action returns a #${id} user`;
|
|
||||||
}
|
|
||||||
|
|
||||||
update(id: number, updateUserDto: UpdateUserDto) {
|
|
||||||
return `This action updates a #${id} user`;
|
|
||||||
}
|
|
||||||
|
|
||||||
remove(id: number) {
|
|
||||||
return `This action removes a #${id} user`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -11,5 +11,11 @@ export const immichAppConfig: ConfigModuleOptions = {
|
|||||||
DB_DATABASE_NAME: Joi.string().required(),
|
DB_DATABASE_NAME: Joi.string().required(),
|
||||||
UPLOAD_LOCATION: Joi.string().required(),
|
UPLOAD_LOCATION: Joi.string().required(),
|
||||||
JWT_SECRET: Joi.string().required(),
|
JWT_SECRET: Joi.string().required(),
|
||||||
|
ENABLE_MAPBOX: Joi.boolean().required().valid(true, false),
|
||||||
|
MAPBOX_KEY: Joi.any().when('ENABLE_MAPBOX', {
|
||||||
|
is: true,
|
||||||
|
then: Joi.string().required(),
|
||||||
|
otherwise: Joi.string().optional,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
29
server/src/migration/1646709533213-AddRegionCityToExIf.ts
Normal file
29
server/src/migration/1646709533213-AddRegionCityToExIf.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddRegionCityToExIf1646709533213 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE exif
|
||||||
|
ADD COLUMN city varchar;
|
||||||
|
|
||||||
|
ALTER TABLE exif
|
||||||
|
ADD COLUMN state varchar;
|
||||||
|
|
||||||
|
ALTER TABLE exif
|
||||||
|
ADD COLUMN country varchar;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE exif
|
||||||
|
DROP COLUMN city;
|
||||||
|
|
||||||
|
ALTER TABLE exif
|
||||||
|
DROP COLUMN state;
|
||||||
|
|
||||||
|
ALTER TABLE exif
|
||||||
|
DROP COLUMN country;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddLocationToExifTextSearch1646710459852 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE exif
|
||||||
|
DROP COLUMN IF EXISTS exif_text_searchable_column;
|
||||||
|
|
||||||
|
ALTER TABLE exif
|
||||||
|
ADD COLUMN IF NOT EXISTS exif_text_searchable_column tsvector
|
||||||
|
GENERATED ALWAYS AS (
|
||||||
|
TO_TSVECTOR('english',
|
||||||
|
COALESCE(make, '') || ' ' ||
|
||||||
|
COALESCE(model, '') || ' ' ||
|
||||||
|
COALESCE(orientation, '') || ' ' ||
|
||||||
|
COALESCE("lensModel", '') || ' ' ||
|
||||||
|
COALESCE("city", '') || ' ' ||
|
||||||
|
COALESCE("state", '') || ' ' ||
|
||||||
|
COALESCE("country", '')
|
||||||
|
)
|
||||||
|
) STORED;
|
||||||
|
|
||||||
|
CREATE INDEX exif_text_searchable_idx
|
||||||
|
ON exif
|
||||||
|
USING GIN (exif_text_searchable_column);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE exif
|
||||||
|
DROP COLUMN IF EXISTS exif_text_searchable_column;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS exif_text_searchable_idx ON exif;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
@ -6,14 +6,18 @@ import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
|
|||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import exifr from 'exifr';
|
import exifr from 'exifr';
|
||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
import fs from 'fs';
|
import fs, { rmSync } from 'fs';
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { ExifEntity } from '../../api-v1/asset/entities/exif.entity';
|
import { ExifEntity } from '../../api-v1/asset/entities/exif.entity';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { SmartInfoEntity } from '../../api-v1/asset/entities/smart-info.entity';
|
import { SmartInfoEntity } from '../../api-v1/asset/entities/smart-info.entity';
|
||||||
|
import mapboxGeocoding, { GeocodeService } from '@mapbox/mapbox-sdk/services/geocoding';
|
||||||
|
import { MapiResponse } from '@mapbox/mapbox-sdk/lib/classes/mapi-response';
|
||||||
|
|
||||||
@Processor('background-task')
|
@Processor('background-task')
|
||||||
export class BackgroundTaskProcessor {
|
export class BackgroundTaskProcessor {
|
||||||
|
private geocodingClient: GeocodeService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(AssetEntity)
|
@InjectRepository(AssetEntity)
|
||||||
private assetRepository: Repository<AssetEntity>,
|
private assetRepository: Repository<AssetEntity>,
|
||||||
@ -25,7 +29,13 @@ export class BackgroundTaskProcessor {
|
|||||||
private exifRepository: Repository<ExifEntity>,
|
private exifRepository: Repository<ExifEntity>,
|
||||||
|
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
) {}
|
) {
|
||||||
|
if (this.configService.get('ENABLE_MAPBOX')) {
|
||||||
|
this.geocodingClient = mapboxGeocoding({
|
||||||
|
accessToken: this.configService.get('MAPBOX_KEY'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Process('extract-exif')
|
@Process('extract-exif')
|
||||||
async extractExif(job: Job) {
|
async extractExif(job: Job) {
|
||||||
@ -55,6 +65,26 @@ export class BackgroundTaskProcessor {
|
|||||||
newExif.latitude = exifData['latitude'] || null;
|
newExif.latitude = exifData['latitude'] || null;
|
||||||
newExif.longitude = exifData['longitude'] || null;
|
newExif.longitude = exifData['longitude'] || null;
|
||||||
|
|
||||||
|
// Reverse GeoCoding
|
||||||
|
if (this.configService.get('ENABLE_MAPBOX') && exifData['longitude'] && exifData['latitude']) {
|
||||||
|
const geoCodeInfo: MapiResponse = await this.geocodingClient
|
||||||
|
.reverseGeocode({
|
||||||
|
query: [exifData['longitude'], exifData['latitude']],
|
||||||
|
types: ['country', 'region', 'place'],
|
||||||
|
})
|
||||||
|
.send();
|
||||||
|
|
||||||
|
const res: [] = geoCodeInfo.body['features'];
|
||||||
|
|
||||||
|
const city = res.filter((geoInfo) => geoInfo['place_type'][0] == 'place')[0]['text'];
|
||||||
|
const state = res.filter((geoInfo) => geoInfo['place_type'][0] == 'region')[0]['text'];
|
||||||
|
const country = res.filter((geoInfo) => geoInfo['place_type'][0] == 'country')[0]['text'];
|
||||||
|
|
||||||
|
newExif.city = city || null;
|
||||||
|
newExif.state = state || null;
|
||||||
|
newExif.country = country || null;
|
||||||
|
}
|
||||||
|
|
||||||
await this.exifRepository.save(newExif);
|
await this.exifRepository.save(newExif);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
Loading…
Reference in New Issue
Block a user