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
30 changed files with 12112 additions and 184 deletions
|
@ -38,6 +38,7 @@ This project is under heavy development, there will be continous functions, feat
|
|||
- Image Tagging/Classification based on ImageNet dataset
|
||||
- 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)
|
||||
- Geocoding to show asset's location information on map (required MapBox registration for their generous free tier)
|
||||
|
||||
# Development
|
||||
|
||||
|
@ -59,16 +60,12 @@ cp .env.example .env
|
|||
|
||||
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.
|
||||
|
||||
To start, run
|
||||
|
||||
```bash
|
||||
docker-compose -f ./docker/docker-compose.yml up
|
||||
```
|
||||
|
||||
To force rebuild node modules after installing new packages
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
# 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 {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.example.immich_mobile"
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
minSdkVersion 20
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
|
|
|
@ -1,39 +1,23 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.immich_mobile">
|
||||
<application
|
||||
android:label="Immich"
|
||||
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
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.immich_mobile">
|
||||
<application android:label="Immich" 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
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<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"/>
|
||||
<meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
</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" />
|
||||
|
||||
|
||||
</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
|
||||
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__))
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
end
|
||||
# post_install do |installer|
|
||||
# installer.pods_project.targets.each do |target|
|
||||
# flutter_additional_ios_build_settings(target)
|
||||
# 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
|
||||
|
|
|
@ -8,6 +8,15 @@ PODS:
|
|||
- FMDB (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):
|
||||
- Flutter
|
||||
- photo_manager (1.0.0):
|
||||
|
@ -26,6 +35,7 @@ DEPENDENCIES:
|
|||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||
- mapbox_gl (from `.symlinks/plugins/mapbox_gl/ios`)
|
||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
|
@ -35,6 +45,9 @@ DEPENDENCIES:
|
|||
SPEC REPOS:
|
||||
trunk:
|
||||
- FMDB
|
||||
- Mapbox-iOS-SDK
|
||||
- MapboxAnnotationExtension
|
||||
- MapboxMobileEvents
|
||||
- Toast
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
|
@ -44,6 +57,8 @@ EXTERNAL SOURCES:
|
|||
:path: Flutter
|
||||
fluttertoast:
|
||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||
mapbox_gl:
|
||||
:path: ".symlinks/plugins/mapbox_gl/ios"
|
||||
path_provider_ios:
|
||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||
photo_manager:
|
||||
|
@ -60,6 +75,10 @@ SPEC CHECKSUMS:
|
|||
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
||||
fluttertoast: 6122fa75143e992b1d3470f61000f591a798cc58
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
Mapbox-iOS-SDK: f870f83cbdc7aa4a74afcee143aafb0dae390c82
|
||||
mapbox_gl: 33c5ab6306cbfa72289bb3606d2cd2e8baee9ff0
|
||||
MapboxAnnotationExtension: 4eee6c26349ef6d909f1a23a7eae2d0f7ca5fa7d
|
||||
MapboxMobileEvents: 5a172cc9bbf8ac0e45ba86095cbee685ede248cc
|
||||
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
|
||||
photo_manager: 84fa94fbeb82e607333ea9a13c43b58e0903a463
|
||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||
|
@ -67,6 +86,6 @@ SPEC CHECKSUMS:
|
|||
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
|
||||
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||
|
||||
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
|
||||
PODFILE CHECKSUM: a44d1ba6d6faf8c61ee449ab69176b941340b431
|
||||
|
||||
COCOAPODS: 1.10.1
|
||||
|
|
|
@ -341,6 +341,7 @@
|
|||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
NEW_SETTING = "";
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
@ -419,6 +420,7 @@
|
|||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
NEW_SETTING = "";
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
@ -468,6 +470,7 @@
|
|||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
NEW_SETTING = "";
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
|
|
|
@ -43,20 +43,27 @@
|
|||
</array>
|
||||
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Light</string>
|
||||
<string>Light</string>
|
||||
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true />
|
||||
<true />
|
||||
|
||||
<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>
|
||||
<dict>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<true />
|
||||
</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>
|
||||
</plist>
|
|
@ -1,7 +1,12 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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/providers/server_info.provider.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mapbox_gl/mapbox_gl.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
class ExifBottomSheet extends ConsumerWidget {
|
||||
|
@ -11,6 +16,54 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
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(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8),
|
||||
child: ListView(
|
||||
|
@ -53,9 +106,11 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||
"LOCATION",
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
|
||||
),
|
||||
_buildMap(),
|
||||
_buildLocationText(),
|
||||
Text(
|
||||
"${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)}",
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Text(
|
||||
"${assetDetail.exifInfo?.exifImageHeight!} x ${assetDetail.exifInfo?.exifImageWidth!} ${assetDetail.exifInfo?.fileSizeInByte!}B "),
|
||||
subtitle: assetDetail.exifInfo?.exifImageHeight != null
|
||||
? Text(
|
||||
"${assetDetail.exifInfo?.exifImageHeight} x ${assetDetail.exifInfo?.exifImageWidth} ${assetDetail.exifInfo?.fileSizeInByte!}B ")
|
||||
: Container(),
|
||||
),
|
||||
assetDetail.exifInfo?.make != null
|
||||
? ListTile(
|
||||
|
|
|
@ -29,9 +29,9 @@ class TopControlAppBar extends StatelessWidget with PreferredSizeWidget {
|
|||
iconSize: iconSize,
|
||||
splashRadius: iconSize,
|
||||
onPressed: () {
|
||||
print("backup");
|
||||
print("download");
|
||||
},
|
||||
icon: const Icon(Icons.backup_outlined),
|
||||
icon: const Icon(Icons.cloud_download_rounded),
|
||||
),
|
||||
IconButton(
|
||||
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/profile_drawer.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:sliver_tools/sliver_tools.dart';
|
||||
|
||||
|
@ -28,6 +29,7 @@ class HomePage extends HookConsumerWidget {
|
|||
useEffect(() {
|
||||
ref.read(websocketProvider.notifier).connect();
|
||||
ref.read(assetProvider.notifier).getAllAsset();
|
||||
ref.read(serverInfoProvider.notifier).getMapboxInfo();
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -19,6 +19,9 @@ class ImmichExif {
|
|||
final double? exposureTime;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
final String? city;
|
||||
final String? state;
|
||||
final String? country;
|
||||
|
||||
ImmichExif({
|
||||
this.id,
|
||||
|
@ -39,6 +42,9 @@ class ImmichExif {
|
|||
this.exposureTime,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.city,
|
||||
this.state,
|
||||
this.country,
|
||||
});
|
||||
|
||||
ImmichExif copyWith({
|
||||
|
@ -60,6 +66,9 @@ class ImmichExif {
|
|||
double? exposureTime,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
String? city,
|
||||
String? state,
|
||||
String? country,
|
||||
}) {
|
||||
return ImmichExif(
|
||||
id: id ?? this.id,
|
||||
|
@ -80,6 +89,9 @@ class ImmichExif {
|
|||
exposureTime: exposureTime ?? this.exposureTime,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
city: city ?? this.city,
|
||||
state: state ?? this.state,
|
||||
country: country ?? this.country,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -103,6 +115,9 @@ class ImmichExif {
|
|||
'exposureTime': exposureTime,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'city': city,
|
||||
'state': state,
|
||||
'country': country,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -126,6 +141,9 @@ class ImmichExif {
|
|||
exposureTime: map['exposureTime']?.toDouble(),
|
||||
latitude: map['latitude']?.toDouble(),
|
||||
longitude: map['longitude']?.toDouble(),
|
||||
city: map['city'],
|
||||
state: map['state'],
|
||||
country: map['country'],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -135,7 +153,7 @@ class ImmichExif {
|
|||
|
||||
@override
|
||||
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
|
||||
|
@ -160,7 +178,10 @@ class ImmichExif {
|
|||
other.iso == iso &&
|
||||
other.exposureTime == exposureTime &&
|
||||
other.latitude == latitude &&
|
||||
other.longitude == longitude;
|
||||
other.longitude == longitude &&
|
||||
other.city == city &&
|
||||
other.state == state &&
|
||||
other.country == country;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -182,6 +203,9 @@ class ImmichExif {
|
|||
iso.hashCode ^
|
||||
exposureTime.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:immich_mobile/shared/models/mapbox_info.model.dart';
|
||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
||||
|
||||
|
@ -12,4 +11,10 @@ class ServerInfoService {
|
|||
|
||||
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"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -34,7 +34,8 @@ dependencies:
|
|||
badges: ^2.0.2
|
||||
photo_view: ^0.13.0
|
||||
socket_io_client: ^2.0.0-beta.4-nullsafety.0
|
||||
|
||||
mapbox_gl: ^0.15.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mapbox/mapbox-sdk": "^0.13.3",
|
||||
"@nestjs/bull": "^0.4.2",
|
||||
"@nestjs/common": "^8.0.0",
|
||||
"@nestjs/config": "^1.1.6",
|
||||
|
|
|
@ -249,7 +249,7 @@ export class AssetService {
|
|||
const possibleSearchTerm = new Set<String>();
|
||||
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
|
||||
left join exif e on a.id = e."assetId"
|
||||
left join smart_info si on a.id = si."assetId"
|
||||
|
@ -274,6 +274,11 @@ export class AssetService {
|
|||
// Make and model
|
||||
possibleSearchTerm.add(row['make']?.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);
|
||||
|
|
|
@ -61,6 +61,15 @@ export class ExifEntity {
|
|||
@Column({ type: 'float', nullable: true })
|
||||
longitude: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
city: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
state: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
country: string;
|
||||
|
||||
@OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
|
||||
@JoinColumn({ name: 'assetId', referencedColumnName: 'id' })
|
||||
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 mapboxGeocoding, { GeocodeService } from '@mapbox/mapbox-sdk/services/geocoding';
|
||||
import { MapiResponse } from '@mapbox/mapbox-sdk/lib/classes/mapi-response';
|
||||
|
||||
@Controller('server-info')
|
||||
export class ServerInfoController {
|
||||
constructor(private readonly serverInfoService: ServerInfoService) {}
|
||||
constructor(private readonly serverInfoService: ServerInfoService, private readonly configService: ConfigService) {}
|
||||
|
||||
@Get()
|
||||
async getServerInfo() {
|
||||
|
@ -16,4 +21,13 @@ export class ServerInfoController {
|
|||
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')
|
||||
export class UserController {
|
||||
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)
|
||||
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(),
|
||||
UPLOAD_LOCATION: 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 exifr from 'exifr';
|
||||
import { readFile } from 'fs/promises';
|
||||
import fs from 'fs';
|
||||
import fs, { rmSync } from 'fs';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { ExifEntity } from '../../api-v1/asset/entities/exif.entity';
|
||||
import axios from 'axios';
|
||||
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')
|
||||
export class BackgroundTaskProcessor {
|
||||
private geocodingClient: GeocodeService;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(AssetEntity)
|
||||
private assetRepository: Repository<AssetEntity>,
|
||||
|
@ -25,7 +29,13 @@ export class BackgroundTaskProcessor {
|
|||
private exifRepository: Repository<ExifEntity>,
|
||||
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
) {
|
||||
if (this.configService.get('ENABLE_MAPBOX')) {
|
||||
this.geocodingClient = mapboxGeocoding({
|
||||
accessToken: this.configService.get('MAPBOX_KEY'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Process('extract-exif')
|
||||
async extractExif(job: Job) {
|
||||
|
@ -55,6 +65,26 @@ export class BackgroundTaskProcessor {
|
|||
newExif.latitude = exifData['latitude'] || 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);
|
||||
|
||||
try {
|
||||
|
|
Loading…
Reference in a new issue