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