mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
chore(mobile): use declarative ui in search
The search page makes use of imperative state, which is buggy and unergonomic - it fights against how flutter wants widgets to be written. Using declarative state simplifies the code and fixes bugs.
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
|
||||
sealed class DateFilterInputModel {
|
||||
const DateFilterInputModel();
|
||||
bool get isEmpty => asDateTimeRange() == null;
|
||||
DateTimeRange<DateTime>? asDateTimeRange();
|
||||
|
||||
String asHumanReadable(BuildContext context) {
|
||||
final date = asDateTimeRange();
|
||||
if (date == null) return '';
|
||||
if (date.end.difference(date.start).inHours < 24) {
|
||||
return DateFormat.yMMMd().format(date.start.toLocal());
|
||||
} else {
|
||||
return 'search_filter_date_interval'.t(
|
||||
context: context,
|
||||
args: {
|
||||
"start": DateFormat.yMMMd().format(date.start.toLocal()),
|
||||
"end": DateFormat.yMMMd().format(date.end.toLocal()),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RecentMonthRangeFilter extends DateFilterInputModel {
|
||||
final int monthDelta;
|
||||
|
||||
const RecentMonthRangeFilter(this.monthDelta);
|
||||
|
||||
@override
|
||||
DateTimeRange<DateTime> asDateTimeRange() {
|
||||
final now = DateTime.now();
|
||||
// Note that DateTime's constructor properly handles month overflow.
|
||||
final from = DateTime(now.year, now.month - monthDelta, 1);
|
||||
return DateTimeRange<DateTime>(start: from, end: now);
|
||||
}
|
||||
|
||||
@override
|
||||
String asHumanReadable(BuildContext context) {
|
||||
return 'last_months'.t(context: context, args: {"count": monthDelta.toString()});
|
||||
}
|
||||
}
|
||||
|
||||
class YearFilter extends DateFilterInputModel {
|
||||
final int year;
|
||||
const YearFilter(this.year);
|
||||
|
||||
@override
|
||||
DateTimeRange<DateTime> asDateTimeRange() {
|
||||
final now = DateTime.now();
|
||||
final from = DateTime(year, 1, 1);
|
||||
|
||||
if (now.year == year) {
|
||||
// To not go beyond today if the user picks the current year
|
||||
return DateTimeRange<DateTime>(start: from, end: now);
|
||||
}
|
||||
|
||||
final to = DateTime(year, 12, 31, 23, 59, 59);
|
||||
return DateTimeRange<DateTime>(start: from, end: to);
|
||||
}
|
||||
|
||||
@override
|
||||
String asHumanReadable(BuildContext context) {
|
||||
return 'in_year'.tr(namedArgs: {"year": year.toString()});
|
||||
}
|
||||
}
|
||||
|
||||
class EmptyDateFilter extends DateFilterInputModel {
|
||||
const EmptyDateFilter();
|
||||
|
||||
@override
|
||||
DateTimeRange<DateTime>? asDateTimeRange() => null;
|
||||
}
|
||||
|
||||
class CustomDateFilter extends DateFilterInputModel {
|
||||
final DateTime start;
|
||||
final DateTime end;
|
||||
|
||||
const CustomDateFilter._(this.start, this.end);
|
||||
|
||||
factory CustomDateFilter.fromRange(DateTimeRange<DateTime> range) {
|
||||
return CustomDateFilter._(range.start, range.end.add(const Duration(hours: 23, minutes: 59, seconds: 59)));
|
||||
}
|
||||
|
||||
@override
|
||||
DateTimeRange<DateTime> asDateTimeRange() {
|
||||
return DateTimeRange<DateTime>(start: start, end: end);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:immich_mobile/domain/models/person.model.dart';
|
||||
import 'package:immich_mobile/domain/models/tag.model.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/models/search/date_filter.model.dart';
|
||||
|
||||
class SearchLocationFilter {
|
||||
String? country;
|
||||
@@ -214,11 +216,11 @@ class SearchFilter {
|
||||
String? ocr;
|
||||
String? language;
|
||||
String? assetId;
|
||||
List<String>? tagIds;
|
||||
List<Tag> tags;
|
||||
Set<PersonDto> people;
|
||||
SearchLocationFilter location;
|
||||
SearchCameraFilter camera;
|
||||
SearchDateFilter date;
|
||||
DateFilterInputModel date;
|
||||
SearchRatingFilter rating;
|
||||
SearchDisplayFilters display;
|
||||
|
||||
@@ -232,7 +234,7 @@ class SearchFilter {
|
||||
this.ocr,
|
||||
this.language,
|
||||
this.assetId,
|
||||
this.tagIds,
|
||||
this.tags = const [],
|
||||
required this.people,
|
||||
required this.location,
|
||||
required this.camera,
|
||||
@@ -248,15 +250,14 @@ class SearchFilter {
|
||||
(description == null || (description!.isEmpty)) &&
|
||||
(assetId == null || (assetId!.isEmpty)) &&
|
||||
(ocr == null || (ocr!.isEmpty)) &&
|
||||
(tagIds ?? []).isEmpty &&
|
||||
tags.isEmpty &&
|
||||
people.isEmpty &&
|
||||
location.country == null &&
|
||||
location.state == null &&
|
||||
location.city == null &&
|
||||
camera.make == null &&
|
||||
camera.model == null &&
|
||||
date.takenBefore == null &&
|
||||
date.takenAfter == null &&
|
||||
date.isEmpty &&
|
||||
display.isNotInAlbum == false &&
|
||||
display.isArchive == false &&
|
||||
display.isFavorite == false &&
|
||||
@@ -272,10 +273,10 @@ class SearchFilter {
|
||||
String? ocr,
|
||||
String? assetId,
|
||||
Set<PersonDto>? people,
|
||||
List<String>? tagIds,
|
||||
List<Tag>? tags,
|
||||
SearchLocationFilter? location,
|
||||
SearchCameraFilter? camera,
|
||||
SearchDateFilter? date,
|
||||
DateFilterInputModel? date,
|
||||
SearchDisplayFilters? display,
|
||||
SearchRatingFilter? rating,
|
||||
AssetType? mediaType,
|
||||
@@ -294,13 +295,13 @@ class SearchFilter {
|
||||
display: display ?? this.display,
|
||||
rating: rating ?? this.rating,
|
||||
mediaType: mediaType ?? this.mediaType,
|
||||
tagIds: tagIds ?? this.tagIds,
|
||||
tags: tags ?? this.tags,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SearchFilter(context: $context, filename: $filename, description: $description, language: $language, ocr: $ocr, people: $people, location: $location, tagIds: $tagIds, camera: $camera, date: $date, display: $display, rating: $rating, mediaType: $mediaType, assetId: $assetId)';
|
||||
return 'SearchFilter(context: $context, filename: $filename, description: $description, language: $language, ocr: $ocr, people: $people, location: $location, tags: $tags, camera: $camera, date: $date, display: $display, rating: $rating, mediaType: $mediaType, assetId: $assetId)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -314,7 +315,7 @@ class SearchFilter {
|
||||
other.ocr == ocr &&
|
||||
other.assetId == assetId &&
|
||||
other.people == people &&
|
||||
other.tagIds == tagIds &&
|
||||
other.tags == tags &&
|
||||
other.location == location &&
|
||||
other.camera == camera &&
|
||||
other.date == date &&
|
||||
@@ -332,7 +333,7 @@ class SearchFilter {
|
||||
ocr.hashCode ^
|
||||
assetId.hashCode ^
|
||||
people.hashCode ^
|
||||
tagIds.hashCode ^
|
||||
tags.hashCode ^
|
||||
location.hashCode ^
|
||||
camera.hashCode ^
|
||||
date.hashCode ^
|
||||
|
||||
Reference in New Issue
Block a user