mirror of
https://github.com/immich-app/immich.git
synced 2026-04-28 12:13:09 -07:00
Compare commits
1 Commits
uhthomas/f
...
uhthomas/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
319b468519 |
@@ -16,6 +16,8 @@ class SearchApiRepository extends ApiRepository {
|
||||
type = AssetTypeEnum.VIDEO;
|
||||
}
|
||||
|
||||
final dateRange = filter.date.asDateTimeRange();
|
||||
|
||||
if ((filter.context != null && filter.context!.isNotEmpty) ||
|
||||
(filter.assetId != null && filter.assetId!.isNotEmpty)) {
|
||||
return _api.searchSmart(
|
||||
@@ -28,14 +30,14 @@ class SearchApiRepository extends ApiRepository {
|
||||
city: filter.location.city,
|
||||
make: filter.camera.make,
|
||||
model: filter.camera.model,
|
||||
takenAfter: filter.date.takenAfter,
|
||||
takenBefore: filter.date.takenBefore,
|
||||
takenAfter: dateRange?.start,
|
||||
takenBefore: dateRange?.end,
|
||||
visibility: filter.display.isArchive ? AssetVisibility.archive : AssetVisibility.timeline,
|
||||
rating: filter.rating.rating,
|
||||
isFavorite: filter.display.isFavorite ? true : null,
|
||||
isNotInAlbum: filter.display.isNotInAlbum ? true : null,
|
||||
personIds: filter.people.map((e) => e.id).toList(),
|
||||
tagIds: filter.tagIds,
|
||||
tagIds: filter.tags.map((t) => t.id).toList(),
|
||||
type: type,
|
||||
page: page,
|
||||
size: 100,
|
||||
@@ -53,14 +55,14 @@ class SearchApiRepository extends ApiRepository {
|
||||
city: filter.location.city,
|
||||
make: filter.camera.make,
|
||||
model: filter.camera.model,
|
||||
takenAfter: filter.date.takenAfter,
|
||||
takenBefore: filter.date.takenBefore,
|
||||
takenAfter: dateRange?.start,
|
||||
takenBefore: dateRange?.end,
|
||||
visibility: filter.display.isArchive ? AssetVisibility.archive : AssetVisibility.timeline,
|
||||
rating: filter.rating.rating,
|
||||
isFavorite: filter.display.isFavorite ? true : null,
|
||||
isNotInAlbum: filter.display.isNotInAlbum ? true : null,
|
||||
personIds: filter.people.map((e) => e.id).toList(),
|
||||
tagIds: filter.tagIds,
|
||||
tagIds: filter.tags.map((t) => t.id).toList(),
|
||||
type: type,
|
||||
page: page,
|
||||
size: 1000,
|
||||
|
||||
91
mobile/lib/models/search/date_filter.model.dart
Normal file
91
mobile/lib/models/search/date_filter.model.dart
Normal file
@@ -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 ^
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/models/search/date_filter.model.dart';
|
||||
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||
@@ -111,7 +112,7 @@ class PlaceTile extends StatelessWidget {
|
||||
people: {},
|
||||
location: SearchLocationFilter(city: name),
|
||||
camera: SearchCameraFilter(),
|
||||
date: SearchDateFilter(),
|
||||
date: const EmptyDateFilter(),
|
||||
display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false),
|
||||
rating: SearchRatingFilter(),
|
||||
mediaType: AssetType.other,
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/domain/models/person.model.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/models/search/date_filter.model.dart';
|
||||
import 'package:immich_mobile/providers/search/paginated_search.provider.dart';
|
||||
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
@@ -40,7 +41,7 @@ class SearchPage extends HookConsumerWidget {
|
||||
people: prefilter?.people ?? {},
|
||||
location: prefilter?.location ?? SearchLocationFilter(),
|
||||
camera: prefilter?.camera ?? SearchCameraFilter(),
|
||||
date: prefilter?.date ?? SearchDateFilter(),
|
||||
date: prefilter?.date ?? const EmptyDateFilter(),
|
||||
display: prefilter?.display ?? SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false),
|
||||
mediaType: prefilter?.mediaType ?? AssetType.other,
|
||||
rating: prefilter?.rating ?? SearchRatingFilter(),
|
||||
@@ -242,15 +243,17 @@ class SearchPage extends HookConsumerWidget {
|
||||
final firstDate = DateTime(1900);
|
||||
final lastDate = DateTime.now();
|
||||
|
||||
final stored = filter.value.date.asDateTimeRange();
|
||||
final dateRange = stored != null
|
||||
? DateTimeRange(start: DateUtils.dateOnly(stored.start), end: DateUtils.dateOnly(stored.end))
|
||||
: DateTimeRange(start: lastDate, end: lastDate);
|
||||
|
||||
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,
|
||||
),
|
||||
initialDateRange: dateRange,
|
||||
helpText: 'search_filter_date_title'.tr(),
|
||||
cancelText: 'cancel'.tr(),
|
||||
confirmText: 'select'.tr(),
|
||||
@@ -264,7 +267,7 @@ class SearchPage extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
if (date == null) {
|
||||
filter.value = filter.value.copyWith(date: SearchDateFilter());
|
||||
filter.value = filter.value.copyWith(date: const EmptyDateFilter());
|
||||
|
||||
dateRangeCurrentFilterWidget.value = null;
|
||||
unawaited(search());
|
||||
@@ -272,10 +275,7 @@ class SearchPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
filter.value = filter.value.copyWith(
|
||||
date: SearchDateFilter(
|
||||
takenAfter: date.start,
|
||||
takenBefore: date.end.add(const Duration(hours: 23, minutes: 59, seconds: 59)),
|
||||
),
|
||||
date: CustomDateFilter.fromRange(date),
|
||||
);
|
||||
|
||||
// If date range is less than 24 hours, set the end date to the end of the day
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/models/search/date_filter.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/search/quick_date_picker.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
@@ -57,26 +58,14 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
people: {},
|
||||
location: SearchLocationFilter(),
|
||||
camera: SearchCameraFilter(),
|
||||
date: SearchDateFilter(),
|
||||
date: const EmptyDateFilter(),
|
||||
display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false),
|
||||
rating: SearchRatingFilter(),
|
||||
mediaType: AssetType.other,
|
||||
language: "${context.locale.languageCode}-${context.locale.countryCode}",
|
||||
tagIds: [],
|
||||
),
|
||||
);
|
||||
|
||||
final dateInputFilter = useState<DateFilterInputModel?>(null);
|
||||
|
||||
final peopleCurrentFilterWidget = useState<Widget?>(null);
|
||||
final dateRangeCurrentFilterWidget = useState<Widget?>(null);
|
||||
final cameraCurrentFilterWidget = useState<Widget?>(null);
|
||||
final locationCurrentFilterWidget = useState<Widget?>(null);
|
||||
final tagCurrentFilterWidget = useState<Widget?>(null);
|
||||
final mediaTypeCurrentFilterWidget = useState<Widget?>(null);
|
||||
final ratingCurrentFilterWidget = useState<Widget?>(null);
|
||||
final displayOptionCurrentFilterWidget = useState<Widget?>(null);
|
||||
|
||||
final userPreferences = ref.watch(userMetadataPreferencesProvider);
|
||||
|
||||
search(SearchFilter f) {
|
||||
@@ -107,14 +96,60 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
Future.microtask(() {
|
||||
textSearchController.clear();
|
||||
search(preFilter);
|
||||
if (preFilter.location.city != null) {
|
||||
locationCurrentFilterWidget.value = Text(preFilter.location.city!, style: context.textTheme.labelLarge);
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}, [preFilter]);
|
||||
|
||||
Widget? chipLabel(String text) => text.isEmpty ? null : Text(text, style: context.textTheme.labelLarge);
|
||||
|
||||
Widget? peopleChip() {
|
||||
final label = filter.value.people.map((e) => e.name != '' ? e.name : 'no_name'.t(context: context)).join(', ');
|
||||
return chipLabel(label);
|
||||
}
|
||||
|
||||
Widget? locationChip() {
|
||||
final l = filter.value.location;
|
||||
final parts = [if (l.country != null) l.country!, if (l.state != null) l.state!, if (l.city != null) l.city!];
|
||||
return chipLabel(parts.join(', '));
|
||||
}
|
||||
|
||||
Widget? tagChip() {
|
||||
final label = filter.value.tags.map((t) => t.value).join(', ');
|
||||
return chipLabel(label);
|
||||
}
|
||||
|
||||
Widget? cameraChip() {
|
||||
final c = filter.value.camera;
|
||||
return chipLabel('${c.make ?? ''} ${c.model ?? ''}'.trim());
|
||||
}
|
||||
|
||||
Widget? dateChip() {
|
||||
final d = filter.value.date;
|
||||
return d.isEmpty ? null : chipLabel(d.asHumanReadable(context));
|
||||
}
|
||||
|
||||
Widget? mediaTypeChip() {
|
||||
final mt = filter.value.mediaType;
|
||||
if (mt == AssetType.other) return null;
|
||||
return chipLabel(mt == AssetType.image ? 'image'.t(context: context) : 'video'.t(context: context));
|
||||
}
|
||||
|
||||
Widget? ratingChip() {
|
||||
final r = filter.value.rating.rating;
|
||||
return r == null ? null : chipLabel('rating_count'.t(args: {'count': r}));
|
||||
}
|
||||
|
||||
Widget? displayChip() {
|
||||
final d = filter.value.display;
|
||||
final parts = [
|
||||
if (d.isNotInAlbum) 'search_filter_display_option_not_in_album'.t(context: context),
|
||||
if (d.isArchive) 'archive'.t(context: context),
|
||||
if (d.isFavorite) 'favorite'.t(context: context),
|
||||
];
|
||||
return chipLabel(parts.join(', '));
|
||||
}
|
||||
|
||||
showPeoplePicker() {
|
||||
var people = filter.value.people;
|
||||
|
||||
@@ -122,17 +157,6 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
people = value;
|
||||
}
|
||||
|
||||
handleClear() {
|
||||
peopleCurrentFilterWidget.value = null;
|
||||
search(filter.value.copyWith(people: {}));
|
||||
}
|
||||
|
||||
handleApply() {
|
||||
final label = people.map((e) => e.name != '' ? e.name : 'no_name'.t(context: context)).join(', ');
|
||||
peopleCurrentFilterWidget.value = label.isNotEmpty ? Text(label, style: context.textTheme.labelLarge) : null;
|
||||
search(filter.value.copyWith(people: people));
|
||||
}
|
||||
|
||||
showFilterBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
@@ -141,8 +165,8 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
child: FilterBottomSheetScaffold(
|
||||
title: 'search_filter_people_title'.t(context: context),
|
||||
expanded: true,
|
||||
onSearch: handleApply,
|
||||
onClear: handleClear,
|
||||
onSearch: () => search(filter.value.copyWith(people: people)),
|
||||
onClear: () => search(filter.value.copyWith(people: {})),
|
||||
child: PeoplePicker(onSelect: handleOnSelect, filter: filter.value.people),
|
||||
),
|
||||
),
|
||||
@@ -150,22 +174,10 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
showTagPicker() {
|
||||
var tagIds = filter.value.tagIds ?? [];
|
||||
String tagLabel = '';
|
||||
var tags = filter.value.tags;
|
||||
|
||||
handleOnSelect(Iterable<Tag> tags) {
|
||||
tagIds = tags.map((t) => t.id).toList();
|
||||
tagLabel = tags.map((t) => t.value).join(', ');
|
||||
}
|
||||
|
||||
handleClear() {
|
||||
tagCurrentFilterWidget.value = null;
|
||||
search(filter.value.copyWith(tagIds: []));
|
||||
}
|
||||
|
||||
handleApply() {
|
||||
tagCurrentFilterWidget.value = tagLabel.isNotEmpty ? Text(tagLabel, style: context.textTheme.labelLarge) : null;
|
||||
search(filter.value.copyWith(tagIds: tagIds));
|
||||
handleOnSelect(Iterable<Tag> selected) {
|
||||
tags = selected.toList();
|
||||
}
|
||||
|
||||
showFilterBottomSheet(
|
||||
@@ -176,9 +188,9 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
child: FilterBottomSheetScaffold(
|
||||
title: 'search_filter_tags_title'.t(context: context),
|
||||
expanded: true,
|
||||
onSearch: handleApply,
|
||||
onClear: handleClear,
|
||||
child: TagPicker(onSelect: handleOnSelect, filter: (filter.value.tagIds ?? []).toSet()),
|
||||
onSearch: () => search(filter.value.copyWith(tags: tags)),
|
||||
onClear: () => search(filter.value.copyWith(tags: [])),
|
||||
child: TagPicker(onSelect: handleOnSelect, filter: filter.value.tags.map((t) => t.id).toSet()),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -191,31 +203,14 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
location = SearchLocationFilter(country: value['country'], city: value['city'], state: value['state']);
|
||||
}
|
||||
|
||||
handleClear() {
|
||||
locationCurrentFilterWidget.value = null;
|
||||
search(filter.value.copyWith(location: SearchLocationFilter()));
|
||||
}
|
||||
|
||||
handleApply() {
|
||||
final locationText = [
|
||||
if (location.country != null) location.country!,
|
||||
if (location.state != null) location.state!,
|
||||
if (location.city != null) location.city!,
|
||||
];
|
||||
locationCurrentFilterWidget.value = locationText.isNotEmpty
|
||||
? Text(locationText.join(', '), style: context.textTheme.labelLarge)
|
||||
: null;
|
||||
search(filter.value.copyWith(location: location));
|
||||
}
|
||||
|
||||
showFilterBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
isDismissible: true,
|
||||
child: FilterBottomSheetScaffold(
|
||||
title: 'search_filter_location_title'.t(context: context),
|
||||
onSearch: handleApply,
|
||||
onClear: handleClear,
|
||||
onSearch: () => search(filter.value.copyWith(location: location)),
|
||||
onClear: () => search(filter.value.copyWith(location: SearchLocationFilter())),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: Container(
|
||||
@@ -237,28 +232,14 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
camera = SearchCameraFilter(make: value['make'], model: value['model']);
|
||||
}
|
||||
|
||||
handleClear() {
|
||||
cameraCurrentFilterWidget.value = null;
|
||||
search(filter.value.copyWith(camera: SearchCameraFilter()));
|
||||
}
|
||||
|
||||
handleApply() {
|
||||
final make = camera.make ?? '';
|
||||
final model = camera.model ?? '';
|
||||
cameraCurrentFilterWidget.value = (make.isNotEmpty || model.isNotEmpty)
|
||||
? Text('$make $model', style: context.textTheme.labelLarge)
|
||||
: null;
|
||||
search(filter.value.copyWith(camera: camera));
|
||||
}
|
||||
|
||||
showFilterBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
isDismissible: true,
|
||||
child: FilterBottomSheetScaffold(
|
||||
title: 'search_filter_camera_title'.t(context: context),
|
||||
onSearch: handleApply,
|
||||
onClear: handleClear,
|
||||
onSearch: () => search(filter.value.copyWith(camera: camera)),
|
||||
onClear: () => search(filter.value.copyWith(camera: SearchCameraFilter())),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: CameraPicker(onSelect: handleOnSelect, filter: filter.value.camera),
|
||||
@@ -268,42 +249,17 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
datePicked(DateFilterInputModel? selectedDate) {
|
||||
dateInputFilter.value = selectedDate;
|
||||
if (selectedDate == null) {
|
||||
dateRangeCurrentFilterWidget.value = null;
|
||||
search(filter.value.copyWith(date: SearchDateFilter()));
|
||||
return;
|
||||
}
|
||||
|
||||
final date = selectedDate.asDateTimeRange();
|
||||
dateRangeCurrentFilterWidget.value = Text(
|
||||
selectedDate.asHumanReadable(context),
|
||||
style: context.textTheme.labelLarge,
|
||||
);
|
||||
search(
|
||||
filter.value.copyWith(
|
||||
date: SearchDateFilter(
|
||||
takenAfter: date.start,
|
||||
takenBefore: date.end.add(const Duration(hours: 23, minutes: 59, seconds: 59)),
|
||||
),
|
||||
),
|
||||
);
|
||||
search(filter.value.copyWith(date: selectedDate ?? const EmptyDateFilter()));
|
||||
}
|
||||
|
||||
showDatePicker() async {
|
||||
final firstDate = DateTime(1900);
|
||||
final lastDate = DateTime.now();
|
||||
|
||||
var dateRange = DateTimeRange(
|
||||
start: filter.value.date.takenAfter ?? lastDate,
|
||||
end: filter.value.date.takenBefore ?? lastDate,
|
||||
);
|
||||
|
||||
// datePicked() may increase the date, this will make the date picker fail an assertion
|
||||
// Fixup the end date to be at most now.
|
||||
if (dateRange.end.isAfter(lastDate)) {
|
||||
dateRange = DateTimeRange(start: dateRange.start, end: lastDate);
|
||||
}
|
||||
final stored = filter.value.date.asDateTimeRange();
|
||||
final dateRange = stored != null
|
||||
? DateTimeRange(start: DateUtils.dateOnly(stored.start), end: DateUtils.dateOnly(stored.end))
|
||||
: DateTimeRange(start: lastDate, end: lastDate);
|
||||
|
||||
final date = await showDateRangePicker(
|
||||
context: context,
|
||||
@@ -338,7 +294,7 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
expanded: true,
|
||||
onClear: () => datePicked(null),
|
||||
child: QuickDatePicker(
|
||||
currentInput: dateInputFilter.value,
|
||||
currentInput: filter.value.date,
|
||||
onRequestPicker: () {
|
||||
context.pop();
|
||||
showDatePicker();
|
||||
@@ -360,27 +316,12 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
mediaType = assetType;
|
||||
}
|
||||
|
||||
handleClear() {
|
||||
mediaTypeCurrentFilterWidget.value = null;
|
||||
search(filter.value.copyWith(mediaType: AssetType.other));
|
||||
}
|
||||
|
||||
handleApply() {
|
||||
mediaTypeCurrentFilterWidget.value = mediaType != AssetType.other
|
||||
? Text(
|
||||
mediaType == AssetType.image ? 'image'.t(context: context) : 'video'.t(context: context),
|
||||
style: context.textTheme.labelLarge,
|
||||
)
|
||||
: null;
|
||||
search(filter.value.copyWith(mediaType: mediaType));
|
||||
}
|
||||
|
||||
showFilterBottomSheet(
|
||||
context: context,
|
||||
child: FilterBottomSheetScaffold(
|
||||
title: 'search_filter_media_type_title'.t(context: context),
|
||||
onSearch: handleApply,
|
||||
onClear: handleClear,
|
||||
onSearch: () => search(filter.value.copyWith(mediaType: mediaType)),
|
||||
onClear: () => search(filter.value.copyWith(mediaType: AssetType.other)),
|
||||
child: MediaTypePicker(onSelect: handleOnSelected, filter: filter.value.mediaType),
|
||||
),
|
||||
);
|
||||
@@ -394,25 +335,13 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
rating = value;
|
||||
}
|
||||
|
||||
handleClear() {
|
||||
ratingCurrentFilterWidget.value = null;
|
||||
search(filter.value.copyWith(rating: SearchRatingFilter(rating: null)));
|
||||
}
|
||||
|
||||
handleApply() {
|
||||
ratingCurrentFilterWidget.value = rating.rating != null
|
||||
? Text('rating_count'.t(args: {'count': rating.rating!}), style: context.textTheme.labelLarge)
|
||||
: null;
|
||||
search(filter.value.copyWith(rating: rating));
|
||||
}
|
||||
|
||||
showFilterBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
child: FilterBottomSheetScaffold(
|
||||
title: 'rating'.t(context: context),
|
||||
onSearch: handleApply,
|
||||
onClear: handleClear,
|
||||
onSearch: () => search(filter.value.copyWith(rating: rating)),
|
||||
onClear: () => search(filter.value.copyWith(rating: SearchRatingFilter(rating: null))),
|
||||
child: StarRatingPicker(onSelect: handleOnSelected, filter: filter.value.rating),
|
||||
),
|
||||
);
|
||||
@@ -430,33 +359,16 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
handleClear() {
|
||||
displayOptionCurrentFilterWidget.value = null;
|
||||
search(
|
||||
filter.value.copyWith(
|
||||
display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
handleApply() {
|
||||
final filterText = [
|
||||
if (display.isNotInAlbum) 'search_filter_display_option_not_in_album'.t(context: context),
|
||||
if (display.isArchive) 'archive'.t(context: context),
|
||||
if (display.isFavorite) 'favorite'.t(context: context),
|
||||
];
|
||||
displayOptionCurrentFilterWidget.value = filterText.isNotEmpty
|
||||
? Text(filterText.join(', '), style: context.textTheme.labelLarge)
|
||||
: null;
|
||||
search(filter.value.copyWith(display: display));
|
||||
}
|
||||
|
||||
showFilterBottomSheet(
|
||||
context: context,
|
||||
child: FilterBottomSheetScaffold(
|
||||
title: 'display_options'.t(context: context),
|
||||
onSearch: handleApply,
|
||||
onClear: handleClear,
|
||||
onSearch: () => search(filter.value.copyWith(display: display)),
|
||||
onClear: () => search(
|
||||
filter.value.copyWith(
|
||||
display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false),
|
||||
),
|
||||
),
|
||||
child: DisplayOptionPicker(onSelect: handleOnSelect, filter: filter.value.display),
|
||||
),
|
||||
);
|
||||
@@ -631,52 +543,52 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
icon: Icons.people_alt_outlined,
|
||||
onTap: showPeoplePicker,
|
||||
label: 'people'.t(context: context),
|
||||
currentFilter: peopleCurrentFilterWidget.value,
|
||||
currentFilter: peopleChip(),
|
||||
),
|
||||
SearchFilterChip(
|
||||
icon: Icons.location_on_outlined,
|
||||
onTap: showLocationPicker,
|
||||
label: 'search_filter_location'.t(context: context),
|
||||
currentFilter: locationCurrentFilterWidget.value,
|
||||
currentFilter: locationChip(),
|
||||
),
|
||||
if (userPreferences.valueOrNull?.tagsEnabled ?? false)
|
||||
SearchFilterChip(
|
||||
icon: Icons.sell_outlined,
|
||||
onTap: showTagPicker,
|
||||
label: 'tags'.t(context: context),
|
||||
currentFilter: tagCurrentFilterWidget.value,
|
||||
currentFilter: tagChip(),
|
||||
),
|
||||
SearchFilterChip(
|
||||
icon: Icons.camera_alt_outlined,
|
||||
onTap: showCameraPicker,
|
||||
label: 'camera'.t(context: context),
|
||||
currentFilter: cameraCurrentFilterWidget.value,
|
||||
currentFilter: cameraChip(),
|
||||
),
|
||||
SearchFilterChip(
|
||||
icon: Icons.date_range_outlined,
|
||||
onTap: showQuickDatePicker,
|
||||
label: 'search_filter_date'.t(context: context),
|
||||
currentFilter: dateRangeCurrentFilterWidget.value,
|
||||
currentFilter: dateChip(),
|
||||
),
|
||||
SearchFilterChip(
|
||||
key: const Key('media_type_chip'),
|
||||
icon: Icons.video_collection_outlined,
|
||||
onTap: showMediaTypePicker,
|
||||
label: 'search_filter_media_type'.t(context: context),
|
||||
currentFilter: mediaTypeCurrentFilterWidget.value,
|
||||
currentFilter: mediaTypeChip(),
|
||||
),
|
||||
if (userPreferences.valueOrNull?.ratingsEnabled ?? false)
|
||||
SearchFilterChip(
|
||||
icon: Icons.star_outline_rounded,
|
||||
onTap: showStarRatingPicker,
|
||||
label: 'search_filter_star_rating'.t(context: context),
|
||||
currentFilter: ratingCurrentFilterWidget.value,
|
||||
currentFilter: ratingChip(),
|
||||
),
|
||||
SearchFilterChip(
|
||||
icon: Icons.display_settings_outlined,
|
||||
onTap: showDisplayOptionPicker,
|
||||
label: 'search_filter_display_options'.t(context: context),
|
||||
currentFilter: displayOptionCurrentFilterWidget.value,
|
||||
currentFilter: displayChip(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/models/search/date_filter.model.dart';
|
||||
import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
@@ -34,7 +35,7 @@ class SimilarPhotosActionButton extends ConsumerWidget {
|
||||
people: {},
|
||||
location: SearchLocationFilter(),
|
||||
camera: SearchCameraFilter(),
|
||||
date: SearchDateFilter(),
|
||||
date: const EmptyDateFilter(),
|
||||
display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false),
|
||||
rating: SearchRatingFilter(),
|
||||
mediaType: AssetType.image,
|
||||
|
||||
@@ -2,85 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
|
||||
sealed class DateFilterInputModel {
|
||||
DateTimeRange<DateTime> asDateTimeRange();
|
||||
|
||||
String asHumanReadable(BuildContext context) {
|
||||
// General implementation for arbitrary date and time ranges
|
||||
// If date range is less than 24 hours, set the end date to the end of the day
|
||||
final date = asDateTimeRange();
|
||||
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;
|
||||
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;
|
||||
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 CustomDateFilter extends DateFilterInputModel {
|
||||
final DateTime start;
|
||||
final DateTime end;
|
||||
|
||||
CustomDateFilter(this.start, this.end);
|
||||
|
||||
factory CustomDateFilter.fromRange(DateTimeRange<DateTime> range) {
|
||||
return CustomDateFilter(range.start, range.end);
|
||||
}
|
||||
|
||||
@override
|
||||
DateTimeRange<DateTime> asDateTimeRange() {
|
||||
return DateTimeRange<DateTime>(start: start, end: end);
|
||||
}
|
||||
}
|
||||
import 'package:immich_mobile/models/search/date_filter.model.dart';
|
||||
|
||||
enum _QuickPickerType { last1Month, last3Months, last9Months, year, custom }
|
||||
|
||||
@@ -102,7 +24,7 @@ class QuickDatePicker extends HookWidget {
|
||||
});
|
||||
|
||||
static int _initialYearFromModel(DateFilterInputModel? model) {
|
||||
return model?.asDateTimeRange().start.year ?? DateTime.now().year;
|
||||
return model?.asDateTimeRange()?.start.year ?? DateTime.now().year;
|
||||
}
|
||||
|
||||
static _QuickPickerType? _selectionFromModel(DateFilterInputModel? model) {
|
||||
@@ -149,7 +71,7 @@ class QuickDatePicker extends HookWidget {
|
||||
// Even if it's already toggled it should always open the full date picker, RadioListTiles don't do that by default
|
||||
// so we wrap it in a InkWell
|
||||
Widget _exactPicker(BuildContext context) {
|
||||
final hasPreviousInput = currentInput != null && currentInput is CustomDateFilter;
|
||||
final hasPreviousInput = currentInput is CustomDateFilter;
|
||||
|
||||
return InkWell(
|
||||
onTap: onRequestPicker,
|
||||
@@ -182,9 +104,9 @@ class QuickDatePicker extends HookWidget {
|
||||
if (value == null) return;
|
||||
final _ = switch (value) {
|
||||
_QuickPickerType.custom => onRequestPicker(),
|
||||
_QuickPickerType.last1Month => onSelect(RecentMonthRangeFilter(1)),
|
||||
_QuickPickerType.last3Months => onSelect(RecentMonthRangeFilter(3)),
|
||||
_QuickPickerType.last9Months => onSelect(RecentMonthRangeFilter(9)),
|
||||
_QuickPickerType.last1Month => onSelect(const RecentMonthRangeFilter(1)),
|
||||
_QuickPickerType.last3Months => onSelect(const RecentMonthRangeFilter(3)),
|
||||
_QuickPickerType.last9Months => onSelect(const RecentMonthRangeFilter(9)),
|
||||
// When a year is selected the combobox triggers onSelect() on its own.
|
||||
// Here we handle the radio button being selected which can only ever be the initial year
|
||||
_QuickPickerType.year => onSelect(YearFilter(_initialYear)),
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/models/search/search_curated_content.model.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/models/search/date_filter.model.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
import 'package:immich_mobile/widgets/search/thumbnail_with_info.dart';
|
||||
@@ -53,7 +54,7 @@ class ExploreGrid extends StatelessWidget {
|
||||
people: {},
|
||||
location: SearchLocationFilter(city: content.label),
|
||||
camera: SearchCameraFilter(),
|
||||
date: SearchDateFilter(),
|
||||
date: const EmptyDateFilter(),
|
||||
display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false),
|
||||
rating: SearchRatingFilter(),
|
||||
mediaType: AssetType.other,
|
||||
|
||||
Reference in New Issue
Block a user