mirror of
https://github.com/immich-app/immich.git
synced 2025-03-11 02:23:09 -05:00
fix(mobile): search page issues (#15804)
* fix: don't repeat search * fix: show snackbar for no result * fix: do not search on empty filter * chore: syling
This commit is contained in:
parent
4fccc09fc1
commit
098bab7c9b
7 changed files with 159 additions and 14 deletions
|
@ -1,4 +1,6 @@
|
||||||
{
|
{
|
||||||
|
"search_no_result": "No results found, try a different search term or combination",
|
||||||
|
"search_no_more_result": "No more results",
|
||||||
"action_common_back": "Back",
|
"action_common_back": "Back",
|
||||||
"action_common_cancel": "Cancel",
|
"action_common_cancel": "Cancel",
|
||||||
"action_common_clear": "Clear",
|
"action_common_clear": "Clear",
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
abstract interface class IPersonApiRepository {
|
abstract interface class IPersonApiRepository {
|
||||||
Future<List<Person>> getAll();
|
Future<List<Person>> getAll();
|
||||||
Future<Person> update(String id, {String? name});
|
Future<Person> update(String id, {String? name});
|
||||||
|
@ -6,10 +9,10 @@ abstract interface class IPersonApiRepository {
|
||||||
class Person {
|
class Person {
|
||||||
Person({
|
Person({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
this.birthDate,
|
||||||
required this.isHidden,
|
required this.isHidden,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.thumbnailPath,
|
required this.thumbnailPath,
|
||||||
this.birthDate,
|
|
||||||
this.updatedAt,
|
this.updatedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -19,4 +22,80 @@ class Person {
|
||||||
final String name;
|
final String name;
|
||||||
final String thumbnailPath;
|
final String thumbnailPath;
|
||||||
final DateTime? updatedAt;
|
final DateTime? updatedAt;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Person(id: $id, birthDate: $birthDate, isHidden: $isHidden, name: $name, thumbnailPath: $thumbnailPath, updatedAt: $updatedAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
Person copyWith({
|
||||||
|
String? id,
|
||||||
|
DateTime? birthDate,
|
||||||
|
bool? isHidden,
|
||||||
|
String? name,
|
||||||
|
String? thumbnailPath,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
}) {
|
||||||
|
return Person(
|
||||||
|
id: id ?? this.id,
|
||||||
|
birthDate: birthDate ?? this.birthDate,
|
||||||
|
isHidden: isHidden ?? this.isHidden,
|
||||||
|
name: name ?? this.name,
|
||||||
|
thumbnailPath: thumbnailPath ?? this.thumbnailPath,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'id': id,
|
||||||
|
'birthDate': birthDate?.millisecondsSinceEpoch,
|
||||||
|
'isHidden': isHidden,
|
||||||
|
'name': name,
|
||||||
|
'thumbnailPath': thumbnailPath,
|
||||||
|
'updatedAt': updatedAt?.millisecondsSinceEpoch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory Person.fromMap(Map<String, dynamic> map) {
|
||||||
|
return Person(
|
||||||
|
id: map['id'] as String,
|
||||||
|
birthDate: map['birthDate'] != null
|
||||||
|
? DateTime.fromMillisecondsSinceEpoch(map['birthDate'] as int)
|
||||||
|
: null,
|
||||||
|
isHidden: map['isHidden'] as bool,
|
||||||
|
name: map['name'] as String,
|
||||||
|
thumbnailPath: map['thumbnailPath'] as String,
|
||||||
|
updatedAt: map['updatedAt'] != null
|
||||||
|
? DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] as int)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory Person.fromJson(String source) =>
|
||||||
|
Person.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant Person other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other.id == id &&
|
||||||
|
other.birthDate == birthDate &&
|
||||||
|
other.isHidden == isHidden &&
|
||||||
|
other.name == name &&
|
||||||
|
other.thumbnailPath == thumbnailPath &&
|
||||||
|
other.updatedAt == updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return id.hashCode ^
|
||||||
|
birthDate.hashCode ^
|
||||||
|
isHidden.hashCode ^
|
||||||
|
name.hashCode ^
|
||||||
|
thumbnailPath.hashCode ^
|
||||||
|
updatedAt.hashCode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -255,6 +255,23 @@ class SearchFilter {
|
||||||
required this.mediaType,
|
required this.mediaType,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bool get isEmpty {
|
||||||
|
return (context == null || (context != null && context!.isEmpty)) &&
|
||||||
|
(filename == null || (filename!.isEmpty)) &&
|
||||||
|
people.isEmpty &&
|
||||||
|
location.country == null &&
|
||||||
|
location.state == null &&
|
||||||
|
location.city == null &&
|
||||||
|
camera.make == null &&
|
||||||
|
camera.model == null &&
|
||||||
|
date.takenBefore == null &&
|
||||||
|
date.takenAfter == null &&
|
||||||
|
display.isNotInAlbum == false &&
|
||||||
|
display.isArchive == false &&
|
||||||
|
display.isFavorite == false &&
|
||||||
|
mediaType == AssetType.other;
|
||||||
|
}
|
||||||
|
|
||||||
SearchFilter copyWith({
|
SearchFilter copyWith({
|
||||||
String? context,
|
String? context,
|
||||||
String? filename,
|
String? filename,
|
||||||
|
|
|
@ -49,7 +49,7 @@ class SearchPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final previousFilter = useState(filter.value);
|
final previousFilter = useState<SearchFilter?>(null);
|
||||||
|
|
||||||
final peopleCurrentFilterWidget = useState<Widget?>(null);
|
final peopleCurrentFilterWidget = useState<Widget?>(null);
|
||||||
final dateRangeCurrentFilterWidget = useState<Widget?>(null);
|
final dateRangeCurrentFilterWidget = useState<Widget?>(null);
|
||||||
|
@ -60,19 +60,55 @@ class SearchPage extends HookConsumerWidget {
|
||||||
|
|
||||||
final isSearching = useState(false);
|
final isSearching = useState(false);
|
||||||
|
|
||||||
|
SnackBar searchInfoSnackBar(String message) {
|
||||||
|
return SnackBar(
|
||||||
|
content: Text(
|
||||||
|
message,
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
showCloseIcon: true,
|
||||||
|
behavior: SnackBarBehavior.fixed,
|
||||||
|
closeIconColor: context.colorScheme.onSurface,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
search() async {
|
search() async {
|
||||||
if (prefilter == null && filter.value == previousFilter.value) return;
|
if (filter.value.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefilter == null && filter.value == previousFilter.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
isSearching.value = true;
|
isSearching.value = true;
|
||||||
ref.watch(paginatedSearchProvider.notifier).clear();
|
ref.watch(paginatedSearchProvider.notifier).clear();
|
||||||
await ref.watch(paginatedSearchProvider.notifier).search(filter.value);
|
final hasResult = await ref
|
||||||
|
.watch(paginatedSearchProvider.notifier)
|
||||||
|
.search(filter.value);
|
||||||
|
|
||||||
|
if (!hasResult) {
|
||||||
|
context.showSnackBar(
|
||||||
|
searchInfoSnackBar('search_no_result'.tr()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
previousFilter.value = filter.value;
|
previousFilter.value = filter.value;
|
||||||
isSearching.value = false;
|
isSearching.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMoreSearchResult() async {
|
loadMoreSearchResult() async {
|
||||||
isSearching.value = true;
|
isSearching.value = true;
|
||||||
await ref.watch(paginatedSearchProvider.notifier).search(filter.value);
|
final hasResult = await ref
|
||||||
|
.watch(paginatedSearchProvider.notifier)
|
||||||
|
.search(filter.value);
|
||||||
|
|
||||||
|
if (!hasResult) {
|
||||||
|
context.showSnackBar(
|
||||||
|
searchInfoSnackBar('search_no_more_result'.tr()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
isSearching.value = false;
|
isSearching.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -596,10 +632,15 @@ class SearchPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SearchResultGrid(
|
if (isSearching.value)
|
||||||
onScrollEnd: loadMoreSearchResult,
|
const Expanded(
|
||||||
isSearching: isSearching.value,
|
child: Center(child: CircularProgressIndicator.adaptive()),
|
||||||
),
|
)
|
||||||
|
else
|
||||||
|
SearchResultGrid(
|
||||||
|
onScrollEnd: loadMoreSearchResult,
|
||||||
|
isSearching: isSearching.value,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -19,17 +19,23 @@ class PaginatedSearchNotifier extends StateNotifier<SearchResult> {
|
||||||
PaginatedSearchNotifier(this._searchService)
|
PaginatedSearchNotifier(this._searchService)
|
||||||
: super(SearchResult(assets: [], nextPage: 1));
|
: super(SearchResult(assets: [], nextPage: 1));
|
||||||
|
|
||||||
search(SearchFilter filter) async {
|
Future<bool> search(SearchFilter filter) async {
|
||||||
if (state.nextPage == null) return;
|
if (state.nextPage == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
final result = await _searchService.search(filter, state.nextPage!);
|
final result = await _searchService.search(filter, state.nextPage!);
|
||||||
|
|
||||||
if (result == null) return;
|
if (result == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
state = SearchResult(
|
state = SearchResult(
|
||||||
assets: [...state.assets, ...result.assets],
|
assets: [...state.assets, ...result.assets],
|
||||||
nextPage: result.nextPage,
|
nextPage: result.nextPage,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
|
|
|
@ -101,7 +101,7 @@ class SearchService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response == null) {
|
if (response == null || response.assets.items.isEmpty) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ class PeoplePicker extends HookConsumerWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final formFocus = useFocusNode();
|
final formFocus = useFocusNode();
|
||||||
final imageSize = 75.0;
|
final imageSize = 60.0;
|
||||||
final searchQuery = useState('');
|
final searchQuery = useState('');
|
||||||
final people = ref.watch(getAllPeopleProvider);
|
final people = ref.watch(getAllPeopleProvider);
|
||||||
final headers = ApiService.getRequestHeaders();
|
final headers = ApiService.getRequestHeaders();
|
||||||
|
|
Loading…
Add table
Reference in a new issue