forked from Cutlery/immich
		
	merge main
This commit is contained in:
		
						commit
						12c5b5fcb7
					
				@ -1,4 +1,4 @@
 | 
			
		||||
# Preparing a pre-existing Postgres server
 | 
			
		||||
# Pre-existing Postgres
 | 
			
		||||
 | 
			
		||||
While not officially recommended, it is possible to run Immich using a pre-existing Postgres server. To use this setup, you should have a baseline level of familiarity with Postgres and the Linux command line. If you do not have these, we recommend using the default setup with a dedicated Postgres container.
 | 
			
		||||
 | 
			
		||||
@ -45,7 +45,7 @@ CREATE EXTENSION vectors;
 | 
			
		||||
CREATE EXTENSION earthdistance CASCADE;
 | 
			
		||||
ALTER DATABASE <immichdatabasename> SET search_path TO "$user", public, vectors;
 | 
			
		||||
GRANT USAGE ON SCHEMA vectors TO <immichdbusername>;
 | 
			
		||||
GRANT SELECT ON TABLE pg_vector_index_stat to <immichdbusername>;
 | 
			
		||||
ALTER DEFAULT PRIVILEGES IN SCHEMA vectors GRANT SELECT ON TABLES TO <immichdbusername>;
 | 
			
		||||
COMMIT;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -27,8 +27,8 @@ The metrics in immich are grouped into API (endpoint calls and response times),
 | 
			
		||||
 | 
			
		||||
Immich will not expose an endpoint for metrics by default. To enable this endpoint, you can add the `IMMICH_METRICS=true` environmental variable to your `.env` file. Note that only the server and microservices containers currently use this variable.
 | 
			
		||||
 | 
			
		||||
:::note
 | 
			
		||||
`IMMICH_METRICS` is equivalent to enabling the following three environmental variables: `IMMICH_API_METRICS`, `IMMICH_HOST_METRICS`, and `IMMICH_IO_METRICS`. If you would like to only expose certain kinds of metrics, you can set only those environmental variables to `true`. Explicitly setting the environmental variable for a metric group overrides `IMMICH_METRICS` for that group.
 | 
			
		||||
:::tip
 | 
			
		||||
`IMMICH_METRICS` enables all metrics, but there are also [environmental variables](/docs/install/environment-variables.md#prometheus) to toggle specific metric groups. If you'd like to only expose certain kinds of metrics, you can set only those environmental variables to `true`. Explicitly setting the environmental variable for a metric group overrides `IMMICH_METRICS` for that group. For example, setting `IMMICH_METRICS=true` and `IMMICH_API_METRICS=false` will enable all metrics except API metrics.
 | 
			
		||||
:::
 | 
			
		||||
 | 
			
		||||
The next step is to configure a new or existing Prometheus instance to scrape this endpoint. The following steps assume that you do not have an existing Prometheus instance, but the steps will be similar either way.
 | 
			
		||||
 | 
			
		||||
@ -41,11 +41,9 @@ These environment variables are used by the `docker-compose.yml` file and do **N
 | 
			
		||||
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory     | `/usr/src/resources` | microservices                                |
 | 
			
		||||
 | 
			
		||||
:::tip
 | 
			
		||||
`TZ` should be set to a `TZ identifier` from [this list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List). For example, `TZ="Etc/UTC"`.
 | 
			
		||||
 | 
			
		||||
`TZ` is only used by the `exiftool` as a fallback in case the timezone cannot be determined from the image metadata.
 | 
			
		||||
 | 
			
		||||
`exiftool` is only present in the microservices container.
 | 
			
		||||
 | 
			
		||||
`TZ` is only used by `exiftool`, which is present in the microservices container, as a fallback in case the timezone cannot be determined from the image metadata.
 | 
			
		||||
:::
 | 
			
		||||
 | 
			
		||||
## Ports
 | 
			
		||||
@ -147,6 +145,18 @@ Other machine learning parameters can be tuned from the admin UI.
 | 
			
		||||
 | 
			
		||||
:::
 | 
			
		||||
 | 
			
		||||
## Prometheus
 | 
			
		||||
 | 
			
		||||
| Variable                       | Description                                                                                   | Default | Services              |
 | 
			
		||||
| :----------------------------- | :-------------------------------------------------------------------------------------------- | :-----: | :-------------------- |
 | 
			
		||||
| `IMMICH_METRICS`<sup>\*1</sup> | Toggle all metrics (one of [`true`, `false`])                                                 |         | server, microservices |
 | 
			
		||||
| `IMMICH_API_METRICS`           | Toggle metrics for endpoints and response times (one of [`true`, `false`])                    |         | server, microservices |
 | 
			
		||||
| `IMMICH_HOST_METRICS`          | Toggle metrics for CPU and memory utilization for host and process (one of [`true`, `false`]) |         | server, microservices |
 | 
			
		||||
| `IMMICH_IO_METRICS`            | Toggle metrics for database queries, image processing, etc. (one of [`true`, `false`])        |         | server, microservices |
 | 
			
		||||
| `IMMICH_JOB_METRICS`           | Toggle metrics for jobs and queues (one of [`true`, `false`])                                 |         | server, microservices |
 | 
			
		||||
 | 
			
		||||
\*1: Overridden for a metric group when its corresponding environmental variable is set.
 | 
			
		||||
 | 
			
		||||
## Docker Secrets
 | 
			
		||||
 | 
			
		||||
The following variables support the use of [Docker secrets](https://docs.docker.com/engine/swarm/secrets/) for additional security.
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,9 @@ PODS:
 | 
			
		||||
  - fluttertoast (0.0.2):
 | 
			
		||||
    - Flutter
 | 
			
		||||
    - Toast
 | 
			
		||||
  - FMDB (2.7.5):
 | 
			
		||||
    - FMDB/standard (= 2.7.5)
 | 
			
		||||
  - FMDB/standard (2.7.5)
 | 
			
		||||
  - geolocator_apple (1.2.0):
 | 
			
		||||
    - Flutter
 | 
			
		||||
  - image_picker_ios (0.0.1):
 | 
			
		||||
@ -36,7 +39,7 @@ PODS:
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
  - path_provider_ios (0.0.1):
 | 
			
		||||
    - Flutter
 | 
			
		||||
  - permission_handler_apple (9.3.0):
 | 
			
		||||
  - permission_handler_apple (9.1.1):
 | 
			
		||||
    - Flutter
 | 
			
		||||
  - photo_manager (2.0.0):
 | 
			
		||||
    - Flutter
 | 
			
		||||
@ -50,7 +53,7 @@ PODS:
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
  - sqflite (0.0.3):
 | 
			
		||||
    - Flutter
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
    - FMDB (>= 2.7.5)
 | 
			
		||||
  - Toast (4.0.0)
 | 
			
		||||
  - url_launcher_ios (0.0.1):
 | 
			
		||||
    - Flutter
 | 
			
		||||
@ -81,13 +84,14 @@ DEPENDENCIES:
 | 
			
		||||
  - photo_manager (from `.symlinks/plugins/photo_manager/ios`)
 | 
			
		||||
  - share_plus (from `.symlinks/plugins/share_plus/ios`)
 | 
			
		||||
  - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
 | 
			
		||||
  - sqflite (from `.symlinks/plugins/sqflite/darwin`)
 | 
			
		||||
  - sqflite (from `.symlinks/plugins/sqflite/ios`)
 | 
			
		||||
  - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
 | 
			
		||||
  - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
 | 
			
		||||
  - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
 | 
			
		||||
 | 
			
		||||
SPEC REPOS:
 | 
			
		||||
  trunk:
 | 
			
		||||
    - FMDB
 | 
			
		||||
    - MapLibre
 | 
			
		||||
    - ReachabilitySwift
 | 
			
		||||
    - SAMKeychain
 | 
			
		||||
@ -135,7 +139,7 @@ EXTERNAL SOURCES:
 | 
			
		||||
  shared_preferences_foundation:
 | 
			
		||||
    :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
 | 
			
		||||
  sqflite:
 | 
			
		||||
    :path: ".symlinks/plugins/sqflite/darwin"
 | 
			
		||||
    :path: ".symlinks/plugins/sqflite/ios"
 | 
			
		||||
  url_launcher_ios:
 | 
			
		||||
    :path: ".symlinks/plugins/url_launcher_ios/ios"
 | 
			
		||||
  video_player_avfoundation:
 | 
			
		||||
@ -151,23 +155,24 @@ SPEC CHECKSUMS:
 | 
			
		||||
  flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
 | 
			
		||||
  flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
 | 
			
		||||
  flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d
 | 
			
		||||
  fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
 | 
			
		||||
  fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
 | 
			
		||||
  FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
 | 
			
		||||
  geolocator_apple: 9157311f654584b9bb72686c55fc02a97b73f461
 | 
			
		||||
  image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
 | 
			
		||||
  image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
 | 
			
		||||
  integration_test: 13825b8a9334a850581300559b8839134b124670
 | 
			
		||||
  isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
 | 
			
		||||
  MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef
 | 
			
		||||
  maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9
 | 
			
		||||
  package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
 | 
			
		||||
  path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
 | 
			
		||||
  path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
 | 
			
		||||
  path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
 | 
			
		||||
  permission_handler_apple: 036b856153a2b1f61f21030ff725f3e6fece2b78
 | 
			
		||||
  permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
 | 
			
		||||
  photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
 | 
			
		||||
  ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
 | 
			
		||||
  SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
 | 
			
		||||
  share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
 | 
			
		||||
  shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
 | 
			
		||||
  sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
 | 
			
		||||
  shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
 | 
			
		||||
  sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
 | 
			
		||||
  Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
 | 
			
		||||
  url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
 | 
			
		||||
  video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
 | 
			
		||||
@ -175,4 +180,4 @@ SPEC CHECKSUMS:
 | 
			
		||||
 | 
			
		||||
PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d
 | 
			
		||||
 | 
			
		||||
COCOAPODS: 1.12.1
 | 
			
		||||
COCOAPODS: 1.15.2
 | 
			
		||||
 | 
			
		||||
@ -243,12 +243,7 @@ class AlbumService {
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await _db.writeTxn(() async {
 | 
			
		||||
          await album.assets.update(link: successAssets);
 | 
			
		||||
          final a = await _db.albums.get(album.id);
 | 
			
		||||
          // trigger watcher
 | 
			
		||||
          await _db.albums.put(a!);
 | 
			
		||||
        });
 | 
			
		||||
        await _updateAssets(album.id, add: successAssets);
 | 
			
		||||
 | 
			
		||||
        return AddAssetsResponse(
 | 
			
		||||
          alreadyInAlbum: duplicatedAssets,
 | 
			
		||||
@ -257,11 +252,28 @@ class AlbumService {
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint("Error addAdditionalAssetToAlbum  ${e.toString()}");
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _updateAssets(
 | 
			
		||||
    int albumId, {
 | 
			
		||||
    Iterable<Asset> add = const [],
 | 
			
		||||
    Iterable<Asset> remove = const [],
 | 
			
		||||
  }) {
 | 
			
		||||
    return _db.writeTxn(() async {
 | 
			
		||||
      final album = await _db.albums.get(albumId);
 | 
			
		||||
      if (album == null) return;
 | 
			
		||||
      await album.assets.update(link: add, unlink: remove);
 | 
			
		||||
      album.startDate =
 | 
			
		||||
          await album.assets.filter().fileCreatedAtProperty().min();
 | 
			
		||||
      album.endDate = await album.assets.filter().fileCreatedAtProperty().max();
 | 
			
		||||
      album.lastModifiedAssetTimestamp =
 | 
			
		||||
          await album.assets.filter().updatedAtProperty().max();
 | 
			
		||||
      await _db.albums.put(album);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<bool> addAdditionalUserToAlbum(
 | 
			
		||||
    List<String> sharedUserIds,
 | 
			
		||||
    Album album,
 | 
			
		||||
@ -342,7 +354,7 @@ class AlbumService {
 | 
			
		||||
      await _apiService.albumApi.removeUserFromAlbum(album.remoteId!, "me");
 | 
			
		||||
      return true;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint("Error deleteAlbum  ${e.toString()}");
 | 
			
		||||
      debugPrint("Error leaveAlbum ${e.toString()}");
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -352,24 +364,25 @@ class AlbumService {
 | 
			
		||||
    Iterable<Asset> assets,
 | 
			
		||||
  ) async {
 | 
			
		||||
    try {
 | 
			
		||||
      await _apiService.albumApi.removeAssetFromAlbum(
 | 
			
		||||
      final response = await _apiService.albumApi.removeAssetFromAlbum(
 | 
			
		||||
        album.remoteId!,
 | 
			
		||||
        BulkIdsDto(
 | 
			
		||||
          ids: assets.map((asset) => asset.remoteId!).toList(),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
      await _db.writeTxn(() async {
 | 
			
		||||
        await album.assets.update(unlink: assets);
 | 
			
		||||
        final a = await _db.albums.get(album.id);
 | 
			
		||||
        // trigger watcher
 | 
			
		||||
        await _db.albums.put(a!);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (response != null) {
 | 
			
		||||
        final toRemove = response.every((e) => e.success)
 | 
			
		||||
            ? assets
 | 
			
		||||
            : response
 | 
			
		||||
                .where((e) => e.success)
 | 
			
		||||
                .map((e) => assets.firstWhere((a) => a.remoteId == e.id));
 | 
			
		||||
        await _updateAssets(album.id, remove: toRemove);
 | 
			
		||||
        return true;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint("Error deleteAlbum  ${e.toString()}");
 | 
			
		||||
      return false;
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint("Error removeAssetFromAlbum ${e.toString()}");
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<bool> removeUserFromAlbum(
 | 
			
		||||
@ -413,7 +426,7 @@ class AlbumService {
 | 
			
		||||
 | 
			
		||||
      return true;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint("Error deleteAlbum  ${e.toString()}");
 | 
			
		||||
      debugPrint("Error changeTitleAlbum  ${e.toString()}");
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -42,7 +42,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
 | 
			
		||||
    this.assetsPerRow,
 | 
			
		||||
    this.showStorageIndicator,
 | 
			
		||||
    this.listener,
 | 
			
		||||
    this.margin = 5.0,
 | 
			
		||||
    this.margin = 2.0,
 | 
			
		||||
    this.selectionActive = false,
 | 
			
		||||
    this.preselectedAssets,
 | 
			
		||||
    this.canDeselect = true,
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,6 @@
 | 
			
		||||
// ignore_for_file: public_member_api_docs, sort_constructors_first
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
/// A wrapper for [CuratedLocationsResponseDto] objects
 | 
			
		||||
/// and [CuratedObjectsResponseDto] to be displayed in
 | 
			
		||||
/// a view
 | 
			
		||||
@ -9,7 +12,49 @@ class CuratedContent {
 | 
			
		||||
  final String id;
 | 
			
		||||
 | 
			
		||||
  CuratedContent({
 | 
			
		||||
    required this.id,
 | 
			
		||||
    required this.label,
 | 
			
		||||
    required this.id,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  CuratedContent copyWith({
 | 
			
		||||
    String? label,
 | 
			
		||||
    String? id,
 | 
			
		||||
  }) {
 | 
			
		||||
    return CuratedContent(
 | 
			
		||||
      label: label ?? this.label,
 | 
			
		||||
      id: id ?? this.id,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toMap() {
 | 
			
		||||
    return <String, dynamic>{
 | 
			
		||||
      'label': label,
 | 
			
		||||
      'id': id,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  factory CuratedContent.fromMap(Map<String, dynamic> map) {
 | 
			
		||||
    return CuratedContent(
 | 
			
		||||
      label: map['label'] as String,
 | 
			
		||||
      id: map['id'] as String,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String toJson() => json.encode(toMap());
 | 
			
		||||
 | 
			
		||||
  factory CuratedContent.fromJson(String source) =>
 | 
			
		||||
      CuratedContent.fromMap(json.decode(source) as Map<String, dynamic>);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() => 'CuratedContent(label: $label, id: $id)';
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  bool operator ==(covariant CuratedContent other) {
 | 
			
		||||
    if (identical(this, other)) return true;
 | 
			
		||||
 | 
			
		||||
    return other.label == label && other.id == id;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  int get hashCode => label.hashCode ^ id.hashCode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										310
									
								
								mobile/lib/modules/search/models/search_filter.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										310
									
								
								mobile/lib/modules/search/models/search_filter.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,310 @@
 | 
			
		||||
// ignore_for_file: public_member_api_docs, sort_constructors_first
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
class SearchLocationFilter {
 | 
			
		||||
  String? country;
 | 
			
		||||
  String? state;
 | 
			
		||||
  String? city;
 | 
			
		||||
  SearchLocationFilter({
 | 
			
		||||
    this.country,
 | 
			
		||||
    this.state,
 | 
			
		||||
    this.city,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  SearchLocationFilter copyWith({
 | 
			
		||||
    String? country,
 | 
			
		||||
    String? state,
 | 
			
		||||
    String? city,
 | 
			
		||||
  }) {
 | 
			
		||||
    return SearchLocationFilter(
 | 
			
		||||
      country: country ?? this.country,
 | 
			
		||||
      state: state ?? this.state,
 | 
			
		||||
      city: city ?? this.city,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toMap() {
 | 
			
		||||
    return <String, dynamic>{
 | 
			
		||||
      'country': country,
 | 
			
		||||
      'state': state,
 | 
			
		||||
      'city': city,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  factory SearchLocationFilter.fromMap(Map<String, dynamic> map) {
 | 
			
		||||
    return SearchLocationFilter(
 | 
			
		||||
      country: map['country'] != null ? map['country'] as String : null,
 | 
			
		||||
      state: map['state'] != null ? map['state'] as String : null,
 | 
			
		||||
      city: map['city'] != null ? map['city'] as String : null,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String toJson() => json.encode(toMap());
 | 
			
		||||
 | 
			
		||||
  factory SearchLocationFilter.fromJson(String source) =>
 | 
			
		||||
      SearchLocationFilter.fromMap(json.decode(source) as Map<String, dynamic>);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() =>
 | 
			
		||||
      'SearchLocationFilter(country: $country, state: $state, city: $city)';
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  bool operator ==(covariant SearchLocationFilter other) {
 | 
			
		||||
    if (identical(this, other)) return true;
 | 
			
		||||
 | 
			
		||||
    return other.country == country &&
 | 
			
		||||
        other.state == state &&
 | 
			
		||||
        other.city == city;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  int get hashCode => country.hashCode ^ state.hashCode ^ city.hashCode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SearchCameraFilter {
 | 
			
		||||
  String? make;
 | 
			
		||||
  String? model;
 | 
			
		||||
  SearchCameraFilter({
 | 
			
		||||
    this.make,
 | 
			
		||||
    this.model,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  SearchCameraFilter copyWith({
 | 
			
		||||
    String? make,
 | 
			
		||||
    String? model,
 | 
			
		||||
  }) {
 | 
			
		||||
    return SearchCameraFilter(
 | 
			
		||||
      make: make ?? this.make,
 | 
			
		||||
      model: model ?? this.model,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toMap() {
 | 
			
		||||
    return <String, dynamic>{
 | 
			
		||||
      'make': make,
 | 
			
		||||
      'model': model,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  factory SearchCameraFilter.fromMap(Map<String, dynamic> map) {
 | 
			
		||||
    return SearchCameraFilter(
 | 
			
		||||
      make: map['make'] != null ? map['make'] as String : null,
 | 
			
		||||
      model: map['model'] != null ? map['model'] as String : null,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String toJson() => json.encode(toMap());
 | 
			
		||||
 | 
			
		||||
  factory SearchCameraFilter.fromJson(String source) =>
 | 
			
		||||
      SearchCameraFilter.fromMap(json.decode(source) as Map<String, dynamic>);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() => 'SearchCameraFilter(make: $make, model: $model)';
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  bool operator ==(covariant SearchCameraFilter other) {
 | 
			
		||||
    if (identical(this, other)) return true;
 | 
			
		||||
 | 
			
		||||
    return other.make == make && other.model == model;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  int get hashCode => make.hashCode ^ model.hashCode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SearchDateFilter {
 | 
			
		||||
  DateTime? takenBefore;
 | 
			
		||||
  DateTime? takenAfter;
 | 
			
		||||
  SearchDateFilter({
 | 
			
		||||
    this.takenBefore,
 | 
			
		||||
    this.takenAfter,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  SearchDateFilter copyWith({
 | 
			
		||||
    DateTime? takenBefore,
 | 
			
		||||
    DateTime? takenAfter,
 | 
			
		||||
  }) {
 | 
			
		||||
    return SearchDateFilter(
 | 
			
		||||
      takenBefore: takenBefore ?? this.takenBefore,
 | 
			
		||||
      takenAfter: takenAfter ?? this.takenAfter,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toMap() {
 | 
			
		||||
    return <String, dynamic>{
 | 
			
		||||
      'takenBefore': takenBefore?.millisecondsSinceEpoch,
 | 
			
		||||
      'takenAfter': takenAfter?.millisecondsSinceEpoch,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  factory SearchDateFilter.fromMap(Map<String, dynamic> map) {
 | 
			
		||||
    return SearchDateFilter(
 | 
			
		||||
      takenBefore: map['takenBefore'] != null
 | 
			
		||||
          ? DateTime.fromMillisecondsSinceEpoch(map['takenBefore'] as int)
 | 
			
		||||
          : null,
 | 
			
		||||
      takenAfter: map['takenAfter'] != null
 | 
			
		||||
          ? DateTime.fromMillisecondsSinceEpoch(map['takenAfter'] as int)
 | 
			
		||||
          : null,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String toJson() => json.encode(toMap());
 | 
			
		||||
 | 
			
		||||
  factory SearchDateFilter.fromJson(String source) =>
 | 
			
		||||
      SearchDateFilter.fromMap(json.decode(source) as Map<String, dynamic>);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() =>
 | 
			
		||||
      'SearchDateFilter(takenBefore: $takenBefore, takenAfter: $takenAfter)';
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  bool operator ==(covariant SearchDateFilter other) {
 | 
			
		||||
    if (identical(this, other)) return true;
 | 
			
		||||
 | 
			
		||||
    return other.takenBefore == takenBefore && other.takenAfter == takenAfter;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  int get hashCode => takenBefore.hashCode ^ takenAfter.hashCode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SearchDisplayFilters {
 | 
			
		||||
  bool isNotInAlbum = false;
 | 
			
		||||
  bool isArchive = false;
 | 
			
		||||
  bool isFavorite = false;
 | 
			
		||||
  SearchDisplayFilters({
 | 
			
		||||
    required this.isNotInAlbum,
 | 
			
		||||
    required this.isArchive,
 | 
			
		||||
    required this.isFavorite,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  SearchDisplayFilters copyWith({
 | 
			
		||||
    bool? isNotInAlbum,
 | 
			
		||||
    bool? isArchive,
 | 
			
		||||
    bool? isFavorite,
 | 
			
		||||
  }) {
 | 
			
		||||
    return SearchDisplayFilters(
 | 
			
		||||
      isNotInAlbum: isNotInAlbum ?? this.isNotInAlbum,
 | 
			
		||||
      isArchive: isArchive ?? this.isArchive,
 | 
			
		||||
      isFavorite: isFavorite ?? this.isFavorite,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toMap() {
 | 
			
		||||
    return <String, dynamic>{
 | 
			
		||||
      'isNotInAlbum': isNotInAlbum,
 | 
			
		||||
      'isArchive': isArchive,
 | 
			
		||||
      'isFavorite': isFavorite,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  factory SearchDisplayFilters.fromMap(Map<String, dynamic> map) {
 | 
			
		||||
    return SearchDisplayFilters(
 | 
			
		||||
      isNotInAlbum: map['isNotInAlbum'] as bool,
 | 
			
		||||
      isArchive: map['isArchive'] as bool,
 | 
			
		||||
      isFavorite: map['isFavorite'] as bool,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String toJson() => json.encode(toMap());
 | 
			
		||||
 | 
			
		||||
  factory SearchDisplayFilters.fromJson(String source) =>
 | 
			
		||||
      SearchDisplayFilters.fromMap(json.decode(source) as Map<String, dynamic>);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() =>
 | 
			
		||||
      'SearchDisplayFilters(isNotInAlbum: $isNotInAlbum, isArchive: $isArchive, isFavorite: $isFavorite)';
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  bool operator ==(covariant SearchDisplayFilters other) {
 | 
			
		||||
    if (identical(this, other)) return true;
 | 
			
		||||
 | 
			
		||||
    return other.isNotInAlbum == isNotInAlbum &&
 | 
			
		||||
        other.isArchive == isArchive &&
 | 
			
		||||
        other.isFavorite == isFavorite;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  int get hashCode =>
 | 
			
		||||
      isNotInAlbum.hashCode ^ isArchive.hashCode ^ isFavorite.hashCode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SearchFilter {
 | 
			
		||||
  String? context;
 | 
			
		||||
  String? filename;
 | 
			
		||||
  Set<PersonResponseDto> people;
 | 
			
		||||
  SearchLocationFilter location;
 | 
			
		||||
  SearchCameraFilter camera;
 | 
			
		||||
  SearchDateFilter date;
 | 
			
		||||
  SearchDisplayFilters display;
 | 
			
		||||
 | 
			
		||||
  // Enum
 | 
			
		||||
  AssetType mediaType;
 | 
			
		||||
 | 
			
		||||
  SearchFilter({
 | 
			
		||||
    this.context,
 | 
			
		||||
    this.filename,
 | 
			
		||||
    required this.people,
 | 
			
		||||
    required this.location,
 | 
			
		||||
    required this.camera,
 | 
			
		||||
    required this.date,
 | 
			
		||||
    required this.display,
 | 
			
		||||
    required this.mediaType,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  SearchFilter copyWith({
 | 
			
		||||
    String? context,
 | 
			
		||||
    String? filename,
 | 
			
		||||
    Set<PersonResponseDto>? people,
 | 
			
		||||
    SearchLocationFilter? location,
 | 
			
		||||
    SearchCameraFilter? camera,
 | 
			
		||||
    SearchDateFilter? date,
 | 
			
		||||
    SearchDisplayFilters? display,
 | 
			
		||||
    AssetType? mediaType,
 | 
			
		||||
  }) {
 | 
			
		||||
    return SearchFilter(
 | 
			
		||||
      context: context ?? this.context,
 | 
			
		||||
      filename: filename ?? this.filename,
 | 
			
		||||
      people: people ?? this.people,
 | 
			
		||||
      location: location ?? this.location,
 | 
			
		||||
      camera: camera ?? this.camera,
 | 
			
		||||
      date: date ?? this.date,
 | 
			
		||||
      display: display ?? this.display,
 | 
			
		||||
      mediaType: mediaType ?? this.mediaType,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return 'SearchFilter(context: $context, filename: $filename, people: $people, location: $location, camera: $camera, date: $date, display: $display, mediaType: $mediaType)';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  bool operator ==(covariant SearchFilter other) {
 | 
			
		||||
    if (identical(this, other)) return true;
 | 
			
		||||
 | 
			
		||||
    return other.context == context &&
 | 
			
		||||
        other.filename == filename &&
 | 
			
		||||
        other.people == people &&
 | 
			
		||||
        other.location == location &&
 | 
			
		||||
        other.camera == camera &&
 | 
			
		||||
        other.date == date &&
 | 
			
		||||
        other.display == display &&
 | 
			
		||||
        other.mediaType == mediaType;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  int get hashCode {
 | 
			
		||||
    return context.hashCode ^
 | 
			
		||||
        filename.hashCode ^
 | 
			
		||||
        people.hashCode ^
 | 
			
		||||
        location.hashCode ^
 | 
			
		||||
        camera.hashCode ^
 | 
			
		||||
        date.hashCode ^
 | 
			
		||||
        display.hashCode ^
 | 
			
		||||
        mediaType.hashCode;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,62 @@
 | 
			
		||||
import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/models/search_filter.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/services/search.service.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
			
		||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
			
		||||
 | 
			
		||||
part 'paginated_search.provider.g.dart';
 | 
			
		||||
 | 
			
		||||
@riverpod
 | 
			
		||||
class PaginatedSearch extends _$PaginatedSearch {
 | 
			
		||||
  Future<List<Asset>?> _search(SearchFilter filter, int page) async {
 | 
			
		||||
    final service = ref.read(searchServiceProvider);
 | 
			
		||||
    final result = await service.search(filter, page);
 | 
			
		||||
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<List<Asset>> build() async {
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<List<Asset>> getNextPage(SearchFilter filter, int nextPage) async {
 | 
			
		||||
    state = const AsyncValue.loading();
 | 
			
		||||
 | 
			
		||||
    final newState = await AsyncValue.guard(() async {
 | 
			
		||||
      final assets = await _search(filter, nextPage);
 | 
			
		||||
 | 
			
		||||
      if (assets != null) {
 | 
			
		||||
        return [...?state.value, ...assets];
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    state = newState.valueOrNull == null
 | 
			
		||||
        ? const AsyncValue.data([])
 | 
			
		||||
        : AsyncValue.data(newState.value!);
 | 
			
		||||
 | 
			
		||||
    return newState.valueOrNull ?? [];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  clear() {
 | 
			
		||||
    state = const AsyncValue.data([]);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@riverpod
 | 
			
		||||
AsyncValue<RenderList> paginatedSearchRenderList(
 | 
			
		||||
  PaginatedSearchRenderListRef ref,
 | 
			
		||||
) {
 | 
			
		||||
  final assets = ref.watch(paginatedSearchProvider).value;
 | 
			
		||||
 | 
			
		||||
  if (assets != null) {
 | 
			
		||||
    return ref.watch(
 | 
			
		||||
      renderListProviderWithGrouping(
 | 
			
		||||
        (assets, GroupAssetsBy.none),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  } else {
 | 
			
		||||
    return const AsyncValue.loading();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										44
									
								
								mobile/lib/modules/search/providers/paginated_search.provider.g.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								mobile/lib/modules/search/providers/paginated_search.provider.g.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,44 @@
 | 
			
		||||
// GENERATED CODE - DO NOT MODIFY BY HAND
 | 
			
		||||
 | 
			
		||||
part of 'paginated_search.provider.dart';
 | 
			
		||||
 | 
			
		||||
// **************************************************************************
 | 
			
		||||
// RiverpodGenerator
 | 
			
		||||
// **************************************************************************
 | 
			
		||||
 | 
			
		||||
String _$paginatedSearchRenderListHash() =>
 | 
			
		||||
    r'c2cc2381ee6ea8f8e08d6d4c1289bbf0c6b9647e';
 | 
			
		||||
 | 
			
		||||
/// See also [paginatedSearchRenderList].
 | 
			
		||||
@ProviderFor(paginatedSearchRenderList)
 | 
			
		||||
final paginatedSearchRenderListProvider =
 | 
			
		||||
    AutoDisposeProvider<AsyncValue<RenderList>>.internal(
 | 
			
		||||
  paginatedSearchRenderList,
 | 
			
		||||
  name: r'paginatedSearchRenderListProvider',
 | 
			
		||||
  debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
 | 
			
		||||
      ? null
 | 
			
		||||
      : _$paginatedSearchRenderListHash,
 | 
			
		||||
  dependencies: null,
 | 
			
		||||
  allTransitiveDependencies: null,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
typedef PaginatedSearchRenderListRef
 | 
			
		||||
    = AutoDisposeProviderRef<AsyncValue<RenderList>>;
 | 
			
		||||
String _$paginatedSearchHash() => r'8312f358261368cf2b5572b839fdd8f8fbe9a62e';
 | 
			
		||||
 | 
			
		||||
/// See also [PaginatedSearch].
 | 
			
		||||
@ProviderFor(PaginatedSearch)
 | 
			
		||||
final paginatedSearchProvider =
 | 
			
		||||
    AutoDisposeAsyncNotifierProvider<PaginatedSearch, List<Asset>>.internal(
 | 
			
		||||
  PaginatedSearch.new,
 | 
			
		||||
  name: r'paginatedSearchProvider',
 | 
			
		||||
  debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
 | 
			
		||||
      ? null
 | 
			
		||||
      : _$paginatedSearchHash,
 | 
			
		||||
  dependencies: null,
 | 
			
		||||
  allTransitiveDependencies: null,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
typedef _$PaginatedSearch = AutoDisposeAsyncNotifier<List<Asset>>;
 | 
			
		||||
// ignore_for_file: type=lint
 | 
			
		||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
 | 
			
		||||
@ -1,23 +1,21 @@
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/models/curated_content.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/services/person.service.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
			
		||||
 | 
			
		||||
part 'people.provider.g.dart';
 | 
			
		||||
 | 
			
		||||
@riverpod
 | 
			
		||||
Future<List<CuratedContent>> getCuratedPeople(
 | 
			
		||||
  GetCuratedPeopleRef ref,
 | 
			
		||||
Future<List<PersonResponseDto>> getAllPeople(
 | 
			
		||||
  GetAllPeopleRef ref,
 | 
			
		||||
) async {
 | 
			
		||||
  final PersonService personService = ref.read(personServiceProvider);
 | 
			
		||||
 | 
			
		||||
  final curatedPeople = await personService.getCuratedPeople();
 | 
			
		||||
  final people = await personService.getAllPeople();
 | 
			
		||||
 | 
			
		||||
  return curatedPeople
 | 
			
		||||
      .map((p) => CuratedContent(id: p.id, label: p.name))
 | 
			
		||||
      .toList();
 | 
			
		||||
  return people;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@riverpod
 | 
			
		||||
@ -44,7 +42,7 @@ Future<bool> updatePersonName(
 | 
			
		||||
  final person = await personService.updateName(personId, updatedName);
 | 
			
		||||
 | 
			
		||||
  if (person != null && person.name == updatedName) {
 | 
			
		||||
    ref.invalidate(getCuratedPeopleProvider);
 | 
			
		||||
    ref.invalidate(getAllPeopleProvider);
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  return false;
 | 
			
		||||
 | 
			
		||||
@ -6,23 +6,21 @@ part of 'people.provider.dart';
 | 
			
		||||
// RiverpodGenerator
 | 
			
		||||
// **************************************************************************
 | 
			
		||||
 | 
			
		||||
String _$getCuratedPeopleHash() => r'2a534553812abe69abce2c2e41aa62b8de16e9d0';
 | 
			
		||||
String _$getAllPeopleHash() => r'4eff6666be5a74710d1e8587e01d8154310d85bd';
 | 
			
		||||
 | 
			
		||||
/// See also [getCuratedPeople].
 | 
			
		||||
@ProviderFor(getCuratedPeople)
 | 
			
		||||
final getCuratedPeopleProvider =
 | 
			
		||||
    AutoDisposeFutureProvider<List<CuratedContent>>.internal(
 | 
			
		||||
  getCuratedPeople,
 | 
			
		||||
  name: r'getCuratedPeopleProvider',
 | 
			
		||||
  debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
 | 
			
		||||
      ? null
 | 
			
		||||
      : _$getCuratedPeopleHash,
 | 
			
		||||
/// See also [getAllPeople].
 | 
			
		||||
@ProviderFor(getAllPeople)
 | 
			
		||||
final getAllPeopleProvider =
 | 
			
		||||
    AutoDisposeFutureProvider<List<PersonResponseDto>>.internal(
 | 
			
		||||
  getAllPeople,
 | 
			
		||||
  name: r'getAllPeopleProvider',
 | 
			
		||||
  debugGetCreateSourceHash:
 | 
			
		||||
      const bool.fromEnvironment('dart.vm.product') ? null : _$getAllPeopleHash,
 | 
			
		||||
  dependencies: null,
 | 
			
		||||
  allTransitiveDependencies: null,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
typedef GetCuratedPeopleRef
 | 
			
		||||
    = AutoDisposeFutureProviderRef<List<CuratedContent>>;
 | 
			
		||||
typedef GetAllPeopleRef = AutoDisposeFutureProviderRef<List<PersonResponseDto>>;
 | 
			
		||||
String _$personAssetsHash() => r'1d6eff5ca3aa630b58c4dad9516193b21896984d';
 | 
			
		||||
 | 
			
		||||
/// Copied from Dart SDK
 | 
			
		||||
@ -172,7 +170,7 @@ class _PersonAssetsProviderElement
 | 
			
		||||
  String get personId => (origin as PersonAssetsProvider).personId;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
String _$updatePersonNameHash() => r'c7179a7cc558669c3b30b03fbca7782a42f2b6fd';
 | 
			
		||||
String _$updatePersonNameHash() => r'7145aaaf6fc38fdafe3a283ebf3d3f4fd0774cd2';
 | 
			
		||||
 | 
			
		||||
/// See also [updatePersonName].
 | 
			
		||||
@ProviderFor(updatePersonName)
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,27 @@
 | 
			
		||||
import 'package:immich_mobile/modules/search/services/search.service.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
			
		||||
 | 
			
		||||
part 'search_filter.provider.g.dart';
 | 
			
		||||
 | 
			
		||||
@riverpod
 | 
			
		||||
Future<List<String>> getSearchSuggestions(
 | 
			
		||||
  GetSearchSuggestionsRef ref,
 | 
			
		||||
  SearchSuggestionType type, {
 | 
			
		||||
  String? locationCountry,
 | 
			
		||||
  String? locationState,
 | 
			
		||||
  String? make,
 | 
			
		||||
  String? model,
 | 
			
		||||
}) async {
 | 
			
		||||
  final SearchService service = ref.read(searchServiceProvider);
 | 
			
		||||
 | 
			
		||||
  final suggestions = await service.getSearchSuggestions(
 | 
			
		||||
    type,
 | 
			
		||||
    country: locationCountry,
 | 
			
		||||
    state: locationState,
 | 
			
		||||
    make: make,
 | 
			
		||||
    model: model,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return suggestions ?? [];
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										229
									
								
								mobile/lib/modules/search/providers/search_filter.provider.g.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								mobile/lib/modules/search/providers/search_filter.provider.g.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,229 @@
 | 
			
		||||
// GENERATED CODE - DO NOT MODIFY BY HAND
 | 
			
		||||
 | 
			
		||||
part of 'search_filter.provider.dart';
 | 
			
		||||
 | 
			
		||||
// **************************************************************************
 | 
			
		||||
// RiverpodGenerator
 | 
			
		||||
// **************************************************************************
 | 
			
		||||
 | 
			
		||||
String _$getSearchSuggestionsHash() =>
 | 
			
		||||
    r'bc1e9a1a060868f14e6eb970d2251dbfe39c6866';
 | 
			
		||||
 | 
			
		||||
/// Copied from Dart SDK
 | 
			
		||||
class _SystemHash {
 | 
			
		||||
  _SystemHash._();
 | 
			
		||||
 | 
			
		||||
  static int combine(int hash, int value) {
 | 
			
		||||
    // ignore: parameter_assignments
 | 
			
		||||
    hash = 0x1fffffff & (hash + value);
 | 
			
		||||
    // ignore: parameter_assignments
 | 
			
		||||
    hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
 | 
			
		||||
    return hash ^ (hash >> 6);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static int finish(int hash) {
 | 
			
		||||
    // ignore: parameter_assignments
 | 
			
		||||
    hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
 | 
			
		||||
    // ignore: parameter_assignments
 | 
			
		||||
    hash = hash ^ (hash >> 11);
 | 
			
		||||
    return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// See also [getSearchSuggestions].
 | 
			
		||||
@ProviderFor(getSearchSuggestions)
 | 
			
		||||
const getSearchSuggestionsProvider = GetSearchSuggestionsFamily();
 | 
			
		||||
 | 
			
		||||
/// See also [getSearchSuggestions].
 | 
			
		||||
class GetSearchSuggestionsFamily extends Family<AsyncValue<List<String>>> {
 | 
			
		||||
  /// See also [getSearchSuggestions].
 | 
			
		||||
  const GetSearchSuggestionsFamily();
 | 
			
		||||
 | 
			
		||||
  /// See also [getSearchSuggestions].
 | 
			
		||||
  GetSearchSuggestionsProvider call(
 | 
			
		||||
    SearchSuggestionType type, {
 | 
			
		||||
    String? locationCountry,
 | 
			
		||||
    String? locationState,
 | 
			
		||||
    String? make,
 | 
			
		||||
    String? model,
 | 
			
		||||
  }) {
 | 
			
		||||
    return GetSearchSuggestionsProvider(
 | 
			
		||||
      type,
 | 
			
		||||
      locationCountry: locationCountry,
 | 
			
		||||
      locationState: locationState,
 | 
			
		||||
      make: make,
 | 
			
		||||
      model: model,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  GetSearchSuggestionsProvider getProviderOverride(
 | 
			
		||||
    covariant GetSearchSuggestionsProvider provider,
 | 
			
		||||
  ) {
 | 
			
		||||
    return call(
 | 
			
		||||
      provider.type,
 | 
			
		||||
      locationCountry: provider.locationCountry,
 | 
			
		||||
      locationState: provider.locationState,
 | 
			
		||||
      make: provider.make,
 | 
			
		||||
      model: provider.model,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static const Iterable<ProviderOrFamily>? _dependencies = null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Iterable<ProviderOrFamily>? get dependencies => _dependencies;
 | 
			
		||||
 | 
			
		||||
  static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
 | 
			
		||||
      _allTransitiveDependencies;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String? get name => r'getSearchSuggestionsProvider';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// See also [getSearchSuggestions].
 | 
			
		||||
class GetSearchSuggestionsProvider
 | 
			
		||||
    extends AutoDisposeFutureProvider<List<String>> {
 | 
			
		||||
  /// See also [getSearchSuggestions].
 | 
			
		||||
  GetSearchSuggestionsProvider(
 | 
			
		||||
    SearchSuggestionType type, {
 | 
			
		||||
    String? locationCountry,
 | 
			
		||||
    String? locationState,
 | 
			
		||||
    String? make,
 | 
			
		||||
    String? model,
 | 
			
		||||
  }) : this._internal(
 | 
			
		||||
          (ref) => getSearchSuggestions(
 | 
			
		||||
            ref as GetSearchSuggestionsRef,
 | 
			
		||||
            type,
 | 
			
		||||
            locationCountry: locationCountry,
 | 
			
		||||
            locationState: locationState,
 | 
			
		||||
            make: make,
 | 
			
		||||
            model: model,
 | 
			
		||||
          ),
 | 
			
		||||
          from: getSearchSuggestionsProvider,
 | 
			
		||||
          name: r'getSearchSuggestionsProvider',
 | 
			
		||||
          debugGetCreateSourceHash:
 | 
			
		||||
              const bool.fromEnvironment('dart.vm.product')
 | 
			
		||||
                  ? null
 | 
			
		||||
                  : _$getSearchSuggestionsHash,
 | 
			
		||||
          dependencies: GetSearchSuggestionsFamily._dependencies,
 | 
			
		||||
          allTransitiveDependencies:
 | 
			
		||||
              GetSearchSuggestionsFamily._allTransitiveDependencies,
 | 
			
		||||
          type: type,
 | 
			
		||||
          locationCountry: locationCountry,
 | 
			
		||||
          locationState: locationState,
 | 
			
		||||
          make: make,
 | 
			
		||||
          model: model,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
  GetSearchSuggestionsProvider._internal(
 | 
			
		||||
    super._createNotifier, {
 | 
			
		||||
    required super.name,
 | 
			
		||||
    required super.dependencies,
 | 
			
		||||
    required super.allTransitiveDependencies,
 | 
			
		||||
    required super.debugGetCreateSourceHash,
 | 
			
		||||
    required super.from,
 | 
			
		||||
    required this.type,
 | 
			
		||||
    required this.locationCountry,
 | 
			
		||||
    required this.locationState,
 | 
			
		||||
    required this.make,
 | 
			
		||||
    required this.model,
 | 
			
		||||
  }) : super.internal();
 | 
			
		||||
 | 
			
		||||
  final SearchSuggestionType type;
 | 
			
		||||
  final String? locationCountry;
 | 
			
		||||
  final String? locationState;
 | 
			
		||||
  final String? make;
 | 
			
		||||
  final String? model;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Override overrideWith(
 | 
			
		||||
    FutureOr<List<String>> Function(GetSearchSuggestionsRef provider) create,
 | 
			
		||||
  ) {
 | 
			
		||||
    return ProviderOverride(
 | 
			
		||||
      origin: this,
 | 
			
		||||
      override: GetSearchSuggestionsProvider._internal(
 | 
			
		||||
        (ref) => create(ref as GetSearchSuggestionsRef),
 | 
			
		||||
        from: from,
 | 
			
		||||
        name: null,
 | 
			
		||||
        dependencies: null,
 | 
			
		||||
        allTransitiveDependencies: null,
 | 
			
		||||
        debugGetCreateSourceHash: null,
 | 
			
		||||
        type: type,
 | 
			
		||||
        locationCountry: locationCountry,
 | 
			
		||||
        locationState: locationState,
 | 
			
		||||
        make: make,
 | 
			
		||||
        model: model,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  AutoDisposeFutureProviderElement<List<String>> createElement() {
 | 
			
		||||
    return _GetSearchSuggestionsProviderElement(this);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  bool operator ==(Object other) {
 | 
			
		||||
    return other is GetSearchSuggestionsProvider &&
 | 
			
		||||
        other.type == type &&
 | 
			
		||||
        other.locationCountry == locationCountry &&
 | 
			
		||||
        other.locationState == locationState &&
 | 
			
		||||
        other.make == make &&
 | 
			
		||||
        other.model == model;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  int get hashCode {
 | 
			
		||||
    var hash = _SystemHash.combine(0, runtimeType.hashCode);
 | 
			
		||||
    hash = _SystemHash.combine(hash, type.hashCode);
 | 
			
		||||
    hash = _SystemHash.combine(hash, locationCountry.hashCode);
 | 
			
		||||
    hash = _SystemHash.combine(hash, locationState.hashCode);
 | 
			
		||||
    hash = _SystemHash.combine(hash, make.hashCode);
 | 
			
		||||
    hash = _SystemHash.combine(hash, model.hashCode);
 | 
			
		||||
 | 
			
		||||
    return _SystemHash.finish(hash);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
mixin GetSearchSuggestionsRef on AutoDisposeFutureProviderRef<List<String>> {
 | 
			
		||||
  /// The parameter `type` of this provider.
 | 
			
		||||
  SearchSuggestionType get type;
 | 
			
		||||
 | 
			
		||||
  /// The parameter `locationCountry` of this provider.
 | 
			
		||||
  String? get locationCountry;
 | 
			
		||||
 | 
			
		||||
  /// The parameter `locationState` of this provider.
 | 
			
		||||
  String? get locationState;
 | 
			
		||||
 | 
			
		||||
  /// The parameter `make` of this provider.
 | 
			
		||||
  String? get make;
 | 
			
		||||
 | 
			
		||||
  /// The parameter `model` of this provider.
 | 
			
		||||
  String? get model;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _GetSearchSuggestionsProviderElement
 | 
			
		||||
    extends AutoDisposeFutureProviderElement<List<String>>
 | 
			
		||||
    with GetSearchSuggestionsRef {
 | 
			
		||||
  _GetSearchSuggestionsProviderElement(super.provider);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  SearchSuggestionType get type =>
 | 
			
		||||
      (origin as GetSearchSuggestionsProvider).type;
 | 
			
		||||
  @override
 | 
			
		||||
  String? get locationCountry =>
 | 
			
		||||
      (origin as GetSearchSuggestionsProvider).locationCountry;
 | 
			
		||||
  @override
 | 
			
		||||
  String? get locationState =>
 | 
			
		||||
      (origin as GetSearchSuggestionsProvider).locationState;
 | 
			
		||||
  @override
 | 
			
		||||
  String? get make => (origin as GetSearchSuggestionsProvider).make;
 | 
			
		||||
  @override
 | 
			
		||||
  String? get model => (origin as GetSearchSuggestionsProvider).model;
 | 
			
		||||
}
 | 
			
		||||
// ignore_for_file: type=lint
 | 
			
		||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
 | 
			
		||||
@ -1,67 +0,0 @@
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:immich_mobile/modules/search/services/search.service.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
			
		||||
 | 
			
		||||
class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
 | 
			
		||||
  SearchResultPageNotifier(this._searchService)
 | 
			
		||||
      : super(
 | 
			
		||||
          SearchResultPageState(
 | 
			
		||||
            searchResult: [],
 | 
			
		||||
            isError: false,
 | 
			
		||||
            isLoading: true,
 | 
			
		||||
            isSuccess: false,
 | 
			
		||||
            isSmart: false,
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
  final SearchService _searchService;
 | 
			
		||||
 | 
			
		||||
  Future<void> search(String searchTerm, {bool smartSearch = true}) async {
 | 
			
		||||
    state = state.copyWith(
 | 
			
		||||
      searchResult: [],
 | 
			
		||||
      isError: false,
 | 
			
		||||
      isLoading: true,
 | 
			
		||||
      isSuccess: false,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    List<Asset>? assets =
 | 
			
		||||
        await _searchService.searchAsset(searchTerm, smartSearch: smartSearch);
 | 
			
		||||
 | 
			
		||||
    if (assets != null) {
 | 
			
		||||
      state = state.copyWith(
 | 
			
		||||
        searchResult: assets,
 | 
			
		||||
        isError: false,
 | 
			
		||||
        isLoading: false,
 | 
			
		||||
        isSuccess: true,
 | 
			
		||||
        isSmart: smartSearch,
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      state = state.copyWith(
 | 
			
		||||
        searchResult: [],
 | 
			
		||||
        isError: true,
 | 
			
		||||
        isLoading: false,
 | 
			
		||||
        isSuccess: false,
 | 
			
		||||
        isSmart: smartSearch,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final searchResultPageProvider =
 | 
			
		||||
    StateNotifierProvider<SearchResultPageNotifier, SearchResultPageState>(
 | 
			
		||||
        (ref) {
 | 
			
		||||
  return SearchResultPageNotifier(ref.watch(searchServiceProvider));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
final searchRenderListProvider = Provider((ref) {
 | 
			
		||||
  final result = ref.watch(searchResultPageProvider);
 | 
			
		||||
  return ref.watch(
 | 
			
		||||
    renderListProviderWithGrouping(
 | 
			
		||||
      (result.searchResult, result.isSmart ? GroupAssetsBy.none : null),
 | 
			
		||||
    ),
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
@ -20,7 +20,7 @@ class PersonService {
 | 
			
		||||
 | 
			
		||||
  PersonService(this._apiService, this._db);
 | 
			
		||||
 | 
			
		||||
  Future<List<PersonResponseDto>> getCuratedPeople() async {
 | 
			
		||||
  Future<List<PersonResponseDto>> getAllPeople() async {
 | 
			
		||||
    try {
 | 
			
		||||
      final peopleResponseDto = await _apiService.personApi.getAllPeople();
 | 
			
		||||
      return peopleResponseDto?.people ?? [];
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/models/search_filter.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
 | 
			
		||||
@ -29,25 +30,92 @@ class SearchService {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<List<Asset>?> searchAsset(
 | 
			
		||||
    String searchTerm, {
 | 
			
		||||
    bool smartSearch = true,
 | 
			
		||||
  Future<List<String>?> getSearchSuggestions(
 | 
			
		||||
    SearchSuggestionType type, {
 | 
			
		||||
    String? country,
 | 
			
		||||
    String? state,
 | 
			
		||||
    String? make,
 | 
			
		||||
    String? model,
 | 
			
		||||
  }) async {
 | 
			
		||||
    // TODO search in local DB: 1. when offline, 2. to find local assets
 | 
			
		||||
    try {
 | 
			
		||||
      final SearchResponseDto? results = await _apiService.searchApi.search(
 | 
			
		||||
        query: searchTerm,
 | 
			
		||||
        smart: smartSearch,
 | 
			
		||||
      return await _apiService.searchApi.getSearchSuggestions(
 | 
			
		||||
        type,
 | 
			
		||||
        country: country,
 | 
			
		||||
        state: state,
 | 
			
		||||
        make: make,
 | 
			
		||||
        model: model,
 | 
			
		||||
      );
 | 
			
		||||
      if (results == null) {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
      // TODO local DB might be out of date; add assets not yet in DB?
 | 
			
		||||
      return _db.assets.getAllByRemoteId(results.assets.items.map((e) => e.id));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint("[ERROR] [searchAsset] ${e.toString()}");
 | 
			
		||||
      debugPrint("[ERROR] [getSearchSuggestions] ${e.toString()}");
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<List<Asset>?> search(SearchFilter filter, int page) async {
 | 
			
		||||
    try {
 | 
			
		||||
      SearchResponseDto? response;
 | 
			
		||||
      AssetTypeEnum? type;
 | 
			
		||||
      if (filter.mediaType == AssetType.image) {
 | 
			
		||||
        type = AssetTypeEnum.IMAGE;
 | 
			
		||||
      } else if (filter.mediaType == AssetType.video) {
 | 
			
		||||
        type = AssetTypeEnum.VIDEO;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (filter.context != null && filter.context!.isNotEmpty) {
 | 
			
		||||
        response = await _apiService.searchApi.searchSmart(
 | 
			
		||||
          SmartSearchDto(
 | 
			
		||||
            query: filter.context!,
 | 
			
		||||
            country: filter.location.country,
 | 
			
		||||
            state: filter.location.state,
 | 
			
		||||
            city: filter.location.city,
 | 
			
		||||
            make: filter.camera.make,
 | 
			
		||||
            model: filter.camera.model,
 | 
			
		||||
            takenAfter: filter.date.takenAfter,
 | 
			
		||||
            takenBefore: filter.date.takenBefore,
 | 
			
		||||
            isArchived: filter.display.isArchive,
 | 
			
		||||
            isFavorite: filter.display.isFavorite,
 | 
			
		||||
            isNotInAlbum: filter.display.isNotInAlbum,
 | 
			
		||||
            personIds: filter.people.map((e) => e.id).toList(),
 | 
			
		||||
            type: type,
 | 
			
		||||
            page: page,
 | 
			
		||||
            size: 1000,
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        response = await _apiService.searchApi.searchMetadata(
 | 
			
		||||
          MetadataSearchDto(
 | 
			
		||||
            originalFileName:
 | 
			
		||||
                filter.filename != null && filter.filename!.isNotEmpty
 | 
			
		||||
                    ? filter.filename
 | 
			
		||||
                    : null,
 | 
			
		||||
            country: filter.location.country,
 | 
			
		||||
            state: filter.location.state,
 | 
			
		||||
            city: filter.location.city,
 | 
			
		||||
            make: filter.camera.make,
 | 
			
		||||
            model: filter.camera.model,
 | 
			
		||||
            takenAfter: filter.date.takenAfter,
 | 
			
		||||
            takenBefore: filter.date.takenBefore,
 | 
			
		||||
            isArchived: filter.display.isArchive,
 | 
			
		||||
            isFavorite: filter.display.isFavorite,
 | 
			
		||||
            isNotInAlbum: filter.display.isNotInAlbum,
 | 
			
		||||
            personIds: filter.people.map((e) => e.id).toList(),
 | 
			
		||||
            type: type,
 | 
			
		||||
            page: page,
 | 
			
		||||
            size: 1000,
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (response == null) {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return _db.assets
 | 
			
		||||
          .getAllByRemoteId(response.assets.items.map((e) => e.id));
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      debugPrint("Error [search] $error");
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<List<CuratedLocationsResponseDto>?> getCuratedLocation() async {
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,10 @@
 | 
			
		||||
import 'package:auto_route/auto_route.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/models/curated_content.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/models/search_filter.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
 | 
			
		||||
import 'package:immich_mobile/routing/router.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/store.dart';
 | 
			
		||||
import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
			
		||||
 | 
			
		||||
@ -57,7 +59,22 @@ class ExploreGrid extends StatelessWidget {
 | 
			
		||||
                    ),
 | 
			
		||||
                  )
 | 
			
		||||
                : context.pushRoute(
 | 
			
		||||
                    SearchResultRoute(searchTerm: 'm:${content.label}'),
 | 
			
		||||
                    SearchInputRoute(
 | 
			
		||||
                      prefilter: SearchFilter(
 | 
			
		||||
                        people: {},
 | 
			
		||||
                        location: SearchLocationFilter(
 | 
			
		||||
                          city: content.label,
 | 
			
		||||
                        ),
 | 
			
		||||
                        camera: SearchCameraFilter(),
 | 
			
		||||
                        date: SearchDateFilter(),
 | 
			
		||||
                        display: SearchDisplayFilters(
 | 
			
		||||
                          isNotInAlbum: false,
 | 
			
		||||
                          isArchive: false,
 | 
			
		||||
                          isFavorite: false,
 | 
			
		||||
                        ),
 | 
			
		||||
                        mediaType: AssetType.other,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  );
 | 
			
		||||
          },
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
@ -1,99 +0,0 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
 | 
			
		||||
 | 
			
		||||
class ImmichSearchBar extends HookConsumerWidget
 | 
			
		||||
    implements PreferredSizeWidget {
 | 
			
		||||
  const ImmichSearchBar({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.searchFocusNode,
 | 
			
		||||
    required this.onSubmitted,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final FocusNode searchFocusNode;
 | 
			
		||||
  final Function(String) onSubmitted;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final searchTermController = useTextEditingController(text: "");
 | 
			
		||||
    final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
 | 
			
		||||
 | 
			
		||||
    focusSearch() {
 | 
			
		||||
      searchTermController.clear();
 | 
			
		||||
      ref.watch(searchPageStateProvider.notifier).getSuggestedSearchTerms();
 | 
			
		||||
      ref.watch(searchPageStateProvider.notifier).enableSearch();
 | 
			
		||||
      ref.watch(searchPageStateProvider.notifier).setSearchTerm("");
 | 
			
		||||
 | 
			
		||||
      searchFocusNode.requestFocus();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    useEffect(
 | 
			
		||||
      () {
 | 
			
		||||
        searchFocusNotifier.addListener(focusSearch);
 | 
			
		||||
        return () {
 | 
			
		||||
          searchFocusNotifier.removeListener(focusSearch);
 | 
			
		||||
        };
 | 
			
		||||
      },
 | 
			
		||||
      [],
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return AppBar(
 | 
			
		||||
      automaticallyImplyLeading: false,
 | 
			
		||||
      leading: isSearchEnabled
 | 
			
		||||
          ? IconButton(
 | 
			
		||||
              onPressed: () {
 | 
			
		||||
                searchFocusNode.unfocus();
 | 
			
		||||
                ref.watch(searchPageStateProvider.notifier).disableSearch();
 | 
			
		||||
                searchTermController.clear();
 | 
			
		||||
              },
 | 
			
		||||
              icon: const Icon(Icons.arrow_back_ios_rounded),
 | 
			
		||||
            )
 | 
			
		||||
          : const Icon(
 | 
			
		||||
              Icons.search_rounded,
 | 
			
		||||
              size: 20,
 | 
			
		||||
            ),
 | 
			
		||||
      title: TextField(
 | 
			
		||||
        controller: searchTermController,
 | 
			
		||||
        focusNode: searchFocusNode,
 | 
			
		||||
        autofocus: false,
 | 
			
		||||
        onTap: focusSearch,
 | 
			
		||||
        onSubmitted: (searchTerm) {
 | 
			
		||||
          onSubmitted(searchTerm);
 | 
			
		||||
          searchTermController.clear();
 | 
			
		||||
          ref.watch(searchPageStateProvider.notifier).setSearchTerm("");
 | 
			
		||||
        },
 | 
			
		||||
        onChanged: (value) {
 | 
			
		||||
          ref.watch(searchPageStateProvider.notifier).setSearchTerm(value);
 | 
			
		||||
        },
 | 
			
		||||
        decoration: InputDecoration(
 | 
			
		||||
          hintText: 'search_bar_hint'.tr(),
 | 
			
		||||
          hintStyle: context.textTheme.bodyLarge?.copyWith(
 | 
			
		||||
            color: context.themeData.colorScheme.onSurface.withOpacity(0.75),
 | 
			
		||||
          ),
 | 
			
		||||
          enabledBorder: const UnderlineInputBorder(
 | 
			
		||||
            borderSide: BorderSide(color: Colors.transparent),
 | 
			
		||||
          ),
 | 
			
		||||
          focusedBorder: const UnderlineInputBorder(
 | 
			
		||||
            borderSide: BorderSide(color: Colors.transparent),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Size get preferredSize => const Size.fromHeight(kToolbarHeight);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Used to focus search from outside this widget.
 | 
			
		||||
// For example when double pressing the search nav icon.
 | 
			
		||||
final searchFocusNotifier = SearchFocusNotifier();
 | 
			
		||||
 | 
			
		||||
class SearchFocusNotifier with ChangeNotifier {
 | 
			
		||||
  void requestFocus() {
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										120
									
								
								mobile/lib/modules/search/ui/search_filter/camera_picker.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								mobile/lib/modules/search/ui/search_filter/camera_picker.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,120 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/models/search_filter.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/providers/search_filter.provider.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
class CameraPicker extends HookConsumerWidget {
 | 
			
		||||
  const CameraPicker({super.key, required this.onSelect, this.filter});
 | 
			
		||||
 | 
			
		||||
  final Function(Map<String, String?>) onSelect;
 | 
			
		||||
  final SearchCameraFilter? filter;
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final makeTextController = useTextEditingController(text: filter?.make);
 | 
			
		||||
    final modelTextController = useTextEditingController(text: filter?.model);
 | 
			
		||||
    final selectedMake = useState<String?>(filter?.make);
 | 
			
		||||
    final selectedModel = useState<String?>(filter?.model);
 | 
			
		||||
 | 
			
		||||
    final make = ref.watch(
 | 
			
		||||
      getSearchSuggestionsProvider(
 | 
			
		||||
        SearchSuggestionType.cameraMake,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    final models = ref.watch(
 | 
			
		||||
      getSearchSuggestionsProvider(
 | 
			
		||||
        SearchSuggestionType.cameraModel,
 | 
			
		||||
        make: selectedMake.value,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    final inputDecorationTheme = InputDecorationTheme(
 | 
			
		||||
      border: OutlineInputBorder(
 | 
			
		||||
        borderRadius: BorderRadius.circular(20),
 | 
			
		||||
      ),
 | 
			
		||||
      contentPadding: const EdgeInsets.only(left: 16),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    final menuStyle = MenuStyle(
 | 
			
		||||
      shape: MaterialStatePropertyAll<OutlinedBorder>(
 | 
			
		||||
        RoundedRectangleBorder(
 | 
			
		||||
          borderRadius: BorderRadius.circular(15),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return Container(
 | 
			
		||||
      padding: const EdgeInsets.only(
 | 
			
		||||
          // bottom: MediaQuery.of(context).viewInsets.bottom,
 | 
			
		||||
          ),
 | 
			
		||||
      child: Row(
 | 
			
		||||
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
 | 
			
		||||
        children: [
 | 
			
		||||
          DropdownMenu(
 | 
			
		||||
            dropdownMenuEntries: switch (make) {
 | 
			
		||||
              AsyncError() => [],
 | 
			
		||||
              AsyncData(:final value) => value
 | 
			
		||||
                  .map(
 | 
			
		||||
                    (e) => DropdownMenuEntry(
 | 
			
		||||
                      value: e,
 | 
			
		||||
                      label: e,
 | 
			
		||||
                    ),
 | 
			
		||||
                  )
 | 
			
		||||
                  .toList(),
 | 
			
		||||
              _ => [],
 | 
			
		||||
            },
 | 
			
		||||
            width: context.width * 0.45,
 | 
			
		||||
            menuHeight: 400,
 | 
			
		||||
            label: const Text('Make'),
 | 
			
		||||
            inputDecorationTheme: inputDecorationTheme,
 | 
			
		||||
            controller: makeTextController,
 | 
			
		||||
            menuStyle: menuStyle,
 | 
			
		||||
            leadingIcon: const Icon(Icons.photo_camera_rounded),
 | 
			
		||||
            trailingIcon: const Icon(Icons.arrow_drop_down_rounded),
 | 
			
		||||
            selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded),
 | 
			
		||||
            onSelected: (value) {
 | 
			
		||||
              selectedMake.value = value.toString();
 | 
			
		||||
              onSelect({
 | 
			
		||||
                'make': selectedMake.value,
 | 
			
		||||
                'model': selectedModel.value,
 | 
			
		||||
              });
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
          DropdownMenu(
 | 
			
		||||
            dropdownMenuEntries: switch (models) {
 | 
			
		||||
              AsyncError() => [],
 | 
			
		||||
              AsyncData(:final value) => value
 | 
			
		||||
                  .map(
 | 
			
		||||
                    (e) => DropdownMenuEntry(
 | 
			
		||||
                      value: e,
 | 
			
		||||
                      label: e,
 | 
			
		||||
                    ),
 | 
			
		||||
                  )
 | 
			
		||||
                  .toList(),
 | 
			
		||||
              _ => [],
 | 
			
		||||
            },
 | 
			
		||||
            width: context.width * 0.45,
 | 
			
		||||
            menuHeight: 400,
 | 
			
		||||
            label: const Text('Model'),
 | 
			
		||||
            inputDecorationTheme: inputDecorationTheme,
 | 
			
		||||
            controller: modelTextController,
 | 
			
		||||
            menuStyle: menuStyle,
 | 
			
		||||
            leadingIcon: const Icon(Icons.camera),
 | 
			
		||||
            trailingIcon: const Icon(Icons.arrow_drop_down_rounded),
 | 
			
		||||
            selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded),
 | 
			
		||||
            onSelected: (value) {
 | 
			
		||||
              selectedModel.value = value.toString();
 | 
			
		||||
              onSelect({
 | 
			
		||||
                'make': selectedMake.value,
 | 
			
		||||
                'model': selectedModel.value,
 | 
			
		||||
              });
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,68 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/models/search_filter.dart';
 | 
			
		||||
 | 
			
		||||
enum DisplayOption {
 | 
			
		||||
  notInAlbum,
 | 
			
		||||
  favorite,
 | 
			
		||||
  archive,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class DisplayOptionPicker extends HookWidget {
 | 
			
		||||
  const DisplayOptionPicker({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.onSelect,
 | 
			
		||||
    this.filter,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final Function(Map<DisplayOption, bool>) onSelect;
 | 
			
		||||
  final SearchDisplayFilters? filter;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final options = useState<Map<DisplayOption, bool>>({
 | 
			
		||||
      DisplayOption.notInAlbum: filter?.isNotInAlbum ?? false,
 | 
			
		||||
      DisplayOption.favorite: filter?.isFavorite ?? false,
 | 
			
		||||
      DisplayOption.archive: filter?.isArchive ?? false,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return ListView(
 | 
			
		||||
      shrinkWrap: true,
 | 
			
		||||
      children: [
 | 
			
		||||
        CheckboxListTile(
 | 
			
		||||
          title: const Text('Not in album'),
 | 
			
		||||
          value: options.value[DisplayOption.notInAlbum],
 | 
			
		||||
          onChanged: (bool? value) {
 | 
			
		||||
            options.value = {
 | 
			
		||||
              ...options.value,
 | 
			
		||||
              DisplayOption.notInAlbum: value!,
 | 
			
		||||
            };
 | 
			
		||||
            onSelect(options.value);
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        CheckboxListTile(
 | 
			
		||||
          title: const Text('Favorite'),
 | 
			
		||||
          value: options.value[DisplayOption.favorite],
 | 
			
		||||
          onChanged: (value) {
 | 
			
		||||
            options.value = {
 | 
			
		||||
              ...options.value,
 | 
			
		||||
              DisplayOption.favorite: value!,
 | 
			
		||||
            };
 | 
			
		||||
            onSelect(options.value);
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        CheckboxListTile(
 | 
			
		||||
          title: const Text('Archive'),
 | 
			
		||||
          value: options.value[DisplayOption.archive],
 | 
			
		||||
          onChanged: (value) {
 | 
			
		||||
            options.value = {
 | 
			
		||||
              ...options.value,
 | 
			
		||||
              DisplayOption.archive: value!,
 | 
			
		||||
            };
 | 
			
		||||
            onSelect(options.value);
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,68 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
 | 
			
		||||
 | 
			
		||||
class FilterBottomSheetScaffold extends StatelessWidget {
 | 
			
		||||
  const FilterBottomSheetScaffold({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.child,
 | 
			
		||||
    required this.onSearch,
 | 
			
		||||
    required this.onClear,
 | 
			
		||||
    required this.title,
 | 
			
		||||
    this.expanded,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final bool? expanded;
 | 
			
		||||
  final String title;
 | 
			
		||||
  final Widget child;
 | 
			
		||||
  final Function() onSearch;
 | 
			
		||||
  final Function() onClear;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    buildChildWidget() {
 | 
			
		||||
      if (expanded != null && expanded == true) {
 | 
			
		||||
        return Expanded(child: child);
 | 
			
		||||
      }
 | 
			
		||||
      return child;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return SafeArea(
 | 
			
		||||
      child: Column(
 | 
			
		||||
        mainAxisSize: MainAxisSize.min,
 | 
			
		||||
        children: [
 | 
			
		||||
          Padding(
 | 
			
		||||
            padding: const EdgeInsets.all(16.0),
 | 
			
		||||
            child: Text(
 | 
			
		||||
              title,
 | 
			
		||||
              style: context.textTheme.headlineSmall,
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          buildChildWidget(),
 | 
			
		||||
          Padding(
 | 
			
		||||
            padding: const EdgeInsets.all(8.0),
 | 
			
		||||
            child: Row(
 | 
			
		||||
              mainAxisAlignment: MainAxisAlignment.end,
 | 
			
		||||
              children: [
 | 
			
		||||
                OutlinedButton(
 | 
			
		||||
                  onPressed: () {
 | 
			
		||||
                    onClear();
 | 
			
		||||
                    Navigator.of(context).pop();
 | 
			
		||||
                  },
 | 
			
		||||
                  child: const Text('Clear'),
 | 
			
		||||
                ),
 | 
			
		||||
                const SizedBox(width: 8),
 | 
			
		||||
                ElevatedButton(
 | 
			
		||||
                  onPressed: () {
 | 
			
		||||
                    onSearch();
 | 
			
		||||
                    Navigator.of(context).pop();
 | 
			
		||||
                  },
 | 
			
		||||
                  child: const Text('Apply filter'),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										166
									
								
								mobile/lib/modules/search/ui/search_filter/location_picker.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								mobile/lib/modules/search/ui/search_filter/location_picker.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,166 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/models/search_filter.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/providers/search_filter.provider.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
class LocationPicker extends HookConsumerWidget {
 | 
			
		||||
  const LocationPicker({super.key, required this.onSelected, this.filter});
 | 
			
		||||
 | 
			
		||||
  final Function(Map<String, String?>) onSelected;
 | 
			
		||||
  final SearchLocationFilter? filter;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final countryTextController =
 | 
			
		||||
        useTextEditingController(text: filter?.country);
 | 
			
		||||
    final stateTextController = useTextEditingController(text: filter?.state);
 | 
			
		||||
    final cityTextController = useTextEditingController(text: filter?.city);
 | 
			
		||||
 | 
			
		||||
    final selectedCountry = useState<String?>(filter?.country);
 | 
			
		||||
    final selectedState = useState<String?>(filter?.state);
 | 
			
		||||
    final selectedCity = useState<String?>(filter?.city);
 | 
			
		||||
 | 
			
		||||
    final countries = ref.watch(
 | 
			
		||||
      getSearchSuggestionsProvider(
 | 
			
		||||
        SearchSuggestionType.country,
 | 
			
		||||
        locationCountry: selectedCountry.value,
 | 
			
		||||
        locationState: selectedState.value,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    final states = ref.watch(
 | 
			
		||||
      getSearchSuggestionsProvider(
 | 
			
		||||
        SearchSuggestionType.state,
 | 
			
		||||
        locationCountry: selectedCountry.value,
 | 
			
		||||
        locationState: selectedState.value,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    final cities = ref.watch(
 | 
			
		||||
      getSearchSuggestionsProvider(
 | 
			
		||||
        SearchSuggestionType.city,
 | 
			
		||||
        locationCountry: selectedCountry.value,
 | 
			
		||||
        locationState: selectedState.value,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    final inputDecorationTheme = InputDecorationTheme(
 | 
			
		||||
      border: OutlineInputBorder(
 | 
			
		||||
        borderRadius: BorderRadius.circular(20),
 | 
			
		||||
      ),
 | 
			
		||||
      contentPadding: const EdgeInsets.only(left: 16),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    final menuStyle = MenuStyle(
 | 
			
		||||
      shape: MaterialStatePropertyAll<OutlinedBorder>(
 | 
			
		||||
        RoundedRectangleBorder(
 | 
			
		||||
          borderRadius: BorderRadius.circular(15),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return Column(
 | 
			
		||||
      children: [
 | 
			
		||||
        DropdownMenu(
 | 
			
		||||
          dropdownMenuEntries: switch (countries) {
 | 
			
		||||
            AsyncError() => [],
 | 
			
		||||
            AsyncData(:final value) => value
 | 
			
		||||
                .map(
 | 
			
		||||
                  (e) => DropdownMenuEntry(
 | 
			
		||||
                    value: e,
 | 
			
		||||
                    label: e,
 | 
			
		||||
                  ),
 | 
			
		||||
                )
 | 
			
		||||
                .toList(),
 | 
			
		||||
            _ => [],
 | 
			
		||||
          },
 | 
			
		||||
          menuHeight: 400,
 | 
			
		||||
          width: context.width * 0.9,
 | 
			
		||||
          label: const Text('Country'),
 | 
			
		||||
          inputDecorationTheme: inputDecorationTheme,
 | 
			
		||||
          menuStyle: menuStyle,
 | 
			
		||||
          controller: countryTextController,
 | 
			
		||||
          trailingIcon: const Icon(Icons.arrow_drop_down_rounded),
 | 
			
		||||
          selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded),
 | 
			
		||||
          onSelected: (value) {
 | 
			
		||||
            selectedCountry.value = value.toString();
 | 
			
		||||
            onSelected({
 | 
			
		||||
              'country': selectedCountry.value,
 | 
			
		||||
              'state': selectedState.value,
 | 
			
		||||
              'city': selectedCity.value,
 | 
			
		||||
            });
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        const SizedBox(
 | 
			
		||||
          height: 16,
 | 
			
		||||
        ),
 | 
			
		||||
        DropdownMenu(
 | 
			
		||||
          dropdownMenuEntries: switch (states) {
 | 
			
		||||
            AsyncError() => [],
 | 
			
		||||
            AsyncData(:final value) => value
 | 
			
		||||
                .map(
 | 
			
		||||
                  (e) => DropdownMenuEntry(
 | 
			
		||||
                    value: e,
 | 
			
		||||
                    label: e,
 | 
			
		||||
                  ),
 | 
			
		||||
                )
 | 
			
		||||
                .toList(),
 | 
			
		||||
            _ => [],
 | 
			
		||||
          },
 | 
			
		||||
          menuHeight: 400,
 | 
			
		||||
          width: context.width * 0.9,
 | 
			
		||||
          label: const Text('State'),
 | 
			
		||||
          inputDecorationTheme: inputDecorationTheme,
 | 
			
		||||
          menuStyle: menuStyle,
 | 
			
		||||
          controller: stateTextController,
 | 
			
		||||
          trailingIcon: const Icon(Icons.arrow_drop_down_rounded),
 | 
			
		||||
          selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded),
 | 
			
		||||
          onSelected: (value) {
 | 
			
		||||
            selectedState.value = value.toString();
 | 
			
		||||
            onSelected({
 | 
			
		||||
              'country': selectedCountry.value,
 | 
			
		||||
              'state': selectedState.value,
 | 
			
		||||
              'city': selectedCity.value,
 | 
			
		||||
            });
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        const SizedBox(
 | 
			
		||||
          height: 16,
 | 
			
		||||
        ),
 | 
			
		||||
        DropdownMenu(
 | 
			
		||||
          dropdownMenuEntries: switch (cities) {
 | 
			
		||||
            AsyncError() => [],
 | 
			
		||||
            AsyncData(:final value) => value
 | 
			
		||||
                .map(
 | 
			
		||||
                  (e) => DropdownMenuEntry(
 | 
			
		||||
                    value: e,
 | 
			
		||||
                    label: e,
 | 
			
		||||
                  ),
 | 
			
		||||
                )
 | 
			
		||||
                .toList(),
 | 
			
		||||
            _ => [],
 | 
			
		||||
          },
 | 
			
		||||
          menuHeight: 400,
 | 
			
		||||
          width: context.width * 0.9,
 | 
			
		||||
          label: const Text('City'),
 | 
			
		||||
          inputDecorationTheme: inputDecorationTheme,
 | 
			
		||||
          menuStyle: menuStyle,
 | 
			
		||||
          controller: cityTextController,
 | 
			
		||||
          trailingIcon: const Icon(Icons.arrow_drop_down_rounded),
 | 
			
		||||
          selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded),
 | 
			
		||||
          onSelected: (value) {
 | 
			
		||||
            selectedCity.value = value.toString();
 | 
			
		||||
            onSelected({
 | 
			
		||||
              'country': selectedCountry.value,
 | 
			
		||||
              'state': selectedState.value,
 | 
			
		||||
              'city': selectedCity.value,
 | 
			
		||||
            });
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,48 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
			
		||||
 | 
			
		||||
class MediaTypePicker extends HookWidget {
 | 
			
		||||
  const MediaTypePicker({super.key, required this.onSelect, this.filter});
 | 
			
		||||
 | 
			
		||||
  final Function(AssetType) onSelect;
 | 
			
		||||
  final AssetType? filter;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final selectedMediaType = useState(filter ?? AssetType.other);
 | 
			
		||||
 | 
			
		||||
    return ListView(
 | 
			
		||||
      shrinkWrap: true,
 | 
			
		||||
      children: [
 | 
			
		||||
        RadioListTile(
 | 
			
		||||
          title: const Text("All"),
 | 
			
		||||
          value: AssetType.other,
 | 
			
		||||
          onChanged: (value) {
 | 
			
		||||
            selectedMediaType.value = value!;
 | 
			
		||||
            onSelect(value);
 | 
			
		||||
          },
 | 
			
		||||
          groupValue: selectedMediaType.value,
 | 
			
		||||
        ),
 | 
			
		||||
        RadioListTile(
 | 
			
		||||
          title: const Text("Image"),
 | 
			
		||||
          value: AssetType.image,
 | 
			
		||||
          onChanged: (value) {
 | 
			
		||||
            selectedMediaType.value = value!;
 | 
			
		||||
            onSelect(value);
 | 
			
		||||
          },
 | 
			
		||||
          groupValue: selectedMediaType.value,
 | 
			
		||||
        ),
 | 
			
		||||
        RadioListTile(
 | 
			
		||||
          title: const Text("Video"),
 | 
			
		||||
          value: AssetType.video,
 | 
			
		||||
          onChanged: (value) {
 | 
			
		||||
            selectedMediaType.value = value!;
 | 
			
		||||
            onSelect(value);
 | 
			
		||||
          },
 | 
			
		||||
          groupValue: selectedMediaType.value,
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,81 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 | 
			
		||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/store.dart' as local_store;
 | 
			
		||||
import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
class PeoplePicker extends HookConsumerWidget {
 | 
			
		||||
  const PeoplePicker({super.key, required this.onSelect, this.filter});
 | 
			
		||||
 | 
			
		||||
  final Function(Set<PersonResponseDto>) onSelect;
 | 
			
		||||
  final Set<PersonResponseDto>? filter;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    var imageSize = 45.0;
 | 
			
		||||
    final people = ref.watch(getAllPeopleProvider);
 | 
			
		||||
    final headers = {
 | 
			
		||||
      "x-immich-user-token":
 | 
			
		||||
          local_store.Store.get(local_store.StoreKey.accessToken),
 | 
			
		||||
    };
 | 
			
		||||
    final selectedPeople = useState<Set<PersonResponseDto>>(filter ?? {});
 | 
			
		||||
 | 
			
		||||
    return people.widgetWhen(
 | 
			
		||||
      onData: (people) {
 | 
			
		||||
        return ListView.builder(
 | 
			
		||||
          shrinkWrap: true,
 | 
			
		||||
          itemCount: people.length,
 | 
			
		||||
          padding: const EdgeInsets.all(8),
 | 
			
		||||
          itemBuilder: (context, index) {
 | 
			
		||||
            final person = people[index];
 | 
			
		||||
            return Card(
 | 
			
		||||
              elevation: 0,
 | 
			
		||||
              shape: const RoundedRectangleBorder(
 | 
			
		||||
                borderRadius: BorderRadius.all(Radius.circular(15)),
 | 
			
		||||
              ),
 | 
			
		||||
              child: ListTile(
 | 
			
		||||
                title: Text(
 | 
			
		||||
                  person.name,
 | 
			
		||||
                  style: context.textTheme.bodyLarge,
 | 
			
		||||
                ),
 | 
			
		||||
                leading: SizedBox(
 | 
			
		||||
                  height: imageSize,
 | 
			
		||||
                  child: Material(
 | 
			
		||||
                    shape: const CircleBorder(side: BorderSide.none),
 | 
			
		||||
                    elevation: 3,
 | 
			
		||||
                    child: CircleAvatar(
 | 
			
		||||
                      maxRadius: imageSize / 2,
 | 
			
		||||
                      backgroundImage: NetworkImage(
 | 
			
		||||
                        getFaceThumbnailUrl(person.id),
 | 
			
		||||
                        headers: headers,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                onTap: () {
 | 
			
		||||
                  if (selectedPeople.value.contains(person)) {
 | 
			
		||||
                    selectedPeople.value.remove(person);
 | 
			
		||||
                  } else {
 | 
			
		||||
                    selectedPeople.value.add(person);
 | 
			
		||||
                  }
 | 
			
		||||
 | 
			
		||||
                  selectedPeople.value = {...selectedPeople.value};
 | 
			
		||||
                  onSelect(selectedPeople.value);
 | 
			
		||||
                },
 | 
			
		||||
                selected: selectedPeople.value.contains(person),
 | 
			
		||||
                selectedTileColor: context.primaryColor.withOpacity(0.2),
 | 
			
		||||
                shape: const RoundedRectangleBorder(
 | 
			
		||||
                  borderRadius: BorderRadius.all(Radius.circular(15)),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,68 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
 | 
			
		||||
 | 
			
		||||
class SearchFilterChip extends StatelessWidget {
 | 
			
		||||
  final String label;
 | 
			
		||||
  final Function() onTap;
 | 
			
		||||
  final Widget? currentFilter;
 | 
			
		||||
  final IconData icon;
 | 
			
		||||
 | 
			
		||||
  const SearchFilterChip({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.label,
 | 
			
		||||
    required this.onTap,
 | 
			
		||||
    required this.icon,
 | 
			
		||||
    this.currentFilter,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    if (currentFilter != null) {
 | 
			
		||||
      return GestureDetector(
 | 
			
		||||
        onTap: onTap,
 | 
			
		||||
        child: Card(
 | 
			
		||||
          elevation: 0,
 | 
			
		||||
          color: context.primaryColor.withAlpha(25),
 | 
			
		||||
          shape: StadiumBorder(
 | 
			
		||||
            side: BorderSide(color: context.primaryColor),
 | 
			
		||||
          ),
 | 
			
		||||
          child: Padding(
 | 
			
		||||
            padding:
 | 
			
		||||
                const EdgeInsets.symmetric(vertical: 2.0, horizontal: 14.0),
 | 
			
		||||
            child: Row(
 | 
			
		||||
              children: [
 | 
			
		||||
                Icon(
 | 
			
		||||
                  icon,
 | 
			
		||||
                  size: 18,
 | 
			
		||||
                ),
 | 
			
		||||
                const SizedBox(width: 4.0),
 | 
			
		||||
                currentFilter!,
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return GestureDetector(
 | 
			
		||||
      onTap: onTap,
 | 
			
		||||
      child: Card(
 | 
			
		||||
        elevation: 0,
 | 
			
		||||
        shape:
 | 
			
		||||
            StadiumBorder(side: BorderSide(color: Colors.grey.withAlpha(100))),
 | 
			
		||||
        child: Padding(
 | 
			
		||||
          padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 14.0),
 | 
			
		||||
          child: Row(
 | 
			
		||||
            children: [
 | 
			
		||||
              Icon(
 | 
			
		||||
                icon,
 | 
			
		||||
                size: 18,
 | 
			
		||||
              ),
 | 
			
		||||
              const SizedBox(width: 4.0),
 | 
			
		||||
              Text(label),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,19 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
Future<T> showFilterBottomSheet<T>({
 | 
			
		||||
  required BuildContext context,
 | 
			
		||||
  required Widget child,
 | 
			
		||||
  bool isScrollControlled = false,
 | 
			
		||||
  bool isDismissible = true,
 | 
			
		||||
}) async {
 | 
			
		||||
  return await showModalBottomSheet(
 | 
			
		||||
    context: context,
 | 
			
		||||
    isScrollControlled: isScrollControlled,
 | 
			
		||||
    useSafeArea: false,
 | 
			
		||||
    isDismissible: isDismissible,
 | 
			
		||||
    showDragHandle: isDismissible,
 | 
			
		||||
    builder: (BuildContext context) {
 | 
			
		||||
      return child;
 | 
			
		||||
    },
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -1,66 +0,0 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
 | 
			
		||||
 | 
			
		||||
class SearchSuggestionList extends ConsumerWidget {
 | 
			
		||||
  const SearchSuggestionList({super.key, required this.onSubmitted});
 | 
			
		||||
 | 
			
		||||
  final Function(String) onSubmitted;
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final searchTerm = ref.watch(searchPageStateProvider).searchTerm;
 | 
			
		||||
    final searchSuggestion =
 | 
			
		||||
        ref.watch(searchPageStateProvider).searchSuggestion;
 | 
			
		||||
 | 
			
		||||
    return Container(
 | 
			
		||||
      color: searchTerm.isEmpty
 | 
			
		||||
          ? Colors.black.withOpacity(0.5)
 | 
			
		||||
          : context.scaffoldBackgroundColor,
 | 
			
		||||
      child: CustomScrollView(
 | 
			
		||||
        slivers: [
 | 
			
		||||
          SliverToBoxAdapter(
 | 
			
		||||
            child: Container(
 | 
			
		||||
              color: context.isDarkTheme ? Colors.grey[800] : Colors.grey[100],
 | 
			
		||||
              child: Padding(
 | 
			
		||||
                padding: const EdgeInsets.all(16.0),
 | 
			
		||||
                child: RichText(
 | 
			
		||||
                  text: TextSpan(
 | 
			
		||||
                    children: [
 | 
			
		||||
                      TextSpan(
 | 
			
		||||
                        text: 'search_suggestion_list_smart_search_hint_1'.tr(),
 | 
			
		||||
                        style: context.textTheme.bodyMedium,
 | 
			
		||||
                      ),
 | 
			
		||||
                      TextSpan(
 | 
			
		||||
                        text: 'search_suggestion_list_smart_search_hint_2'.tr(),
 | 
			
		||||
                        style: context.textTheme.bodyMedium?.copyWith(
 | 
			
		||||
                          color: context.primaryColor,
 | 
			
		||||
                          fontWeight: FontWeight.bold,
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          SliverFillRemaining(
 | 
			
		||||
            hasScrollBody: true,
 | 
			
		||||
            child: ListView.builder(
 | 
			
		||||
              itemBuilder: ((context, index) {
 | 
			
		||||
                return ListTile(
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    onSubmitted("m:${searchSuggestion[index]}");
 | 
			
		||||
                  },
 | 
			
		||||
                  title: Text(searchSuggestion[index]),
 | 
			
		||||
                );
 | 
			
		||||
              }),
 | 
			
		||||
              itemCount: searchSuggestion.length,
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/models/curated_content.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/ui/explore_grid.dart';
 | 
			
		||||
 | 
			
		||||
@ -12,7 +13,7 @@ class AllPeoplePage extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final curatedPeople = ref.watch(getCuratedPeopleProvider);
 | 
			
		||||
    final curatedPeople = ref.watch(getAllPeopleProvider);
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
@ -27,7 +28,9 @@ class AllPeoplePage extends HookConsumerWidget {
 | 
			
		||||
      body: curatedPeople.widgetWhen(
 | 
			
		||||
        onData: (people) => ExploreGrid(
 | 
			
		||||
          isPeople: true,
 | 
			
		||||
          curatedContent: people,
 | 
			
		||||
          curatedContent: people
 | 
			
		||||
              .map((e) => CuratedContent(label: e.name, id: e.id))
 | 
			
		||||
              .toList(),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										563
									
								
								mobile/lib/modules/search/views/search_input_page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										563
									
								
								mobile/lib/modules/search/views/search_input_page.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,563 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
 | 
			
		||||
import 'package:auto_route/auto_route.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/models/search_filter.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/providers/paginated_search.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/ui/search_filter/camera_picker.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/ui/search_filter/display_option_picker.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/ui/search_filter/filter_bottom_sheet_scaffold.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/ui/search_filter/location_picker.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/ui/search_filter/media_type_picker.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/ui/search_filter/people_picker.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/ui/search_filter/search_filter_chip.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/ui/search_filter/search_filter_utils.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
@RoutePage()
 | 
			
		||||
class SearchInputPage extends HookConsumerWidget {
 | 
			
		||||
  const SearchInputPage({super.key, this.prefilter});
 | 
			
		||||
 | 
			
		||||
  final SearchFilter? prefilter;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final isContextualSearch = useState(true);
 | 
			
		||||
    final textSearchController = useTextEditingController();
 | 
			
		||||
    final filter = useState<SearchFilter>(
 | 
			
		||||
      SearchFilter(
 | 
			
		||||
        people: prefilter?.people ?? {},
 | 
			
		||||
        location: prefilter?.location ?? SearchLocationFilter(),
 | 
			
		||||
        camera: prefilter?.camera ?? SearchCameraFilter(),
 | 
			
		||||
        date: prefilter?.date ?? SearchDateFilter(),
 | 
			
		||||
        display: prefilter?.display ??
 | 
			
		||||
            SearchDisplayFilters(
 | 
			
		||||
              isNotInAlbum: false,
 | 
			
		||||
              isArchive: false,
 | 
			
		||||
              isFavorite: false,
 | 
			
		||||
            ),
 | 
			
		||||
        mediaType: prefilter?.mediaType ?? AssetType.other,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    final previousFilter = useState(filter.value);
 | 
			
		||||
 | 
			
		||||
    final peopleCurrentFilterWidget = useState<Widget?>(null);
 | 
			
		||||
    final dateRangeCurrentFilterWidget = useState<Widget?>(null);
 | 
			
		||||
    final cameraCurrentFilterWidget = useState<Widget?>(null);
 | 
			
		||||
    final locationCurrentFilterWidget = useState<Widget?>(null);
 | 
			
		||||
    final mediaTypeCurrentFilterWidget = useState<Widget?>(null);
 | 
			
		||||
    final displayOptionCurrentFilterWidget = useState<Widget?>(null);
 | 
			
		||||
 | 
			
		||||
    final currentPage = useState(1);
 | 
			
		||||
    final searchProvider = ref.watch(paginatedSearchProvider);
 | 
			
		||||
    final searchResultCount = useState(0);
 | 
			
		||||
 | 
			
		||||
    search() async {
 | 
			
		||||
      if (prefilter == null && filter.value == previousFilter.value) return;
 | 
			
		||||
 | 
			
		||||
      ref.watch(paginatedSearchProvider.notifier).clear();
 | 
			
		||||
 | 
			
		||||
      currentPage.value = 1;
 | 
			
		||||
 | 
			
		||||
      final searchResult = await ref
 | 
			
		||||
          .watch(paginatedSearchProvider.notifier)
 | 
			
		||||
          .getNextPage(filter.value, currentPage.value);
 | 
			
		||||
      previousFilter.value = filter.value;
 | 
			
		||||
 | 
			
		||||
      searchResultCount.value = searchResult.length;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    searchPrefilter() {
 | 
			
		||||
      if (prefilter != null) {
 | 
			
		||||
        Future.delayed(
 | 
			
		||||
          Duration.zero,
 | 
			
		||||
          () {
 | 
			
		||||
            search();
 | 
			
		||||
 | 
			
		||||
            if (prefilter!.location.city != null) {
 | 
			
		||||
              locationCurrentFilterWidget.value = Text(
 | 
			
		||||
                prefilter!.location.city!,
 | 
			
		||||
                style: context.textTheme.labelLarge,
 | 
			
		||||
              );
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    useEffect(
 | 
			
		||||
      () {
 | 
			
		||||
        searchPrefilter();
 | 
			
		||||
        return null;
 | 
			
		||||
      },
 | 
			
		||||
      [],
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    loadMoreSearchResult() async {
 | 
			
		||||
      currentPage.value += 1;
 | 
			
		||||
      final searchResult = await ref
 | 
			
		||||
          .watch(paginatedSearchProvider.notifier)
 | 
			
		||||
          .getNextPage(filter.value, currentPage.value);
 | 
			
		||||
      searchResultCount.value = searchResult.length;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showPeoplePicker() {
 | 
			
		||||
      handleOnSelect(Set<PersonResponseDto> value) {
 | 
			
		||||
        filter.value = filter.value.copyWith(
 | 
			
		||||
          people: value,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        peopleCurrentFilterWidget.value = Text(
 | 
			
		||||
          value.map((e) => e.name != '' ? e.name : "No name").join(', '),
 | 
			
		||||
          style: context.textTheme.labelLarge,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      handleClear() {
 | 
			
		||||
        filter.value = filter.value.copyWith(
 | 
			
		||||
          people: {},
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        peopleCurrentFilterWidget.value = null;
 | 
			
		||||
        search();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      showFilterBottomSheet(
 | 
			
		||||
        context: context,
 | 
			
		||||
        isScrollControlled: true,
 | 
			
		||||
        child: FractionallySizedBox(
 | 
			
		||||
          heightFactor: 0.8,
 | 
			
		||||
          child: FilterBottomSheetScaffold(
 | 
			
		||||
            title: 'Select people',
 | 
			
		||||
            expanded: true,
 | 
			
		||||
            onSearch: search,
 | 
			
		||||
            onClear: handleClear,
 | 
			
		||||
            child: PeoplePicker(
 | 
			
		||||
              onSelect: handleOnSelect,
 | 
			
		||||
              filter: filter.value.people,
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showLocationPicker() {
 | 
			
		||||
      handleOnSelect(Map<String, String?> value) {
 | 
			
		||||
        filter.value = filter.value.copyWith(
 | 
			
		||||
          location: SearchLocationFilter(
 | 
			
		||||
            country: value['country'],
 | 
			
		||||
            city: value['city'],
 | 
			
		||||
            state: value['state'],
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        final locationText = <String>[];
 | 
			
		||||
        if (value['country'] != null) {
 | 
			
		||||
          locationText.add(value['country']!);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (value['state'] != null) {
 | 
			
		||||
          locationText.add(value['state']!);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (value['city'] != null) {
 | 
			
		||||
          locationText.add(value['city']!);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        locationCurrentFilterWidget.value = Text(
 | 
			
		||||
          locationText.join(', '),
 | 
			
		||||
          style: context.textTheme.labelLarge,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      handleClear() {
 | 
			
		||||
        filter.value = filter.value.copyWith(
 | 
			
		||||
          location: SearchLocationFilter(),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        locationCurrentFilterWidget.value = null;
 | 
			
		||||
        search();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      showFilterBottomSheet(
 | 
			
		||||
        context: context,
 | 
			
		||||
        isScrollControlled: true,
 | 
			
		||||
        isDismissible: false,
 | 
			
		||||
        child: FilterBottomSheetScaffold(
 | 
			
		||||
          title: 'Select location',
 | 
			
		||||
          onSearch: search,
 | 
			
		||||
          onClear: handleClear,
 | 
			
		||||
          child: Padding(
 | 
			
		||||
            padding: const EdgeInsets.symmetric(vertical: 16.0),
 | 
			
		||||
            child: Container(
 | 
			
		||||
              padding: EdgeInsets.only(
 | 
			
		||||
                bottom: MediaQuery.of(context).viewInsets.bottom,
 | 
			
		||||
              ),
 | 
			
		||||
              child: LocationPicker(
 | 
			
		||||
                onSelected: handleOnSelect,
 | 
			
		||||
                filter: filter.value.location,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showCameraPicker() {
 | 
			
		||||
      handleOnSelect(Map<String, String?> value) {
 | 
			
		||||
        filter.value = filter.value.copyWith(
 | 
			
		||||
          camera: SearchCameraFilter(
 | 
			
		||||
            make: value['make'],
 | 
			
		||||
            model: value['model'],
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        cameraCurrentFilterWidget.value = Text(
 | 
			
		||||
          '${value['make'] ?? ''} ${value['model'] ?? ''}',
 | 
			
		||||
          style: context.textTheme.labelLarge,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      handleClear() {
 | 
			
		||||
        filter.value = filter.value.copyWith(
 | 
			
		||||
          camera: SearchCameraFilter(),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        cameraCurrentFilterWidget.value = null;
 | 
			
		||||
        search();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      showFilterBottomSheet(
 | 
			
		||||
        context: context,
 | 
			
		||||
        isScrollControlled: true,
 | 
			
		||||
        isDismissible: false,
 | 
			
		||||
        child: FilterBottomSheetScaffold(
 | 
			
		||||
          title: 'Select camera type',
 | 
			
		||||
          onSearch: search,
 | 
			
		||||
          onClear: handleClear,
 | 
			
		||||
          child: Padding(
 | 
			
		||||
            padding: const EdgeInsets.symmetric(vertical: 16.0),
 | 
			
		||||
            child: CameraPicker(
 | 
			
		||||
              onSelect: handleOnSelect,
 | 
			
		||||
              filter: filter.value.camera,
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showDatePicker() async {
 | 
			
		||||
      final firstDate = DateTime(1900);
 | 
			
		||||
      final lastDate = DateTime.now();
 | 
			
		||||
 | 
			
		||||
      final date = await showDateRangePicker(
 | 
			
		||||
        context: context,
 | 
			
		||||
        firstDate: firstDate,
 | 
			
		||||
        lastDate: lastDate,
 | 
			
		||||
        currentDate: DateTime.now(),
 | 
			
		||||
        initialDateRange: DateTimeRange(
 | 
			
		||||
          start: filter.value.date.takenAfter ?? lastDate,
 | 
			
		||||
          end: filter.value.date.takenBefore ?? lastDate,
 | 
			
		||||
        ),
 | 
			
		||||
        helpText: 'Select a date range',
 | 
			
		||||
        cancelText: 'Cancel',
 | 
			
		||||
        confirmText: 'Select',
 | 
			
		||||
        saveText: 'Save',
 | 
			
		||||
        errorFormatText: 'Invalid date format',
 | 
			
		||||
        errorInvalidText: 'Invalid date',
 | 
			
		||||
        fieldStartHintText: 'Start date',
 | 
			
		||||
        fieldEndHintText: 'End date',
 | 
			
		||||
        initialEntryMode: DatePickerEntryMode.input,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (date == null) {
 | 
			
		||||
        filter.value = filter.value.copyWith(
 | 
			
		||||
          date: SearchDateFilter(),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        dateRangeCurrentFilterWidget.value = null;
 | 
			
		||||
        search();
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      filter.value = filter.value.copyWith(
 | 
			
		||||
        date: SearchDateFilter(
 | 
			
		||||
          takenAfter: date.start,
 | 
			
		||||
          takenBefore: date.end.add(
 | 
			
		||||
            const Duration(
 | 
			
		||||
              hours: 23,
 | 
			
		||||
              minutes: 59,
 | 
			
		||||
              seconds: 59,
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // If date range is less than 24 hours, set the end date to the end of the day
 | 
			
		||||
      if (date.end.difference(date.start).inHours < 24) {
 | 
			
		||||
        dateRangeCurrentFilterWidget.value = Text(
 | 
			
		||||
          date.start.toLocal().toIso8601String().split('T').first,
 | 
			
		||||
          style: context.textTheme.labelLarge,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        dateRangeCurrentFilterWidget.value = Text(
 | 
			
		||||
          '${date.start.toLocal().toIso8601String().split('T').first} to ${date.end.toLocal().toIso8601String().split('T').first}',
 | 
			
		||||
          style: context.textTheme.labelLarge,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      search();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // MEDIA PICKER
 | 
			
		||||
    showMediaTypePicker() {
 | 
			
		||||
      handleOnSelected(AssetType assetType) {
 | 
			
		||||
        filter.value = filter.value.copyWith(
 | 
			
		||||
          mediaType: assetType,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        mediaTypeCurrentFilterWidget.value = Text(
 | 
			
		||||
          assetType == AssetType.image ? 'Image' : 'Video',
 | 
			
		||||
          style: context.textTheme.labelLarge,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      handleClear() {
 | 
			
		||||
        filter.value = filter.value.copyWith(
 | 
			
		||||
          mediaType: AssetType.other,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        mediaTypeCurrentFilterWidget.value = null;
 | 
			
		||||
        search();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      showFilterBottomSheet(
 | 
			
		||||
        context: context,
 | 
			
		||||
        child: FilterBottomSheetScaffold(
 | 
			
		||||
          title: 'Select media type',
 | 
			
		||||
          onSearch: search,
 | 
			
		||||
          onClear: handleClear,
 | 
			
		||||
          child: MediaTypePicker(
 | 
			
		||||
            onSelect: handleOnSelected,
 | 
			
		||||
            filter: filter.value.mediaType,
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // DISPLAY OPTION
 | 
			
		||||
    showDisplayOptionPicker() {
 | 
			
		||||
      handleOnSelect(Map<DisplayOption, bool> value) {
 | 
			
		||||
        final filterText = <String>[];
 | 
			
		||||
 | 
			
		||||
        value.forEach((key, value) {
 | 
			
		||||
          switch (key) {
 | 
			
		||||
            case DisplayOption.notInAlbum:
 | 
			
		||||
              filter.value = filter.value.copyWith(
 | 
			
		||||
                display: filter.value.display.copyWith(
 | 
			
		||||
                  isNotInAlbum: value,
 | 
			
		||||
                ),
 | 
			
		||||
              );
 | 
			
		||||
              if (value) filterText.add('Not in album');
 | 
			
		||||
              break;
 | 
			
		||||
            case DisplayOption.archive:
 | 
			
		||||
              filter.value = filter.value.copyWith(
 | 
			
		||||
                display: filter.value.display.copyWith(
 | 
			
		||||
                  isArchive: value,
 | 
			
		||||
                ),
 | 
			
		||||
              );
 | 
			
		||||
              if (value) filterText.add('Archive');
 | 
			
		||||
              break;
 | 
			
		||||
            case DisplayOption.favorite:
 | 
			
		||||
              filter.value = filter.value.copyWith(
 | 
			
		||||
                display: filter.value.display.copyWith(
 | 
			
		||||
                  isFavorite: value,
 | 
			
		||||
                ),
 | 
			
		||||
              );
 | 
			
		||||
              if (value) filterText.add('Favorite');
 | 
			
		||||
              break;
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        displayOptionCurrentFilterWidget.value = Text(
 | 
			
		||||
          filterText.join(', '),
 | 
			
		||||
          style: context.textTheme.labelLarge,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      handleClear() {
 | 
			
		||||
        filter.value = filter.value.copyWith(
 | 
			
		||||
          display: SearchDisplayFilters(
 | 
			
		||||
            isNotInAlbum: false,
 | 
			
		||||
            isArchive: false,
 | 
			
		||||
            isFavorite: false,
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        displayOptionCurrentFilterWidget.value = null;
 | 
			
		||||
        search();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      showFilterBottomSheet(
 | 
			
		||||
        context: context,
 | 
			
		||||
        child: FilterBottomSheetScaffold(
 | 
			
		||||
          title: 'Display options',
 | 
			
		||||
          onSearch: search,
 | 
			
		||||
          onClear: handleClear,
 | 
			
		||||
          child: DisplayOptionPicker(
 | 
			
		||||
            onSelect: handleOnSelect,
 | 
			
		||||
            filter: filter.value.display,
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleTextSubmitted(String value) {
 | 
			
		||||
      if (isContextualSearch.value) {
 | 
			
		||||
        filter.value = filter.value.copyWith(
 | 
			
		||||
          context: value,
 | 
			
		||||
          filename: null,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        filter.value = filter.value.copyWith(filename: value, context: null);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      search();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    buildSearchResult() {
 | 
			
		||||
      return switch (searchProvider) {
 | 
			
		||||
        AsyncData() => Expanded(
 | 
			
		||||
            child: Padding(
 | 
			
		||||
              padding: const EdgeInsets.only(top: 8.0),
 | 
			
		||||
              child: NotificationListener<ScrollEndNotification>(
 | 
			
		||||
                onNotification: (notification) {
 | 
			
		||||
                  final metrics = notification.metrics;
 | 
			
		||||
                  final shouldLoadMore = searchResultCount.value > 75;
 | 
			
		||||
                  if (metrics.pixels >= metrics.maxScrollExtent &&
 | 
			
		||||
                      shouldLoadMore) {
 | 
			
		||||
                    loadMoreSearchResult();
 | 
			
		||||
                  }
 | 
			
		||||
                  return true;
 | 
			
		||||
                },
 | 
			
		||||
                child: MultiselectGrid(
 | 
			
		||||
                  renderListProvider: paginatedSearchRenderListProvider,
 | 
			
		||||
                  archiveEnabled: true,
 | 
			
		||||
                  deleteEnabled: true,
 | 
			
		||||
                  editEnabled: true,
 | 
			
		||||
                  favoriteEnabled: true,
 | 
			
		||||
                  stackEnabled: false,
 | 
			
		||||
                  emptyIndicator: const SizedBox(),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        AsyncError(:final error) => Text('Error: $error'),
 | 
			
		||||
        _ => const Expanded(child: Center(child: CircularProgressIndicator())),
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      resizeToAvoidBottomInset: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        automaticallyImplyLeading: true,
 | 
			
		||||
        actions: [
 | 
			
		||||
          IconButton(
 | 
			
		||||
            icon: isContextualSearch.value
 | 
			
		||||
                ? const Icon(Icons.abc_rounded)
 | 
			
		||||
                : const Icon(Icons.image_search_rounded),
 | 
			
		||||
            onPressed: () {
 | 
			
		||||
              isContextualSearch.value = !isContextualSearch.value;
 | 
			
		||||
              textSearchController.clear();
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
        leading: IconButton(
 | 
			
		||||
          icon: const Icon(Icons.arrow_back_ios_new_rounded),
 | 
			
		||||
          onPressed: () {
 | 
			
		||||
            context.router.pop();
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        title: TextField(
 | 
			
		||||
          controller: textSearchController,
 | 
			
		||||
          decoration: InputDecoration(
 | 
			
		||||
            hintText: isContextualSearch.value
 | 
			
		||||
                ? 'Sunrise on the beach'
 | 
			
		||||
                : 'File name or extension',
 | 
			
		||||
            hintStyle: context.textTheme.bodyLarge?.copyWith(
 | 
			
		||||
              color: context.themeData.colorScheme.onSurface.withOpacity(0.75),
 | 
			
		||||
              fontWeight: FontWeight.w500,
 | 
			
		||||
            ),
 | 
			
		||||
            enabledBorder: const UnderlineInputBorder(
 | 
			
		||||
              borderSide: BorderSide(color: Colors.transparent),
 | 
			
		||||
            ),
 | 
			
		||||
            focusedBorder: const UnderlineInputBorder(
 | 
			
		||||
              borderSide: BorderSide(color: Colors.transparent),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          onSubmitted: handleTextSubmitted,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      body: Column(
 | 
			
		||||
        children: [
 | 
			
		||||
          Padding(
 | 
			
		||||
            padding: const EdgeInsets.only(top: 12.0),
 | 
			
		||||
            child: SizedBox(
 | 
			
		||||
              height: 50,
 | 
			
		||||
              child: ListView(
 | 
			
		||||
                shrinkWrap: true,
 | 
			
		||||
                scrollDirection: Axis.horizontal,
 | 
			
		||||
                padding: const EdgeInsets.symmetric(horizontal: 16),
 | 
			
		||||
                children: [
 | 
			
		||||
                  SearchFilterChip(
 | 
			
		||||
                    icon: Icons.people_alt_rounded,
 | 
			
		||||
                    onTap: showPeoplePicker,
 | 
			
		||||
                    label: 'People',
 | 
			
		||||
                    currentFilter: peopleCurrentFilterWidget.value,
 | 
			
		||||
                  ),
 | 
			
		||||
                  SearchFilterChip(
 | 
			
		||||
                    icon: Icons.location_pin,
 | 
			
		||||
                    onTap: showLocationPicker,
 | 
			
		||||
                    label: 'Location',
 | 
			
		||||
                    currentFilter: locationCurrentFilterWidget.value,
 | 
			
		||||
                  ),
 | 
			
		||||
                  SearchFilterChip(
 | 
			
		||||
                    icon: Icons.camera_alt_rounded,
 | 
			
		||||
                    onTap: showCameraPicker,
 | 
			
		||||
                    label: 'Camera',
 | 
			
		||||
                    currentFilter: cameraCurrentFilterWidget.value,
 | 
			
		||||
                  ),
 | 
			
		||||
                  SearchFilterChip(
 | 
			
		||||
                    icon: Icons.date_range_rounded,
 | 
			
		||||
                    onTap: showDatePicker,
 | 
			
		||||
                    label: 'Date',
 | 
			
		||||
                    currentFilter: dateRangeCurrentFilterWidget.value,
 | 
			
		||||
                  ),
 | 
			
		||||
                  SearchFilterChip(
 | 
			
		||||
                    icon: Icons.video_collection_outlined,
 | 
			
		||||
                    onTap: showMediaTypePicker,
 | 
			
		||||
                    label: 'Media Type',
 | 
			
		||||
                    currentFilter: mediaTypeCurrentFilterWidget.value,
 | 
			
		||||
                  ),
 | 
			
		||||
                  SearchFilterChip(
 | 
			
		||||
                    icon: Icons.display_settings_outlined,
 | 
			
		||||
                    onTap: showDisplayOptionPicker,
 | 
			
		||||
                    label: 'Display Options',
 | 
			
		||||
                    currentFilter: displayOptionCurrentFilterWidget.value,
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          buildSearchResult(),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,36 +1,34 @@
 | 
			
		||||
import 'dart:math' as math;
 | 
			
		||||
 | 
			
		||||
import 'package:auto_route/auto_route.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 | 
			
		||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/models/curated_content.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/models/search_filter.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/ui/curated_people_row.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/ui/curated_places_row.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/ui/immich_search_bar.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/ui/search_row_title.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
 | 
			
		||||
import 'package:immich_mobile/routing/router.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/ui/immich_app_bar.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/ui/scaffold_error_body.dart';
 | 
			
		||||
 | 
			
		||||
@RoutePage()
 | 
			
		||||
// ignore: must_be_immutable
 | 
			
		||||
class SearchPage extends HookConsumerWidget {
 | 
			
		||||
  SearchPage({super.key});
 | 
			
		||||
 | 
			
		||||
  FocusNode searchFocusNode = FocusNode();
 | 
			
		||||
  const SearchPage({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
 | 
			
		||||
    final curatedLocation = ref.watch(getCuratedLocationProvider);
 | 
			
		||||
    final curatedPeople = ref.watch(getCuratedPeopleProvider);
 | 
			
		||||
    final curatedPeople = ref.watch(getAllPeopleProvider);
 | 
			
		||||
    final isMapEnabled =
 | 
			
		||||
        ref.watch(serverInfoProvider.select((v) => v.serverFeatures.map));
 | 
			
		||||
    double imageSize = math.min(context.width / 3, 150);
 | 
			
		||||
@ -42,25 +40,6 @@ class SearchPage extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
    Color categoryIconColor = context.isDarkTheme ? Colors.white : Colors.black;
 | 
			
		||||
 | 
			
		||||
    useEffect(
 | 
			
		||||
      () {
 | 
			
		||||
        searchFocusNode = FocusNode();
 | 
			
		||||
        return () => searchFocusNode.dispose();
 | 
			
		||||
      },
 | 
			
		||||
      [],
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    onSearchSubmitted(String searchTerm) async {
 | 
			
		||||
      searchFocusNode.unfocus();
 | 
			
		||||
      ref.watch(searchPageStateProvider.notifier).disableSearch();
 | 
			
		||||
 | 
			
		||||
      context.pushRoute(
 | 
			
		||||
        SearchResultRoute(
 | 
			
		||||
          searchTerm: searchTerm,
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showNameEditModel(
 | 
			
		||||
      String personId,
 | 
			
		||||
      String personName,
 | 
			
		||||
@ -84,7 +63,10 @@ class SearchPage extends HookConsumerWidget {
 | 
			
		||||
              top: 8,
 | 
			
		||||
            ),
 | 
			
		||||
            child: CuratedPeopleRow(
 | 
			
		||||
              content: people.take(12).toList(),
 | 
			
		||||
              content: people
 | 
			
		||||
                  .map((e) => CuratedContent(label: e.name, id: e.id))
 | 
			
		||||
                  .take(12)
 | 
			
		||||
                  .toList(),
 | 
			
		||||
              onTap: (content, index) {
 | 
			
		||||
                context.pushRoute(
 | 
			
		||||
                  PersonResultRoute(
 | 
			
		||||
@ -120,8 +102,21 @@ class SearchPage extends HookConsumerWidget {
 | 
			
		||||
            imageSize: imageSize,
 | 
			
		||||
            onTap: (content, index) {
 | 
			
		||||
              context.pushRoute(
 | 
			
		||||
                SearchResultRoute(
 | 
			
		||||
                  searchTerm: 'm:${content.label}',
 | 
			
		||||
                SearchInputRoute(
 | 
			
		||||
                  prefilter: SearchFilter(
 | 
			
		||||
                    people: {},
 | 
			
		||||
                    location: SearchLocationFilter(
 | 
			
		||||
                      city: content.label,
 | 
			
		||||
                    ),
 | 
			
		||||
                    camera: SearchCameraFilter(),
 | 
			
		||||
                    date: SearchDateFilter(),
 | 
			
		||||
                    display: SearchDisplayFilters(
 | 
			
		||||
                      isNotInAlbum: false,
 | 
			
		||||
                      isArchive: false,
 | 
			
		||||
                      isFavorite: false,
 | 
			
		||||
                    ),
 | 
			
		||||
                    mediaType: AssetType.other,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              );
 | 
			
		||||
            },
 | 
			
		||||
@ -130,20 +125,53 @@ class SearchPage extends HookConsumerWidget {
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: ImmichSearchBar(
 | 
			
		||||
        searchFocusNode: searchFocusNode,
 | 
			
		||||
        onSubmitted: onSearchSubmitted,
 | 
			
		||||
      ),
 | 
			
		||||
      body: GestureDetector(
 | 
			
		||||
    buildSearchButton() {
 | 
			
		||||
      return GestureDetector(
 | 
			
		||||
        onTap: () {
 | 
			
		||||
          searchFocusNode.unfocus();
 | 
			
		||||
          ref.watch(searchPageStateProvider.notifier).disableSearch();
 | 
			
		||||
          context.pushRoute(SearchInputRoute());
 | 
			
		||||
        },
 | 
			
		||||
        child: Stack(
 | 
			
		||||
        child: Card(
 | 
			
		||||
          elevation: 0,
 | 
			
		||||
          shape: RoundedRectangleBorder(
 | 
			
		||||
            borderRadius: BorderRadius.circular(20),
 | 
			
		||||
            side: BorderSide(
 | 
			
		||||
              color: context.isDarkTheme
 | 
			
		||||
                  ? Colors.grey[800]!
 | 
			
		||||
                  : const Color.fromARGB(255, 225, 225, 225),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
 | 
			
		||||
          child: Padding(
 | 
			
		||||
            padding: const EdgeInsets.symmetric(
 | 
			
		||||
              horizontal: 16.0,
 | 
			
		||||
              vertical: 12.0,
 | 
			
		||||
            ),
 | 
			
		||||
            child: Row(
 | 
			
		||||
              children: [
 | 
			
		||||
                Icon(Icons.search, color: context.primaryColor),
 | 
			
		||||
                const SizedBox(width: 16.0),
 | 
			
		||||
                Text(
 | 
			
		||||
                  "Search your photos",
 | 
			
		||||
                  style: context.textTheme.bodyLarge?.copyWith(
 | 
			
		||||
                    color:
 | 
			
		||||
                        context.isDarkTheme ? Colors.white70 : Colors.black54,
 | 
			
		||||
                    fontWeight: FontWeight.w400,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: const ImmichAppBar(),
 | 
			
		||||
      body: Stack(
 | 
			
		||||
        children: [
 | 
			
		||||
          ListView(
 | 
			
		||||
            children: [
 | 
			
		||||
              buildSearchButton(),
 | 
			
		||||
              SearchRowTitle(
 | 
			
		||||
                title: "search_page_people".tr(),
 | 
			
		||||
                onViewAllPressed: () =>
 | 
			
		||||
@ -173,8 +201,7 @@ class SearchPage extends HookConsumerWidget {
 | 
			
		||||
                  Icons.favorite_border_rounded,
 | 
			
		||||
                  color: categoryIconColor,
 | 
			
		||||
                ),
 | 
			
		||||
                  title:
 | 
			
		||||
                      Text('search_page_favorites', style: categoryTitleStyle)
 | 
			
		||||
                title: Text('search_page_favorites', style: categoryTitleStyle)
 | 
			
		||||
                    .tr(),
 | 
			
		||||
                onTap: () => context.pushRoute(const FavoritesRoute()),
 | 
			
		||||
              ),
 | 
			
		||||
@ -202,36 +229,7 @@ class SearchPage extends HookConsumerWidget {
 | 
			
		||||
              ),
 | 
			
		||||
              ListTile(
 | 
			
		||||
                title:
 | 
			
		||||
                      Text('search_page_screenshots', style: categoryTitleStyle)
 | 
			
		||||
                          .tr(),
 | 
			
		||||
                  leading: Icon(
 | 
			
		||||
                    Icons.screenshot,
 | 
			
		||||
                    color: categoryIconColor,
 | 
			
		||||
                  ),
 | 
			
		||||
                  onTap: () => context.pushRoute(
 | 
			
		||||
                    SearchResultRoute(
 | 
			
		||||
                      searchTerm: 'screenshots',
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                const CategoryDivider(),
 | 
			
		||||
                ListTile(
 | 
			
		||||
                  title: Text('search_page_selfies', style: categoryTitleStyle)
 | 
			
		||||
                      .tr(),
 | 
			
		||||
                  leading: Icon(
 | 
			
		||||
                    Icons.photo_camera_front_outlined,
 | 
			
		||||
                    color: categoryIconColor,
 | 
			
		||||
                  ),
 | 
			
		||||
                  onTap: () => context.pushRoute(
 | 
			
		||||
                    SearchResultRoute(
 | 
			
		||||
                      searchTerm: 'selfies',
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                const CategoryDivider(),
 | 
			
		||||
                ListTile(
 | 
			
		||||
                  title: Text('search_page_videos', style: categoryTitleStyle)
 | 
			
		||||
                      .tr(),
 | 
			
		||||
                    Text('search_page_videos', style: categoryTitleStyle).tr(),
 | 
			
		||||
                leading: Icon(
 | 
			
		||||
                  Icons.play_circle_outline,
 | 
			
		||||
                  color: categoryIconColor,
 | 
			
		||||
@ -252,11 +250,8 @@ class SearchPage extends HookConsumerWidget {
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
            if (isSearchEnabled)
 | 
			
		||||
              SearchSuggestionList(onSubmitted: onSearchSubmitted),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,213 +0,0 @@
 | 
			
		||||
import 'package:auto_route/auto_route.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/providers/search_result_page.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 | 
			
		||||
 | 
			
		||||
class SearchType {
 | 
			
		||||
  SearchType({required this.isSmart, required this.searchTerm});
 | 
			
		||||
 | 
			
		||||
  final bool isSmart;
 | 
			
		||||
  final String searchTerm;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SearchType _getSearchType(String searchTerm) {
 | 
			
		||||
  if (searchTerm.startsWith('m:')) {
 | 
			
		||||
    return SearchType(isSmart: false, searchTerm: searchTerm.substring(2));
 | 
			
		||||
  } else {
 | 
			
		||||
    return SearchType(isSmart: true, searchTerm: searchTerm);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@RoutePage()
 | 
			
		||||
class SearchResultPage extends HookConsumerWidget {
 | 
			
		||||
  const SearchResultPage({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.searchTerm,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final String searchTerm;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final searchTermController = useTextEditingController(text: "");
 | 
			
		||||
    final isNewSearch = useState(false);
 | 
			
		||||
    final currentSearchTerm = useState(searchTerm);
 | 
			
		||||
 | 
			
		||||
    FocusNode? searchFocusNode;
 | 
			
		||||
 | 
			
		||||
    useEffect(
 | 
			
		||||
      () {
 | 
			
		||||
        searchFocusNode = FocusNode();
 | 
			
		||||
 | 
			
		||||
        var searchType = _getSearchType(searchTerm);
 | 
			
		||||
 | 
			
		||||
        Future.delayed(
 | 
			
		||||
          Duration.zero,
 | 
			
		||||
          () => ref
 | 
			
		||||
              .read(searchResultPageProvider.notifier)
 | 
			
		||||
              .search(searchType.searchTerm, smartSearch: searchType.isSmart),
 | 
			
		||||
        );
 | 
			
		||||
        return () => searchFocusNode?.dispose();
 | 
			
		||||
      },
 | 
			
		||||
      [],
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    Future<void> onSearchSubmitted(String newSearchTerm) {
 | 
			
		||||
      debugPrint("Re-Search with $newSearchTerm");
 | 
			
		||||
      searchFocusNode?.unfocus();
 | 
			
		||||
      isNewSearch.value = false;
 | 
			
		||||
      currentSearchTerm.value = newSearchTerm;
 | 
			
		||||
      var searchType = _getSearchType(newSearchTerm);
 | 
			
		||||
      return ref
 | 
			
		||||
          .watch(searchResultPageProvider.notifier)
 | 
			
		||||
          .search(searchType.searchTerm, smartSearch: searchType.isSmart);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    buildTextField() {
 | 
			
		||||
      return TextField(
 | 
			
		||||
        controller: searchTermController,
 | 
			
		||||
        focusNode: searchFocusNode,
 | 
			
		||||
        autofocus: false,
 | 
			
		||||
        onTap: () {
 | 
			
		||||
          searchTermController.clear();
 | 
			
		||||
          ref.watch(searchPageStateProvider.notifier).setSearchTerm("");
 | 
			
		||||
          searchFocusNode?.requestFocus();
 | 
			
		||||
        },
 | 
			
		||||
        textInputAction: TextInputAction.search,
 | 
			
		||||
        onSubmitted: (searchTerm) {
 | 
			
		||||
          if (searchTerm.isNotEmpty) {
 | 
			
		||||
            searchTermController.clear();
 | 
			
		||||
            onSearchSubmitted(searchTerm);
 | 
			
		||||
          } else {
 | 
			
		||||
            isNewSearch.value = false;
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        onChanged: (value) {
 | 
			
		||||
          ref.watch(searchPageStateProvider.notifier).setSearchTerm(value);
 | 
			
		||||
        },
 | 
			
		||||
        decoration: InputDecoration(
 | 
			
		||||
          hintText: 'search_result_page_new_search_hint'.tr(),
 | 
			
		||||
          enabledBorder: const UnderlineInputBorder(
 | 
			
		||||
            borderSide: BorderSide(color: Colors.transparent),
 | 
			
		||||
          ),
 | 
			
		||||
          focusedBorder: const UnderlineInputBorder(
 | 
			
		||||
            borderSide: BorderSide(color: Colors.transparent),
 | 
			
		||||
          ),
 | 
			
		||||
          hintStyle: TextStyle(
 | 
			
		||||
            fontWeight: FontWeight.bold,
 | 
			
		||||
            fontSize: 16.0,
 | 
			
		||||
            color: context.isDarkTheme
 | 
			
		||||
                ? Colors.grey[500]
 | 
			
		||||
                : Colors.black.withOpacity(0.5),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    buildChip() {
 | 
			
		||||
      return Chip(
 | 
			
		||||
        label: Wrap(
 | 
			
		||||
          spacing: 5,
 | 
			
		||||
          runAlignment: WrapAlignment.center,
 | 
			
		||||
          crossAxisAlignment: WrapCrossAlignment.center,
 | 
			
		||||
          alignment: WrapAlignment.center,
 | 
			
		||||
          children: [
 | 
			
		||||
            Text(
 | 
			
		||||
              currentSearchTerm.value,
 | 
			
		||||
              style: TextStyle(
 | 
			
		||||
                color: context.primaryColor,
 | 
			
		||||
                fontSize: 13,
 | 
			
		||||
                fontWeight: FontWeight.bold,
 | 
			
		||||
              ),
 | 
			
		||||
              maxLines: 1,
 | 
			
		||||
            ),
 | 
			
		||||
            Icon(
 | 
			
		||||
              Icons.close_rounded,
 | 
			
		||||
              color: context.primaryColor,
 | 
			
		||||
              size: 20,
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        backgroundColor: context.primaryColor.withAlpha(50),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Future<void> refresh() async => onSearchSubmitted(currentSearchTerm.value);
 | 
			
		||||
 | 
			
		||||
    buildSearchResult() {
 | 
			
		||||
      final searchResultPageState = ref.watch(searchResultPageProvider);
 | 
			
		||||
 | 
			
		||||
      if (searchResultPageState.isError) {
 | 
			
		||||
        return Padding(
 | 
			
		||||
          padding: const EdgeInsets.all(12),
 | 
			
		||||
          child: const Text("common_server_error").tr(),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (searchResultPageState.isLoading) {
 | 
			
		||||
        return const Center(child: ImmichLoadingIndicator());
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (searchResultPageState.isSuccess) {
 | 
			
		||||
        return MultiselectGrid(
 | 
			
		||||
          renderListProvider: searchRenderListProvider,
 | 
			
		||||
          archiveEnabled: true,
 | 
			
		||||
          deleteEnabled: true,
 | 
			
		||||
          editEnabled: true,
 | 
			
		||||
          favoriteEnabled: true,
 | 
			
		||||
          stackEnabled: false,
 | 
			
		||||
          onRefresh: refresh,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return const SizedBox();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: IconButton(
 | 
			
		||||
          splashRadius: 20,
 | 
			
		||||
          onPressed: () {
 | 
			
		||||
            if (isNewSearch.value) {
 | 
			
		||||
              isNewSearch.value = false;
 | 
			
		||||
            } else {
 | 
			
		||||
              context.popRoute(true);
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          icon: const Icon(Icons.arrow_back_ios_rounded),
 | 
			
		||||
        ),
 | 
			
		||||
        title: GestureDetector(
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            isNewSearch.value = true;
 | 
			
		||||
            searchFocusNode?.requestFocus();
 | 
			
		||||
          },
 | 
			
		||||
          child: isNewSearch.value ? buildTextField() : buildChip(),
 | 
			
		||||
        ),
 | 
			
		||||
        centerTitle: false,
 | 
			
		||||
      ),
 | 
			
		||||
      body: GestureDetector(
 | 
			
		||||
        onTap: () {
 | 
			
		||||
          if (searchFocusNode != null) {
 | 
			
		||||
            searchFocusNode?.unfocus();
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          ref.watch(searchPageStateProvider.notifier).disableSearch();
 | 
			
		||||
        },
 | 
			
		||||
        child: Stack(
 | 
			
		||||
          children: [
 | 
			
		||||
            buildSearchResult(),
 | 
			
		||||
            if (isNewSearch.value)
 | 
			
		||||
              SearchSuggestionList(onSubmitted: onSearchSubmitted),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -31,7 +31,9 @@ import 'package:immich_mobile/modules/login/views/change_password_page.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/login/views/login_page.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/onboarding/views/permission_onboarding_page.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/models/search_filter.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/settings/views/settings_sub_page.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/views/search_input_page.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/shared_link/models/shared_link.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/shared_link/views/shared_link_edit_page.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/shared_link/views/shared_link_page.dart';
 | 
			
		||||
@ -43,7 +45,6 @@ import 'package:immich_mobile/modules/search/views/curated_location_page.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/views/person_result_page.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/views/recently_added_page.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/views/search_page.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/views/search_result_page.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/settings/views/settings_page.dart';
 | 
			
		||||
import 'package:immich_mobile/routing/auth_guard.dart';
 | 
			
		||||
import 'package:immich_mobile/routing/custom_transition_builders.dart';
 | 
			
		||||
@ -125,10 +126,6 @@ class AppRouter extends _$AppRouter {
 | 
			
		||||
      page: BackupControllerRoute.page,
 | 
			
		||||
      guards: [_authGuard, _duplicateGuard, _backupPermissionGuard],
 | 
			
		||||
    ),
 | 
			
		||||
    AutoRoute(
 | 
			
		||||
      page: SearchResultRoute.page,
 | 
			
		||||
      guards: [_authGuard, _duplicateGuard],
 | 
			
		||||
    ),
 | 
			
		||||
    AutoRoute(
 | 
			
		||||
      page: CuratedLocationRoute.page,
 | 
			
		||||
      guards: [_authGuard, _duplicateGuard],
 | 
			
		||||
@ -223,6 +220,11 @@ class AppRouter extends _$AppRouter {
 | 
			
		||||
      page: BackupOptionsRoute.page,
 | 
			
		||||
      guards: [_authGuard, _duplicateGuard],
 | 
			
		||||
    ),
 | 
			
		||||
    CustomRoute(
 | 
			
		||||
      page: SearchInputRoute.page,
 | 
			
		||||
      guards: [_authGuard, _duplicateGuard],
 | 
			
		||||
      transitionsBuilder: TransitionsBuilders.noTransition,
 | 
			
		||||
    ),
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -255,22 +255,21 @@ abstract class _$AppRouter extends RootStackRouter {
 | 
			
		||||
        child: const RecentlyAddedPage(),
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    SearchRoute.name: (routeData) {
 | 
			
		||||
      final args = routeData.argsAs<SearchRouteArgs>(
 | 
			
		||||
          orElse: () => const SearchRouteArgs());
 | 
			
		||||
    SearchInputRoute.name: (routeData) {
 | 
			
		||||
      final args = routeData.argsAs<SearchInputRouteArgs>(
 | 
			
		||||
          orElse: () => const SearchInputRouteArgs());
 | 
			
		||||
      return AutoRoutePage<dynamic>(
 | 
			
		||||
        routeData: routeData,
 | 
			
		||||
        child: SearchPage(key: args.key),
 | 
			
		||||
        child: SearchInputPage(
 | 
			
		||||
          key: args.key,
 | 
			
		||||
          prefilter: args.prefilter,
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    SearchResultRoute.name: (routeData) {
 | 
			
		||||
      final args = routeData.argsAs<SearchResultRouteArgs>();
 | 
			
		||||
    SearchRoute.name: (routeData) {
 | 
			
		||||
      return AutoRoutePage<dynamic>(
 | 
			
		||||
        routeData: routeData,
 | 
			
		||||
        child: SearchResultPage(
 | 
			
		||||
          key: args.key,
 | 
			
		||||
          searchTerm: args.searchTerm,
 | 
			
		||||
        ),
 | 
			
		||||
        child: const SearchPage(),
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    SelectAdditionalUserForSharingRoute.name: (routeData) {
 | 
			
		||||
@ -1113,69 +1112,55 @@ class RecentlyAddedRoute extends PageRouteInfo<void> {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// generated route for
 | 
			
		||||
/// [SearchPage]
 | 
			
		||||
class SearchRoute extends PageRouteInfo<SearchRouteArgs> {
 | 
			
		||||
  SearchRoute({
 | 
			
		||||
/// [SearchInputPage]
 | 
			
		||||
class SearchInputRoute extends PageRouteInfo<SearchInputRouteArgs> {
 | 
			
		||||
  SearchInputRoute({
 | 
			
		||||
    Key? key,
 | 
			
		||||
    SearchFilter? prefilter,
 | 
			
		||||
    List<PageRouteInfo>? children,
 | 
			
		||||
  }) : super(
 | 
			
		||||
          SearchInputRoute.name,
 | 
			
		||||
          args: SearchInputRouteArgs(
 | 
			
		||||
            key: key,
 | 
			
		||||
            prefilter: prefilter,
 | 
			
		||||
          ),
 | 
			
		||||
          initialChildren: children,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
  static const String name = 'SearchInputRoute';
 | 
			
		||||
 | 
			
		||||
  static const PageInfo<SearchInputRouteArgs> page =
 | 
			
		||||
      PageInfo<SearchInputRouteArgs>(name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SearchInputRouteArgs {
 | 
			
		||||
  const SearchInputRouteArgs({
 | 
			
		||||
    this.key,
 | 
			
		||||
    this.prefilter,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final Key? key;
 | 
			
		||||
 | 
			
		||||
  final SearchFilter? prefilter;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return 'SearchInputRouteArgs{key: $key, prefilter: $prefilter}';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// generated route for
 | 
			
		||||
/// [SearchPage]
 | 
			
		||||
class SearchRoute extends PageRouteInfo<void> {
 | 
			
		||||
  const SearchRoute({List<PageRouteInfo>? children})
 | 
			
		||||
      : super(
 | 
			
		||||
          SearchRoute.name,
 | 
			
		||||
          args: SearchRouteArgs(key: key),
 | 
			
		||||
          initialChildren: children,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
  static const String name = 'SearchRoute';
 | 
			
		||||
 | 
			
		||||
  static const PageInfo<SearchRouteArgs> page = PageInfo<SearchRouteArgs>(name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SearchRouteArgs {
 | 
			
		||||
  const SearchRouteArgs({this.key});
 | 
			
		||||
 | 
			
		||||
  final Key? key;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return 'SearchRouteArgs{key: $key}';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// generated route for
 | 
			
		||||
/// [SearchResultPage]
 | 
			
		||||
class SearchResultRoute extends PageRouteInfo<SearchResultRouteArgs> {
 | 
			
		||||
  SearchResultRoute({
 | 
			
		||||
    Key? key,
 | 
			
		||||
    required String searchTerm,
 | 
			
		||||
    List<PageRouteInfo>? children,
 | 
			
		||||
  }) : super(
 | 
			
		||||
          SearchResultRoute.name,
 | 
			
		||||
          args: SearchResultRouteArgs(
 | 
			
		||||
            key: key,
 | 
			
		||||
            searchTerm: searchTerm,
 | 
			
		||||
          ),
 | 
			
		||||
          initialChildren: children,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
  static const String name = 'SearchResultRoute';
 | 
			
		||||
 | 
			
		||||
  static const PageInfo<SearchResultRouteArgs> page =
 | 
			
		||||
      PageInfo<SearchResultRouteArgs>(name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SearchResultRouteArgs {
 | 
			
		||||
  const SearchResultRouteArgs({
 | 
			
		||||
    this.key,
 | 
			
		||||
    required this.searchTerm,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final Key? key;
 | 
			
		||||
 | 
			
		||||
  final String searchTerm;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return 'SearchResultRouteArgs{key: $key, searchTerm: $searchTerm}';
 | 
			
		||||
  }
 | 
			
		||||
  static const PageInfo<void> page = PageInfo<void>(name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// generated route for
 | 
			
		||||
 | 
			
		||||
@ -38,7 +38,7 @@ class TabNavigationObserver extends AutoRouterObserver {
 | 
			
		||||
    if (route.name == 'SearchRoute') {
 | 
			
		||||
      // Refresh Location State
 | 
			
		||||
      ref.invalidate(getCuratedLocationProvider);
 | 
			
		||||
      ref.invalidate(getCuratedPeopleProvider);
 | 
			
		||||
      ref.invalidate(getAllPeopleProvider);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (route.name == 'SharingRoute') {
 | 
			
		||||
 | 
			
		||||
@ -43,6 +43,7 @@ class MultiselectGrid extends HookConsumerWidget {
 | 
			
		||||
    this.editEnabled = false,
 | 
			
		||||
    this.unarchive = false,
 | 
			
		||||
    this.unfavorite = false,
 | 
			
		||||
    this.emptyIndicator,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final ProviderListenable<AsyncValue<RenderList>> renderListProvider;
 | 
			
		||||
@ -57,12 +58,12 @@ class MultiselectGrid extends HookConsumerWidget {
 | 
			
		||||
  final bool favoriteEnabled;
 | 
			
		||||
  final bool unfavorite;
 | 
			
		||||
  final bool editEnabled;
 | 
			
		||||
 | 
			
		||||
  final Widget? emptyIndicator;
 | 
			
		||||
  Widget buildDefaultLoadingIndicator() =>
 | 
			
		||||
      const Center(child: ImmichLoadingIndicator());
 | 
			
		||||
 | 
			
		||||
  Widget buildEmptyIndicator() =>
 | 
			
		||||
      const Center(child: Text("No assets to show"));
 | 
			
		||||
      emptyIndicator ?? const Center(child: Text("No assets to show"));
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/ui/immich_search_bar.dart';
 | 
			
		||||
import 'package:immich_mobile/routing/router.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/tab.provider.dart';
 | 
			
		||||
@ -53,10 +52,6 @@ class TabControllerPage extends HookConsumerWidget {
 | 
			
		||||
            // Scroll to top
 | 
			
		||||
            scrollToTopNotifierProvider.scrollToTop();
 | 
			
		||||
          }
 | 
			
		||||
          if (tabsRouter.activeIndex == 1 && index == 1) {
 | 
			
		||||
            // Focus search
 | 
			
		||||
            searchFocusNotifier.requestFocus();
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          HapticFeedback.selectionClick();
 | 
			
		||||
          tabsRouter.setActiveIndex(index);
 | 
			
		||||
@ -111,10 +106,7 @@ class TabControllerPage extends HookConsumerWidget {
 | 
			
		||||
            // Scroll to top
 | 
			
		||||
            scrollToTopNotifierProvider.scrollToTop();
 | 
			
		||||
          }
 | 
			
		||||
          if (tabsRouter.activeIndex == 1 && index == 1) {
 | 
			
		||||
            // Focus search
 | 
			
		||||
            searchFocusNotifier.requestFocus();
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          HapticFeedback.selectionClick();
 | 
			
		||||
          tabsRouter.setActiveIndex(index);
 | 
			
		||||
          ref.read(tabProvider.notifier).state = TabEnum.values[index];
 | 
			
		||||
@ -170,11 +162,11 @@ class TabControllerPage extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
    final multiselectEnabled = ref.watch(multiselectProvider);
 | 
			
		||||
    return AutoTabsRouter(
 | 
			
		||||
      routes: [
 | 
			
		||||
        const HomeRoute(),
 | 
			
		||||
      routes: const [
 | 
			
		||||
        HomeRoute(),
 | 
			
		||||
        SearchRoute(),
 | 
			
		||||
        const SharingRoute(),
 | 
			
		||||
        const LibraryRoute(),
 | 
			
		||||
        SharingRoute(),
 | 
			
		||||
        LibraryRoute(),
 | 
			
		||||
      ],
 | 
			
		||||
      duration: const Duration(milliseconds: 600),
 | 
			
		||||
      transitionBuilder: (context, child, animation) => FadeTransition(
 | 
			
		||||
 | 
			
		||||
@ -33,6 +33,9 @@ final ThemeData base = ThemeData(
 | 
			
		||||
final ThemeData immichLightTheme = ThemeData(
 | 
			
		||||
  useMaterial3: true,
 | 
			
		||||
  brightness: Brightness.light,
 | 
			
		||||
  colorScheme: ColorScheme.fromSeed(
 | 
			
		||||
    seedColor: Colors.indigo,
 | 
			
		||||
  ),
 | 
			
		||||
  primarySwatch: Colors.indigo,
 | 
			
		||||
  primaryColor: Colors.indigo,
 | 
			
		||||
  hintColor: Colors.indigo,
 | 
			
		||||
@ -158,6 +161,10 @@ final ThemeData immichDarkTheme = ThemeData(
 | 
			
		||||
  brightness: Brightness.dark,
 | 
			
		||||
  primarySwatch: Colors.indigo,
 | 
			
		||||
  primaryColor: immichDarkThemePrimaryColor,
 | 
			
		||||
  colorScheme: ColorScheme.fromSeed(
 | 
			
		||||
    seedColor: immichDarkThemePrimaryColor,
 | 
			
		||||
    brightness: Brightness.dark,
 | 
			
		||||
  ),
 | 
			
		||||
  scaffoldBackgroundColor: immichDarkBackgroundColor,
 | 
			
		||||
  hintColor: Colors.grey[600],
 | 
			
		||||
  fontFamily: 'Overpass',
 | 
			
		||||
 | 
			
		||||
@ -61,10 +61,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: auto_route
 | 
			
		||||
      sha256: "82f8df1d177416bc6b7a449127d0270ff1f0f633a91f2ceb7a85d4f07c3affa1"
 | 
			
		||||
      sha256: eb33554581a0a4aa7e6da0f13a44291a55bf71359012f1d9feb41634ff908ff8
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "7.8.4"
 | 
			
		||||
    version: "7.9.2"
 | 
			
		||||
  auto_route_generator:
 | 
			
		||||
    dependency: "direct dev"
 | 
			
		||||
    description:
 | 
			
		||||
@ -117,10 +117,10 @@ packages:
 | 
			
		||||
    dependency: "direct dev"
 | 
			
		||||
    description:
 | 
			
		||||
      name: build_runner
 | 
			
		||||
      sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21"
 | 
			
		||||
      sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.4.8"
 | 
			
		||||
    version: "2.4.9"
 | 
			
		||||
  build_runner_core:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -141,10 +141,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: built_value
 | 
			
		||||
      sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6
 | 
			
		||||
      sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "8.9.0"
 | 
			
		||||
    version: "8.9.1"
 | 
			
		||||
  cached_network_image:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@ -309,34 +309,34 @@ packages:
 | 
			
		||||
    dependency: "direct dev"
 | 
			
		||||
    description:
 | 
			
		||||
      name: custom_lint
 | 
			
		||||
      sha256: f89ff83efdba7c8996e86bb3bad0b759d58f9b19ae4d0e277a386ddd8b481217
 | 
			
		||||
      sha256: "7c0aec12df22f9082146c354692056677f1e70bc43471644d1fdb36c6fdda799"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.6.0"
 | 
			
		||||
    version: "0.6.4"
 | 
			
		||||
  custom_lint_builder:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: custom_lint_builder
 | 
			
		||||
      sha256: "3a14687fc71a5e2124a29722106f7b7e67dd5a6d58e33f2859650b46acff1d54"
 | 
			
		||||
      sha256: d7dc41e709dde223806660268678be7993559e523eb3164e2a1425fd6f7615a9
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.6.1"
 | 
			
		||||
    version: "0.6.4"
 | 
			
		||||
  custom_lint_core:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: custom_lint_core
 | 
			
		||||
      sha256: "1e9128e095ad5e0973469bdaac1ead8bfc86c485954c23cf617299de5e6fa029"
 | 
			
		||||
      sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.6.1"
 | 
			
		||||
    version: "0.6.3"
 | 
			
		||||
  dart_style:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: dart_style
 | 
			
		||||
      sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368"
 | 
			
		||||
      sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.3.4"
 | 
			
		||||
    version: "2.3.6"
 | 
			
		||||
  dartx:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -381,10 +381,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: easy_localization
 | 
			
		||||
      sha256: "9c86754b22aaa3e74e471635b25b33729f958dd6fb83df0ad6612948a7b231af"
 | 
			
		||||
      sha256: c145aeb6584aedc7c862ab8c737c3277788f47488bfdf9bae0fe112bd0a4789c
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.0.4"
 | 
			
		||||
    version: "3.0.5"
 | 
			
		||||
  easy_logger:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -405,10 +405,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: ffi
 | 
			
		||||
      sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
 | 
			
		||||
      sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.1.0"
 | 
			
		||||
    version: "2.1.2"
 | 
			
		||||
  file:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -503,18 +503,18 @@ packages:
 | 
			
		||||
    dependency: "direct dev"
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_lints
 | 
			
		||||
      sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7
 | 
			
		||||
      sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.0.1"
 | 
			
		||||
    version: "3.0.2"
 | 
			
		||||
  flutter_local_notifications:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_local_notifications
 | 
			
		||||
      sha256: c18f1de98fe0bb9dd5ba91e1330d4febc8b6a7de6aae3ffe475ef423723e72f3
 | 
			
		||||
      sha256: "55b9b229307a10974b26296ff29f2e132256ba4bd74266939118eaefa941cb00"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "16.3.2"
 | 
			
		||||
    version: "16.3.3"
 | 
			
		||||
  flutter_local_notifications_linux:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -540,10 +540,10 @@ packages:
 | 
			
		||||
    dependency: "direct dev"
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_native_splash
 | 
			
		||||
      sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e"
 | 
			
		||||
      sha256: edf39bcf4d74aca1eb2c1e43c3e445fd9f494013df7f0da752fefe72020eedc0
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.3.10"
 | 
			
		||||
    version: "2.4.0"
 | 
			
		||||
  flutter_plugin_android_lifecycle:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -556,10 +556,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_riverpod
 | 
			
		||||
      sha256: "4bce556b7ecbfea26109638d5237684538d4abc509d253e6c5c4c5733b360098"
 | 
			
		||||
      sha256: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.4.10"
 | 
			
		||||
    version: "2.5.1"
 | 
			
		||||
  flutter_svg:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@ -614,10 +614,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: frontend_server_client
 | 
			
		||||
      sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612"
 | 
			
		||||
      sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.2.0"
 | 
			
		||||
    version: "4.0.0"
 | 
			
		||||
  fuchsia_remote_debug_protocol:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description: flutter
 | 
			
		||||
@ -635,18 +635,18 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: geolocator_android
 | 
			
		||||
      sha256: "136f1c97e1903366393bda514c5d9e98843418baea52899aa45edae9af8a5cd6"
 | 
			
		||||
      sha256: f15d1536cd01b1399578f1da1eb5d566e7a718db6a3648f2c24d2e2f859f0692
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "4.5.2"
 | 
			
		||||
    version: "4.5.4"
 | 
			
		||||
  geolocator_apple:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: geolocator_apple
 | 
			
		||||
      sha256: "2f2d4ee16c4df269e93c0e382be075cc01d5db6703c3196e4af20a634fe49ef4"
 | 
			
		||||
      sha256: bc2aca02423ad429cb0556121f56e60360a2b7d694c8570301d06ea0c00732fd
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.3.6"
 | 
			
		||||
    version: "2.3.7"
 | 
			
		||||
  geolocator_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -667,10 +667,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: geolocator_windows
 | 
			
		||||
      sha256: a92fae29779d5c6dc60e8411302f5221ade464968fe80a36d330e80a71f087af
 | 
			
		||||
      sha256: "53da08937d07c24b0d9952eb57a3b474e29aae2abf9dd717f7e1230995f13f0e"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.2.2"
 | 
			
		||||
    version: "0.2.3"
 | 
			
		||||
  glob:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -691,10 +691,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: hooks_riverpod
 | 
			
		||||
      sha256: "758b07eba336e3cbacbd81dba481f2228a14102083fdde07045e8514e8054c49"
 | 
			
		||||
      sha256: "45b2030a18bcd6dbd680c2c91bc3b33e3fe7c323e3acb5ecec93a613e2fbaa8a"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.4.10"
 | 
			
		||||
    version: "2.5.1"
 | 
			
		||||
  hotreloader:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -755,10 +755,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: image_picker_android
 | 
			
		||||
      sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1"
 | 
			
		||||
      sha256: "42c098e7fb6334746be37cdc30369ade356ed4f14d48b7a0313f95a9159f4321"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.8.9+3"
 | 
			
		||||
    version: "0.8.9+5"
 | 
			
		||||
  image_picker_for_web:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -771,10 +771,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: image_picker_ios
 | 
			
		||||
      sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3
 | 
			
		||||
      sha256: "917a5cadd67d052554cfb258595e54217de53fac5b52939426e26319a02e6297"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.8.9+1"
 | 
			
		||||
    version: "0.8.9+2"
 | 
			
		||||
  image_picker_linux:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -795,10 +795,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: image_picker_platform_interface
 | 
			
		||||
      sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b
 | 
			
		||||
      sha256: "3d2c323daea9d60608f1caf30be32a938916f4975434b8352e6f73dae496da38"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.9.3"
 | 
			
		||||
    version: "2.9.4"
 | 
			
		||||
  image_picker_windows:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -922,7 +922,7 @@ packages:
 | 
			
		||||
    description:
 | 
			
		||||
      path: maplibre_gl_platform_interface
 | 
			
		||||
      ref: main
 | 
			
		||||
      resolved-ref: "3cf0abb051849ca3f14e6aa19d2261ad18f22ce1"
 | 
			
		||||
      resolved-ref: ec5a29dea08e8c2fadf9c55bd5bc500ef5b2a685
 | 
			
		||||
      url: "https://github.com/maplibre/flutter-maplibre-gl.git"
 | 
			
		||||
    source: git
 | 
			
		||||
    version: "0.18.0"
 | 
			
		||||
@ -931,7 +931,7 @@ packages:
 | 
			
		||||
    description:
 | 
			
		||||
      path: maplibre_gl_web
 | 
			
		||||
      ref: main
 | 
			
		||||
      resolved-ref: "3cf0abb051849ca3f14e6aa19d2261ad18f22ce1"
 | 
			
		||||
      resolved-ref: ec5a29dea08e8c2fadf9c55bd5bc500ef5b2a685
 | 
			
		||||
      url: "https://github.com/maplibre/flutter-maplibre-gl.git"
 | 
			
		||||
    source: git
 | 
			
		||||
    version: "0.18.0"
 | 
			
		||||
@ -1106,10 +1106,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: permission_handler
 | 
			
		||||
      sha256: "74e962b7fad7ff75959161bb2c0ad8fe7f2568ee82621c9c2660b751146bfe44"
 | 
			
		||||
      sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "11.3.0"
 | 
			
		||||
    version: "11.3.1"
 | 
			
		||||
  permission_handler_android:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -1122,10 +1122,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: permission_handler_apple
 | 
			
		||||
      sha256: bdafc6db74253abb63907f4e357302e6bb786ab41465e8635f362ee71fd8707b
 | 
			
		||||
      sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "9.4.0"
 | 
			
		||||
    version: "9.4.4"
 | 
			
		||||
  permission_handler_html:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -1138,10 +1138,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: permission_handler_platform_interface
 | 
			
		||||
      sha256: "23dfba8447c076ab5be3dee9ceb66aad345c4a648f0cac292c77b1eb0e800b78"
 | 
			
		||||
      sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "4.2.0"
 | 
			
		||||
    version: "4.2.1"
 | 
			
		||||
  permission_handler_windows:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -1162,18 +1162,18 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: photo_manager
 | 
			
		||||
      sha256: "8cf79918f6de9843b394a1670fe1aec54ebcac852b4b4c9ef88211894547dc61"
 | 
			
		||||
      sha256: df594f989f0c31cdb3ed48f3d49cb9ffadf11cc3700d2c3460b1912c93432621
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.0.0-dev.5"
 | 
			
		||||
    version: "3.0.0"
 | 
			
		||||
  photo_manager_image_provider:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: photo_manager_image_provider
 | 
			
		||||
      sha256: c187f60c3fdbe5630735d9a0bccbb071397ec03dcb1ba6085c29c8adece798a0
 | 
			
		||||
      sha256: "38ef1023dc11de3a8669f16e7c981673b3c5cfee715d17120f4b87daa2cdd0af"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.1.0"
 | 
			
		||||
    version: "2.1.1"
 | 
			
		||||
  platform:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -1218,10 +1218,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: provider
 | 
			
		||||
      sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096"
 | 
			
		||||
      sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "6.1.1"
 | 
			
		||||
    version: "6.1.2"
 | 
			
		||||
  pub_semver:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -1242,10 +1242,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: riverpod
 | 
			
		||||
      sha256: "548e2192eb7aeb826eb89387f814edb76594f3363e2c0bb99dd733d795ba3589"
 | 
			
		||||
      sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.5.0"
 | 
			
		||||
    version: "2.5.1"
 | 
			
		||||
  riverpod_analyzer_utils:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -1258,26 +1258,26 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: riverpod_annotation
 | 
			
		||||
      sha256: "77e5d51afa4fa3e67903fb8746f33d368728d7051a0b6c292bcee60aeba46d95"
 | 
			
		||||
      sha256: e5e796c0eba4030c704e9dae1b834a6541814963292839dcf9638d53eba84f5c
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.3.4"
 | 
			
		||||
    version: "2.3.5"
 | 
			
		||||
  riverpod_generator:
 | 
			
		||||
    dependency: "direct dev"
 | 
			
		||||
    description:
 | 
			
		||||
      name: riverpod_generator
 | 
			
		||||
      sha256: "359068f04879347ae4edbe66c81cc95f83fa1743806d1a0c86e55dd3c33ebb32"
 | 
			
		||||
      sha256: d451608bf17a372025fc36058863737636625dfdb7e3cbf6142e0dfeb366ab22
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.3.11"
 | 
			
		||||
    version: "2.4.0"
 | 
			
		||||
  riverpod_lint:
 | 
			
		||||
    dependency: "direct dev"
 | 
			
		||||
    description:
 | 
			
		||||
      name: riverpod_lint
 | 
			
		||||
      sha256: e9bbd02e9e89e18eecb183bbca556d7b523a0669024da9b8167c08903f442937
 | 
			
		||||
      sha256: "3c67c14ccd16f0c9d53e35ef70d06cd9d072e2fb14557326886bbde903b230a5"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.3.9"
 | 
			
		||||
    version: "2.3.10"
 | 
			
		||||
  rxdart:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -1306,10 +1306,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: share_plus_platform_interface
 | 
			
		||||
      sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956
 | 
			
		||||
      sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.3.1"
 | 
			
		||||
    version: "3.4.0"
 | 
			
		||||
  shared_preferences:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -1439,10 +1439,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: sqflite_common
 | 
			
		||||
      sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5"
 | 
			
		||||
      sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.5.3"
 | 
			
		||||
    version: "2.5.4"
 | 
			
		||||
  stack_trace:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -1567,10 +1567,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: url_launcher
 | 
			
		||||
      sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c
 | 
			
		||||
      sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "6.2.4"
 | 
			
		||||
    version: "6.2.5"
 | 
			
		||||
  url_launcher_android:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -1583,10 +1583,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: url_launcher_ios
 | 
			
		||||
      sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03"
 | 
			
		||||
      sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "6.2.4"
 | 
			
		||||
    version: "6.2.5"
 | 
			
		||||
  url_launcher_linux:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -1671,10 +1671,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: video_player
 | 
			
		||||
      sha256: fbf28ce8bcfe709ad91b5789166c832cb7a684d14f571a81891858fefb5bb1c2
 | 
			
		||||
      sha256: afc65f4b8bcb2c188f64a591f84fb471f4f2e19fc607c65fd8d2f8fedb3dec23
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.8.2"
 | 
			
		||||
    version: "2.8.3"
 | 
			
		||||
  video_player_android:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -1703,10 +1703,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: video_player_web
 | 
			
		||||
      sha256: "34beb3a07d4331a24f7e7b2f75b8e2b103289038e07e65529699a671b6a6e2cb"
 | 
			
		||||
      sha256: "8e9cb7fe94e49490e67bbc15149691792b58a0ade31b32e3f3688d104a0e057b"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.1.3"
 | 
			
		||||
    version: "2.2.0"
 | 
			
		||||
  vm_service:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -1727,10 +1727,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: wakelock_plus_platform_interface
 | 
			
		||||
      sha256: "40fabed5da06caff0796dc638e1f07ee395fb18801fbff3255a2372db2d80385"
 | 
			
		||||
      sha256: "582f2f7aecc7376332d961a0dd1efa9378ce117657e0ade55d9ff72699a55e82"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.1.0"
 | 
			
		||||
    version: "1.2.0"
 | 
			
		||||
  watcher:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -1743,18 +1743,18 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: web
 | 
			
		||||
      sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
 | 
			
		||||
      sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.3.0"
 | 
			
		||||
    version: "0.4.2"
 | 
			
		||||
  web_socket_channel:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: web_socket_channel
 | 
			
		||||
      sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
 | 
			
		||||
      sha256: "939ab60734a4f8fa95feacb55804fa278de28bdeef38e616dc08e44a84adea23"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.4.0"
 | 
			
		||||
    version: "2.4.3"
 | 
			
		||||
  webdriver:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -1767,10 +1767,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: win32
 | 
			
		||||
      sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8"
 | 
			
		||||
      sha256: "0a989dc7ca2bb51eac91e8fd00851297cfffd641aa7538b165c62637ca0eaa4a"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "5.2.0"
 | 
			
		||||
    version: "5.4.0"
 | 
			
		||||
  win32_registry:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -1813,4 +1813,4 @@ packages:
 | 
			
		||||
    version: "3.1.2"
 | 
			
		||||
sdks:
 | 
			
		||||
  dart: ">=3.3.0 <4.0.0"
 | 
			
		||||
  flutter: ">=3.16.0"
 | 
			
		||||
  flutter: ">=3.19.0"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										29
									
								
								server/src/cores/storage.core.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								server/src/cores/storage.core.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
			
		||||
import { StorageCore } from 'src/cores/storage.core';
 | 
			
		||||
 | 
			
		||||
jest.mock('src/constants', () => ({
 | 
			
		||||
  APP_MEDIA_LOCATION: '/photos',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
describe('StorageCore', () => {
 | 
			
		||||
  describe('isImmichPath', () => {
 | 
			
		||||
    it('should return true for APP_MEDIA_LOCATION path', () => {
 | 
			
		||||
      const immichPath = '/photos';
 | 
			
		||||
      expect(StorageCore.isImmichPath(immichPath)).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should return true for paths within the APP_MEDIA_LOCATION', () => {
 | 
			
		||||
      const immichPath = '/photos/new/';
 | 
			
		||||
      expect(StorageCore.isImmichPath(immichPath)).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should return false for paths outside the APP_MEDIA_LOCATION and same starts', () => {
 | 
			
		||||
      const nonImmichPath = '/photos_new';
 | 
			
		||||
      expect(StorageCore.isImmichPath(nonImmichPath)).toBe(false);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should return false for paths outside the APP_MEDIA_LOCATION', () => {
 | 
			
		||||
      const nonImmichPath = '/some/other/path';
 | 
			
		||||
      expect(StorageCore.isImmichPath(nonImmichPath)).toBe(false);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -115,7 +115,13 @@ export class StorageCore {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static isImmichPath(path: string) {
 | 
			
		||||
    return resolve(path).startsWith(resolve(APP_MEDIA_LOCATION));
 | 
			
		||||
    const resolvedPath = resolve(path);
 | 
			
		||||
    const resolvedAppMediaLocation = resolve(APP_MEDIA_LOCATION);
 | 
			
		||||
    const normalizedPath = resolvedPath.endsWith('/') ? resolvedPath : resolvedPath + '/';
 | 
			
		||||
    const normalizedAppMediaLocation = resolvedAppMediaLocation.endsWith('/')
 | 
			
		||||
      ? resolvedAppMediaLocation
 | 
			
		||||
      : resolvedAppMediaLocation + '/';
 | 
			
		||||
    return normalizedPath.startsWith(normalizedAppMediaLocation);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static isGeneratedAsset(path: string) {
 | 
			
		||||
 | 
			
		||||
@ -254,15 +254,15 @@ WHERE
 | 
			
		||||
  OR f_unaccent ("admin1Name") %>> f_unaccent ($1)
 | 
			
		||||
  OR f_unaccent ("alternateNames") %>> f_unaccent ($1)
 | 
			
		||||
ORDER BY
 | 
			
		||||
  COALESCE(f_unaccent (name) <->>> f_unaccent ($1), 0) + COALESCE(
 | 
			
		||||
  COALESCE(f_unaccent (name) <->>> f_unaccent ($1), 0.1) + COALESCE(
 | 
			
		||||
    f_unaccent ("admin2Name") <->>> f_unaccent ($1),
 | 
			
		||||
    0
 | 
			
		||||
    0.1
 | 
			
		||||
  ) + COALESCE(
 | 
			
		||||
    f_unaccent ("admin1Name") <->>> f_unaccent ($1),
 | 
			
		||||
    0
 | 
			
		||||
    0.1
 | 
			
		||||
  ) + COALESCE(
 | 
			
		||||
    f_unaccent ("alternateNames") <->>> f_unaccent ($1),
 | 
			
		||||
    0
 | 
			
		||||
    0.1
 | 
			
		||||
  ) ASC
 | 
			
		||||
LIMIT
 | 
			
		||||
  20
 | 
			
		||||
 | 
			
		||||
@ -214,10 +214,10 @@ export class SearchRepository implements ISearchRepository {
 | 
			
		||||
      .orWhere(`f_unaccent("alternateNames") %>> f_unaccent(:placeName)`)
 | 
			
		||||
      .orderBy(
 | 
			
		||||
        `
 | 
			
		||||
        COALESCE(f_unaccent(name) <->>> f_unaccent(:placeName), 0) +
 | 
			
		||||
        COALESCE(f_unaccent("admin2Name") <->>> f_unaccent(:placeName), 0) +
 | 
			
		||||
        COALESCE(f_unaccent("admin1Name") <->>> f_unaccent(:placeName), 0) +
 | 
			
		||||
        COALESCE(f_unaccent("alternateNames") <->>> f_unaccent(:placeName), 0)
 | 
			
		||||
        COALESCE(f_unaccent(name) <->>> f_unaccent(:placeName), 0.1) +
 | 
			
		||||
        COALESCE(f_unaccent("admin2Name") <->>> f_unaccent(:placeName), 0.1) +
 | 
			
		||||
        COALESCE(f_unaccent("admin1Name") <->>> f_unaccent(:placeName), 0.1) +
 | 
			
		||||
        COALESCE(f_unaccent("alternateNames") <->>> f_unaccent(:placeName), 0.1)
 | 
			
		||||
        `,
 | 
			
		||||
      )
 | 
			
		||||
      .setParameters({ placeName })
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user