diff --git a/mobile/lib/modules/home/ui/immich_sliver_appbar.dart b/mobile/lib/modules/home/ui/immich_sliver_appbar.dart index 71d08dbe58..ecd46fdc4c 100644 --- a/mobile/lib/modules/home/ui/immich_sliver_appbar.dart +++ b/mobile/lib/modules/home/ui/immich_sliver_appbar.dart @@ -12,11 +12,9 @@ import 'package:immich_mobile/shared/providers/backup.provider.dart'; class ImmichSliverAppBar extends ConsumerWidget { const ImmichSliverAppBar({ Key? key, - required this.imageGridGroup, this.onPopBack, }) : super(key: key); - final List imageGridGroup; final Function? onPopBack; @override @@ -46,7 +44,7 @@ class ImmichSliverAppBar extends ConsumerWidget { style: GoogleFonts.snowburstOne( textStyle: TextStyle( fontWeight: FontWeight.bold, - fontSize: 18, + fontSize: 22, color: Theme.of(context).primaryColor, ), ), diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index 6cb1694610..7a64013313 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -31,10 +31,6 @@ class HomePage extends HookConsumerWidget { return null; }, []); - onPopBackFromBackupPage() { - // ref.read(assetProvider.notifier).getAllAsset(); - } - Widget _buildBody() { if (assetGroupByDateTime.isNotEmpty) { int? lastMonth; @@ -88,10 +84,7 @@ class HomePage extends HookConsumerWidget { child: null, ), ) - : ImmichSliverAppBar( - imageGridGroup: _imageGridGroup, - onPopBack: onPopBackFromBackupPage, - ), + : const ImmichSliverAppBar(), duration: const Duration(milliseconds: 350), ), ..._imageGridGroup diff --git a/mobile/lib/modules/login/ui/login_form.dart b/mobile/lib/modules/login/ui/login_form.dart index c60d67e688..f6e07c188a 100644 --- a/mobile/lib/modules/login/ui/login_form.dart +++ b/mobile/lib/modules/login/ui/login_form.dart @@ -15,7 +15,7 @@ class LoginForm extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final usernameController = useTextEditingController(text: 'testuser@email.com'); final passwordController = useTextEditingController(text: 'password'); - final serverEndpointController = useTextEditingController(text: 'http://192.168.1.103:2283'); + final serverEndpointController = useTextEditingController(text: 'http://192.168.1.216:2283'); return Center( child: ConstrainedBox( @@ -124,7 +124,8 @@ class LoginButton extends ConsumerWidget { if (isAuthenicated) { // Resume backup (if enable) then navigate ref.watch(backupProvider.notifier).resumeBackup(); - AutoRouter.of(context).pushNamed("/home-page"); + // AutoRouter.of(context).pushNamed("/home-page"); + AutoRouter.of(context).pushNamed("/tab-controller-page"); } else { ImmichToast.show( context: context, diff --git a/mobile/lib/modules/search/providers/search_page_state.provider.dart b/mobile/lib/modules/search/providers/search_page_state.provider.dart index 2782da2af3..d286004f30 100644 --- a/mobile/lib/modules/search/providers/search_page_state.provider.dart +++ b/mobile/lib/modules/search/providers/search_page_state.provider.dart @@ -3,26 +3,47 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +<<<<<<< HEAD +======= +import 'package:immich_mobile/modules/search/services/search.service.dart'; + +>>>>>>> bfde3084924e247bc8f7004babf38605fe341a18 class SearchPageState { final String searchTerm; final bool isSearchEnabled; final List searchSuggestion; +<<<<<<< HEAD +======= + final List userSuggestedSearchTerms; +>>>>>>> bfde3084924e247bc8f7004babf38605fe341a18 SearchPageState({ required this.searchTerm, required this.isSearchEnabled, required this.searchSuggestion, +<<<<<<< HEAD +======= + required this.userSuggestedSearchTerms, +>>>>>>> bfde3084924e247bc8f7004babf38605fe341a18 }); SearchPageState copyWith({ String? searchTerm, bool? isSearchEnabled, List? searchSuggestion, +<<<<<<< HEAD +======= + List? userSuggestedSearchTerms, +>>>>>>> bfde3084924e247bc8f7004babf38605fe341a18 }) { return SearchPageState( searchTerm: searchTerm ?? this.searchTerm, isSearchEnabled: isSearchEnabled ?? this.isSearchEnabled, searchSuggestion: searchSuggestion ?? this.searchSuggestion, +<<<<<<< HEAD +======= + userSuggestedSearchTerms: userSuggestedSearchTerms ?? this.userSuggestedSearchTerms, +>>>>>>> bfde3084924e247bc8f7004babf38605fe341a18 ); } @@ -31,6 +52,10 @@ class SearchPageState { 'searchTerm': searchTerm, 'isSearchEnabled': isSearchEnabled, 'searchSuggestion': searchSuggestion, +<<<<<<< HEAD +======= + 'userSuggestedSearchTerms': userSuggestedSearchTerms, +>>>>>>> bfde3084924e247bc8f7004babf38605fe341a18 }; } @@ -39,6 +64,10 @@ class SearchPageState { searchTerm: map['searchTerm'] ?? '', isSearchEnabled: map['isSearchEnabled'] ?? false, searchSuggestion: List.from(map['searchSuggestion']), +<<<<<<< HEAD +======= + userSuggestedSearchTerms: List.from(map['userSuggestedSearchTerms']), +>>>>>>> bfde3084924e247bc8f7004babf38605fe341a18 ); } @@ -47,8 +76,14 @@ class SearchPageState { factory SearchPageState.fromJson(String source) => SearchPageState.fromMap(json.decode(source)); @override +<<<<<<< HEAD String toString() => 'SearchPageState(searchTerm: $searchTerm, isSearchEnabled: $isSearchEnabled, searchSuggestion: $searchSuggestion)'; +======= + String toString() { + return 'SearchPageState(searchTerm: $searchTerm, isSearchEnabled: $isSearchEnabled, searchSuggestion: $searchSuggestion, userSuggestedSearchTerms: $userSuggestedSearchTerms)'; + } +>>>>>>> bfde3084924e247bc8f7004babf38605fe341a18 @override bool operator ==(Object other) { @@ -58,11 +93,25 @@ class SearchPageState { return other is SearchPageState && other.searchTerm == searchTerm && other.isSearchEnabled == isSearchEnabled && +<<<<<<< HEAD listEquals(other.searchSuggestion, searchSuggestion); } @override int get hashCode => searchTerm.hashCode ^ isSearchEnabled.hashCode ^ searchSuggestion.hashCode; +======= + listEquals(other.searchSuggestion, searchSuggestion) && + listEquals(other.userSuggestedSearchTerms, userSuggestedSearchTerms); + } + + @override + int get hashCode { + return searchTerm.hashCode ^ + isSearchEnabled.hashCode ^ + searchSuggestion.hashCode ^ + userSuggestedSearchTerms.hashCode; + } +>>>>>>> bfde3084924e247bc8f7004babf38605fe341a18 } class SearchPageStateNotifier extends StateNotifier { @@ -72,9 +121,18 @@ class SearchPageStateNotifier extends StateNotifier { searchTerm: "", isSearchEnabled: false, searchSuggestion: [], +<<<<<<< HEAD ), ); +======= + userSuggestedSearchTerms: [], + ), + ); + + final SearchService _searchService = SearchService(); + +>>>>>>> bfde3084924e247bc8f7004babf38605fe341a18 void enableSearch() { state = state.copyWith(isSearchEnabled: true); } @@ -90,7 +148,11 @@ class SearchPageStateNotifier extends StateNotifier { } void _getSearchSuggestion(String searchTerm) { +<<<<<<< HEAD var searchList = ['January', '01 2022', 'feburary', "February", 'home', '3413']; +======= + var searchList = state.userSuggestedSearchTerms; +>>>>>>> bfde3084924e247bc8f7004babf38605fe341a18 var newList = searchList.where((e) => e.toLowerCase().contains(searchTerm)); @@ -100,6 +162,15 @@ class SearchPageStateNotifier extends StateNotifier { state = state.copyWith(searchSuggestion: []); } } +<<<<<<< HEAD +======= + + void getSuggestedSearchTerms() async { + var userSuggestedSearchTerms = await _searchService.getUserSuggestedSearchTerms(); + + state = state.copyWith(userSuggestedSearchTerms: userSuggestedSearchTerms); + } +>>>>>>> bfde3084924e247bc8f7004babf38605fe341a18 } final searchPageStateProvider = StateNotifierProvider((ref) { diff --git a/mobile/lib/modules/search/services/search.service.dart b/mobile/lib/modules/search/services/search.service.dart new file mode 100644 index 0000000000..d7c9101ca2 --- /dev/null +++ b/mobile/lib/modules/search/services/search.service.dart @@ -0,0 +1,20 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:immich_mobile/shared/services/network.service.dart'; + +class SearchService { + final NetworkService _networkService = NetworkService(); + + Future?> getUserSuggestedSearchTerms() async { + try { + var res = await _networkService.getRequest(url: "asset/searchTerm"); + List decodedData = jsonDecode(res.toString()); + + return List.from(decodedData); + } catch (e) { + debugPrint("[ERROR] [getUserSuggestedSearchTerms] ${e.toString()}"); + return []; + } + } +} diff --git a/mobile/lib/modules/search/ui/search_bar.dart b/mobile/lib/modules/search/ui/search_bar.dart index af3b2fd6ce..a7be32a7de 100644 --- a/mobile/lib/modules/search/ui/search_bar.dart +++ b/mobile/lib/modules/search/ui/search_bar.dart @@ -27,6 +27,10 @@ class SearchBar extends HookConsumerWidget with PreferredSizeWidget { focusNode: searchFocusNode, autofocus: false, onTap: () { +<<<<<<< HEAD +======= + ref.watch(searchPageStateProvider.notifier).getSuggestedSearchTerms(); +>>>>>>> bfde3084924e247bc8f7004babf38605fe341a18 ref.watch(searchPageStateProvider.notifier).enableSearch(); searchFocusNode.requestFocus(); }, diff --git a/mobile/lib/modules/search/views/search_page.dart b/mobile/lib/modules/search/views/search_page.dart index b6b36db1b4..4f1b6527d5 100644 --- a/mobile/lib/modules/search/views/search_page.dart +++ b/mobile/lib/modules/search/views/search_page.dart @@ -16,10 +16,16 @@ class SearchPage extends HookConsumerWidget { final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled; useEffect(() { +<<<<<<< HEAD searchFocusNode = FocusNode(); return () { searchFocusNode.dispose(); }; +======= + print("search"); + searchFocusNode = FocusNode(); + return () => searchFocusNode.dispose(); +>>>>>>> bfde3084924e247bc8f7004babf38605fe341a18 }, []); return Scaffold( diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 32e21727f3..ccea91bd1f 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -2,10 +2,12 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/widgets.dart'; import 'package:immich_mobile/modules/login/views/login_page.dart'; import 'package:immich_mobile/modules/home/views/home_page.dart'; +import 'package:immich_mobile/modules/search/views/search_page.dart'; import 'package:immich_mobile/routing/auth_guard.dart'; import 'package:immich_mobile/shared/models/immich_asset.model.dart'; import 'package:immich_mobile/shared/views/backup_controller_page.dart'; import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart'; +import 'package:immich_mobile/shared/views/tab_controller_page.dart'; import 'package:immich_mobile/shared/views/video_viewer_page.dart'; part 'router.gr.dart'; @@ -14,10 +16,17 @@ part 'router.gr.dart'; replaceInRouteName: 'Page,Route', routes: [ AutoRoute(page: LoginPage, initial: true), - AutoRoute(page: HomePage, guards: [AuthGuard]), - AutoRoute(page: BackupControllerPage, guards: [AuthGuard]), + AutoRoute( + page: TabControllerPage, + guards: [AuthGuard], + children: [ + AutoRoute(page: HomePage, guards: [AuthGuard]), + AutoRoute(page: SearchPage, guards: [AuthGuard]) + ], + ), AutoRoute(page: ImageViewerPage, guards: [AuthGuard]), AutoRoute(page: VideoViewerPage, guards: [AuthGuard]), + AutoRoute(page: BackupControllerPage, guards: [AuthGuard]), ], ) class AppRouter extends _$AppRouter { diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 197abc1b1f..e6fe3d3828 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -25,13 +25,9 @@ class _$AppRouter extends RootStackRouter { return MaterialPageX( routeData: routeData, child: const LoginPage()); }, - HomeRoute.name: (routeData) { + TabControllerRoute.name: (routeData) { return MaterialPageX( - routeData: routeData, child: const HomePage()); - }, - BackupControllerRoute.name: (routeData) { - return MaterialPageX( - routeData: routeData, child: const BackupControllerPage()); + routeData: routeData, child: const TabControllerPage()); }, ImageViewerRoute.name: (routeData) { final args = routeData.argsAs(); @@ -49,19 +45,47 @@ class _$AppRouter extends RootStackRouter { return MaterialPageX( routeData: routeData, child: VideoViewerPage(key: args.key, videoUrl: args.videoUrl)); + }, + BackupControllerRoute.name: (routeData) { + return MaterialPageX( + routeData: routeData, child: const BackupControllerPage()); + }, + HomeRoute.name: (routeData) { + return MaterialPageX( + routeData: routeData, child: const HomePage()); + }, + SearchRoute.name: (routeData) { + final args = routeData.argsAs( + orElse: () => const SearchRouteArgs()); + return MaterialPageX( + routeData: routeData, child: SearchPage(key: args.key)); } }; @override List get routes => [ RouteConfig(LoginRoute.name, path: '/'), - RouteConfig(HomeRoute.name, path: '/home-page', guards: [authGuard]), - RouteConfig(BackupControllerRoute.name, - path: '/backup-controller-page', guards: [authGuard]), + RouteConfig(TabControllerRoute.name, + path: '/tab-controller-page', + guards: [ + authGuard + ], + children: [ + RouteConfig(HomeRoute.name, + path: 'home-page', + parent: TabControllerRoute.name, + guards: [authGuard]), + RouteConfig(SearchRoute.name, + path: 'search-page', + parent: TabControllerRoute.name, + guards: [authGuard]) + ]), RouteConfig(ImageViewerRoute.name, path: '/image-viewer-page', guards: [authGuard]), RouteConfig(VideoViewerRoute.name, - path: '/video-viewer-page', guards: [authGuard]) + path: '/video-viewer-page', guards: [authGuard]), + RouteConfig(BackupControllerRoute.name, + path: '/backup-controller-page', guards: [authGuard]) ]; } @@ -74,20 +98,13 @@ class LoginRoute extends PageRouteInfo { } /// generated route for -/// [HomePage] -class HomeRoute extends PageRouteInfo { - const HomeRoute() : super(HomeRoute.name, path: '/home-page'); +/// [TabControllerPage] +class TabControllerRoute extends PageRouteInfo { + const TabControllerRoute({List? children}) + : super(TabControllerRoute.name, + path: '/tab-controller-page', initialChildren: children); - static const String name = 'HomeRoute'; -} - -/// generated route for -/// [BackupControllerPage] -class BackupControllerRoute extends PageRouteInfo { - const BackupControllerRoute() - : super(BackupControllerRoute.name, path: '/backup-controller-page'); - - static const String name = 'BackupControllerRoute'; + static const String name = 'TabControllerRoute'; } /// generated route for @@ -158,3 +175,41 @@ class VideoViewerRouteArgs { return 'VideoViewerRouteArgs{key: $key, videoUrl: $videoUrl}'; } } + +/// generated route for +/// [BackupControllerPage] +class BackupControllerRoute extends PageRouteInfo { + const BackupControllerRoute() + : super(BackupControllerRoute.name, path: '/backup-controller-page'); + + static const String name = 'BackupControllerRoute'; +} + +/// generated route for +/// [HomePage] +class HomeRoute extends PageRouteInfo { + const HomeRoute() : super(HomeRoute.name, path: 'home-page'); + + static const String name = 'HomeRoute'; +} + +/// generated route for +/// [SearchPage] +class SearchRoute extends PageRouteInfo { + SearchRoute({Key? key}) + : super(SearchRoute.name, + path: 'search-page', args: SearchRouteArgs(key: key)); + + static const String name = 'SearchRoute'; +} + +class SearchRouteArgs { + const SearchRouteArgs({this.key}); + + final Key? key; + + @override + String toString() { + return 'SearchRouteArgs{key: $key}'; + } +} diff --git a/mobile/lib/shared/views/tab_controller_page.dart b/mobile/lib/shared/views/tab_controller_page.dart new file mode 100644 index 0000000000..f699e1ccaf --- /dev/null +++ b/mobile/lib/shared/views/tab_controller_page.dart @@ -0,0 +1,44 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; + +class TabControllerPage extends ConsumerWidget { + const TabControllerPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable; + + return AutoTabsRouter( + routes: [ + const HomeRoute(), + SearchRoute(), + ], + builder: (context, child, animation) { + final tabsRouter = AutoTabsRouter.of(context); + return Scaffold( + body: FadeTransition( + opacity: animation, + child: child, + ), + bottomNavigationBar: isMultiSelectEnable + ? null + : BottomNavigationBar( + selectedLabelStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600), + unselectedLabelStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600), + currentIndex: tabsRouter.activeIndex, + onTap: (index) { + tabsRouter.setActiveIndex(index); + }, + items: const [ + BottomNavigationBarItem(label: 'Photos', icon: Icon(Icons.photo)), + BottomNavigationBarItem(label: 'Seach', icon: Icon(Icons.search)), + ], + ), + ); + }, + ); + } +} diff --git a/server/src/api-v1/asset/asset.controller.ts b/server/src/api-v1/asset/asset.controller.ts index 48247a7ecd..3f77e5688d 100644 --- a/server/src/api-v1/asset/asset.controller.ts +++ b/server/src/api-v1/asset/asset.controller.ts @@ -71,6 +71,11 @@ export class AssetController { return this.assetService.serveFile(authUser, query, res, headers); } + @Get('/searchTerm') + async getAssetSearchTerm(@GetAuthUser() authUser: AuthUserDto) { + return this.assetService.getAssetSearchTerm(authUser); + } + @Get('/new') async getNewAssets(@GetAuthUser() authUser: AuthUserDto, @Query(ValidationPipe) query: GetNewAssetQueryDto) { return await this.assetService.getNewAssets(authUser, query.latestDate); diff --git a/server/src/api-v1/asset/asset.service.ts b/server/src/api-v1/asset/asset.service.ts index a3c8b86637..503c9ddbe3 100644 --- a/server/src/api-v1/asset/asset.service.ts +++ b/server/src/api-v1/asset/asset.service.ts @@ -67,7 +67,7 @@ export class AssetService { .orderBy('a."createdAt"::date', 'DESC') .getMany(); - return assets; + return assets; } catch (e) { Logger.error(e, 'getAllAssets'); } @@ -243,4 +243,38 @@ export class AssetService { return result; } + + async getAssetSearchTerm(authUser: AuthUserDto): Promise { + const possibleSearchTerm = new Set(); + const rows = await this.assetRepository.query( + ` + select distinct si.tags, e.orientation, e."lensModel", e.make, e.model , a.type + from assets a + left join exif e on a.id = e."assetId" + left join smart_info si on a.id = si."assetId" + where a."userId" = $1; + `, + [authUser.id], + ); + + rows.forEach((row) => { + // tags + row['tags']?.map((tag) => possibleSearchTerm.add(tag?.toLowerCase())); + + // asset's tyoe + possibleSearchTerm.add(row['type']?.toLowerCase()); + + // image orientation + possibleSearchTerm.add(row['orientation']?.toLowerCase()); + + // Lens model + possibleSearchTerm.add(row['lensModel']?.toLowerCase()); + + // Make and model + possibleSearchTerm.add(row['make']?.toLowerCase()); + possibleSearchTerm.add(row['model']?.toLowerCase()); + }); + + return Array.from(possibleSearchTerm).filter((x) => x != null); + } }