mirror of
https://github.com/immich-app/immich.git
synced 2025-01-16 00:36:47 +01:00
chore(mobile): search page minor enhancements (#13403)
* chore(mobile): search page retouched * add placeholder photos * remove unused page * focus the search input when tapping on the search controller button * detail fixed * remove print statements * disable scrolling of empty content
This commit is contained in:
parent
1193adf0f5
commit
f59b813ffe
14 changed files with 821 additions and 847 deletions
BIN
mobile/assets/polaroid-dark.png
Normal file
BIN
mobile/assets/polaroid-dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 314 KiB |
BIN
mobile/assets/polaroid-light.png
Normal file
BIN
mobile/assets/polaroid-light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 312 KiB |
|
@ -266,8 +266,8 @@ class SearchFilter {
|
||||||
AssetType? mediaType,
|
AssetType? mediaType,
|
||||||
}) {
|
}) {
|
||||||
return SearchFilter(
|
return SearchFilter(
|
||||||
context: context ?? this.context,
|
context: context,
|
||||||
filename: filename ?? this.filename,
|
filename: filename,
|
||||||
people: people ?? this.people,
|
people: people ?? this.people,
|
||||||
location: location ?? this.location,
|
location: location ?? this.location,
|
||||||
camera: camera ?? this.camera,
|
camera: camera ?? this.camera,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
|
||||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
|
@ -44,21 +45,28 @@ class TabControllerPage extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onNavigationSelected(TabsRouter router, int index) {
|
||||||
|
// On Photos page menu tapped
|
||||||
|
if (router.activeIndex == 0 && index == 0) {
|
||||||
|
scrollToTopNotifierProvider.scrollToTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Search page tapped
|
||||||
|
if (router.activeIndex == 1 && index == 1) {
|
||||||
|
ref.read(searchInputFocusProvider).requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||||
|
router.setActiveIndex(index);
|
||||||
|
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
||||||
|
}
|
||||||
|
|
||||||
navigationRail(TabsRouter tabsRouter) {
|
navigationRail(TabsRouter tabsRouter) {
|
||||||
return NavigationRail(
|
return NavigationRail(
|
||||||
labelType: NavigationRailLabelType.all,
|
labelType: NavigationRailLabelType.all,
|
||||||
selectedIndex: tabsRouter.activeIndex,
|
selectedIndex: tabsRouter.activeIndex,
|
||||||
onDestinationSelected: (index) {
|
onDestinationSelected: (index) =>
|
||||||
// Selected Photos while it is active
|
onNavigationSelected(tabsRouter, index),
|
||||||
if (tabsRouter.activeIndex == 0 && index == 0) {
|
|
||||||
// Scroll to top
|
|
||||||
scrollToTopNotifierProvider.scrollToTop();
|
|
||||||
}
|
|
||||||
|
|
||||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
|
||||||
tabsRouter.setActiveIndex(index);
|
|
||||||
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
|
||||||
},
|
|
||||||
selectedIconTheme: IconThemeData(
|
selectedIconTheme: IconThemeData(
|
||||||
color: context.primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
|
@ -103,16 +111,8 @@ class TabControllerPage extends HookConsumerWidget {
|
||||||
bottomNavigationBar(TabsRouter tabsRouter) {
|
bottomNavigationBar(TabsRouter tabsRouter) {
|
||||||
return NavigationBar(
|
return NavigationBar(
|
||||||
selectedIndex: tabsRouter.activeIndex,
|
selectedIndex: tabsRouter.activeIndex,
|
||||||
onDestinationSelected: (index) {
|
onDestinationSelected: (index) =>
|
||||||
if (tabsRouter.activeIndex == 0 && index == 0) {
|
onNavigationSelected(tabsRouter, index),
|
||||||
// Scroll to top
|
|
||||||
scrollToTopNotifierProvider.scrollToTop();
|
|
||||||
}
|
|
||||||
|
|
||||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
|
||||||
tabsRouter.setActiveIndex(index);
|
|
||||||
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
|
||||||
},
|
|
||||||
destinations: [
|
destinations: [
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'tab_controller_nav_photos'.tr(),
|
label: 'tab_controller_nav_photos'.tr(),
|
||||||
|
@ -171,7 +171,7 @@ class TabControllerPage extends HookConsumerWidget {
|
||||||
return AutoTabsRouter(
|
return AutoTabsRouter(
|
||||||
routes: [
|
routes: [
|
||||||
const PhotosRoute(),
|
const PhotosRoute(),
|
||||||
SearchInputRoute(),
|
SearchRoute(),
|
||||||
const AlbumsRoute(),
|
const AlbumsRoute(),
|
||||||
const LibraryRoute(),
|
const LibraryRoute(),
|
||||||
],
|
],
|
||||||
|
|
|
@ -108,6 +108,7 @@ class QuickAccessButtons extends ConsumerWidget {
|
||||||
colors: [
|
colors: [
|
||||||
context.colorScheme.primary.withAlpha(10),
|
context.colorScheme.primary.withAlpha(10),
|
||||||
context.colorScheme.primary.withAlpha(15),
|
context.colorScheme.primary.withAlpha(15),
|
||||||
|
context.colorScheme.primary.withAlpha(20),
|
||||||
],
|
],
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
|
|
|
@ -81,7 +81,7 @@ class PlaceTile extends StatelessWidget {
|
||||||
|
|
||||||
void navigateToPlace() {
|
void navigateToPlace() {
|
||||||
context.pushRoute(
|
context.pushRoute(
|
||||||
SearchInputRoute(
|
SearchRoute(
|
||||||
prefilter: SearchFilter(
|
prefilter: SearchFilter(
|
||||||
people: {},
|
people: {},
|
||||||
location: SearchLocationFilter(
|
location: SearchLocationFilter(
|
|
@ -1,141 +1,768 @@
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'dart:async';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
@RoutePage()
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
// ignore: must_be_immutable
|
import 'package:immich_mobile/interfaces/person_api.interface.dart';
|
||||||
class SearchPage extends HookConsumerWidget {
|
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||||
const SearchPage({super.key});
|
import 'package:immich_mobile/providers/search/paginated_search.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||||
@override
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
|
||||||
TextStyle categoryTitleStyle = const TextStyle(
|
import 'package:immich_mobile/widgets/search/search_filter/camera_picker.dart';
|
||||||
fontWeight: FontWeight.w500,
|
import 'package:immich_mobile/widgets/search/search_filter/display_option_picker.dart';
|
||||||
fontSize: 15.0,
|
import 'package:immich_mobile/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart';
|
||||||
);
|
import 'package:immich_mobile/widgets/search/search_filter/location_picker.dart';
|
||||||
|
import 'package:immich_mobile/widgets/search/search_filter/media_type_picker.dart';
|
||||||
Color categoryIconColor = context.colorScheme.onSurface;
|
import 'package:immich_mobile/widgets/search/search_filter/people_picker.dart';
|
||||||
|
import 'package:immich_mobile/widgets/search/search_filter/search_filter_chip.dart';
|
||||||
buildSearchButton() {
|
import 'package:immich_mobile/widgets/search/search_filter/search_filter_utils.dart';
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {
|
@RoutePage()
|
||||||
context.pushRoute(SearchInputRoute());
|
class SearchPage extends HookConsumerWidget {
|
||||||
},
|
const SearchPage({super.key, this.prefilter});
|
||||||
child: Card(
|
|
||||||
elevation: 0,
|
final SearchFilter? prefilter;
|
||||||
color: context.colorScheme.surfaceContainerHigh,
|
|
||||||
shape: RoundedRectangleBorder(
|
@override
|
||||||
borderRadius: BorderRadius.circular(50),
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
),
|
final isContextualSearch = useState(true);
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
final textSearchController = useTextEditingController();
|
||||||
child: Padding(
|
final filter = useState<SearchFilter>(
|
||||||
padding: const EdgeInsets.symmetric(
|
SearchFilter(
|
||||||
horizontal: 16.0,
|
people: prefilter?.people ?? {},
|
||||||
vertical: 12.0,
|
location: prefilter?.location ?? SearchLocationFilter(),
|
||||||
),
|
camera: prefilter?.camera ?? SearchCameraFilter(),
|
||||||
child: Row(
|
date: prefilter?.date ?? SearchDateFilter(),
|
||||||
children: [
|
display: prefilter?.display ??
|
||||||
Icon(
|
SearchDisplayFilters(
|
||||||
Icons.search,
|
isNotInAlbum: false,
|
||||||
color: context.colorScheme.onSurfaceSecondary,
|
isArchive: false,
|
||||||
),
|
isFavorite: false,
|
||||||
const SizedBox(width: 16.0),
|
),
|
||||||
Text(
|
mediaType: prefilter?.mediaType ?? AssetType.other,
|
||||||
"search_bar_hint",
|
),
|
||||||
style: context.textTheme.bodyLarge?.copyWith(
|
);
|
||||||
color: context.colorScheme.onSurfaceSecondary,
|
|
||||||
fontWeight: FontWeight.w400,
|
final previousFilter = useState(filter.value);
|
||||||
),
|
|
||||||
).tr(),
|
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);
|
||||||
return Scaffold(
|
final searchProvider = ref.watch(paginatedSearchProvider);
|
||||||
appBar: const ImmichAppBar(),
|
final searchResultCount = useState(0);
|
||||||
body: ListView(
|
|
||||||
children: [
|
search() async {
|
||||||
buildSearchButton(),
|
if (prefilter == null && filter.value == previousFilter.value) return;
|
||||||
const SizedBox(height: 24.0),
|
|
||||||
Padding(
|
ref.watch(paginatedSearchProvider.notifier).clear();
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: Text(
|
currentPage.value = 1;
|
||||||
'search_page_categories',
|
|
||||||
style: context.textTheme.bodyLarge?.copyWith(
|
final searchResult = await ref
|
||||||
fontWeight: FontWeight.w500,
|
.watch(paginatedSearchProvider.notifier)
|
||||||
),
|
.getNextPage(filter.value, currentPage.value);
|
||||||
).tr(),
|
|
||||||
),
|
previousFilter.value = filter.value;
|
||||||
const SizedBox(height: 12.0),
|
searchResultCount.value = searchResult.length;
|
||||||
ListTile(
|
}
|
||||||
leading: Icon(
|
|
||||||
Icons.favorite_border_rounded,
|
searchPrefilter() {
|
||||||
color: categoryIconColor,
|
if (prefilter != null) {
|
||||||
),
|
Future.delayed(
|
||||||
title:
|
Duration.zero,
|
||||||
Text('search_page_favorites', style: categoryTitleStyle).tr(),
|
() {
|
||||||
onTap: () => context.pushRoute(const FavoritesRoute()),
|
search();
|
||||||
),
|
|
||||||
const CategoryDivider(),
|
if (prefilter!.location.city != null) {
|
||||||
ListTile(
|
locationCurrentFilterWidget.value = Text(
|
||||||
leading: Icon(
|
prefilter!.location.city!,
|
||||||
Icons.schedule_outlined,
|
style: context.textTheme.labelLarge,
|
||||||
color: categoryIconColor,
|
);
|
||||||
),
|
}
|
||||||
title: Text(
|
},
|
||||||
'search_page_recently_added',
|
);
|
||||||
style: categoryTitleStyle,
|
}
|
||||||
).tr(),
|
}
|
||||||
onTap: () => context.pushRoute(const RecentlyAddedRoute()),
|
|
||||||
),
|
useEffect(
|
||||||
const CategoryDivider(),
|
() {
|
||||||
ListTile(
|
searchPrefilter();
|
||||||
title: Text('search_page_videos', style: categoryTitleStyle).tr(),
|
return null;
|
||||||
leading: Icon(
|
},
|
||||||
Icons.play_circle_outline,
|
[],
|
||||||
color: categoryIconColor,
|
);
|
||||||
),
|
|
||||||
onTap: () => context.pushRoute(const AllVideosRoute()),
|
loadMoreSearchResult() async {
|
||||||
),
|
currentPage.value += 1;
|
||||||
const CategoryDivider(),
|
final searchResult = await ref
|
||||||
ListTile(
|
.watch(paginatedSearchProvider.notifier)
|
||||||
title: Text(
|
.getNextPage(filter.value, currentPage.value);
|
||||||
'search_page_motion_photos',
|
searchResultCount.value = searchResult.length;
|
||||||
style: categoryTitleStyle,
|
}
|
||||||
).tr(),
|
|
||||||
leading: Icon(
|
showPeoplePicker() {
|
||||||
Icons.motion_photos_on_outlined,
|
handleOnSelect(Set<Person> value) {
|
||||||
color: categoryIconColor,
|
filter.value = filter.value.copyWith(
|
||||||
),
|
people: value,
|
||||||
onTap: () => context.pushRoute(const AllMotionPhotosRoute()),
|
);
|
||||||
),
|
|
||||||
],
|
peopleCurrentFilterWidget.value = Text(
|
||||||
),
|
value.map((e) => e.name != '' ? e.name : 'no_name'.tr()).join(', '),
|
||||||
);
|
style: context.textTheme.labelLarge,
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class CategoryDivider extends StatelessWidget {
|
handleClear() {
|
||||||
const CategoryDivider({super.key});
|
filter.value = filter.value.copyWith(
|
||||||
|
people: {},
|
||||||
@override
|
);
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return const Padding(
|
peopleCurrentFilterWidget.value = null;
|
||||||
padding: EdgeInsets.only(
|
search();
|
||||||
left: 56,
|
}
|
||||||
right: 16,
|
|
||||||
),
|
showFilterBottomSheet(
|
||||||
child: Divider(
|
context: context,
|
||||||
height: 0,
|
isScrollControlled: true,
|
||||||
),
|
child: FractionallySizedBox(
|
||||||
);
|
heightFactor: 0.8,
|
||||||
}
|
child: FilterBottomSheetScaffold(
|
||||||
}
|
title: 'search_filter_people_title'.tr(),
|
||||||
|
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: 'search_filter_location_title'.tr(),
|
||||||
|
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: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
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: 'search_filter_camera_title'.tr(),
|
||||||
|
onSearch: search,
|
||||||
|
onClear: handleClear,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(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: 'search_filter_date_title'.tr(),
|
||||||
|
cancelText: 'action_common_cancel'.tr(),
|
||||||
|
confirmText: 'action_common_select'.tr(),
|
||||||
|
saveText: 'action_common_save'.tr(),
|
||||||
|
errorFormatText: 'invalid_date_format'.tr(),
|
||||||
|
errorInvalidText: 'invalid_date'.tr(),
|
||||||
|
fieldStartHintText: 'start_date'.tr(),
|
||||||
|
fieldEndHintText: 'end_date'.tr(),
|
||||||
|
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(
|
||||||
|
DateFormat.yMMMd().format(date.start.toLocal()),
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
dateRangeCurrentFilterWidget.value = Text(
|
||||||
|
'search_filter_date_interval'.tr(
|
||||||
|
namedArgs: {
|
||||||
|
"start": DateFormat.yMMMd().format(date.start.toLocal()),
|
||||||
|
"end": DateFormat.yMMMd().format(date.end.toLocal()),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
|
||||||
|
// MEDIA PICKER
|
||||||
|
showMediaTypePicker() {
|
||||||
|
handleOnSelected(AssetType assetType) {
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
mediaType: assetType,
|
||||||
|
);
|
||||||
|
|
||||||
|
mediaTypeCurrentFilterWidget.value = Text(
|
||||||
|
assetType == AssetType.image
|
||||||
|
? 'search_filter_media_type_image'.tr()
|
||||||
|
: assetType == AssetType.video
|
||||||
|
? 'search_filter_media_type_video'.tr()
|
||||||
|
: 'search_filter_media_type_all'.tr(),
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClear() {
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
mediaType: AssetType.other,
|
||||||
|
);
|
||||||
|
|
||||||
|
mediaTypeCurrentFilterWidget.value = null;
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
|
||||||
|
showFilterBottomSheet(
|
||||||
|
context: context,
|
||||||
|
child: FilterBottomSheetScaffold(
|
||||||
|
title: 'search_filter_media_type_title'.tr(),
|
||||||
|
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('search_filter_display_option_not_in_album'.tr());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DisplayOption.archive:
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
display: filter.value.display.copyWith(
|
||||||
|
isArchive: value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (value) {
|
||||||
|
filterText.add('search_filter_display_option_archive'.tr());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DisplayOption.favorite:
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
display: filter.value.display.copyWith(
|
||||||
|
isFavorite: value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (value) {
|
||||||
|
filterText.add('search_filter_display_option_favorite'.tr());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filterText.isEmpty) {
|
||||||
|
displayOptionCurrentFilterWidget.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: 'search_filter_display_options_title'.tr(),
|
||||||
|
onSearch: search,
|
||||||
|
onClear: handleClear,
|
||||||
|
child: DisplayOptionPicker(
|
||||||
|
onSelect: handleOnSelect,
|
||||||
|
filter: filter.value.display,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTextSubmitted(String value) {
|
||||||
|
if (value.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isContextualSearch.value) {
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
filename: null,
|
||||||
|
context: value,
|
||||||
|
);
|
||||||
|
} 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: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: SearchEmptyContent(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AsyncError(:final error) => Text('Error: $error'),
|
||||||
|
_ => const Expanded(child: Center(child: CircularProgressIndicator())),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
resizeToAvoidBottomInset: true,
|
||||||
|
appBar: AppBar(
|
||||||
|
automaticallyImplyLeading: true,
|
||||||
|
actions: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 14.0),
|
||||||
|
child: IconButton(
|
||||||
|
icon: isContextualSearch.value
|
||||||
|
? const Icon(Icons.abc_rounded)
|
||||||
|
: const Icon(Icons.image_search_rounded),
|
||||||
|
onPressed: () {
|
||||||
|
isContextualSearch.value = !isContextualSearch.value;
|
||||||
|
textSearchController.clear();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
title: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(0),
|
||||||
|
width: 0,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
context.colorScheme.primary.withOpacity(0.075),
|
||||||
|
context.colorScheme.primary.withOpacity(0.09),
|
||||||
|
context.colorScheme.primary.withOpacity(0.075),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
controller: textSearchController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: prefilter != null
|
||||||
|
? EdgeInsets.only(left: 24)
|
||||||
|
: EdgeInsets.all(8),
|
||||||
|
prefixIcon: prefilter != null
|
||||||
|
? null
|
||||||
|
: Icon(
|
||||||
|
Icons.search_rounded,
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
),
|
||||||
|
hintText: isContextualSearch.value
|
||||||
|
? 'contextual_search'.tr()
|
||||||
|
: 'filename_search'.tr(),
|
||||||
|
hintStyle: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: context.themeData.colorScheme.onSurfaceSecondary,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.colorScheme.surfaceDim,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
disabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.colorScheme.surfaceDim,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.colorScheme.primary.withAlpha(100),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onSubmitted: handleTextSubmitted,
|
||||||
|
focusNode: ref.watch(searchInputFocusProvider),
|
||||||
|
onTapOutside: (_) => ref.read(searchInputFocusProvider).unfocus(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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: 'search_filter_people'.tr(),
|
||||||
|
currentFilter: peopleCurrentFilterWidget.value,
|
||||||
|
),
|
||||||
|
SearchFilterChip(
|
||||||
|
icon: Icons.location_pin,
|
||||||
|
onTap: showLocationPicker,
|
||||||
|
label: 'search_filter_location'.tr(),
|
||||||
|
currentFilter: locationCurrentFilterWidget.value,
|
||||||
|
),
|
||||||
|
SearchFilterChip(
|
||||||
|
icon: Icons.camera_alt_rounded,
|
||||||
|
onTap: showCameraPicker,
|
||||||
|
label: 'search_filter_camera'.tr(),
|
||||||
|
currentFilter: cameraCurrentFilterWidget.value,
|
||||||
|
),
|
||||||
|
SearchFilterChip(
|
||||||
|
icon: Icons.date_range_rounded,
|
||||||
|
onTap: showDatePicker,
|
||||||
|
label: 'search_filter_date'.tr(),
|
||||||
|
currentFilter: dateRangeCurrentFilterWidget.value,
|
||||||
|
),
|
||||||
|
SearchFilterChip(
|
||||||
|
icon: Icons.video_collection_outlined,
|
||||||
|
onTap: showMediaTypePicker,
|
||||||
|
label: 'search_filter_media_type'.tr(),
|
||||||
|
currentFilter: mediaTypeCurrentFilterWidget.value,
|
||||||
|
),
|
||||||
|
SearchFilterChip(
|
||||||
|
icon: Icons.display_settings_outlined,
|
||||||
|
onTap: showDisplayOptionPicker,
|
||||||
|
label: 'search_filter_display_options'.tr(),
|
||||||
|
currentFilter: displayOptionCurrentFilterWidget.value,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
buildSearchResult(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchEmptyContent extends StatelessWidget {
|
||||||
|
const SearchEmptyContent({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: NeverScrollableScrollPhysics(),
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 40),
|
||||||
|
Center(
|
||||||
|
child: Image.asset(
|
||||||
|
context.isDarkTheme
|
||||||
|
? 'assets/polaroid-dark.png'
|
||||||
|
: 'assets/polaroid-light.png',
|
||||||
|
height: 125,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
"Search for your photos and videos",
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 32),
|
||||||
|
QuickLinkList(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class QuickLinkList extends StatelessWidget {
|
||||||
|
const QuickLinkList({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(
|
||||||
|
color: context.colorScheme.outline.withAlpha(10),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
context.colorScheme.primary.withAlpha(10),
|
||||||
|
context.colorScheme.primary.withAlpha(15),
|
||||||
|
context.colorScheme.primary.withAlpha(20),
|
||||||
|
],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
children: [
|
||||||
|
QuickLink(
|
||||||
|
title: 'recently_added'.tr(),
|
||||||
|
icon: Icons.schedule_outlined,
|
||||||
|
isTop: true,
|
||||||
|
onTap: () => context.pushRoute(const RecentlyAddedRoute()),
|
||||||
|
),
|
||||||
|
QuickLink(
|
||||||
|
title: 'videos'.tr(),
|
||||||
|
icon: Icons.play_circle_outline_rounded,
|
||||||
|
onTap: () => context.pushRoute(AllVideosRoute()),
|
||||||
|
),
|
||||||
|
QuickLink(
|
||||||
|
title: 'favorites'.tr(),
|
||||||
|
icon: Icons.favorite_border_rounded,
|
||||||
|
isBottom: true,
|
||||||
|
onTap: () => context.pushRoute(FavoritesRoute()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class QuickLink extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final IconData icon;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final bool isTop;
|
||||||
|
final bool isBottom;
|
||||||
|
|
||||||
|
const QuickLink({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.icon,
|
||||||
|
required this.onTap,
|
||||||
|
this.isTop = false,
|
||||||
|
this.isBottom = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final borderRadius = BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(isTop ? 20 : 0),
|
||||||
|
topRight: Radius.circular(isTop ? 20 : 0),
|
||||||
|
bottomLeft: Radius.circular(isBottom ? 20 : 0),
|
||||||
|
bottomRight: Radius.circular(isBottom ? 20 : 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
),
|
||||||
|
leading: Icon(
|
||||||
|
icon,
|
||||||
|
size: 26,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
title,
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: onTap,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,631 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
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/entities/asset.entity.dart';
|
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
|
||||||
import 'package:immich_mobile/interfaces/person_api.interface.dart';
|
|
||||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
|
||||||
import 'package:immich_mobile/providers/search/paginated_search.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
|
|
||||||
import 'package:immich_mobile/widgets/search/search_filter/camera_picker.dart';
|
|
||||||
import 'package:immich_mobile/widgets/search/search_filter/display_option_picker.dart';
|
|
||||||
import 'package:immich_mobile/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart';
|
|
||||||
import 'package:immich_mobile/widgets/search/search_filter/location_picker.dart';
|
|
||||||
import 'package:immich_mobile/widgets/search/search_filter/media_type_picker.dart';
|
|
||||||
import 'package:immich_mobile/widgets/search/search_filter/people_picker.dart';
|
|
||||||
import 'package:immich_mobile/widgets/search/search_filter/search_filter_chip.dart';
|
|
||||||
import 'package:immich_mobile/widgets/search/search_filter/search_filter_utils.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 focusNode = useFocusNode();
|
|
||||||
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<Person> value) {
|
|
||||||
filter.value = filter.value.copyWith(
|
|
||||||
people: value,
|
|
||||||
);
|
|
||||||
|
|
||||||
peopleCurrentFilterWidget.value = Text(
|
|
||||||
value.map((e) => e.name != '' ? e.name : 'no_name'.tr()).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: 'search_filter_people_title'.tr(),
|
|
||||||
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: 'search_filter_location_title'.tr(),
|
|
||||||
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: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
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: 'search_filter_camera_title'.tr(),
|
|
||||||
onSearch: search,
|
|
||||||
onClear: handleClear,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(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: 'search_filter_date_title'.tr(),
|
|
||||||
cancelText: 'action_common_cancel'.tr(),
|
|
||||||
confirmText: 'action_common_select'.tr(),
|
|
||||||
saveText: 'action_common_save'.tr(),
|
|
||||||
errorFormatText: 'invalid_date_format'.tr(),
|
|
||||||
errorInvalidText: 'invalid_date'.tr(),
|
|
||||||
fieldStartHintText: 'start_date'.tr(),
|
|
||||||
fieldEndHintText: 'end_date'.tr(),
|
|
||||||
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(
|
|
||||||
DateFormat.yMMMd().format(date.start.toLocal()),
|
|
||||||
style: context.textTheme.labelLarge,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
dateRangeCurrentFilterWidget.value = Text(
|
|
||||||
'search_filter_date_interval'.tr(
|
|
||||||
namedArgs: {
|
|
||||||
"start": DateFormat.yMMMd().format(date.start.toLocal()),
|
|
||||||
"end": DateFormat.yMMMd().format(date.end.toLocal()),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
style: context.textTheme.labelLarge,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
search();
|
|
||||||
}
|
|
||||||
|
|
||||||
// MEDIA PICKER
|
|
||||||
showMediaTypePicker() {
|
|
||||||
handleOnSelected(AssetType assetType) {
|
|
||||||
filter.value = filter.value.copyWith(
|
|
||||||
mediaType: assetType,
|
|
||||||
);
|
|
||||||
|
|
||||||
mediaTypeCurrentFilterWidget.value = Text(
|
|
||||||
assetType == AssetType.image
|
|
||||||
? 'search_filter_media_type_image'.tr()
|
|
||||||
: assetType == AssetType.video
|
|
||||||
? 'search_filter_media_type_video'.tr()
|
|
||||||
: 'search_filter_media_type_all'.tr(),
|
|
||||||
style: context.textTheme.labelLarge,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClear() {
|
|
||||||
filter.value = filter.value.copyWith(
|
|
||||||
mediaType: AssetType.other,
|
|
||||||
);
|
|
||||||
|
|
||||||
mediaTypeCurrentFilterWidget.value = null;
|
|
||||||
search();
|
|
||||||
}
|
|
||||||
|
|
||||||
showFilterBottomSheet(
|
|
||||||
context: context,
|
|
||||||
child: FilterBottomSheetScaffold(
|
|
||||||
title: 'search_filter_media_type_title'.tr(),
|
|
||||||
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('search_filter_display_option_not_in_album'.tr());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case DisplayOption.archive:
|
|
||||||
filter.value = filter.value.copyWith(
|
|
||||||
display: filter.value.display.copyWith(
|
|
||||||
isArchive: value,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (value) {
|
|
||||||
filterText.add('search_filter_display_option_archive'.tr());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case DisplayOption.favorite:
|
|
||||||
filter.value = filter.value.copyWith(
|
|
||||||
display: filter.value.display.copyWith(
|
|
||||||
isFavorite: value,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (value) {
|
|
||||||
filterText.add('search_filter_display_option_favorite'.tr());
|
|
||||||
}
|
|
||||||
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: 'search_filter_display_options_title'.tr(),
|
|
||||||
onSearch: search,
|
|
||||||
onClear: handleClear,
|
|
||||||
child: DisplayOptionPicker(
|
|
||||||
onSelect: handleOnSelect,
|
|
||||||
filter: filter.value.display,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTextSubmitted(String value) {
|
|
||||||
if (value.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 14.0),
|
|
||||||
child: IconButton(
|
|
||||||
icon: isContextualSearch.value
|
|
||||||
? const Icon(Icons.abc_rounded)
|
|
||||||
: const Icon(Icons.image_search_rounded),
|
|
||||||
onPressed: () {
|
|
||||||
isContextualSearch.value = !isContextualSearch.value;
|
|
||||||
textSearchController.clear();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
title: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: context.colorScheme.onSurface.withAlpha(0),
|
|
||||||
width: 0,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [
|
|
||||||
context.colorScheme.primary.withOpacity(0.075),
|
|
||||||
context.colorScheme.primary.withOpacity(0.09),
|
|
||||||
context.colorScheme.primary.withOpacity(0.075),
|
|
||||||
],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: TextField(
|
|
||||||
controller: textSearchController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
contentPadding: EdgeInsets.all(8),
|
|
||||||
prefixIcon: prefilter != null
|
|
||||||
? null
|
|
||||||
: Icon(
|
|
||||||
Icons.search_rounded,
|
|
||||||
color: context.colorScheme.primary,
|
|
||||||
),
|
|
||||||
hintText: isContextualSearch.value
|
|
||||||
? 'contextual_search'.tr()
|
|
||||||
: 'filename_search'.tr(),
|
|
||||||
hintStyle: context.textTheme.bodyLarge?.copyWith(
|
|
||||||
color: context.themeData.colorScheme.onSurfaceSecondary,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(25),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: context.colorScheme.surfaceDim,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(25),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: context.colorScheme.surfaceContainer,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
disabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(25),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: context.colorScheme.surfaceDim,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(25),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: context.colorScheme.primary.withAlpha(100),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onSubmitted: handleTextSubmitted,
|
|
||||||
focusNode: focusNode,
|
|
||||||
onTapOutside: (_) => focusNode.unfocus(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
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: 'search_filter_people'.tr(),
|
|
||||||
currentFilter: peopleCurrentFilterWidget.value,
|
|
||||||
),
|
|
||||||
SearchFilterChip(
|
|
||||||
icon: Icons.location_pin,
|
|
||||||
onTap: showLocationPicker,
|
|
||||||
label: 'search_filter_location'.tr(),
|
|
||||||
currentFilter: locationCurrentFilterWidget.value,
|
|
||||||
),
|
|
||||||
SearchFilterChip(
|
|
||||||
icon: Icons.camera_alt_rounded,
|
|
||||||
onTap: showCameraPicker,
|
|
||||||
label: 'search_filter_camera'.tr(),
|
|
||||||
currentFilter: cameraCurrentFilterWidget.value,
|
|
||||||
),
|
|
||||||
SearchFilterChip(
|
|
||||||
icon: Icons.date_range_rounded,
|
|
||||||
onTap: showDatePicker,
|
|
||||||
label: 'search_filter_date'.tr(),
|
|
||||||
currentFilter: dateRangeCurrentFilterWidget.value,
|
|
||||||
),
|
|
||||||
SearchFilterChip(
|
|
||||||
icon: Icons.video_collection_outlined,
|
|
||||||
onTap: showMediaTypePicker,
|
|
||||||
label: 'search_filter_media_type'.tr(),
|
|
||||||
currentFilter: mediaTypeCurrentFilterWidget.value,
|
|
||||||
),
|
|
||||||
SearchFilterChip(
|
|
||||||
icon: Icons.display_settings_outlined,
|
|
||||||
onTap: showDisplayOptionPicker,
|
|
||||||
label: 'search_filter_display_options'.tr(),
|
|
||||||
currentFilter: displayOptionCurrentFilterWidget.value,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
buildSearchResult(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
final searchInputFocusProvider = Provider((ref) {
|
||||||
|
return FocusNode();
|
||||||
|
});
|
|
@ -16,7 +16,7 @@ import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
|
||||||
import 'package:immich_mobile/pages/albums/albums.page.dart';
|
import 'package:immich_mobile/pages/albums/albums.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/local_albums.page.dart';
|
import 'package:immich_mobile/pages/library/local_albums.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/people/people_collection.page.dart';
|
import 'package:immich_mobile/pages/library/people/people_collection.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/places/places_collection.part.dart';
|
import 'package:immich_mobile/pages/library/places/places_collection.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/library.page.dart';
|
import 'package:immich_mobile/pages/library/library.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/activities.page.dart';
|
import 'package:immich_mobile/pages/common/activities.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/album_additional_shared_user_selection.page.dart';
|
import 'package:immich_mobile/pages/common/album_additional_shared_user_selection.page.dart';
|
||||||
|
@ -52,7 +52,6 @@ import 'package:immich_mobile/pages/search/map/map_location_picker.page.dart';
|
||||||
import 'package:immich_mobile/pages/search/person_result.page.dart';
|
import 'package:immich_mobile/pages/search/person_result.page.dart';
|
||||||
import 'package:immich_mobile/pages/search/recently_added.page.dart';
|
import 'package:immich_mobile/pages/search/recently_added.page.dart';
|
||||||
import 'package:immich_mobile/pages/search/search.page.dart';
|
import 'package:immich_mobile/pages/search/search.page.dart';
|
||||||
import 'package:immich_mobile/pages/search/search_input.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/library/partner/partner.page.dart';
|
import 'package:immich_mobile/pages/library/partner/partner.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart';
|
import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/shared_link/shared_link.page.dart';
|
import 'package:immich_mobile/pages/library/shared_link/shared_link.page.dart';
|
||||||
|
@ -97,6 +96,11 @@ class AppRouter extends RootStackRouter {
|
||||||
),
|
),
|
||||||
AutoRoute(page: LoginRoute.page, guards: [_duplicateGuard]),
|
AutoRoute(page: LoginRoute.page, guards: [_duplicateGuard]),
|
||||||
AutoRoute(page: ChangePasswordRoute.page),
|
AutoRoute(page: ChangePasswordRoute.page),
|
||||||
|
AutoRoute(
|
||||||
|
page: SearchRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
maintainState: false,
|
||||||
|
),
|
||||||
CustomRoute(
|
CustomRoute(
|
||||||
page: TabControllerRoute.page,
|
page: TabControllerRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
@ -106,7 +110,7 @@ class AppRouter extends RootStackRouter {
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: SearchInputRoute.page,
|
page: SearchRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
maintainState: false,
|
maintainState: false,
|
||||||
),
|
),
|
||||||
|
@ -244,11 +248,6 @@ class AppRouter extends RootStackRouter {
|
||||||
page: BackupOptionsRoute.page,
|
page: BackupOptionsRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
CustomRoute(
|
|
||||||
page: SearchInputRoute.page,
|
|
||||||
guards: [_authGuard, _duplicateGuard],
|
|
||||||
transitionsBuilder: TransitionsBuilders.noTransition,
|
|
||||||
),
|
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: HeaderSettingsRoute.page,
|
page: HeaderSettingsRoute.page,
|
||||||
guards: [_duplicateGuard],
|
guards: [_duplicateGuard],
|
||||||
|
|
|
@ -1292,29 +1292,29 @@ class RecentlyAddedRoute extends PageRouteInfo<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [SearchInputPage]
|
/// [SearchPage]
|
||||||
class SearchInputRoute extends PageRouteInfo<SearchInputRouteArgs> {
|
class SearchRoute extends PageRouteInfo<SearchRouteArgs> {
|
||||||
SearchInputRoute({
|
SearchRoute({
|
||||||
Key? key,
|
Key? key,
|
||||||
SearchFilter? prefilter,
|
SearchFilter? prefilter,
|
||||||
List<PageRouteInfo>? children,
|
List<PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
SearchInputRoute.name,
|
SearchRoute.name,
|
||||||
args: SearchInputRouteArgs(
|
args: SearchRouteArgs(
|
||||||
key: key,
|
key: key,
|
||||||
prefilter: prefilter,
|
prefilter: prefilter,
|
||||||
),
|
),
|
||||||
initialChildren: children,
|
initialChildren: children,
|
||||||
);
|
);
|
||||||
|
|
||||||
static const String name = 'SearchInputRoute';
|
static const String name = 'SearchRoute';
|
||||||
|
|
||||||
static PageInfo page = PageInfo(
|
static PageInfo page = PageInfo(
|
||||||
name,
|
name,
|
||||||
builder: (data) {
|
builder: (data) {
|
||||||
final args = data.argsAs<SearchInputRouteArgs>(
|
final args =
|
||||||
orElse: () => const SearchInputRouteArgs());
|
data.argsAs<SearchRouteArgs>(orElse: () => const SearchRouteArgs());
|
||||||
return SearchInputPage(
|
return SearchPage(
|
||||||
key: args.key,
|
key: args.key,
|
||||||
prefilter: args.prefilter,
|
prefilter: args.prefilter,
|
||||||
);
|
);
|
||||||
|
@ -1322,8 +1322,8 @@ class SearchInputRoute extends PageRouteInfo<SearchInputRouteArgs> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchInputRouteArgs {
|
class SearchRouteArgs {
|
||||||
const SearchInputRouteArgs({
|
const SearchRouteArgs({
|
||||||
this.key,
|
this.key,
|
||||||
this.prefilter,
|
this.prefilter,
|
||||||
});
|
});
|
||||||
|
@ -1334,29 +1334,10 @@ class SearchInputRouteArgs {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SearchInputRouteArgs{key: $key, prefilter: $prefilter}';
|
return 'SearchRouteArgs{key: $key, prefilter: $prefilter}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// generated route for
|
|
||||||
/// [SearchPage]
|
|
||||||
class SearchRoute extends PageRouteInfo<void> {
|
|
||||||
const SearchRoute({List<PageRouteInfo>? children})
|
|
||||||
: super(
|
|
||||||
SearchRoute.name,
|
|
||||||
initialChildren: children,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const String name = 'SearchRoute';
|
|
||||||
|
|
||||||
static PageInfo page = PageInfo(
|
|
||||||
name,
|
|
||||||
builder: (data) {
|
|
||||||
return const SearchPage();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [SettingsPage]
|
/// [SettingsPage]
|
||||||
class SettingsRoute extends PageRouteInfo<void> {
|
class SettingsRoute extends PageRouteInfo<void> {
|
||||||
|
|
|
@ -2,9 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/providers/memory.provider.dart';
|
import 'package:immich_mobile/providers/memory.provider.dart';
|
||||||
import 'package:immich_mobile/providers/search/people.provider.dart';
|
|
||||||
|
|
||||||
import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
|
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
|
@ -24,13 +22,6 @@ class TabNavigationObserver extends AutoRouterObserver {
|
||||||
TabPageRoute route,
|
TabPageRoute route,
|
||||||
TabPageRoute previousRoute,
|
TabPageRoute previousRoute,
|
||||||
) async {
|
) async {
|
||||||
// Perform tasks on re-visit to SearchRoute
|
|
||||||
if (route.name == 'SearchRoute') {
|
|
||||||
// Refresh Location State
|
|
||||||
ref.invalidate(getPreviewPlacesProvider);
|
|
||||||
ref.invalidate(getAllPeopleProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route.name == 'HomeRoute') {
|
if (route.name == 'HomeRoute') {
|
||||||
ref.invalidate(memoryFutureProvider);
|
ref.invalidate(memoryFutureProvider);
|
||||||
Future(() => ref.read(assetProvider.notifier).getAllAsset());
|
Future(() => ref.read(assetProvider.notifier).getAllAsset());
|
||||||
|
|
|
@ -59,7 +59,7 @@ class ExploreGrid extends StatelessWidget {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: context.pushRoute(
|
: context.pushRoute(
|
||||||
SearchInputRoute(
|
SearchRoute(
|
||||||
prefilter: SearchFilter(
|
prefilter: SearchFilter(
|
||||||
people: {},
|
people: {},
|
||||||
location: SearchLocationFilter(
|
location: SearchLocationFilter(
|
||||||
|
|
|
@ -48,7 +48,7 @@ class SearchFilterChip extends StatelessWidget {
|
||||||
child: Card(
|
child: Card(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: StadiumBorder(
|
shape: StadiumBorder(
|
||||||
side: BorderSide(color: context.colorScheme.outline.withOpacity(.5)),
|
side: BorderSide(color: context.colorScheme.outline.withAlpha(15)),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 14.0),
|
padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 14.0),
|
||||||
|
|
Loading…
Reference in a new issue