feat(mobile): drift search page

This commit is contained in:
wuzihao051119 2025-07-08 12:56:24 +08:00
parent 87dd09d103
commit 3a878ddd29
5 changed files with 336 additions and 8 deletions

View File

@ -116,7 +116,7 @@ class TabShellPage extends ConsumerWidget {
return AutoTabsRouter( return AutoTabsRouter(
routes: [ routes: [
const MainTimelineRoute(), const MainTimelineRoute(),
SearchRoute(), const DriftSearchRoute(),
const DriftAlbumsRoute(), const DriftAlbumsRoute(),
const DriftLibraryRoute(), const DriftLibraryRoute(),
], ],

View File

@ -0,0 +1,313 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
@RoutePage()
class DriftSearchPage extends ConsumerStatefulWidget {
const DriftSearchPage({super.key});
@override
ConsumerState<DriftSearchPage> createState() => _DriftSearchPageState();
}
class _DriftSearchPageState extends ConsumerState<DriftSearchPage> {
final searchController = TextEditingController();
final searchFocusNode = FocusNode();
@override
void initState() {
super.initState();
}
void onSearch(String searchTerm) {
final userId = ref.watch(currentUserProvider)?.id;
}
void clearSearch() {
}
@override
void dispose() {
searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final userId = ref.watch(currentUserProvider)?.id;
return Scaffold(
body: CustomScrollView(
slivers: [
const ImmichSliverAppBar(),
_SearchBar(
searchController: searchController,
searchFocusNode: searchFocusNode,
onClearSearch: clearSearch,
),
const _SearchFilterChipRow(),
const _QuickLinkList(),
],
),
);
}
}
class _SearchBar extends StatelessWidget {
const _SearchBar({
required this.searchController,
required this.searchFocusNode,
required this.onClearSearch,
});
final TextEditingController searchController;
final FocusNode searchFocusNode;
final VoidCallback onClearSearch;
@override
Widget build(BuildContext context) {
return const SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
child: SizedBox.shrink(),
),
);
}
}
class _SearchFilterChipRow extends StatelessWidget {
const _SearchFilterChipRow();
void showPeoplePicker() {
}
void showLocationPicker() {
}
void showCameraPicker() {
}
void showDatePicker() {
}
void showMediaTypePicker() {
}
void showDisplayOptionPicker() {
}
@override
Widget build(BuildContext context) {
return SliverPadding(
padding: const EdgeInsets.only(left: 16, top: 12, right: 16),
sliver: SliverToBoxAdapter(
child: SizedBox(
height: 50,
child: ListView(
key: const Key('search_filter_chip_list'),
shrinkWrap: true,
scrollDirection: Axis.horizontal,
children: [
_SearchFilterChip(
icon: Icons.people_alt_outlined,
onTap: showPeoplePicker,
label: 'people'.t(context: context),
// currentFilter: peopleCurrentFilterWidget.value,
),
_SearchFilterChip(
icon: Icons.location_on_outlined,
onTap: showLocationPicker,
label: 'search_filter_location'.t(context: context),
// currentFilter: locationCurrentFilterWidget.value,
),
_SearchFilterChip(
icon: Icons.camera_alt_outlined,
onTap: showCameraPicker,
label: 'camera'.t(context: context),
// currentFilter: cameraCurrentFilterWidget.value,
),
_SearchFilterChip(
icon: Icons.date_range_outlined,
onTap: showDatePicker,
label: 'search_filter_date'.t(context: context),
// currentFilter: dateRangeCurrentFilterWidget.value,
),
_SearchFilterChip(
icon: Icons.video_collection_outlined,
onTap: showMediaTypePicker,
label: 'search_filter_media_type'.t(context: context),
// currentFilter: mediaTypeCurrentFilterWidget.value,
),
_SearchFilterChip(
icon: Icons.display_settings_outlined,
onTap: showDisplayOptionPicker,
label: 'search_filter_display_options'.t(context: context),
// currentFilter: displayOptionCurrentFilterWidget.value,
),
],
),
),
),
);
}
}
class _SearchFilterChip extends StatelessWidget {
const _SearchFilterChip({
required this.label,
required this.icon,
required this.onTap,
this.currentFilter,
});
final String label;
final IconData icon;
final VoidCallback onTap;
final Widget? currentFilter;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Card(
elevation: 0,
color: currentFilter == null
? context.colorScheme.surfaceContainerLow
: context.primaryColor.withValues(alpha: .5),
shape: StadiumBorder(
side: BorderSide(
color: currentFilter == null
? context.colorScheme.outline.withAlpha(15)
: context.colorScheme.secondaryContainer,
),
),
child: Padding(
padding:
const EdgeInsets.symmetric(vertical: 2.0, horizontal: 14.0),
child: Row(
children: [
Icon(
icon,
size: 18,
),
const SizedBox(width: 4.0),
currentFilter ?? Text(label),
],
),
),
),
);
}
}
class _QuickLinkList extends StatelessWidget {
const _QuickLinkList();
@override
Widget build(BuildContext context) {
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.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,
padding: const EdgeInsets.all(0),
physics: const NeverScrollableScrollPhysics(),
children: [
_QuickLink(
label: 'recently_taken'.t(context: context),
icon: Icons.schedule_outlined,
onTap: () => context.pushRoute(const RecentlyTakenRoute()),
isTop: true,
),
_QuickLink(
label: 'videos'.t(context: context),
icon: Icons.play_circle_outline_rounded,
onTap: () => context.pushRoute(const DriftVideoRoute()),
),
_QuickLink(
label: 'favorites'.t(context: context),
icon: Icons.favorite_border_rounded,
onTap: () => context.pushRoute(const DriftFavoriteRoute()),
isBottom: true,
),
],
),
),
),
);
}
}
class _QuickLink extends StatelessWidget {
const _QuickLink({
required this.label,
required this.icon,
required this.onTap,
this.isTop = false,
this.isBottom = false,
});
final String label;
final IconData icon;
final VoidCallback onTap;
final bool isTop;
final bool isBottom;
@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(
label,
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
onTap: onTap,
);
}
}

View File

@ -127,11 +127,6 @@ final _features = [
icon: Icons.timeline_rounded, icon: Icons.timeline_rounded,
onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()), onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()),
), ),
_Feature(
name: 'Video',
icon: Icons.video_collection_outlined,
onTap: (ctx, _) => ctx.pushRoute(const DriftVideoRoute()),
),
]; ];
@RoutePage() @RoutePage()

View File

@ -68,6 +68,7 @@ import 'package:immich_mobile/pages/search/recently_taken.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/share_intent/share_intent.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_favorite.page.dart'; import 'package:immich_mobile/presentation/pages/dev/drift_favorite.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_search.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_video.page.dart'; import 'package:immich_mobile/presentation/pages/dev/drift_video.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_trash.page.dart'; import 'package:immich_mobile/presentation/pages/dev/drift_trash.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_archive.page.dart'; import 'package:immich_mobile/presentation/pages/dev/drift_archive.page.dart';
@ -177,7 +178,7 @@ class AppRouter extends RootStackRouter {
guards: [_authGuard, _duplicateGuard], guards: [_authGuard, _duplicateGuard],
), ),
AutoRoute( AutoRoute(
page: SearchRoute.page, page: DriftSearchRoute.page,
guards: [_authGuard, _duplicateGuard], guards: [_authGuard, _duplicateGuard],
maintainState: false, maintainState: false,
), ),
@ -428,7 +429,10 @@ class AppRouter extends RootStackRouter {
page: DriftAssetSelectionTimelineRoute.page, page: DriftAssetSelectionTimelineRoute.page,
guards: [_authGuard, _duplicateGuard], guards: [_authGuard, _duplicateGuard],
), ),
AutoRoute(
page: DriftSearchRoute.page,
guards: [_authGuard, _duplicateGuard],
),
// required to handle all deeplinks in deep_link.service.dart // required to handle all deeplinks in deep_link.service.dart
// auto_route_library#1722 // auto_route_library#1722
RedirectRoute(path: '*', redirectTo: '/'), RedirectRoute(path: '*', redirectTo: '/'),

View File

@ -783,6 +783,22 @@ class DriftMemoryRouteArgs {
} }
} }
/// generated route for
/// [DriftSearchPage]
class DriftSearchRoute extends PageRouteInfo<void> {
const DriftSearchRoute({List<PageRouteInfo>? children})
: super(DriftSearchRoute.name, initialChildren: children);
static const String name = 'DriftSearchRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const DriftSearchPage();
},
);
}
/// generated route for /// generated route for
/// [DriftTrashPage] /// [DriftTrashPage]
class DriftTrashRoute extends PageRouteInfo<void> { class DriftTrashRoute extends PageRouteInfo<void> {