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.
|
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;
|
CREATE EXTENSION earthdistance CASCADE;
|
||||||
ALTER DATABASE <immichdatabasename> SET search_path TO "$user", public, vectors;
|
ALTER DATABASE <immichdatabasename> SET search_path TO "$user", public, vectors;
|
||||||
GRANT USAGE ON SCHEMA vectors TO <immichdbusername>;
|
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;
|
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.
|
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
|
:::tip
|
||||||
`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.
|
`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.
|
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 |
|
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | microservices |
|
||||||
|
|
||||||
:::tip
|
:::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.
|
`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.
|
||||||
|
|
||||||
`exiftool` is only present in the microservices container.
|
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Ports
|
## 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
|
## Docker Secrets
|
||||||
|
|
||||||
The following variables support the use of [Docker secrets](https://docs.docker.com/engine/swarm/secrets/) for additional security.
|
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):
|
- fluttertoast (0.0.2):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Toast
|
- Toast
|
||||||
|
- FMDB (2.7.5):
|
||||||
|
- FMDB/standard (= 2.7.5)
|
||||||
|
- FMDB/standard (2.7.5)
|
||||||
- geolocator_apple (1.2.0):
|
- geolocator_apple (1.2.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- image_picker_ios (0.0.1):
|
- image_picker_ios (0.0.1):
|
||||||
@ -36,7 +39,7 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- path_provider_ios (0.0.1):
|
- path_provider_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- permission_handler_apple (9.3.0):
|
- permission_handler_apple (9.1.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- photo_manager (2.0.0):
|
- photo_manager (2.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
@ -50,7 +53,7 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- sqflite (0.0.3):
|
- sqflite (0.0.3):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FMDB (>= 2.7.5)
|
||||||
- Toast (4.0.0)
|
- Toast (4.0.0)
|
||||||
- url_launcher_ios (0.0.1):
|
- url_launcher_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
@ -81,13 +84,14 @@ DEPENDENCIES:
|
|||||||
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- 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`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
|
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
|
||||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
|
- FMDB
|
||||||
- MapLibre
|
- MapLibre
|
||||||
- ReachabilitySwift
|
- ReachabilitySwift
|
||||||
- SAMKeychain
|
- SAMKeychain
|
||||||
@ -135,7 +139,7 @@ EXTERNAL SOURCES:
|
|||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||||
sqflite:
|
sqflite:
|
||||||
:path: ".symlinks/plugins/sqflite/darwin"
|
:path: ".symlinks/plugins/sqflite/ios"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
video_player_avfoundation:
|
video_player_avfoundation:
|
||||||
@ -151,23 +155,24 @@ SPEC CHECKSUMS:
|
|||||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||||
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
|
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
|
||||||
flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d
|
flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d
|
||||||
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
|
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
|
||||||
|
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||||
geolocator_apple: 9157311f654584b9bb72686c55fc02a97b73f461
|
geolocator_apple: 9157311f654584b9bb72686c55fc02a97b73f461
|
||||||
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
|
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
|
||||||
integration_test: 13825b8a9334a850581300559b8839134b124670
|
integration_test: 13825b8a9334a850581300559b8839134b124670
|
||||||
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
|
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
|
||||||
MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef
|
MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef
|
||||||
maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9
|
maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9
|
||||||
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
|
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
|
||||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
||||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||||
permission_handler_apple: 036b856153a2b1f61f21030ff725f3e6fece2b78
|
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
|
||||||
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
|
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
|
||||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
|
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
|
||||||
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
|
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
|
||||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
||||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||||
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
|
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
|
||||||
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
|
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
|
||||||
@ -175,4 +180,4 @@ SPEC CHECKSUMS:
|
|||||||
|
|
||||||
PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d
|
PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d
|
||||||
|
|
||||||
COCOAPODS: 1.12.1
|
COCOAPODS: 1.15.2
|
||||||
|
@ -243,12 +243,7 @@ class AlbumService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await _db.writeTxn(() async {
|
await _updateAssets(album.id, add: successAssets);
|
||||||
await album.assets.update(link: successAssets);
|
|
||||||
final a = await _db.albums.get(album.id);
|
|
||||||
// trigger watcher
|
|
||||||
await _db.albums.put(a!);
|
|
||||||
});
|
|
||||||
|
|
||||||
return AddAssetsResponse(
|
return AddAssetsResponse(
|
||||||
alreadyInAlbum: duplicatedAssets,
|
alreadyInAlbum: duplicatedAssets,
|
||||||
@ -257,11 +252,28 @@ class AlbumService {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
|
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
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(
|
Future<bool> addAdditionalUserToAlbum(
|
||||||
List<String> sharedUserIds,
|
List<String> sharedUserIds,
|
||||||
Album album,
|
Album album,
|
||||||
@ -342,7 +354,7 @@ class AlbumService {
|
|||||||
await _apiService.albumApi.removeUserFromAlbum(album.remoteId!, "me");
|
await _apiService.albumApi.removeUserFromAlbum(album.remoteId!, "me");
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error deleteAlbum ${e.toString()}");
|
debugPrint("Error leaveAlbum ${e.toString()}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -352,24 +364,25 @@ class AlbumService {
|
|||||||
Iterable<Asset> assets,
|
Iterable<Asset> assets,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
await _apiService.albumApi.removeAssetFromAlbum(
|
final response = await _apiService.albumApi.removeAssetFromAlbum(
|
||||||
album.remoteId!,
|
album.remoteId!,
|
||||||
BulkIdsDto(
|
BulkIdsDto(
|
||||||
ids: assets.map((asset) => asset.remoteId!).toList(),
|
ids: assets.map((asset) => asset.remoteId!).toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await _db.writeTxn(() async {
|
if (response != null) {
|
||||||
await album.assets.update(unlink: assets);
|
final toRemove = response.every((e) => e.success)
|
||||||
final a = await _db.albums.get(album.id);
|
? assets
|
||||||
// trigger watcher
|
: response
|
||||||
await _db.albums.put(a!);
|
.where((e) => e.success)
|
||||||
});
|
.map((e) => assets.firstWhere((a) => a.remoteId == e.id));
|
||||||
|
await _updateAssets(album.id, remove: toRemove);
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error deleteAlbum ${e.toString()}");
|
debugPrint("Error removeAssetFromAlbum ${e.toString()}");
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> removeUserFromAlbum(
|
Future<bool> removeUserFromAlbum(
|
||||||
@ -413,7 +426,7 @@ class AlbumService {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error deleteAlbum ${e.toString()}");
|
debugPrint("Error changeTitleAlbum ${e.toString()}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
|||||||
this.assetsPerRow,
|
this.assetsPerRow,
|
||||||
this.showStorageIndicator,
|
this.showStorageIndicator,
|
||||||
this.listener,
|
this.listener,
|
||||||
this.margin = 5.0,
|
this.margin = 2.0,
|
||||||
this.selectionActive = false,
|
this.selectionActive = false,
|
||||||
this.preselectedAssets,
|
this.preselectedAssets,
|
||||||
this.canDeselect = true,
|
this.canDeselect = true,
|
||||||
|
@ -1,15 +1,60 @@
|
|||||||
/// A wrapper for [CuratedLocationsResponseDto] objects
|
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||||
/// and [CuratedObjectsResponseDto] to be displayed in
|
import 'dart:convert';
|
||||||
/// a view
|
|
||||||
class CuratedContent {
|
/// A wrapper for [CuratedLocationsResponseDto] objects
|
||||||
/// The label to show associated with this curated object
|
/// and [CuratedObjectsResponseDto] to be displayed in
|
||||||
final String label;
|
/// a view
|
||||||
|
class CuratedContent {
|
||||||
/// The id to lookup the asset from the server
|
/// The label to show associated with this curated object
|
||||||
final String id;
|
final String label;
|
||||||
|
|
||||||
CuratedContent({
|
/// The id to lookup the asset from the server
|
||||||
required this.id,
|
final String id;
|
||||||
required this.label,
|
|
||||||
});
|
CuratedContent({
|
||||||
}
|
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,51 +1,49 @@
|
|||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
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/search/services/person.service.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.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:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'people.provider.g.dart';
|
part 'people.provider.g.dart';
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<List<CuratedContent>> getCuratedPeople(
|
Future<List<PersonResponseDto>> getAllPeople(
|
||||||
GetCuratedPeopleRef ref,
|
GetAllPeopleRef ref,
|
||||||
) async {
|
) async {
|
||||||
final PersonService personService = ref.read(personServiceProvider);
|
final PersonService personService = ref.read(personServiceProvider);
|
||||||
|
|
||||||
final curatedPeople = await personService.getCuratedPeople();
|
final people = await personService.getAllPeople();
|
||||||
|
|
||||||
return curatedPeople
|
return people;
|
||||||
.map((p) => CuratedContent(id: p.id, label: p.name))
|
}
|
||||||
.toList();
|
|
||||||
}
|
@riverpod
|
||||||
|
Future<RenderList> personAssets(PersonAssetsRef ref, String personId) async {
|
||||||
@riverpod
|
final PersonService personService = ref.read(personServiceProvider);
|
||||||
Future<RenderList> personAssets(PersonAssetsRef ref, String personId) async {
|
final assets = await personService.getPersonAssets(personId);
|
||||||
final PersonService personService = ref.read(personServiceProvider);
|
if (assets == null) {
|
||||||
final assets = await personService.getPersonAssets(personId);
|
return RenderList.empty();
|
||||||
if (assets == null) {
|
}
|
||||||
return RenderList.empty();
|
|
||||||
}
|
final settings = ref.read(appSettingsServiceProvider);
|
||||||
|
final groupBy =
|
||||||
final settings = ref.read(appSettingsServiceProvider);
|
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
|
||||||
final groupBy =
|
return await RenderList.fromAssets(assets, groupBy);
|
||||||
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
|
}
|
||||||
return await RenderList.fromAssets(assets, groupBy);
|
|
||||||
}
|
@riverpod
|
||||||
|
Future<bool> updatePersonName(
|
||||||
@riverpod
|
UpdatePersonNameRef ref,
|
||||||
Future<bool> updatePersonName(
|
String personId,
|
||||||
UpdatePersonNameRef ref,
|
String updatedName,
|
||||||
String personId,
|
) async {
|
||||||
String updatedName,
|
final PersonService personService = ref.read(personServiceProvider);
|
||||||
) async {
|
final person = await personService.updateName(personId, updatedName);
|
||||||
final PersonService personService = ref.read(personServiceProvider);
|
|
||||||
final person = await personService.updateName(personId, updatedName);
|
if (person != null && person.name == updatedName) {
|
||||||
|
ref.invalidate(getAllPeopleProvider);
|
||||||
if (person != null && person.name == updatedName) {
|
return true;
|
||||||
ref.invalidate(getCuratedPeopleProvider);
|
}
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
@ -6,23 +6,21 @@ part of 'people.provider.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$getCuratedPeopleHash() => r'2a534553812abe69abce2c2e41aa62b8de16e9d0';
|
String _$getAllPeopleHash() => r'4eff6666be5a74710d1e8587e01d8154310d85bd';
|
||||||
|
|
||||||
/// See also [getCuratedPeople].
|
/// See also [getAllPeople].
|
||||||
@ProviderFor(getCuratedPeople)
|
@ProviderFor(getAllPeople)
|
||||||
final getCuratedPeopleProvider =
|
final getAllPeopleProvider =
|
||||||
AutoDisposeFutureProvider<List<CuratedContent>>.internal(
|
AutoDisposeFutureProvider<List<PersonResponseDto>>.internal(
|
||||||
getCuratedPeople,
|
getAllPeople,
|
||||||
name: r'getCuratedPeopleProvider',
|
name: r'getAllPeopleProvider',
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
debugGetCreateSourceHash:
|
||||||
? null
|
const bool.fromEnvironment('dart.vm.product') ? null : _$getAllPeopleHash,
|
||||||
: _$getCuratedPeopleHash,
|
|
||||||
dependencies: null,
|
dependencies: null,
|
||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef GetCuratedPeopleRef
|
typedef GetAllPeopleRef = AutoDisposeFutureProviderRef<List<PersonResponseDto>>;
|
||||||
= AutoDisposeFutureProviderRef<List<CuratedContent>>;
|
|
||||||
String _$personAssetsHash() => r'1d6eff5ca3aa630b58c4dad9516193b21896984d';
|
String _$personAssetsHash() => r'1d6eff5ca3aa630b58c4dad9516193b21896984d';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
@ -172,7 +170,7 @@ class _PersonAssetsProviderElement
|
|||||||
String get personId => (origin as PersonAssetsProvider).personId;
|
String get personId => (origin as PersonAssetsProvider).personId;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$updatePersonNameHash() => r'c7179a7cc558669c3b30b03fbca7782a42f2b6fd';
|
String _$updatePersonNameHash() => r'7145aaaf6fc38fdafe3a283ebf3d3f4fd0774cd2';
|
||||||
|
|
||||||
/// See also [updatePersonName].
|
/// See also [updatePersonName].
|
||||||
@ProviderFor(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);
|
PersonService(this._apiService, this._db);
|
||||||
|
|
||||||
Future<List<PersonResponseDto>> getCuratedPeople() async {
|
Future<List<PersonResponseDto>> getAllPeople() async {
|
||||||
try {
|
try {
|
||||||
final peopleResponseDto = await _apiService.personApi.getAllPeople();
|
final peopleResponseDto = await _apiService.personApi.getAllPeople();
|
||||||
return peopleResponseDto?.people ?? [];
|
return peopleResponseDto?.people ?? [];
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||||
@ -29,25 +30,92 @@ class SearchService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Asset>?> searchAsset(
|
Future<List<String>?> getSearchSuggestions(
|
||||||
String searchTerm, {
|
SearchSuggestionType type, {
|
||||||
bool smartSearch = true,
|
String? country,
|
||||||
|
String? state,
|
||||||
|
String? make,
|
||||||
|
String? model,
|
||||||
}) async {
|
}) async {
|
||||||
// TODO search in local DB: 1. when offline, 2. to find local assets
|
|
||||||
try {
|
try {
|
||||||
final SearchResponseDto? results = await _apiService.searchApi.search(
|
return await _apiService.searchApi.getSearchSuggestions(
|
||||||
query: searchTerm,
|
type,
|
||||||
smart: smartSearch,
|
country: country,
|
||||||
|
state: state,
|
||||||
|
make: make,
|
||||||
|
model: model,
|
||||||
);
|
);
|
||||||
if (results == null) {
|
} catch (e) {
|
||||||
|
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 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));
|
return _db.assets
|
||||||
} catch (e) {
|
.getAllByRemoteId(response.assets.items.map((e) => e.id));
|
||||||
debugPrint("[ERROR] [searchAsset] ${e.toString()}");
|
} catch (error) {
|
||||||
return null;
|
debugPrint("Error [search] $error");
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<CuratedLocationsResponseDto>?> getCuratedLocation() async {
|
Future<List<CuratedLocationsResponseDto>?> getCuratedLocation() async {
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/modules/search/models/curated_content.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/modules/search/ui/thumbnail_with_info.dart';
|
||||||
import 'package:immich_mobile/routing/router.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/shared/models/store.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
|
||||||
@ -57,7 +59,22 @@ class ExploreGrid extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: context.pushRoute(
|
: 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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +1,38 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
|
import 'package:immich_mobile/modules/search/models/curated_content.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/explore_grid.dart';
|
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/ui/explore_grid.dart';
|
||||||
@RoutePage()
|
|
||||||
class AllPeoplePage extends HookConsumerWidget {
|
@RoutePage()
|
||||||
const AllPeoplePage({super.key});
|
class AllPeoplePage extends HookConsumerWidget {
|
||||||
|
const AllPeoplePage({super.key});
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
@override
|
||||||
final curatedPeople = ref.watch(getCuratedPeopleProvider);
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final curatedPeople = ref.watch(getAllPeopleProvider);
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
return Scaffold(
|
||||||
title: const Text(
|
appBar: AppBar(
|
||||||
'all_people_page_title',
|
title: const Text(
|
||||||
).tr(),
|
'all_people_page_title',
|
||||||
leading: IconButton(
|
).tr(),
|
||||||
onPressed: () => context.popRoute(),
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
onPressed: () => context.popRoute(),
|
||||||
),
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
),
|
),
|
||||||
body: curatedPeople.widgetWhen(
|
),
|
||||||
onData: (people) => ExploreGrid(
|
body: curatedPeople.widgetWhen(
|
||||||
isPeople: true,
|
onData: (people) => ExploreGrid(
|
||||||
curatedContent: people,
|
isPeople: true,
|
||||||
),
|
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,279 +1,274 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_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/curated_content.dart';
|
||||||
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
|
import 'package:immich_mobile/modules/search/models/search_filter.dart';
|
||||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/curated_people_row.dart';
|
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/curated_places_row.dart';
|
import 'package:immich_mobile/modules/search/ui/curated_people_row.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/immich_search_bar.dart';
|
import 'package:immich_mobile/modules/search/ui/curated_places_row.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/person_name_edit_form.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_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/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/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/ui/scaffold_error_body.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
|
@RoutePage()
|
||||||
class SearchPage extends HookConsumerWidget {
|
// ignore: must_be_immutable
|
||||||
SearchPage({super.key});
|
class SearchPage extends HookConsumerWidget {
|
||||||
|
const SearchPage({super.key});
|
||||||
FocusNode searchFocusNode = FocusNode();
|
|
||||||
|
@override
|
||||||
@override
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
final curatedLocation = ref.watch(getCuratedLocationProvider);
|
||||||
final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
|
final curatedPeople = ref.watch(getAllPeopleProvider);
|
||||||
final curatedLocation = ref.watch(getCuratedLocationProvider);
|
final isMapEnabled =
|
||||||
final curatedPeople = ref.watch(getCuratedPeopleProvider);
|
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.map));
|
||||||
final isMapEnabled =
|
double imageSize = math.min(context.width / 3, 150);
|
||||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.map));
|
|
||||||
double imageSize = math.min(context.width / 3, 150);
|
TextStyle categoryTitleStyle = const TextStyle(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
TextStyle categoryTitleStyle = const TextStyle(
|
fontSize: 15.0,
|
||||||
fontWeight: FontWeight.w500,
|
);
|
||||||
fontSize: 15.0,
|
|
||||||
);
|
Color categoryIconColor = context.isDarkTheme ? Colors.white : Colors.black;
|
||||||
|
|
||||||
Color categoryIconColor = context.isDarkTheme ? Colors.white : Colors.black;
|
showNameEditModel(
|
||||||
|
String personId,
|
||||||
useEffect(
|
String personName,
|
||||||
() {
|
) {
|
||||||
searchFocusNode = FocusNode();
|
return showDialog(
|
||||||
return () => searchFocusNode.dispose();
|
context: context,
|
||||||
},
|
builder: (BuildContext context) {
|
||||||
[],
|
return PersonNameEditForm(personId: personId, personName: personName);
|
||||||
);
|
},
|
||||||
|
);
|
||||||
onSearchSubmitted(String searchTerm) async {
|
}
|
||||||
searchFocusNode.unfocus();
|
|
||||||
ref.watch(searchPageStateProvider.notifier).disableSearch();
|
buildPeople() {
|
||||||
|
return SizedBox(
|
||||||
context.pushRoute(
|
height: imageSize,
|
||||||
SearchResultRoute(
|
child: curatedPeople.widgetWhen(
|
||||||
searchTerm: searchTerm,
|
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
|
||||||
),
|
onData: (people) => Padding(
|
||||||
);
|
padding: const EdgeInsets.only(
|
||||||
}
|
left: 16,
|
||||||
|
top: 8,
|
||||||
showNameEditModel(
|
),
|
||||||
String personId,
|
child: CuratedPeopleRow(
|
||||||
String personName,
|
content: people
|
||||||
) {
|
.map((e) => CuratedContent(label: e.name, id: e.id))
|
||||||
return showDialog(
|
.take(12)
|
||||||
context: context,
|
.toList(),
|
||||||
builder: (BuildContext context) {
|
onTap: (content, index) {
|
||||||
return PersonNameEditForm(personId: personId, personName: personName);
|
context.pushRoute(
|
||||||
},
|
PersonResultRoute(
|
||||||
);
|
personId: content.id,
|
||||||
}
|
personName: content.label,
|
||||||
|
),
|
||||||
buildPeople() {
|
);
|
||||||
return SizedBox(
|
},
|
||||||
height: imageSize,
|
onNameTap: (person, index) => {
|
||||||
child: curatedPeople.widgetWhen(
|
showNameEditModel(person.id, person.label),
|
||||||
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
|
},
|
||||||
onData: (people) => Padding(
|
),
|
||||||
padding: const EdgeInsets.only(
|
),
|
||||||
left: 16,
|
),
|
||||||
top: 8,
|
);
|
||||||
),
|
}
|
||||||
child: CuratedPeopleRow(
|
|
||||||
content: people.take(12).toList(),
|
buildPlaces() {
|
||||||
onTap: (content, index) {
|
return SizedBox(
|
||||||
context.pushRoute(
|
height: imageSize,
|
||||||
PersonResultRoute(
|
child: curatedLocation.widgetWhen(
|
||||||
personId: content.id,
|
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
|
||||||
personName: content.label,
|
onData: (locations) => CuratedPlacesRow(
|
||||||
),
|
isMapEnabled: isMapEnabled,
|
||||||
);
|
content: locations
|
||||||
},
|
.map(
|
||||||
onNameTap: (person, index) => {
|
(o) => CuratedContent(
|
||||||
showNameEditModel(person.id, person.label),
|
id: o.id,
|
||||||
},
|
label: o.city,
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
),
|
.toList(),
|
||||||
);
|
imageSize: imageSize,
|
||||||
}
|
onTap: (content, index) {
|
||||||
|
context.pushRoute(
|
||||||
buildPlaces() {
|
SearchInputRoute(
|
||||||
return SizedBox(
|
prefilter: SearchFilter(
|
||||||
height: imageSize,
|
people: {},
|
||||||
child: curatedLocation.widgetWhen(
|
location: SearchLocationFilter(
|
||||||
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
|
city: content.label,
|
||||||
onData: (locations) => CuratedPlacesRow(
|
),
|
||||||
isMapEnabled: isMapEnabled,
|
camera: SearchCameraFilter(),
|
||||||
content: locations
|
date: SearchDateFilter(),
|
||||||
.map(
|
display: SearchDisplayFilters(
|
||||||
(o) => CuratedContent(
|
isNotInAlbum: false,
|
||||||
id: o.id,
|
isArchive: false,
|
||||||
label: o.city,
|
isFavorite: false,
|
||||||
),
|
),
|
||||||
)
|
mediaType: AssetType.other,
|
||||||
.toList(),
|
),
|
||||||
imageSize: imageSize,
|
),
|
||||||
onTap: (content, index) {
|
);
|
||||||
context.pushRoute(
|
},
|
||||||
SearchResultRoute(
|
),
|
||||||
searchTerm: 'm:${content.label}',
|
),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
},
|
|
||||||
),
|
buildSearchButton() {
|
||||||
),
|
return GestureDetector(
|
||||||
);
|
onTap: () {
|
||||||
}
|
context.pushRoute(SearchInputRoute());
|
||||||
|
},
|
||||||
return Scaffold(
|
child: Card(
|
||||||
appBar: ImmichSearchBar(
|
elevation: 0,
|
||||||
searchFocusNode: searchFocusNode,
|
shape: RoundedRectangleBorder(
|
||||||
onSubmitted: onSearchSubmitted,
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
side: BorderSide(
|
||||||
body: GestureDetector(
|
color: context.isDarkTheme
|
||||||
onTap: () {
|
? Colors.grey[800]!
|
||||||
searchFocusNode.unfocus();
|
: const Color.fromARGB(255, 225, 225, 225),
|
||||||
ref.watch(searchPageStateProvider.notifier).disableSearch();
|
),
|
||||||
},
|
),
|
||||||
child: Stack(
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
children: [
|
child: Padding(
|
||||||
ListView(
|
padding: const EdgeInsets.symmetric(
|
||||||
children: [
|
horizontal: 16.0,
|
||||||
SearchRowTitle(
|
vertical: 12.0,
|
||||||
title: "search_page_people".tr(),
|
),
|
||||||
onViewAllPressed: () =>
|
child: Row(
|
||||||
context.pushRoute(const AllPeopleRoute()),
|
children: [
|
||||||
),
|
Icon(Icons.search, color: context.primaryColor),
|
||||||
buildPeople(),
|
const SizedBox(width: 16.0),
|
||||||
SearchRowTitle(
|
Text(
|
||||||
title: "search_page_places".tr(),
|
"Search your photos",
|
||||||
onViewAllPressed: () =>
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
context.pushRoute(const CuratedLocationRoute()),
|
color:
|
||||||
top: 0,
|
context.isDarkTheme ? Colors.white70 : Colors.black54,
|
||||||
),
|
fontWeight: FontWeight.w400,
|
||||||
const SizedBox(height: 10.0),
|
),
|
||||||
buildPlaces(),
|
),
|
||||||
const SizedBox(height: 24.0),
|
],
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
),
|
||||||
child: Text(
|
),
|
||||||
'search_page_your_activity',
|
);
|
||||||
style: context.textTheme.bodyLarge?.copyWith(
|
}
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
return Scaffold(
|
||||||
).tr(),
|
appBar: const ImmichAppBar(),
|
||||||
),
|
body: Stack(
|
||||||
ListTile(
|
children: [
|
||||||
leading: Icon(
|
ListView(
|
||||||
Icons.favorite_border_rounded,
|
children: [
|
||||||
color: categoryIconColor,
|
buildSearchButton(),
|
||||||
),
|
SearchRowTitle(
|
||||||
title:
|
title: "search_page_people".tr(),
|
||||||
Text('search_page_favorites', style: categoryTitleStyle)
|
onViewAllPressed: () =>
|
||||||
.tr(),
|
context.pushRoute(const AllPeopleRoute()),
|
||||||
onTap: () => context.pushRoute(const FavoritesRoute()),
|
),
|
||||||
),
|
buildPeople(),
|
||||||
const CategoryDivider(),
|
SearchRowTitle(
|
||||||
ListTile(
|
title: "search_page_places".tr(),
|
||||||
leading: Icon(
|
onViewAllPressed: () =>
|
||||||
Icons.schedule_outlined,
|
context.pushRoute(const CuratedLocationRoute()),
|
||||||
color: categoryIconColor,
|
top: 0,
|
||||||
),
|
),
|
||||||
title: Text(
|
const SizedBox(height: 10.0),
|
||||||
'search_page_recently_added',
|
buildPlaces(),
|
||||||
style: categoryTitleStyle,
|
const SizedBox(height: 24.0),
|
||||||
).tr(),
|
Padding(
|
||||||
onTap: () => context.pushRoute(const RecentlyAddedRoute()),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
),
|
child: Text(
|
||||||
const SizedBox(height: 24.0),
|
'search_page_your_activity',
|
||||||
Padding(
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
fontWeight: FontWeight.w500,
|
||||||
child: Text(
|
),
|
||||||
'search_page_categories',
|
).tr(),
|
||||||
style: context.textTheme.bodyLarge?.copyWith(
|
),
|
||||||
fontWeight: FontWeight.w500,
|
ListTile(
|
||||||
),
|
leading: Icon(
|
||||||
).tr(),
|
Icons.favorite_border_rounded,
|
||||||
),
|
color: categoryIconColor,
|
||||||
ListTile(
|
),
|
||||||
title:
|
title: Text('search_page_favorites', style: categoryTitleStyle)
|
||||||
Text('search_page_screenshots', style: categoryTitleStyle)
|
.tr(),
|
||||||
.tr(),
|
onTap: () => context.pushRoute(const FavoritesRoute()),
|
||||||
leading: Icon(
|
),
|
||||||
Icons.screenshot,
|
const CategoryDivider(),
|
||||||
color: categoryIconColor,
|
ListTile(
|
||||||
),
|
leading: Icon(
|
||||||
onTap: () => context.pushRoute(
|
Icons.schedule_outlined,
|
||||||
SearchResultRoute(
|
color: categoryIconColor,
|
||||||
searchTerm: 'screenshots',
|
),
|
||||||
),
|
title: Text(
|
||||||
),
|
'search_page_recently_added',
|
||||||
),
|
style: categoryTitleStyle,
|
||||||
const CategoryDivider(),
|
).tr(),
|
||||||
ListTile(
|
onTap: () => context.pushRoute(const RecentlyAddedRoute()),
|
||||||
title: Text('search_page_selfies', style: categoryTitleStyle)
|
),
|
||||||
.tr(),
|
const SizedBox(height: 24.0),
|
||||||
leading: Icon(
|
Padding(
|
||||||
Icons.photo_camera_front_outlined,
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
color: categoryIconColor,
|
child: Text(
|
||||||
),
|
'search_page_categories',
|
||||||
onTap: () => context.pushRoute(
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
SearchResultRoute(
|
fontWeight: FontWeight.w500,
|
||||||
searchTerm: 'selfies',
|
),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
),
|
ListTile(
|
||||||
const CategoryDivider(),
|
title:
|
||||||
ListTile(
|
Text('search_page_videos', style: categoryTitleStyle).tr(),
|
||||||
title: Text('search_page_videos', style: categoryTitleStyle)
|
leading: Icon(
|
||||||
.tr(),
|
Icons.play_circle_outline,
|
||||||
leading: Icon(
|
color: categoryIconColor,
|
||||||
Icons.play_circle_outline,
|
),
|
||||||
color: categoryIconColor,
|
onTap: () => context.pushRoute(const AllVideosRoute()),
|
||||||
),
|
),
|
||||||
onTap: () => context.pushRoute(const AllVideosRoute()),
|
const CategoryDivider(),
|
||||||
),
|
ListTile(
|
||||||
const CategoryDivider(),
|
title: Text(
|
||||||
ListTile(
|
'search_page_motion_photos',
|
||||||
title: Text(
|
style: categoryTitleStyle,
|
||||||
'search_page_motion_photos',
|
).tr(),
|
||||||
style: categoryTitleStyle,
|
leading: Icon(
|
||||||
).tr(),
|
Icons.motion_photos_on_outlined,
|
||||||
leading: Icon(
|
color: categoryIconColor,
|
||||||
Icons.motion_photos_on_outlined,
|
),
|
||||||
color: categoryIconColor,
|
onTap: () => context.pushRoute(const AllMotionPhotosRoute()),
|
||||||
),
|
),
|
||||||
onTap: () => context.pushRoute(const AllMotionPhotosRoute()),
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (isSearchEnabled)
|
);
|
||||||
SearchSuggestionList(onSubmitted: onSearchSubmitted),
|
}
|
||||||
],
|
}
|
||||||
),
|
|
||||||
),
|
class CategoryDivider extends StatelessWidget {
|
||||||
);
|
const CategoryDivider({super.key});
|
||||||
}
|
|
||||||
}
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
class CategoryDivider extends StatelessWidget {
|
return const Padding(
|
||||||
const CategoryDivider({super.key});
|
padding: EdgeInsets.only(
|
||||||
|
left: 56,
|
||||||
@override
|
right: 16,
|
||||||
Widget build(BuildContext context) {
|
),
|
||||||
return const Padding(
|
child: Divider(
|
||||||
padding: EdgeInsets.only(
|
height: 0,
|
||||||
left: 56,
|
),
|
||||||
right: 16,
|
);
|
||||||
),
|
}
|
||||||
child: Divider(
|
}
|
||||||
height: 0,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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/login/views/login_page.dart';
|
||||||
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.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/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/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/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_edit_page.dart';
|
||||||
import 'package:immich_mobile/modules/shared_link/views/shared_link_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/person_result_page.dart';
|
||||||
import 'package:immich_mobile/modules/search/views/recently_added_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_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/modules/settings/views/settings_page.dart';
|
||||||
import 'package:immich_mobile/routing/auth_guard.dart';
|
import 'package:immich_mobile/routing/auth_guard.dart';
|
||||||
import 'package:immich_mobile/routing/custom_transition_builders.dart';
|
import 'package:immich_mobile/routing/custom_transition_builders.dart';
|
||||||
@ -125,10 +126,6 @@ class AppRouter extends _$AppRouter {
|
|||||||
page: BackupControllerRoute.page,
|
page: BackupControllerRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard, _backupPermissionGuard],
|
guards: [_authGuard, _duplicateGuard, _backupPermissionGuard],
|
||||||
),
|
),
|
||||||
AutoRoute(
|
|
||||||
page: SearchResultRoute.page,
|
|
||||||
guards: [_authGuard, _duplicateGuard],
|
|
||||||
),
|
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: CuratedLocationRoute.page,
|
page: CuratedLocationRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
@ -223,6 +220,11 @@ class AppRouter extends _$AppRouter {
|
|||||||
page: BackupOptionsRoute.page,
|
page: BackupOptionsRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
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(),
|
child: const RecentlyAddedPage(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
SearchRoute.name: (routeData) {
|
SearchInputRoute.name: (routeData) {
|
||||||
final args = routeData.argsAs<SearchRouteArgs>(
|
final args = routeData.argsAs<SearchInputRouteArgs>(
|
||||||
orElse: () => const SearchRouteArgs());
|
orElse: () => const SearchInputRouteArgs());
|
||||||
return AutoRoutePage<dynamic>(
|
return AutoRoutePage<dynamic>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
child: SearchPage(key: args.key),
|
child: SearchInputPage(
|
||||||
|
key: args.key,
|
||||||
|
prefilter: args.prefilter,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
SearchResultRoute.name: (routeData) {
|
SearchRoute.name: (routeData) {
|
||||||
final args = routeData.argsAs<SearchResultRouteArgs>();
|
|
||||||
return AutoRoutePage<dynamic>(
|
return AutoRoutePage<dynamic>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
child: SearchResultPage(
|
child: const SearchPage(),
|
||||||
key: args.key,
|
|
||||||
searchTerm: args.searchTerm,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
SelectAdditionalUserForSharingRoute.name: (routeData) {
|
SelectAdditionalUserForSharingRoute.name: (routeData) {
|
||||||
@ -1113,69 +1112,55 @@ class RecentlyAddedRoute extends PageRouteInfo<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [SearchPage]
|
/// [SearchInputPage]
|
||||||
class SearchRoute extends PageRouteInfo<SearchRouteArgs> {
|
class SearchInputRoute extends PageRouteInfo<SearchInputRouteArgs> {
|
||||||
SearchRoute({
|
SearchInputRoute({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
SearchFilter? prefilter,
|
||||||
List<PageRouteInfo>? children,
|
List<PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : 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,
|
SearchRoute.name,
|
||||||
args: SearchRouteArgs(key: key),
|
|
||||||
initialChildren: children,
|
initialChildren: children,
|
||||||
);
|
);
|
||||||
|
|
||||||
static const String name = 'SearchRoute';
|
static const String name = 'SearchRoute';
|
||||||
|
|
||||||
static const PageInfo<SearchRouteArgs> page = PageInfo<SearchRouteArgs>(name);
|
static const PageInfo<void> page = PageInfo<void>(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}';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
|
@ -38,7 +38,7 @@ class TabNavigationObserver extends AutoRouterObserver {
|
|||||||
if (route.name == 'SearchRoute') {
|
if (route.name == 'SearchRoute') {
|
||||||
// Refresh Location State
|
// Refresh Location State
|
||||||
ref.invalidate(getCuratedLocationProvider);
|
ref.invalidate(getCuratedLocationProvider);
|
||||||
ref.invalidate(getCuratedPeopleProvider);
|
ref.invalidate(getAllPeopleProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.name == 'SharingRoute') {
|
if (route.name == 'SharingRoute') {
|
||||||
|
@ -43,6 +43,7 @@ class MultiselectGrid extends HookConsumerWidget {
|
|||||||
this.editEnabled = false,
|
this.editEnabled = false,
|
||||||
this.unarchive = false,
|
this.unarchive = false,
|
||||||
this.unfavorite = false,
|
this.unfavorite = false,
|
||||||
|
this.emptyIndicator,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ProviderListenable<AsyncValue<RenderList>> renderListProvider;
|
final ProviderListenable<AsyncValue<RenderList>> renderListProvider;
|
||||||
@ -57,12 +58,12 @@ class MultiselectGrid extends HookConsumerWidget {
|
|||||||
final bool favoriteEnabled;
|
final bool favoriteEnabled;
|
||||||
final bool unfavorite;
|
final bool unfavorite;
|
||||||
final bool editEnabled;
|
final bool editEnabled;
|
||||||
|
final Widget? emptyIndicator;
|
||||||
Widget buildDefaultLoadingIndicator() =>
|
Widget buildDefaultLoadingIndicator() =>
|
||||||
const Center(child: ImmichLoadingIndicator());
|
const Center(child: ImmichLoadingIndicator());
|
||||||
|
|
||||||
Widget buildEmptyIndicator() =>
|
Widget buildEmptyIndicator() =>
|
||||||
const Center(child: Text("No assets to show"));
|
emptyIndicator ?? const Center(child: Text("No assets to show"));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
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/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.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/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/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/tab.provider.dart';
|
import 'package:immich_mobile/shared/providers/tab.provider.dart';
|
||||||
@ -53,10 +52,6 @@ class TabControllerPage extends HookConsumerWidget {
|
|||||||
// Scroll to top
|
// Scroll to top
|
||||||
scrollToTopNotifierProvider.scrollToTop();
|
scrollToTopNotifierProvider.scrollToTop();
|
||||||
}
|
}
|
||||||
if (tabsRouter.activeIndex == 1 && index == 1) {
|
|
||||||
// Focus search
|
|
||||||
searchFocusNotifier.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
tabsRouter.setActiveIndex(index);
|
tabsRouter.setActiveIndex(index);
|
||||||
@ -111,10 +106,7 @@ class TabControllerPage extends HookConsumerWidget {
|
|||||||
// Scroll to top
|
// Scroll to top
|
||||||
scrollToTopNotifierProvider.scrollToTop();
|
scrollToTopNotifierProvider.scrollToTop();
|
||||||
}
|
}
|
||||||
if (tabsRouter.activeIndex == 1 && index == 1) {
|
|
||||||
// Focus search
|
|
||||||
searchFocusNotifier.requestFocus();
|
|
||||||
}
|
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
tabsRouter.setActiveIndex(index);
|
tabsRouter.setActiveIndex(index);
|
||||||
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
||||||
@ -170,11 +162,11 @@ class TabControllerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
final multiselectEnabled = ref.watch(multiselectProvider);
|
final multiselectEnabled = ref.watch(multiselectProvider);
|
||||||
return AutoTabsRouter(
|
return AutoTabsRouter(
|
||||||
routes: [
|
routes: const [
|
||||||
const HomeRoute(),
|
HomeRoute(),
|
||||||
SearchRoute(),
|
SearchRoute(),
|
||||||
const SharingRoute(),
|
SharingRoute(),
|
||||||
const LibraryRoute(),
|
LibraryRoute(),
|
||||||
],
|
],
|
||||||
duration: const Duration(milliseconds: 600),
|
duration: const Duration(milliseconds: 600),
|
||||||
transitionBuilder: (context, child, animation) => FadeTransition(
|
transitionBuilder: (context, child, animation) => FadeTransition(
|
||||||
|
@ -33,6 +33,9 @@ final ThemeData base = ThemeData(
|
|||||||
final ThemeData immichLightTheme = ThemeData(
|
final ThemeData immichLightTheme = ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
brightness: Brightness.light,
|
brightness: Brightness.light,
|
||||||
|
colorScheme: ColorScheme.fromSeed(
|
||||||
|
seedColor: Colors.indigo,
|
||||||
|
),
|
||||||
primarySwatch: Colors.indigo,
|
primarySwatch: Colors.indigo,
|
||||||
primaryColor: Colors.indigo,
|
primaryColor: Colors.indigo,
|
||||||
hintColor: Colors.indigo,
|
hintColor: Colors.indigo,
|
||||||
@ -158,6 +161,10 @@ final ThemeData immichDarkTheme = ThemeData(
|
|||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
primarySwatch: Colors.indigo,
|
primarySwatch: Colors.indigo,
|
||||||
primaryColor: immichDarkThemePrimaryColor,
|
primaryColor: immichDarkThemePrimaryColor,
|
||||||
|
colorScheme: ColorScheme.fromSeed(
|
||||||
|
seedColor: immichDarkThemePrimaryColor,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
),
|
||||||
scaffoldBackgroundColor: immichDarkBackgroundColor,
|
scaffoldBackgroundColor: immichDarkBackgroundColor,
|
||||||
hintColor: Colors.grey[600],
|
hintColor: Colors.grey[600],
|
||||||
fontFamily: 'Overpass',
|
fontFamily: 'Overpass',
|
||||||
|
@ -61,10 +61,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: auto_route
|
name: auto_route
|
||||||
sha256: "82f8df1d177416bc6b7a449127d0270ff1f0f633a91f2ceb7a85d4f07c3affa1"
|
sha256: eb33554581a0a4aa7e6da0f13a44291a55bf71359012f1d9feb41634ff908ff8
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.8.4"
|
version: "7.9.2"
|
||||||
auto_route_generator:
|
auto_route_generator:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -117,10 +117,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: build_runner
|
name: build_runner
|
||||||
sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21"
|
sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.8"
|
version: "2.4.9"
|
||||||
build_runner_core:
|
build_runner_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -141,10 +141,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: built_value
|
name: built_value
|
||||||
sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6
|
sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.9.0"
|
version: "8.9.1"
|
||||||
cached_network_image:
|
cached_network_image:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -309,34 +309,34 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: custom_lint
|
name: custom_lint
|
||||||
sha256: f89ff83efdba7c8996e86bb3bad0b759d58f9b19ae4d0e277a386ddd8b481217
|
sha256: "7c0aec12df22f9082146c354692056677f1e70bc43471644d1fdb36c6fdda799"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.0"
|
version: "0.6.4"
|
||||||
custom_lint_builder:
|
custom_lint_builder:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: custom_lint_builder
|
name: custom_lint_builder
|
||||||
sha256: "3a14687fc71a5e2124a29722106f7b7e67dd5a6d58e33f2859650b46acff1d54"
|
sha256: d7dc41e709dde223806660268678be7993559e523eb3164e2a1425fd6f7615a9
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.1"
|
version: "0.6.4"
|
||||||
custom_lint_core:
|
custom_lint_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: custom_lint_core
|
name: custom_lint_core
|
||||||
sha256: "1e9128e095ad5e0973469bdaac1ead8bfc86c485954c23cf617299de5e6fa029"
|
sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.1"
|
version: "0.6.3"
|
||||||
dart_style:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: dart_style
|
name: dart_style
|
||||||
sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368"
|
sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.4"
|
version: "2.3.6"
|
||||||
dartx:
|
dartx:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -381,10 +381,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: easy_localization
|
name: easy_localization
|
||||||
sha256: "9c86754b22aaa3e74e471635b25b33729f958dd6fb83df0ad6612948a7b231af"
|
sha256: c145aeb6584aedc7c862ab8c737c3277788f47488bfdf9bae0fe112bd0a4789c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.4"
|
version: "3.0.5"
|
||||||
easy_logger:
|
easy_logger:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -405,10 +405,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: ffi
|
name: ffi
|
||||||
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
|
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.2"
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -503,18 +503,18 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: flutter_lints
|
name: flutter_lints
|
||||||
sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7
|
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.2"
|
||||||
flutter_local_notifications:
|
flutter_local_notifications:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_local_notifications
|
name: flutter_local_notifications
|
||||||
sha256: c18f1de98fe0bb9dd5ba91e1330d4febc8b6a7de6aae3ffe475ef423723e72f3
|
sha256: "55b9b229307a10974b26296ff29f2e132256ba4bd74266939118eaefa941cb00"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "16.3.2"
|
version: "16.3.3"
|
||||||
flutter_local_notifications_linux:
|
flutter_local_notifications_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -540,10 +540,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: flutter_native_splash
|
name: flutter_native_splash
|
||||||
sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e"
|
sha256: edf39bcf4d74aca1eb2c1e43c3e445fd9f494013df7f0da752fefe72020eedc0
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.10"
|
version: "2.4.0"
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -556,10 +556,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_riverpod
|
name: flutter_riverpod
|
||||||
sha256: "4bce556b7ecbfea26109638d5237684538d4abc509d253e6c5c4c5733b360098"
|
sha256: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.10"
|
version: "2.5.1"
|
||||||
flutter_svg:
|
flutter_svg:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -614,10 +614,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: frontend_server_client
|
name: frontend_server_client
|
||||||
sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612"
|
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.0"
|
version: "4.0.0"
|
||||||
fuchsia_remote_debug_protocol:
|
fuchsia_remote_debug_protocol:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -635,18 +635,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: geolocator_android
|
name: geolocator_android
|
||||||
sha256: "136f1c97e1903366393bda514c5d9e98843418baea52899aa45edae9af8a5cd6"
|
sha256: f15d1536cd01b1399578f1da1eb5d566e7a718db6a3648f2c24d2e2f859f0692
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.5.2"
|
version: "4.5.4"
|
||||||
geolocator_apple:
|
geolocator_apple:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: geolocator_apple
|
name: geolocator_apple
|
||||||
sha256: "2f2d4ee16c4df269e93c0e382be075cc01d5db6703c3196e4af20a634fe49ef4"
|
sha256: bc2aca02423ad429cb0556121f56e60360a2b7d694c8570301d06ea0c00732fd
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.6"
|
version: "2.3.7"
|
||||||
geolocator_platform_interface:
|
geolocator_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -667,10 +667,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: geolocator_windows
|
name: geolocator_windows
|
||||||
sha256: a92fae29779d5c6dc60e8411302f5221ade464968fe80a36d330e80a71f087af
|
sha256: "53da08937d07c24b0d9952eb57a3b474e29aae2abf9dd717f7e1230995f13f0e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.2"
|
version: "0.2.3"
|
||||||
glob:
|
glob:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -691,10 +691,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: hooks_riverpod
|
name: hooks_riverpod
|
||||||
sha256: "758b07eba336e3cbacbd81dba481f2228a14102083fdde07045e8514e8054c49"
|
sha256: "45b2030a18bcd6dbd680c2c91bc3b33e3fe7c323e3acb5ecec93a613e2fbaa8a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.10"
|
version: "2.5.1"
|
||||||
hotreloader:
|
hotreloader:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -755,10 +755,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_android
|
name: image_picker_android
|
||||||
sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1"
|
sha256: "42c098e7fb6334746be37cdc30369ade356ed4f14d48b7a0313f95a9159f4321"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.9+3"
|
version: "0.8.9+5"
|
||||||
image_picker_for_web:
|
image_picker_for_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -771,10 +771,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_ios
|
name: image_picker_ios
|
||||||
sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3
|
sha256: "917a5cadd67d052554cfb258595e54217de53fac5b52939426e26319a02e6297"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.9+1"
|
version: "0.8.9+2"
|
||||||
image_picker_linux:
|
image_picker_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -795,10 +795,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_platform_interface
|
name: image_picker_platform_interface
|
||||||
sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b
|
sha256: "3d2c323daea9d60608f1caf30be32a938916f4975434b8352e6f73dae496da38"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.9.3"
|
version: "2.9.4"
|
||||||
image_picker_windows:
|
image_picker_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -922,7 +922,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
path: maplibre_gl_platform_interface
|
path: maplibre_gl_platform_interface
|
||||||
ref: main
|
ref: main
|
||||||
resolved-ref: "3cf0abb051849ca3f14e6aa19d2261ad18f22ce1"
|
resolved-ref: ec5a29dea08e8c2fadf9c55bd5bc500ef5b2a685
|
||||||
url: "https://github.com/maplibre/flutter-maplibre-gl.git"
|
url: "https://github.com/maplibre/flutter-maplibre-gl.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.18.0"
|
version: "0.18.0"
|
||||||
@ -931,7 +931,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
path: maplibre_gl_web
|
path: maplibre_gl_web
|
||||||
ref: main
|
ref: main
|
||||||
resolved-ref: "3cf0abb051849ca3f14e6aa19d2261ad18f22ce1"
|
resolved-ref: ec5a29dea08e8c2fadf9c55bd5bc500ef5b2a685
|
||||||
url: "https://github.com/maplibre/flutter-maplibre-gl.git"
|
url: "https://github.com/maplibre/flutter-maplibre-gl.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.18.0"
|
version: "0.18.0"
|
||||||
@ -1106,10 +1106,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: permission_handler
|
name: permission_handler
|
||||||
sha256: "74e962b7fad7ff75959161bb2c0ad8fe7f2568ee82621c9c2660b751146bfe44"
|
sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "11.3.0"
|
version: "11.3.1"
|
||||||
permission_handler_android:
|
permission_handler_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1122,10 +1122,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: permission_handler_apple
|
name: permission_handler_apple
|
||||||
sha256: bdafc6db74253abb63907f4e357302e6bb786ab41465e8635f362ee71fd8707b
|
sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.4.0"
|
version: "9.4.4"
|
||||||
permission_handler_html:
|
permission_handler_html:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1138,10 +1138,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: permission_handler_platform_interface
|
name: permission_handler_platform_interface
|
||||||
sha256: "23dfba8447c076ab5be3dee9ceb66aad345c4a648f0cac292c77b1eb0e800b78"
|
sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.0"
|
version: "4.2.1"
|
||||||
permission_handler_windows:
|
permission_handler_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1162,18 +1162,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: photo_manager
|
name: photo_manager
|
||||||
sha256: "8cf79918f6de9843b394a1670fe1aec54ebcac852b4b4c9ef88211894547dc61"
|
sha256: df594f989f0c31cdb3ed48f3d49cb9ffadf11cc3700d2c3460b1912c93432621
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0-dev.5"
|
version: "3.0.0"
|
||||||
photo_manager_image_provider:
|
photo_manager_image_provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: photo_manager_image_provider
|
name: photo_manager_image_provider
|
||||||
sha256: c187f60c3fdbe5630735d9a0bccbb071397ec03dcb1ba6085c29c8adece798a0
|
sha256: "38ef1023dc11de3a8669f16e7c981673b3c5cfee715d17120f4b87daa2cdd0af"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.1"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1218,10 +1218,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: provider
|
name: provider
|
||||||
sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096"
|
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.1"
|
version: "6.1.2"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1242,10 +1242,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: riverpod
|
name: riverpod
|
||||||
sha256: "548e2192eb7aeb826eb89387f814edb76594f3363e2c0bb99dd733d795ba3589"
|
sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.0"
|
version: "2.5.1"
|
||||||
riverpod_analyzer_utils:
|
riverpod_analyzer_utils:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1258,26 +1258,26 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: riverpod_annotation
|
name: riverpod_annotation
|
||||||
sha256: "77e5d51afa4fa3e67903fb8746f33d368728d7051a0b6c292bcee60aeba46d95"
|
sha256: e5e796c0eba4030c704e9dae1b834a6541814963292839dcf9638d53eba84f5c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.4"
|
version: "2.3.5"
|
||||||
riverpod_generator:
|
riverpod_generator:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: riverpod_generator
|
name: riverpod_generator
|
||||||
sha256: "359068f04879347ae4edbe66c81cc95f83fa1743806d1a0c86e55dd3c33ebb32"
|
sha256: d451608bf17a372025fc36058863737636625dfdb7e3cbf6142e0dfeb366ab22
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.11"
|
version: "2.4.0"
|
||||||
riverpod_lint:
|
riverpod_lint:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: riverpod_lint
|
name: riverpod_lint
|
||||||
sha256: e9bbd02e9e89e18eecb183bbca556d7b523a0669024da9b8167c08903f442937
|
sha256: "3c67c14ccd16f0c9d53e35ef70d06cd9d072e2fb14557326886bbde903b230a5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.9"
|
version: "2.3.10"
|
||||||
rxdart:
|
rxdart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1306,10 +1306,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: share_plus_platform_interface
|
name: share_plus_platform_interface
|
||||||
sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956
|
sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.3.1"
|
version: "3.4.0"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1439,10 +1439,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqflite_common
|
name: sqflite_common
|
||||||
sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5"
|
sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.3"
|
version: "2.5.4"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1567,10 +1567,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: url_launcher
|
name: url_launcher
|
||||||
sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c
|
sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.4"
|
version: "6.2.5"
|
||||||
url_launcher_android:
|
url_launcher_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1583,10 +1583,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_ios
|
name: url_launcher_ios
|
||||||
sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03"
|
sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.4"
|
version: "6.2.5"
|
||||||
url_launcher_linux:
|
url_launcher_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1671,10 +1671,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: video_player
|
name: video_player
|
||||||
sha256: fbf28ce8bcfe709ad91b5789166c832cb7a684d14f571a81891858fefb5bb1c2
|
sha256: afc65f4b8bcb2c188f64a591f84fb471f4f2e19fc607c65fd8d2f8fedb3dec23
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.8.2"
|
version: "2.8.3"
|
||||||
video_player_android:
|
video_player_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1703,10 +1703,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: video_player_web
|
name: video_player_web
|
||||||
sha256: "34beb3a07d4331a24f7e7b2f75b8e2b103289038e07e65529699a671b6a6e2cb"
|
sha256: "8e9cb7fe94e49490e67bbc15149691792b58a0ade31b32e3f3688d104a0e057b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.2.0"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1727,10 +1727,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: wakelock_plus_platform_interface
|
name: wakelock_plus_platform_interface
|
||||||
sha256: "40fabed5da06caff0796dc638e1f07ee395fb18801fbff3255a2372db2d80385"
|
sha256: "582f2f7aecc7376332d961a0dd1efa9378ce117657e0ade55d9ff72699a55e82"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.2.0"
|
||||||
watcher:
|
watcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1743,18 +1743,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: web
|
name: web
|
||||||
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
|
sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.0"
|
version: "0.4.2"
|
||||||
web_socket_channel:
|
web_socket_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: web_socket_channel
|
name: web_socket_channel
|
||||||
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
|
sha256: "939ab60734a4f8fa95feacb55804fa278de28bdeef38e616dc08e44a84adea23"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.0"
|
version: "2.4.3"
|
||||||
webdriver:
|
webdriver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1767,10 +1767,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: win32
|
name: win32
|
||||||
sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8"
|
sha256: "0a989dc7ca2bb51eac91e8fd00851297cfffd641aa7538b165c62637ca0eaa4a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.2.0"
|
version: "5.4.0"
|
||||||
win32_registry:
|
win32_registry:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1813,4 +1813,4 @@ packages:
|
|||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.3.0 <4.0.0"
|
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) {
|
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) {
|
static isGeneratedAsset(path: string) {
|
||||||
|
@ -254,15 +254,15 @@ WHERE
|
|||||||
OR f_unaccent ("admin1Name") %>> f_unaccent ($1)
|
OR f_unaccent ("admin1Name") %>> f_unaccent ($1)
|
||||||
OR f_unaccent ("alternateNames") %>> f_unaccent ($1)
|
OR f_unaccent ("alternateNames") %>> f_unaccent ($1)
|
||||||
ORDER BY
|
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),
|
f_unaccent ("admin2Name") <->>> f_unaccent ($1),
|
||||||
0
|
0.1
|
||||||
) + COALESCE(
|
) + COALESCE(
|
||||||
f_unaccent ("admin1Name") <->>> f_unaccent ($1),
|
f_unaccent ("admin1Name") <->>> f_unaccent ($1),
|
||||||
0
|
0.1
|
||||||
) + COALESCE(
|
) + COALESCE(
|
||||||
f_unaccent ("alternateNames") <->>> f_unaccent ($1),
|
f_unaccent ("alternateNames") <->>> f_unaccent ($1),
|
||||||
0
|
0.1
|
||||||
) ASC
|
) ASC
|
||||||
LIMIT
|
LIMIT
|
||||||
20
|
20
|
||||||
|
@ -214,10 +214,10 @@ export class SearchRepository implements ISearchRepository {
|
|||||||
.orWhere(`f_unaccent("alternateNames") %>> f_unaccent(:placeName)`)
|
.orWhere(`f_unaccent("alternateNames") %>> f_unaccent(:placeName)`)
|
||||||
.orderBy(
|
.orderBy(
|
||||||
`
|
`
|
||||||
COALESCE(f_unaccent(name) <->>> f_unaccent(:placeName), 0) +
|
COALESCE(f_unaccent(name) <->>> f_unaccent(:placeName), 0.1) +
|
||||||
COALESCE(f_unaccent("admin2Name") <->>> f_unaccent(:placeName), 0) +
|
COALESCE(f_unaccent("admin2Name") <->>> f_unaccent(:placeName), 0.1) +
|
||||||
COALESCE(f_unaccent("admin1Name") <->>> f_unaccent(:placeName), 0) +
|
COALESCE(f_unaccent("admin1Name") <->>> f_unaccent(:placeName), 0.1) +
|
||||||
COALESCE(f_unaccent("alternateNames") <->>> f_unaccent(:placeName), 0)
|
COALESCE(f_unaccent("alternateNames") <->>> f_unaccent(:placeName), 0.1)
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
.setParameters({ placeName })
|
.setParameters({ placeName })
|
||||||
|
Loading…
x
Reference in New Issue
Block a user