Browse Source

Added Tab Bar, search bar and suggested search terms from assets metadata, tags (#35)

Alex 3 years ago
parent
commit
bfde308492

+ 1 - 3
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 {
 class ImmichSliverAppBar extends ConsumerWidget {
   const ImmichSliverAppBar({
   const ImmichSliverAppBar({
     Key? key,
     Key? key,
-    required this.imageGridGroup,
     this.onPopBack,
     this.onPopBack,
   }) : super(key: key);
   }) : super(key: key);
 
 
-  final List<Widget> imageGridGroup;
   final Function? onPopBack;
   final Function? onPopBack;
 
 
   @override
   @override
@@ -46,7 +44,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
         style: GoogleFonts.snowburstOne(
         style: GoogleFonts.snowburstOne(
           textStyle: TextStyle(
           textStyle: TextStyle(
             fontWeight: FontWeight.bold,
             fontWeight: FontWeight.bold,
-            fontSize: 18,
+            fontSize: 22,
             color: Theme.of(context).primaryColor,
             color: Theme.of(context).primaryColor,
           ),
           ),
         ),
         ),

+ 1 - 8
mobile/lib/modules/home/views/home_page.dart

@@ -31,10 +31,6 @@ class HomePage extends HookConsumerWidget {
       return null;
       return null;
     }, []);
     }, []);
 
 
-    onPopBackFromBackupPage() {
-      // ref.read(assetProvider.notifier).getAllAsset();
-    }
-
     Widget _buildBody() {
     Widget _buildBody() {
       if (assetGroupByDateTime.isNotEmpty) {
       if (assetGroupByDateTime.isNotEmpty) {
         int? lastMonth;
         int? lastMonth;
@@ -88,10 +84,7 @@ class HomePage extends HookConsumerWidget {
                               child: null,
                               child: null,
                             ),
                             ),
                           )
                           )
-                        : ImmichSliverAppBar(
-                            imageGridGroup: _imageGridGroup,
-                            onPopBack: onPopBackFromBackupPage,
-                          ),
+                        : const ImmichSliverAppBar(),
                     duration: const Duration(milliseconds: 350),
                     duration: const Duration(milliseconds: 350),
                   ),
                   ),
                   ..._imageGridGroup
                   ..._imageGridGroup

+ 3 - 2
mobile/lib/modules/login/ui/login_form.dart

@@ -15,7 +15,7 @@ class LoginForm extends HookConsumerWidget {
   Widget build(BuildContext context, WidgetRef ref) {
   Widget build(BuildContext context, WidgetRef ref) {
     final usernameController = useTextEditingController(text: 'testuser@email.com');
     final usernameController = useTextEditingController(text: 'testuser@email.com');
     final passwordController = useTextEditingController(text: 'password');
     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(
     return Center(
       child: ConstrainedBox(
       child: ConstrainedBox(
@@ -124,7 +124,8 @@ class LoginButton extends ConsumerWidget {
           if (isAuthenicated) {
           if (isAuthenicated) {
             // Resume backup (if enable) then navigate
             // Resume backup (if enable) then navigate
             ref.watch(backupProvider.notifier).resumeBackup();
             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 {
           } else {
             ImmichToast.show(
             ImmichToast.show(
                 context: context,
                 context: context,

+ 0 - 0
mobile/lib/modules/search/models/store_model_here.txt


+ 131 - 0
mobile/lib/modules/search/providers/search_page_state.provider.dart

@@ -0,0 +1,131 @@
+import 'dart:convert';
+
+import 'package:collection/collection.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+
+import 'package:immich_mobile/modules/search/services/search.service.dart';
+
+class SearchPageState {
+  final String searchTerm;
+  final bool isSearchEnabled;
+  final List<String> searchSuggestion;
+  final List<String> userSuggestedSearchTerms;
+
+  SearchPageState({
+    required this.searchTerm,
+    required this.isSearchEnabled,
+    required this.searchSuggestion,
+    required this.userSuggestedSearchTerms,
+  });
+
+  SearchPageState copyWith({
+    String? searchTerm,
+    bool? isSearchEnabled,
+    List<String>? searchSuggestion,
+    List<String>? userSuggestedSearchTerms,
+  }) {
+    return SearchPageState(
+      searchTerm: searchTerm ?? this.searchTerm,
+      isSearchEnabled: isSearchEnabled ?? this.isSearchEnabled,
+      searchSuggestion: searchSuggestion ?? this.searchSuggestion,
+      userSuggestedSearchTerms: userSuggestedSearchTerms ?? this.userSuggestedSearchTerms,
+    );
+  }
+
+  Map<String, dynamic> toMap() {
+    return {
+      'searchTerm': searchTerm,
+      'isSearchEnabled': isSearchEnabled,
+      'searchSuggestion': searchSuggestion,
+      'userSuggestedSearchTerms': userSuggestedSearchTerms,
+    };
+  }
+
+  factory SearchPageState.fromMap(Map<String, dynamic> map) {
+    return SearchPageState(
+      searchTerm: map['searchTerm'] ?? '',
+      isSearchEnabled: map['isSearchEnabled'] ?? false,
+      searchSuggestion: List<String>.from(map['searchSuggestion']),
+      userSuggestedSearchTerms: List<String>.from(map['userSuggestedSearchTerms']),
+    );
+  }
+
+  String toJson() => json.encode(toMap());
+
+  factory SearchPageState.fromJson(String source) => SearchPageState.fromMap(json.decode(source));
+
+  @override
+  String toString() {
+    return 'SearchPageState(searchTerm: $searchTerm, isSearchEnabled: $isSearchEnabled, searchSuggestion: $searchSuggestion, userSuggestedSearchTerms: $userSuggestedSearchTerms)';
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+    final listEquals = const DeepCollectionEquality().equals;
+
+    return other is SearchPageState &&
+        other.searchTerm == searchTerm &&
+        other.isSearchEnabled == isSearchEnabled &&
+        listEquals(other.searchSuggestion, searchSuggestion) &&
+        listEquals(other.userSuggestedSearchTerms, userSuggestedSearchTerms);
+  }
+
+  @override
+  int get hashCode {
+    return searchTerm.hashCode ^
+        isSearchEnabled.hashCode ^
+        searchSuggestion.hashCode ^
+        userSuggestedSearchTerms.hashCode;
+  }
+}
+
+class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
+  SearchPageStateNotifier()
+      : super(
+          SearchPageState(
+            searchTerm: "",
+            isSearchEnabled: false,
+            searchSuggestion: [],
+            userSuggestedSearchTerms: [],
+          ),
+        );
+
+  final SearchService _searchService = SearchService();
+
+  void enableSearch() {
+    state = state.copyWith(isSearchEnabled: true);
+  }
+
+  void disableSearch() {
+    state = state.copyWith(isSearchEnabled: false);
+  }
+
+  void setSearchTerm(String value) {
+    state = state.copyWith(searchTerm: value);
+
+    _getSearchSuggestion(state.searchTerm);
+  }
+
+  void _getSearchSuggestion(String searchTerm) {
+    var searchList = state.userSuggestedSearchTerms;
+
+    var newList = searchList.where((e) => e.toLowerCase().contains(searchTerm));
+
+    state = state.copyWith(searchSuggestion: [...newList]);
+
+    if (searchTerm.isEmpty) {
+      state = state.copyWith(searchSuggestion: []);
+    }
+  }
+
+  void getSuggestedSearchTerms() async {
+    var userSuggestedSearchTerms = await _searchService.getUserSuggestedSearchTerms();
+
+    state = state.copyWith(userSuggestedSearchTerms: userSuggestedSearchTerms);
+  }
+}
+
+final searchPageStateProvider = StateNotifierProvider<SearchPageStateNotifier, SearchPageState>((ref) {
+  return SearchPageStateNotifier();
+});

+ 20 - 0
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<List<String>?> getUserSuggestedSearchTerms() async {
+    try {
+      var res = await _networkService.getRequest(url: "asset/searchTerm");
+      List<dynamic> decodedData = jsonDecode(res.toString());
+
+      return List.from(decodedData);
+    } catch (e) {
+      debugPrint("[ERROR] [getUserSuggestedSearchTerms] ${e.toString()}");
+      return [];
+    }
+  }
+}

+ 56 - 0
mobile/lib/modules/search/ui/search_bar.dart

@@ -0,0 +1,56 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
+
+class SearchBar extends HookConsumerWidget with PreferredSizeWidget {
+  SearchBar({Key? key, required this.searchFocusNode}) : super(key: key);
+  FocusNode searchFocusNode;
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final searchTermController = useTextEditingController(text: "");
+    final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
+
+    return AppBar(
+      automaticallyImplyLeading: false,
+      leading: isSearchEnabled
+          ? IconButton(
+              onPressed: () {
+                searchFocusNode.unfocus();
+                ref.watch(searchPageStateProvider.notifier).disableSearch();
+              },
+              icon: const Icon(Icons.arrow_back_ios_rounded))
+          : const Icon(Icons.search_rounded),
+      title: TextField(
+        controller: searchTermController,
+        focusNode: searchFocusNode,
+        autofocus: false,
+        onTap: () {
+          ref.watch(searchPageStateProvider.notifier).getSuggestedSearchTerms();
+          ref.watch(searchPageStateProvider.notifier).enableSearch();
+          searchFocusNode.requestFocus();
+        },
+        onSubmitted: (searchTerm) {
+          ref.watch(searchPageStateProvider.notifier).disableSearch();
+          searchFocusNode.unfocus();
+        },
+        onChanged: (value) {
+          ref.watch(searchPageStateProvider.notifier).setSearchTerm(value);
+        },
+        decoration: const InputDecoration(
+          hintText: 'Search your photos',
+          enabledBorder: UnderlineInputBorder(
+            borderSide: BorderSide(color: Colors.transparent),
+          ),
+          focusedBorder: UnderlineInputBorder(
+            borderSide: BorderSide(color: Colors.transparent),
+          ),
+        ),
+      ),
+    );
+  }
+
+  @override
+  Size get preferredSize => const Size.fromHeight(kToolbarHeight);
+}

+ 35 - 0
mobile/lib/modules/search/ui/search_suggestion_list.dart

@@ -0,0 +1,35 @@
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
+
+class SearchSuggestionList extends ConsumerWidget {
+  const SearchSuggestionList({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final searchTerm = ref.watch(searchPageStateProvider).searchTerm;
+    final searchSuggestion = ref.watch(searchPageStateProvider).searchSuggestion;
+
+    return Container(
+      color: searchTerm.isEmpty ? Colors.black.withOpacity(0.5) : Theme.of(context).scaffoldBackgroundColor,
+      child: CustomScrollView(
+        slivers: [
+          SliverFillRemaining(
+            hasScrollBody: true,
+            child: ListView.builder(
+              itemBuilder: ((context, index) {
+                return ListTile(
+                  onTap: () {
+                    print("navigate to this search result: ${searchSuggestion[index]} ");
+                  },
+                  title: Text(searchSuggestion[index]),
+                );
+              }),
+              itemCount: searchSuggestion.length,
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 67 - 0
mobile/lib/modules/search/views/search_page.dart

@@ -0,0 +1,67 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
+import 'package:immich_mobile/modules/search/ui/search_bar.dart';
+import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
+
+// ignore: must_be_immutable
+class SearchPage extends HookConsumerWidget {
+  SearchPage({Key? key}) : super(key: key);
+
+  late FocusNode searchFocusNode;
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
+
+    useEffect(() {
+      print("search");
+      searchFocusNode = FocusNode();
+      return () => searchFocusNode.dispose();
+    }, []);
+
+    return Scaffold(
+      appBar: SearchBar(searchFocusNode: searchFocusNode),
+      body: GestureDetector(
+        onTap: () {
+          searchFocusNode.unfocus();
+          ref.watch(searchPageStateProvider.notifier).disableSearch();
+        },
+        child: Stack(
+          children: [
+            ListView(
+              children: [
+                Container(
+                  height: 300,
+                  color: Colors.blue,
+                ),
+                Container(
+                  height: 300,
+                  color: Colors.red,
+                ),
+                Container(
+                  height: 300,
+                  color: Colors.green,
+                ),
+                Container(
+                  height: 300,
+                  color: Colors.blue,
+                ),
+                Container(
+                  height: 300,
+                  color: Colors.red,
+                ),
+                Container(
+                  height: 300,
+                  color: Colors.green,
+                ),
+              ],
+            ),
+            isSearchEnabled ? const SearchSuggestionList() : Container(),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 11 - 2
mobile/lib/routing/router.dart

@@ -2,10 +2,12 @@ import 'package:auto_route/auto_route.dart';
 import 'package:flutter/widgets.dart';
 import 'package:flutter/widgets.dart';
 import 'package:immich_mobile/modules/login/views/login_page.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/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/routing/auth_guard.dart';
 import 'package:immich_mobile/shared/models/immich_asset.model.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/shared/views/backup_controller_page.dart';
 import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_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';
 import 'package:immich_mobile/shared/views/video_viewer_page.dart';
 
 
 part 'router.gr.dart';
 part 'router.gr.dart';
@@ -14,10 +16,17 @@ part 'router.gr.dart';
   replaceInRouteName: 'Page,Route',
   replaceInRouteName: 'Page,Route',
   routes: <AutoRoute>[
   routes: <AutoRoute>[
     AutoRoute(page: LoginPage, initial: true),
     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: ImageViewerPage, guards: [AuthGuard]),
     AutoRoute(page: VideoViewerPage, guards: [AuthGuard]),
     AutoRoute(page: VideoViewerPage, guards: [AuthGuard]),
+    AutoRoute(page: BackupControllerPage, guards: [AuthGuard]),
   ],
   ],
 )
 )
 class AppRouter extends _$AppRouter {
 class AppRouter extends _$AppRouter {

+ 78 - 23
mobile/lib/routing/router.gr.dart

@@ -25,13 +25,9 @@ class _$AppRouter extends RootStackRouter {
       return MaterialPageX<dynamic>(
       return MaterialPageX<dynamic>(
           routeData: routeData, child: const LoginPage());
           routeData: routeData, child: const LoginPage());
     },
     },
-    HomeRoute.name: (routeData) {
-      return MaterialPageX<dynamic>(
-          routeData: routeData, child: const HomePage());
-    },
-    BackupControllerRoute.name: (routeData) {
+    TabControllerRoute.name: (routeData) {
       return MaterialPageX<dynamic>(
       return MaterialPageX<dynamic>(
-          routeData: routeData, child: const BackupControllerPage());
+          routeData: routeData, child: const TabControllerPage());
     },
     },
     ImageViewerRoute.name: (routeData) {
     ImageViewerRoute.name: (routeData) {
       final args = routeData.argsAs<ImageViewerRouteArgs>();
       final args = routeData.argsAs<ImageViewerRouteArgs>();
@@ -49,19 +45,47 @@ class _$AppRouter extends RootStackRouter {
       return MaterialPageX<dynamic>(
       return MaterialPageX<dynamic>(
           routeData: routeData,
           routeData: routeData,
           child: VideoViewerPage(key: args.key, videoUrl: args.videoUrl));
           child: VideoViewerPage(key: args.key, videoUrl: args.videoUrl));
+    },
+    BackupControllerRoute.name: (routeData) {
+      return MaterialPageX<dynamic>(
+          routeData: routeData, child: const BackupControllerPage());
+    },
+    HomeRoute.name: (routeData) {
+      return MaterialPageX<dynamic>(
+          routeData: routeData, child: const HomePage());
+    },
+    SearchRoute.name: (routeData) {
+      final args = routeData.argsAs<SearchRouteArgs>(
+          orElse: () => const SearchRouteArgs());
+      return MaterialPageX<dynamic>(
+          routeData: routeData, child: SearchPage(key: args.key));
     }
     }
   };
   };
 
 
   @override
   @override
   List<RouteConfig> get routes => [
   List<RouteConfig> get routes => [
         RouteConfig(LoginRoute.name, path: '/'),
         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,
         RouteConfig(ImageViewerRoute.name,
             path: '/image-viewer-page', guards: [authGuard]),
             path: '/image-viewer-page', guards: [authGuard]),
         RouteConfig(VideoViewerRoute.name,
         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<void> {
 }
 }
 
 
 /// generated route for
 /// generated route for
-/// [HomePage]
-class HomeRoute extends PageRouteInfo<void> {
-  const HomeRoute() : super(HomeRoute.name, path: '/home-page');
-
-  static const String name = 'HomeRoute';
-}
-
-/// generated route for
-/// [BackupControllerPage]
-class BackupControllerRoute extends PageRouteInfo<void> {
-  const BackupControllerRoute()
-      : super(BackupControllerRoute.name, path: '/backup-controller-page');
+/// [TabControllerPage]
+class TabControllerRoute extends PageRouteInfo<void> {
+  const TabControllerRoute({List<PageRouteInfo>? children})
+      : super(TabControllerRoute.name,
+            path: '/tab-controller-page', initialChildren: children);
 
 
-  static const String name = 'BackupControllerRoute';
+  static const String name = 'TabControllerRoute';
 }
 }
 
 
 /// generated route for
 /// generated route for
@@ -158,3 +175,41 @@ class VideoViewerRouteArgs {
     return 'VideoViewerRouteArgs{key: $key, videoUrl: $videoUrl}';
     return 'VideoViewerRouteArgs{key: $key, videoUrl: $videoUrl}';
   }
   }
 }
 }
+
+/// generated route for
+/// [BackupControllerPage]
+class BackupControllerRoute extends PageRouteInfo<void> {
+  const BackupControllerRoute()
+      : super(BackupControllerRoute.name, path: '/backup-controller-page');
+
+  static const String name = 'BackupControllerRoute';
+}
+
+/// generated route for
+/// [HomePage]
+class HomeRoute extends PageRouteInfo<void> {
+  const HomeRoute() : super(HomeRoute.name, path: 'home-page');
+
+  static const String name = 'HomeRoute';
+}
+
+/// generated route for
+/// [SearchPage]
+class SearchRoute extends PageRouteInfo<SearchRouteArgs> {
+  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}';
+  }
+}

+ 44 - 0
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)),
+                  ],
+                ),
+        );
+      },
+    );
+  }
+}

+ 5 - 0
server/src/api-v1/asset/asset.controller.ts

@@ -71,6 +71,11 @@ export class AssetController {
     return this.assetService.serveFile(authUser, query, res, headers);
     return this.assetService.serveFile(authUser, query, res, headers);
   }
   }
 
 
+  @Get('/searchTerm')
+  async getAssetSearchTerm(@GetAuthUser() authUser: AuthUserDto) {
+    return this.assetService.getAssetSearchTerm(authUser);
+  }
+
   @Get('/new')
   @Get('/new')
   async getNewAssets(@GetAuthUser() authUser: AuthUserDto, @Query(ValidationPipe) query: GetNewAssetQueryDto) {
   async getNewAssets(@GetAuthUser() authUser: AuthUserDto, @Query(ValidationPipe) query: GetNewAssetQueryDto) {
     return await this.assetService.getNewAssets(authUser, query.latestDate);
     return await this.assetService.getNewAssets(authUser, query.latestDate);

+ 35 - 1
server/src/api-v1/asset/asset.service.ts

@@ -67,7 +67,7 @@ export class AssetService {
         .orderBy('a."createdAt"::date', 'DESC')
         .orderBy('a."createdAt"::date', 'DESC')
         .getMany();
         .getMany();
 
 
-        return assets;
+      return assets;
     } catch (e) {
     } catch (e) {
       Logger.error(e, 'getAllAssets');
       Logger.error(e, 'getAllAssets');
     }
     }
@@ -243,4 +243,38 @@ export class AssetService {
 
 
     return result;
     return result;
   }
   }
+
+  async getAssetSearchTerm(authUser: AuthUserDto): Promise<String[]> {
+    const possibleSearchTerm = new Set<String>();
+    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);
+  }
 }
 }