mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	Show curated asset's location in search page (#55)
* Added Tab Navigation Observer to trigger event handling for tab page navigation * Added query to get access with distinct location * Showed places in search page as a horizontal list * Showed location search result on tapped
This commit is contained in:
		
							parent
							
								
									348d395b21
								
							
						
					
					
						commit
						8c7080eaef
					
				
							
								
								
									
										25
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								README.md
									
									
									
									
									
								
							@ -1,13 +1,28 @@
 | 
				
			|||||||
<p align="center">
 | 
					<p align="center">
 | 
				
			||||||
  <img src="design/immich-logo.svg" width="150" title="hover text">
 | 
					  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-green.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: MIT"></a>
 | 
				
			||||||
 | 
					  <a href="https://github.com/alextran1502/immich"><img src="https://img.shields.io/github/stars/alextran1502/immich.svg?style=for-the-badge&logo=github&color=3F51B5&label=Stars&logoColor=000000&labelColor=ececec" alt="Star on Github"></a>
 | 
				
			||||||
 | 
					  <a href="https://immichci.little-home.net/viewType.html?buildTypeId=Immich_BuildAndroidAndGetArtifact&guest=1">
 | 
				
			||||||
 | 
					    <img src="https://img.shields.io/teamcity/http/immichci.little-home.net/s/Immich_BuildAndroidAndGetArtifact.svg?style=for-the-badge&label=Android&logo=teamcity&logoColor=000000&labelColor=ececec" alt="Android Build"/>
 | 
				
			||||||
 | 
					  </a>
 | 
				
			||||||
 | 
					  <a href="https://immichci.little-home.net/viewType.html?buildTypeId=Immich_BuildAndPublishIOSToTestFlight&guest=1">
 | 
				
			||||||
 | 
					    <img src="https://img.shields.io/teamcity/http/immichci.little-home.net/s/Immich_BuildAndPublishIOSToTestFlight.svg?style=for-the-badge&label=iOS&logo=teamcity&logoColor=000000&labelColor=ececec" alt="iOS Build"/>
 | 
				
			||||||
 | 
					  </a>
 | 
				
			||||||
 | 
					  <a href="https://actions-badge.atrox.dev/alextran1502/immich/goto?ref=main">
 | 
				
			||||||
 | 
					    <img alt="Build Status" src="https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Falextran1502%2Fimmich%2Fbadge%3Fref%3Dmain&style=for-the-badge&label=Server Docker&logo=docker&labelColor=ececec" />
 | 
				
			||||||
 | 
					  </a>
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  <br/>  
 | 
				
			||||||
 | 
					  <br/>  
 | 
				
			||||||
 | 
					  <br/>  
 | 
				
			||||||
 | 
					  <br/>  
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <p align="center">
 | 
				
			||||||
 | 
					    <img src="design/immich-logo.svg" width="200" title="Immich Logo">
 | 
				
			||||||
 | 
					  </p>
 | 
				
			||||||
</p>
 | 
					</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Immich
 | 
					# Immich
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| Android Build | iOS Build | Server Docker Build |
 | 
					 | 
				
			||||||
| --- | --- | --- |
 | 
					 | 
				
			||||||
| [/statusIcon>)](https://immichci.little-home.net/viewType.html?buildTypeId=Immich_BuildAndroidAndGetArtifact&guest=1) | [/statusIcon>)](https://immichci.little-home.net/viewType.html?buildTypeId=Immich_BuildAndroidAndGetArtifact&guest=1) |  |
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Self-hosted photo and video backup solution directly from your mobile phone.
 | 
					Self-hosted photo and video backup solution directly from your mobile phone.
 | 
				
			||||||
 | 
					
 | 
				
			||||||

 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ import 'package:hive_flutter/hive_flutter.dart';
 | 
				
			|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
 | 
					import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/routing/router.dart';
 | 
					import 'package:immich_mobile/routing/router.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/routing/tab_navigation_observer.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
 | 
					import 'package:immich_mobile/shared/providers/app_state.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
 | 
					import 'package:immich_mobile/shared/providers/backup.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
 | 
					import 'package:immich_mobile/shared/providers/websocket.provider.dart';
 | 
				
			||||||
@ -100,7 +101,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
 | 
				
			|||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      routeInformationParser: _immichRouter.defaultRouteParser(),
 | 
					      routeInformationParser: _immichRouter.defaultRouteParser(),
 | 
				
			||||||
      routerDelegate: _immichRouter.delegate(),
 | 
					      routerDelegate: _immichRouter.delegate(navigatorObservers: () => [TabNavigationObserver(ref: ref)]),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										79
									
								
								mobile/lib/modules/search/models/curated_location.model.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								mobile/lib/modules/search/models/curated_location.model.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,79 @@
 | 
				
			|||||||
 | 
					import 'dart:convert';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CuratedLocation {
 | 
				
			||||||
 | 
					  final String id;
 | 
				
			||||||
 | 
					  final String city;
 | 
				
			||||||
 | 
					  final String resizePath;
 | 
				
			||||||
 | 
					  final String deviceAssetId;
 | 
				
			||||||
 | 
					  final String deviceId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  CuratedLocation({
 | 
				
			||||||
 | 
					    required this.id,
 | 
				
			||||||
 | 
					    required this.city,
 | 
				
			||||||
 | 
					    required this.resizePath,
 | 
				
			||||||
 | 
					    required this.deviceAssetId,
 | 
				
			||||||
 | 
					    required this.deviceId,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  CuratedLocation copyWith({
 | 
				
			||||||
 | 
					    String? id,
 | 
				
			||||||
 | 
					    String? city,
 | 
				
			||||||
 | 
					    String? resizePath,
 | 
				
			||||||
 | 
					    String? deviceAssetId,
 | 
				
			||||||
 | 
					    String? deviceId,
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    return CuratedLocation(
 | 
				
			||||||
 | 
					      id: id ?? this.id,
 | 
				
			||||||
 | 
					      city: city ?? this.city,
 | 
				
			||||||
 | 
					      resizePath: resizePath ?? this.resizePath,
 | 
				
			||||||
 | 
					      deviceAssetId: deviceAssetId ?? this.deviceAssetId,
 | 
				
			||||||
 | 
					      deviceId: deviceId ?? this.deviceId,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toMap() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      'id': id,
 | 
				
			||||||
 | 
					      'city': city,
 | 
				
			||||||
 | 
					      'resizePath': resizePath,
 | 
				
			||||||
 | 
					      'deviceAssetId': deviceAssetId,
 | 
				
			||||||
 | 
					      'deviceId': deviceId,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory CuratedLocation.fromMap(Map<String, dynamic> map) {
 | 
				
			||||||
 | 
					    return CuratedLocation(
 | 
				
			||||||
 | 
					      id: map['id'] ?? '',
 | 
				
			||||||
 | 
					      city: map['city'] ?? '',
 | 
				
			||||||
 | 
					      resizePath: map['resizePath'] ?? '',
 | 
				
			||||||
 | 
					      deviceAssetId: map['deviceAssetId'] ?? '',
 | 
				
			||||||
 | 
					      deviceId: map['deviceId'] ?? '',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory CuratedLocation.fromJson(String source) => CuratedLocation.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() {
 | 
				
			||||||
 | 
					    return 'CuratedLocation(id: $id, city: $city, resizePath: $resizePath, deviceAssetId: $deviceAssetId, deviceId: $deviceId)';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
 | 
					    if (identical(this, other)) return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return other is CuratedLocation &&
 | 
				
			||||||
 | 
					        other.id == id &&
 | 
				
			||||||
 | 
					        other.city == city &&
 | 
				
			||||||
 | 
					        other.resizePath == resizePath &&
 | 
				
			||||||
 | 
					        other.deviceAssetId == deviceAssetId &&
 | 
				
			||||||
 | 
					        other.deviceId == deviceId;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode {
 | 
				
			||||||
 | 
					    return id.hashCode ^ city.hashCode ^ resizePath.hashCode ^ deviceAssetId.hashCode ^ deviceId.hashCode;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,78 @@
 | 
				
			|||||||
 | 
					import 'dart:convert';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:collection/collection.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;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,32 +1,28 @@
 | 
				
			|||||||
import 'dart:convert';
 | 
					import 'dart:convert';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:collection/collection.dart';
 | 
					import 'package:collection/collection.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:immich_mobile/modules/search/services/search.service.dart';
 | 
					 | 
				
			||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
 | 
					import 'package:immich_mobile/shared/models/immich_asset.model.dart';
 | 
				
			||||||
import 'package:intl/intl.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SearchresultPageState {
 | 
					class SearchResultPageState {
 | 
				
			||||||
  final bool isLoading;
 | 
					  final bool isLoading;
 | 
				
			||||||
  final bool isSuccess;
 | 
					  final bool isSuccess;
 | 
				
			||||||
  final bool isError;
 | 
					  final bool isError;
 | 
				
			||||||
  final List<ImmichAsset> searchResult;
 | 
					  final List<ImmichAsset> searchResult;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  SearchresultPageState({
 | 
					  SearchResultPageState({
 | 
				
			||||||
    required this.isLoading,
 | 
					    required this.isLoading,
 | 
				
			||||||
    required this.isSuccess,
 | 
					    required this.isSuccess,
 | 
				
			||||||
    required this.isError,
 | 
					    required this.isError,
 | 
				
			||||||
    required this.searchResult,
 | 
					    required this.searchResult,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  SearchresultPageState copyWith({
 | 
					  SearchResultPageState copyWith({
 | 
				
			||||||
    bool? isLoading,
 | 
					    bool? isLoading,
 | 
				
			||||||
    bool? isSuccess,
 | 
					    bool? isSuccess,
 | 
				
			||||||
    bool? isError,
 | 
					    bool? isError,
 | 
				
			||||||
    List<ImmichAsset>? searchResult,
 | 
					    List<ImmichAsset>? searchResult,
 | 
				
			||||||
  }) {
 | 
					  }) {
 | 
				
			||||||
    return SearchresultPageState(
 | 
					    return SearchResultPageState(
 | 
				
			||||||
      isLoading: isLoading ?? this.isLoading,
 | 
					      isLoading: isLoading ?? this.isLoading,
 | 
				
			||||||
      isSuccess: isSuccess ?? this.isSuccess,
 | 
					      isSuccess: isSuccess ?? this.isSuccess,
 | 
				
			||||||
      isError: isError ?? this.isError,
 | 
					      isError: isError ?? this.isError,
 | 
				
			||||||
@ -43,8 +39,8 @@ class SearchresultPageState {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory SearchresultPageState.fromMap(Map<String, dynamic> map) {
 | 
					  factory SearchResultPageState.fromMap(Map<String, dynamic> map) {
 | 
				
			||||||
    return SearchresultPageState(
 | 
					    return SearchResultPageState(
 | 
				
			||||||
      isLoading: map['isLoading'] ?? false,
 | 
					      isLoading: map['isLoading'] ?? false,
 | 
				
			||||||
      isSuccess: map['isSuccess'] ?? false,
 | 
					      isSuccess: map['isSuccess'] ?? false,
 | 
				
			||||||
      isError: map['isError'] ?? false,
 | 
					      isError: map['isError'] ?? false,
 | 
				
			||||||
@ -54,7 +50,7 @@ class SearchresultPageState {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory SearchresultPageState.fromJson(String source) => SearchresultPageState.fromMap(json.decode(source));
 | 
					  factory SearchResultPageState.fromJson(String source) => SearchResultPageState.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
@ -66,7 +62,7 @@ class SearchresultPageState {
 | 
				
			|||||||
    if (identical(this, other)) return true;
 | 
					    if (identical(this, other)) return true;
 | 
				
			||||||
    final listEquals = const DeepCollectionEquality().equals;
 | 
					    final listEquals = const DeepCollectionEquality().equals;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return other is SearchresultPageState &&
 | 
					    return other is SearchResultPageState &&
 | 
				
			||||||
        other.isLoading == isLoading &&
 | 
					        other.isLoading == isLoading &&
 | 
				
			||||||
        other.isSuccess == isSuccess &&
 | 
					        other.isSuccess == isSuccess &&
 | 
				
			||||||
        other.isError == isError &&
 | 
					        other.isError == isError &&
 | 
				
			||||||
@ -78,34 +74,3 @@ class SearchresultPageState {
 | 
				
			|||||||
    return isLoading.hashCode ^ isSuccess.hashCode ^ isError.hashCode ^ searchResult.hashCode;
 | 
					    return isLoading.hashCode ^ isSuccess.hashCode ^ isError.hashCode ^ searchResult.hashCode;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
class SearchResultPageStateNotifier extends StateNotifier<SearchresultPageState> {
 | 
					 | 
				
			||||||
  SearchResultPageStateNotifier()
 | 
					 | 
				
			||||||
      : super(SearchresultPageState(searchResult: [], isError: false, isLoading: true, isSuccess: false));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  final SearchService _searchService = SearchService();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  search(String searchTerm) async {
 | 
					 | 
				
			||||||
    state = state.copyWith(searchResult: [], isError: false, isLoading: true, isSuccess: false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    List<ImmichAsset>? assets = await _searchService.searchAsset(searchTerm);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (assets != null) {
 | 
					 | 
				
			||||||
      state = state.copyWith(searchResult: assets, isError: false, isLoading: false, isSuccess: true);
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      state = state.copyWith(searchResult: [], isError: true, isLoading: false, isSuccess: false);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
final searchResultPageStateProvider =
 | 
					 | 
				
			||||||
    StateNotifierProvider<SearchResultPageStateNotifier, SearchresultPageState>((ref) {
 | 
					 | 
				
			||||||
  return SearchResultPageStateNotifier();
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
final searchResultGroupByDateTimeProvider = StateProvider((ref) {
 | 
					 | 
				
			||||||
  var assets = ref.watch(searchResultPageStateProvider).searchResult;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  assets.sortByCompare<DateTime>((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
 | 
					 | 
				
			||||||
  return assets.groupListsBy((element) => DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)));
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
@ -1,85 +1,9 @@
 | 
				
			|||||||
import 'dart:convert';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:collection/collection.dart';
 | 
					 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/search/models/search_page_state.model.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:immich_mobile/modules/search/services/search.service.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> {
 | 
					class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
 | 
				
			||||||
  SearchPageStateNotifier()
 | 
					  SearchPageStateNotifier()
 | 
				
			||||||
      : super(
 | 
					      : super(
 | 
				
			||||||
@ -129,3 +53,14 @@ class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
 | 
				
			|||||||
final searchPageStateProvider = StateNotifierProvider<SearchPageStateNotifier, SearchPageState>((ref) {
 | 
					final searchPageStateProvider = StateNotifierProvider<SearchPageStateNotifier, SearchPageState>((ref) {
 | 
				
			||||||
  return SearchPageStateNotifier();
 | 
					  return SearchPageStateNotifier();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final getCuratedLocationProvider = FutureProvider.autoDispose<List<CuratedLocation>>((ref) async {
 | 
				
			||||||
 | 
					  final SearchService _searchService = SearchService();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  var curatedLocation = await _searchService.getCuratedLocation();
 | 
				
			||||||
 | 
					  if (curatedLocation != null) {
 | 
				
			||||||
 | 
					    return curatedLocation;
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					import 'package:collection/collection.dart';
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/search/services/search.service.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/models/immich_asset.model.dart';
 | 
				
			||||||
 | 
					import 'package:intl/intl.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
 | 
				
			||||||
 | 
					  SearchResultPageNotifier()
 | 
				
			||||||
 | 
					      : super(SearchResultPageState(searchResult: [], isError: false, isLoading: true, isSuccess: false));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final SearchService _searchService = SearchService();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void search(String searchTerm) async {
 | 
				
			||||||
 | 
					    state = state.copyWith(searchResult: [], isError: false, isLoading: true, isSuccess: false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    List<ImmichAsset>? assets = await _searchService.searchAsset(searchTerm);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (assets != null) {
 | 
				
			||||||
 | 
					      state = state.copyWith(searchResult: assets, isError: false, isLoading: false, isSuccess: true);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      state = state.copyWith(searchResult: [], isError: true, isLoading: false, isSuccess: false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final searchResultPageProvider = StateNotifierProvider<SearchResultPageNotifier, SearchResultPageState>((ref) {
 | 
				
			||||||
 | 
					  return SearchResultPageNotifier();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final searchResultGroupByDateTimeProvider = StateProvider((ref) {
 | 
				
			||||||
 | 
					  var assets = ref.watch(searchResultPageProvider).searchResult;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  assets.sortByCompare<DateTime>((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
 | 
				
			||||||
 | 
					  return assets.groupListsBy((element) => DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)));
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
import 'dart:convert';
 | 
					import 'dart:convert';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/search/models/curated_location.model.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/services/network.service.dart';
 | 
					import 'package:immich_mobile/shared/services/network.service.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -36,4 +37,19 @@ class SearchService {
 | 
				
			|||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<List<CuratedLocation>?> getCuratedLocation() async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      var res = await _networkService.getRequest(url: "asset/allLocation");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      List<dynamic> decodedData = jsonDecode(res.toString());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      List<CuratedLocation> result = List.from(decodedData.map((a) => CuratedLocation.fromMap(a)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return result;
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      debugPrint("[ERROR] [getCuratedLocation] ${e.toString()}");
 | 
				
			||||||
 | 
					      throw Error();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,11 @@
 | 
				
			|||||||
import 'package:auto_route/auto_route.dart';
 | 
					import 'package:auto_route/auto_route.dart';
 | 
				
			||||||
 | 
					import 'package:cached_network_image/cached_network_image.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
 | 
					import 'package:hive_flutter/hive_flutter.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/constants/hive_box.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.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_bar.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
 | 
					import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
 | 
				
			||||||
@ -15,7 +19,9 @@ class SearchPage extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    var box = Hive.box(userInfoBox);
 | 
				
			||||||
    final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
 | 
					    final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
 | 
				
			||||||
 | 
					    AsyncValue<List<CuratedLocation>> curatedLocation = ref.watch(getCuratedLocationProvider);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() {
 | 
					    useEffect(() {
 | 
				
			||||||
      searchFocusNode = FocusNode();
 | 
					      searchFocusNode = FocusNode();
 | 
				
			||||||
@ -29,6 +35,53 @@ class SearchPage extends HookConsumerWidget {
 | 
				
			|||||||
      AutoRouter.of(context).push(SearchResultRoute(searchTerm: searchTerm));
 | 
					      AutoRouter.of(context).push(SearchResultRoute(searchTerm: searchTerm));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _buildPlaces() {
 | 
				
			||||||
 | 
					      return curatedLocation.when(
 | 
				
			||||||
 | 
					        loading: () => const CircularProgressIndicator(),
 | 
				
			||||||
 | 
					        error: (err, stack) => Text('Error: $err'),
 | 
				
			||||||
 | 
					        data: (curatedLocations) {
 | 
				
			||||||
 | 
					          return curatedLocations.isNotEmpty
 | 
				
			||||||
 | 
					              ? SizedBox(
 | 
				
			||||||
 | 
					                  height: MediaQuery.of(context).size.width / 3,
 | 
				
			||||||
 | 
					                  child: ListView.builder(
 | 
				
			||||||
 | 
					                    padding: const EdgeInsets.only(left: 16),
 | 
				
			||||||
 | 
					                    scrollDirection: Axis.horizontal,
 | 
				
			||||||
 | 
					                    itemCount: curatedLocation.value?.length,
 | 
				
			||||||
 | 
					                    itemBuilder: ((context, index) {
 | 
				
			||||||
 | 
					                      CuratedLocation locationInfo = curatedLocations[index];
 | 
				
			||||||
 | 
					                      var thumbnailRequestUrl =
 | 
				
			||||||
 | 
					                          '${box.get(serverEndpointKey)}/asset/file?aid=${locationInfo.deviceAssetId}&did=${locationInfo.deviceId}&isThumb=true';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                      return ThumbnailWithInfo(
 | 
				
			||||||
 | 
					                        imageUrl: thumbnailRequestUrl,
 | 
				
			||||||
 | 
					                        textInfo: locationInfo.city,
 | 
				
			||||||
 | 
					                        onTap: () {
 | 
				
			||||||
 | 
					                          AutoRouter.of(context).push(SearchResultRoute(searchTerm: locationInfo.city));
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                      );
 | 
				
			||||||
 | 
					                    }),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              : SizedBox(
 | 
				
			||||||
 | 
					                  height: MediaQuery.of(context).size.width / 3,
 | 
				
			||||||
 | 
					                  child: ListView.builder(
 | 
				
			||||||
 | 
					                    padding: const EdgeInsets.only(left: 16),
 | 
				
			||||||
 | 
					                    scrollDirection: Axis.horizontal,
 | 
				
			||||||
 | 
					                    itemCount: 1,
 | 
				
			||||||
 | 
					                    itemBuilder: ((context, index) {
 | 
				
			||||||
 | 
					                      return ThumbnailWithInfo(
 | 
				
			||||||
 | 
					                        imageUrl:
 | 
				
			||||||
 | 
					                            'https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60',
 | 
				
			||||||
 | 
					                        textInfo: 'No Places Info Available',
 | 
				
			||||||
 | 
					                        onTap: () {},
 | 
				
			||||||
 | 
					                      );
 | 
				
			||||||
 | 
					                    }),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return Scaffold(
 | 
				
			||||||
      appBar: SearchBar(
 | 
					      appBar: SearchBar(
 | 
				
			||||||
        searchFocusNode: searchFocusNode,
 | 
					        searchFocusNode: searchFocusNode,
 | 
				
			||||||
@ -41,11 +94,17 @@ class SearchPage extends HookConsumerWidget {
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        child: Stack(
 | 
					        child: Stack(
 | 
				
			||||||
          children: [
 | 
					          children: [
 | 
				
			||||||
            const Center(
 | 
					 | 
				
			||||||
              child: Text("Start typing to search for your photos"),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            ListView(
 | 
					            ListView(
 | 
				
			||||||
              children: const [],
 | 
					              children: [
 | 
				
			||||||
 | 
					                const Padding(
 | 
				
			||||||
 | 
					                  padding: EdgeInsets.all(16.0),
 | 
				
			||||||
 | 
					                  child: Text(
 | 
				
			||||||
 | 
					                    "Places",
 | 
				
			||||||
 | 
					                    style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                _buildPlaces(),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            isSearchEnabled ? SearchSuggestionList(onSubmitted: _onSearchSubmitted) : Container(),
 | 
					            isSearchEnabled ? SearchSuggestionList(onSubmitted: _onSearchSubmitted) : Container(),
 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
@ -54,3 +113,66 @@ class SearchPage extends HookConsumerWidget {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ThumbnailWithInfo extends StatelessWidget {
 | 
				
			||||||
 | 
					  const ThumbnailWithInfo({Key? key, required this.textInfo, required this.imageUrl, required this.onTap})
 | 
				
			||||||
 | 
					      : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final String textInfo;
 | 
				
			||||||
 | 
					  final String imageUrl;
 | 
				
			||||||
 | 
					  final Function onTap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    var box = Hive.box(userInfoBox);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return GestureDetector(
 | 
				
			||||||
 | 
					      onTap: () {
 | 
				
			||||||
 | 
					        onTap();
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      child: Padding(
 | 
				
			||||||
 | 
					        padding: const EdgeInsets.only(right: 8.0),
 | 
				
			||||||
 | 
					        child: SizedBox(
 | 
				
			||||||
 | 
					          width: MediaQuery.of(context).size.width / 3,
 | 
				
			||||||
 | 
					          height: MediaQuery.of(context).size.width / 3,
 | 
				
			||||||
 | 
					          child: Stack(
 | 
				
			||||||
 | 
					            alignment: Alignment.bottomCenter,
 | 
				
			||||||
 | 
					            children: [
 | 
				
			||||||
 | 
					              Container(
 | 
				
			||||||
 | 
					                foregroundDecoration: BoxDecoration(
 | 
				
			||||||
 | 
					                  borderRadius: BorderRadius.circular(10),
 | 
				
			||||||
 | 
					                  color: Colors.black26,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                child: ClipRRect(
 | 
				
			||||||
 | 
					                  borderRadius: BorderRadius.circular(10),
 | 
				
			||||||
 | 
					                  child: CachedNetworkImage(
 | 
				
			||||||
 | 
					                    width: 150,
 | 
				
			||||||
 | 
					                    height: 150,
 | 
				
			||||||
 | 
					                    fit: BoxFit.cover,
 | 
				
			||||||
 | 
					                    imageUrl: imageUrl,
 | 
				
			||||||
 | 
					                    httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              Positioned(
 | 
				
			||||||
 | 
					                bottom: 8,
 | 
				
			||||||
 | 
					                left: 10,
 | 
				
			||||||
 | 
					                child: SizedBox(
 | 
				
			||||||
 | 
					                  width: MediaQuery.of(context).size.width / 3,
 | 
				
			||||||
 | 
					                  child: Text(
 | 
				
			||||||
 | 
					                    textInfo,
 | 
				
			||||||
 | 
					                    style: const TextStyle(
 | 
				
			||||||
 | 
					                      color: Colors.white,
 | 
				
			||||||
 | 
					                      fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					                      fontSize: 12,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
 | 
				
			|||||||
import 'package:immich_mobile/modules/home/ui/image_grid.dart';
 | 
					import 'package:immich_mobile/modules/home/ui/image_grid.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
 | 
					import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
 | 
					import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/search/providers/search_result_page_state.provider.dart';
 | 
					import 'package:immich_mobile/modules/search/providers/search_result_page.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
 | 
					import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SearchResultPage extends HookConsumerWidget {
 | 
					class SearchResultPage extends HookConsumerWidget {
 | 
				
			||||||
@ -28,7 +28,7 @@ class SearchResultPage extends HookConsumerWidget {
 | 
				
			|||||||
    useEffect(() {
 | 
					    useEffect(() {
 | 
				
			||||||
      searchFocusNode = FocusNode();
 | 
					      searchFocusNode = FocusNode();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      Future.delayed(Duration.zero, () => ref.read(searchResultPageStateProvider.notifier).search(searchTerm));
 | 
					      Future.delayed(Duration.zero, () => ref.read(searchResultPageProvider.notifier).search(searchTerm));
 | 
				
			||||||
      return () => searchFocusNode.dispose();
 | 
					      return () => searchFocusNode.dispose();
 | 
				
			||||||
    }, []);
 | 
					    }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -37,7 +37,7 @@ class SearchResultPage extends HookConsumerWidget {
 | 
				
			|||||||
      searchFocusNode.unfocus();
 | 
					      searchFocusNode.unfocus();
 | 
				
			||||||
      isNewSearch.value = false;
 | 
					      isNewSearch.value = false;
 | 
				
			||||||
      currentSearchTerm.value = newSearchTerm;
 | 
					      currentSearchTerm.value = newSearchTerm;
 | 
				
			||||||
      ref.watch(searchResultPageStateProvider.notifier).search(newSearchTerm);
 | 
					      ref.watch(searchResultPageProvider.notifier).search(newSearchTerm);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _buildTextField() {
 | 
					    _buildTextField() {
 | 
				
			||||||
@ -99,7 +99,7 @@ class SearchResultPage extends HookConsumerWidget {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _buildSearchResult() {
 | 
					    _buildSearchResult() {
 | 
				
			||||||
      var searchResultPageState = ref.watch(searchResultPageStateProvider);
 | 
					      var searchResultPageState = ref.watch(searchResultPageProvider);
 | 
				
			||||||
      var assetGroupByDateTime = ref.watch(searchResultGroupByDateTimeProvider);
 | 
					      var assetGroupByDateTime = ref.watch(searchResultGroupByDateTimeProvider);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (searchResultPageState.isError) {
 | 
					      if (searchResultPageState.isError) {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										30
									
								
								mobile/lib/routing/tab_navigation_observer.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								mobile/lib/routing/tab_navigation_observer.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					import 'package:auto_route/auto_route.dart';
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TabNavigationObserver extends AutoRouterObserver {
 | 
				
			||||||
 | 
					  /// Riverpod Instance
 | 
				
			||||||
 | 
					  final WidgetRef ref;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  TabNavigationObserver({
 | 
				
			||||||
 | 
					    required this.ref,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void didInitTabRoute(TabPageRoute route, TabPageRoute? previousRoute) {
 | 
				
			||||||
 | 
					    // Perform tasks on first navigation to SearchRoute
 | 
				
			||||||
 | 
					    if (route.name == 'SearchRoute') {
 | 
				
			||||||
 | 
					      // ref.refresh(getCuratedLocationProvider);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<void> didChangeTabRoute(TabPageRoute route, TabPageRoute previousRoute) async {
 | 
				
			||||||
 | 
					    // Perform tasks on re-visit to SearchRoute
 | 
				
			||||||
 | 
					    if (route.name == 'SearchRoute') {
 | 
				
			||||||
 | 
					      // Refresh Location State
 | 
				
			||||||
 | 
					      ref.refresh(getCuratedLocationProvider);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,30 +0,0 @@
 | 
				
			|||||||
// This is a basic Flutter widget test.
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// To perform an interaction with a widget in your test, use the WidgetTester
 | 
					 | 
				
			||||||
// utility that Flutter provides. For example, you can send tap and scroll
 | 
					 | 
				
			||||||
// gestures. You can also use WidgetTester to find child widgets in the widget
 | 
					 | 
				
			||||||
// tree, read text, and verify that the values of widget properties are correct.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					 | 
				
			||||||
import 'package:flutter_test/flutter_test.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:immich_mobile/main.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void main() {
 | 
					 | 
				
			||||||
  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
 | 
					 | 
				
			||||||
    // Build our app and trigger a frame.
 | 
					 | 
				
			||||||
    await tester.pumpWidget(const ImmichApp());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Verify that our counter starts at 0.
 | 
					 | 
				
			||||||
    expect(find.text('0'), findsOneWidget);
 | 
					 | 
				
			||||||
    expect(find.text('1'), findsNothing);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Tap the '+' icon and trigger a frame.
 | 
					 | 
				
			||||||
    await tester.tap(find.byIcon(Icons.add));
 | 
					 | 
				
			||||||
    await tester.pump();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Verify that our counter has incremented.
 | 
					 | 
				
			||||||
    expect(find.text('0'), findsNothing);
 | 
					 | 
				
			||||||
    expect(find.text('1'), findsOneWidget);
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -72,6 +72,11 @@ export class AssetController {
 | 
				
			|||||||
    return this.assetService.serveFile(authUser, query, res, headers);
 | 
					    return this.assetService.serveFile(authUser, query, res, headers);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Get('/allLocation')
 | 
				
			||||||
 | 
					  async getCuratedLocation(@GetAuthUser() authUser: AuthUserDto) {
 | 
				
			||||||
 | 
					    return this.assetService.getCuratedLocation(authUser);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Get('/searchTerm')
 | 
					  @Get('/searchTerm')
 | 
				
			||||||
  async getAssetSearchTerm(@GetAuthUser() authUser: AuthUserDto) {
 | 
					  async getAssetSearchTerm(@GetAuthUser() authUser: AuthUserDto) {
 | 
				
			||||||
    return this.assetService.getAssetSearchTerm(authUser);
 | 
					    return this.assetService.getAssetSearchTerm(authUser);
 | 
				
			||||||
 | 
				
			|||||||
@ -303,4 +303,20 @@ export class AssetService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return rows;
 | 
					    return rows;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getCuratedLocation(authUser: AuthUserDto) {
 | 
				
			||||||
 | 
					    const rows = await this.assetRepository.query(
 | 
				
			||||||
 | 
					      `
 | 
				
			||||||
 | 
					        select distinct on (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId"
 | 
				
			||||||
 | 
					        from assets a
 | 
				
			||||||
 | 
					        left join exif e on a.id = e."assetId"
 | 
				
			||||||
 | 
					        where a."userId" = $1 
 | 
				
			||||||
 | 
					        and e.city is not null
 | 
				
			||||||
 | 
					        and a.type = 'IMAGE';
 | 
				
			||||||
 | 
					      `,
 | 
				
			||||||
 | 
					      [authUser.id],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return rows;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user