From 3a878ddd29a1c2ad912b89fcbb623455211c4619 Mon Sep 17 00:00:00 2001 From: wuzihao051119 Date: Tue, 8 Jul 2025 12:56:24 +0800 Subject: [PATCH] feat(mobile): drift search page --- mobile/lib/pages/common/tab_shell.page.dart | 2 +- .../pages/dev/drift_search.page.dart | 313 ++++++++++++++++++ .../pages/dev/feat_in_development.page.dart | 5 - mobile/lib/routing/router.dart | 8 +- mobile/lib/routing/router.gr.dart | 16 + 5 files changed, 336 insertions(+), 8 deletions(-) create mode 100644 mobile/lib/presentation/pages/dev/drift_search.page.dart diff --git a/mobile/lib/pages/common/tab_shell.page.dart b/mobile/lib/pages/common/tab_shell.page.dart index d5d53360b0..bbb74add5d 100644 --- a/mobile/lib/pages/common/tab_shell.page.dart +++ b/mobile/lib/pages/common/tab_shell.page.dart @@ -116,7 +116,7 @@ class TabShellPage extends ConsumerWidget { return AutoTabsRouter( routes: [ const MainTimelineRoute(), - SearchRoute(), + const DriftSearchRoute(), const DriftAlbumsRoute(), const DriftLibraryRoute(), ], diff --git a/mobile/lib/presentation/pages/dev/drift_search.page.dart b/mobile/lib/presentation/pages/dev/drift_search.page.dart new file mode 100644 index 0000000000..623ad35f06 --- /dev/null +++ b/mobile/lib/presentation/pages/dev/drift_search.page.dart @@ -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 createState() => _DriftSearchPageState(); +} + +class _DriftSearchPageState extends ConsumerState { + 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, + ); + } +} diff --git a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart index a4343b720f..19fa18b78c 100644 --- a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart +++ b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart @@ -127,11 +127,6 @@ final _features = [ icon: Icons.timeline_rounded, onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()), ), - _Feature( - name: 'Video', - icon: Icons.video_collection_outlined, - onTap: (ctx, _) => ctx.pushRoute(const DriftVideoRoute()), - ), ]; @RoutePage() diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 4bf287c08a..0aa49ab21f 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -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/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_search.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_archive.page.dart'; @@ -177,7 +178,7 @@ class AppRouter extends RootStackRouter { guards: [_authGuard, _duplicateGuard], ), AutoRoute( - page: SearchRoute.page, + page: DriftSearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false, ), @@ -428,7 +429,10 @@ class AppRouter extends RootStackRouter { page: DriftAssetSelectionTimelineRoute.page, guards: [_authGuard, _duplicateGuard], ), - + AutoRoute( + page: DriftSearchRoute.page, + guards: [_authGuard, _duplicateGuard], + ), // required to handle all deeplinks in deep_link.service.dart // auto_route_library#1722 RedirectRoute(path: '*', redirectTo: '/'), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index f752c2dc8a..1f2187b5cf 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -783,6 +783,22 @@ class DriftMemoryRouteArgs { } } +/// generated route for +/// [DriftSearchPage] +class DriftSearchRoute extends PageRouteInfo { + const DriftSearchRoute({List? children}) + : super(DriftSearchRoute.name, initialChildren: children); + + static const String name = 'DriftSearchRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftSearchPage(); + }, + ); +} + /// generated route for /// [DriftTrashPage] class DriftTrashRoute extends PageRouteInfo {