mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00: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:
		@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user