Selaa lähdekoodia

Resolve merge conflict

Alex Tran 3 vuotta sitten
vanhempi
commit
e608c61ba5

+ 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 {
   const ImmichSliverAppBar({
     Key? key,
-    required this.imageGridGroup,
     this.onPopBack,
   }) : super(key: key);
 
-  final List<Widget> 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,
           ),
         ),

+ 1 - 8
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

+ 3 - 2
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,

+ 71 - 0
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<String> searchSuggestion;
+<<<<<<< HEAD
+=======
+  final List<String> userSuggestedSearchTerms;
+>>>>>>> bfde3084924e247bc8f7004babf38605fe341a18
 
   SearchPageState({
     required this.searchTerm,
     required this.isSearchEnabled,
     required this.searchSuggestion,
+<<<<<<< HEAD
+=======
+    required this.userSuggestedSearchTerms,
+>>>>>>> bfde3084924e247bc8f7004babf38605fe341a18
   });
 
   SearchPageState copyWith({
     String? searchTerm,
     bool? isSearchEnabled,
     List<String>? searchSuggestion,
+<<<<<<< HEAD
+=======
+    List<String>? 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<String>.from(map['searchSuggestion']),
+<<<<<<< HEAD
+=======
+      userSuggestedSearchTerms: List<String>.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<SearchPageState> {
@@ -72,9 +121,18 @@ class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
             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<SearchPageState> {
   }
 
   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<SearchPageState> {
       state = state.copyWith(searchSuggestion: []);
     }
   }
+<<<<<<< HEAD
+=======
+
+  void getSuggestedSearchTerms() async {
+    var userSuggestedSearchTerms = await _searchService.getUserSuggestedSearchTerms();
+
+    state = state.copyWith(userSuggestedSearchTerms: userSuggestedSearchTerms);
+  }
+>>>>>>> bfde3084924e247bc8f7004babf38605fe341a18
 }
 
 final searchPageStateProvider = StateNotifierProvider<SearchPageStateNotifier, SearchPageState>((ref) {

+ 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 [];
+    }
+  }
+}

+ 4 - 0
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();
         },

+ 6 - 0
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(

+ 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: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>[
     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 {

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

@@ -25,13 +25,9 @@ class _$AppRouter extends RootStackRouter {
       return MaterialPageX<dynamic>(
           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>(
-          routeData: routeData, child: const BackupControllerPage());
+          routeData: routeData, child: const TabControllerPage());
     },
     ImageViewerRoute.name: (routeData) {
       final args = routeData.argsAs<ImageViewerRouteArgs>();
@@ -49,19 +45,47 @@ class _$AppRouter extends RootStackRouter {
       return MaterialPageX<dynamic>(
           routeData: routeData,
           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
   List<RouteConfig> 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<void> {
 }
 
 /// 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
@@ -158,3 +175,41 @@ class VideoViewerRouteArgs {
     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);
   }
 
+  @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);

+ 35 - 1
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<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);
+  }
 }