From 8c7080eaefa7e60ef9810568674b2a1b47fece27 Mon Sep 17 00:00:00 2001
From: Alex
Date: Wed, 16 Mar 2022 10:19:31 -0500
Subject: [PATCH] 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
---
README.md | 25 +++-
mobile/lib/main.dart | 3 +-
.../search/models/curated_location.model.dart | 79 +++++++++++
.../models/search_page_state.model.dart | 78 +++++++++++
.../search_result_page_state.model.dart} | 51 ++-----
.../search/models/store_model_here.txt | 0
.../providers/search_page_state.provider.dart | 91 ++----------
.../search_result_page.provider.dart | 37 +++++
.../search/services/search.service.dart | 16 +++
.../lib/modules/search/views/search_page.dart | 130 +++++++++++++++++-
.../search/views/search_result_page.dart | 8 +-
.../lib/routing/tab_navigation_observer.dart | 30 ++++
mobile/test/widget_test.dart | 30 ----
server/src/api-v1/asset/asset.controller.ts | 5 +
server/src/api-v1/asset/asset.service.ts | 16 +++
15 files changed, 434 insertions(+), 165 deletions(-)
create mode 100644 mobile/lib/modules/search/models/curated_location.model.dart
create mode 100644 mobile/lib/modules/search/models/search_page_state.model.dart
rename mobile/lib/modules/search/{providers/search_result_page_state.provider.dart => models/search_result_page_state.model.dart} (50%)
delete mode 100644 mobile/lib/modules/search/models/store_model_here.txt
create mode 100644 mobile/lib/modules/search/providers/search_result_page.provider.dart
create mode 100644 mobile/lib/routing/tab_navigation_observer.dart
delete mode 100644 mobile/test/widget_test.dart
diff --git a/README.md b/README.md
index 291aaac465..9c6c9f6d1f 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,28 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
# Immich
-| Android Build | iOS Build | Server Docker Build |
-| --- | --- | --- |
-| [![Build Status]()](https://immichci.little-home.net/viewType.html?buildTypeId=Immich_BuildAndroidAndGetArtifact&guest=1) | [![Build Status]()](https://immichci.little-home.net/viewType.html?buildTypeId=Immich_BuildAndroidAndGetArtifact&guest=1) | ![example workflow](https://github.com/alextran1502/immich/actions/workflows/build_push_server.yml/badge.svg) |
-
Self-hosted photo and video backup solution directly from your mobile phone.
![](https://media.giphy.com/media/y8ZeaAigGmNvlSoKhU/giphy.gif)
diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart
index 67d9a3c260..f57849f2ea 100644
--- a/mobile/lib/main.dart
+++ b/mobile/lib/main.dart
@@ -4,6 +4,7 @@ import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/providers/asset.provider.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/backup.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
@@ -100,7 +101,7 @@ class _ImmichAppState extends ConsumerState with WidgetsBindingObserv
),
),
routeInformationParser: _immichRouter.defaultRouteParser(),
- routerDelegate: _immichRouter.delegate(),
+ routerDelegate: _immichRouter.delegate(navigatorObservers: () => [TabNavigationObserver(ref: ref)]),
);
}
}
diff --git a/mobile/lib/modules/search/models/curated_location.model.dart b/mobile/lib/modules/search/models/curated_location.model.dart
new file mode 100644
index 0000000000..dfdcea95aa
--- /dev/null
+++ b/mobile/lib/modules/search/models/curated_location.model.dart
@@ -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 toMap() {
+ return {
+ 'id': id,
+ 'city': city,
+ 'resizePath': resizePath,
+ 'deviceAssetId': deviceAssetId,
+ 'deviceId': deviceId,
+ };
+ }
+
+ factory CuratedLocation.fromMap(Map 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;
+ }
+}
diff --git a/mobile/lib/modules/search/models/search_page_state.model.dart b/mobile/lib/modules/search/models/search_page_state.model.dart
new file mode 100644
index 0000000000..8ee09a6b54
--- /dev/null
+++ b/mobile/lib/modules/search/models/search_page_state.model.dart
@@ -0,0 +1,78 @@
+import 'dart:convert';
+
+import 'package:collection/collection.dart';
+
+class SearchPageState {
+ final String searchTerm;
+ final bool isSearchEnabled;
+ final List searchSuggestion;
+ final List userSuggestedSearchTerms;
+
+ SearchPageState({
+ required this.searchTerm,
+ required this.isSearchEnabled,
+ required this.searchSuggestion,
+ required this.userSuggestedSearchTerms,
+ });
+
+ SearchPageState copyWith({
+ String? searchTerm,
+ bool? isSearchEnabled,
+ List? searchSuggestion,
+ List? userSuggestedSearchTerms,
+ }) {
+ return SearchPageState(
+ searchTerm: searchTerm ?? this.searchTerm,
+ isSearchEnabled: isSearchEnabled ?? this.isSearchEnabled,
+ searchSuggestion: searchSuggestion ?? this.searchSuggestion,
+ userSuggestedSearchTerms: userSuggestedSearchTerms ?? this.userSuggestedSearchTerms,
+ );
+ }
+
+ Map toMap() {
+ return {
+ 'searchTerm': searchTerm,
+ 'isSearchEnabled': isSearchEnabled,
+ 'searchSuggestion': searchSuggestion,
+ 'userSuggestedSearchTerms': userSuggestedSearchTerms,
+ };
+ }
+
+ factory SearchPageState.fromMap(Map map) {
+ return SearchPageState(
+ searchTerm: map['searchTerm'] ?? '',
+ isSearchEnabled: map['isSearchEnabled'] ?? false,
+ searchSuggestion: List.from(map['searchSuggestion']),
+ userSuggestedSearchTerms: List.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;
+ }
+}
diff --git a/mobile/lib/modules/search/providers/search_result_page_state.provider.dart b/mobile/lib/modules/search/models/search_result_page_state.model.dart
similarity index 50%
rename from mobile/lib/modules/search/providers/search_result_page_state.provider.dart
rename to mobile/lib/modules/search/models/search_result_page_state.model.dart
index 3c3960e04c..7b7d1e30de 100644
--- a/mobile/lib/modules/search/providers/search_result_page_state.provider.dart
+++ b/mobile/lib/modules/search/models/search_result_page_state.model.dart
@@ -1,32 +1,28 @@
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';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
-import 'package:intl/intl.dart';
-class SearchresultPageState {
+class SearchResultPageState {
final bool isLoading;
final bool isSuccess;
final bool isError;
final List searchResult;
- SearchresultPageState({
+ SearchResultPageState({
required this.isLoading,
required this.isSuccess,
required this.isError,
required this.searchResult,
});
- SearchresultPageState copyWith({
+ SearchResultPageState copyWith({
bool? isLoading,
bool? isSuccess,
bool? isError,
List? searchResult,
}) {
- return SearchresultPageState(
+ return SearchResultPageState(
isLoading: isLoading ?? this.isLoading,
isSuccess: isSuccess ?? this.isSuccess,
isError: isError ?? this.isError,
@@ -43,8 +39,8 @@ class SearchresultPageState {
};
}
- factory SearchresultPageState.fromMap(Map map) {
- return SearchresultPageState(
+ factory SearchResultPageState.fromMap(Map map) {
+ return SearchResultPageState(
isLoading: map['isLoading'] ?? false,
isSuccess: map['isSuccess'] ?? false,
isError: map['isError'] ?? false,
@@ -54,7 +50,7 @@ class SearchresultPageState {
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
String toString() {
@@ -66,7 +62,7 @@ class SearchresultPageState {
if (identical(this, other)) return true;
final listEquals = const DeepCollectionEquality().equals;
- return other is SearchresultPageState &&
+ return other is SearchResultPageState &&
other.isLoading == isLoading &&
other.isSuccess == isSuccess &&
other.isError == isError &&
@@ -78,34 +74,3 @@ class SearchresultPageState {
return isLoading.hashCode ^ isSuccess.hashCode ^ isError.hashCode ^ searchResult.hashCode;
}
}
-
-class SearchResultPageStateNotifier extends StateNotifier {
- 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? 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((ref) {
- return SearchResultPageStateNotifier();
-});
-
-final searchResultGroupByDateTimeProvider = StateProvider((ref) {
- var assets = ref.watch(searchResultPageStateProvider).searchResult;
-
- assets.sortByCompare((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
- return assets.groupListsBy((element) => DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)));
-});
diff --git a/mobile/lib/modules/search/models/store_model_here.txt b/mobile/lib/modules/search/models/store_model_here.txt
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/mobile/lib/modules/search/providers/search_page_state.provider.dart b/mobile/lib/modules/search/providers/search_page_state.provider.dart
index 544c6b184f..3d3d5f7c42 100644
--- a/mobile/lib/modules/search/providers/search_page_state.provider.dart
+++ b/mobile/lib/modules/search/providers/search_page_state.provider.dart
@@ -1,85 +1,9 @@
-import 'dart:convert';
-
-import 'package:collection/collection.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';
-class SearchPageState {
- final String searchTerm;
- final bool isSearchEnabled;
- final List searchSuggestion;
- final List userSuggestedSearchTerms;
-
- SearchPageState({
- required this.searchTerm,
- required this.isSearchEnabled,
- required this.searchSuggestion,
- required this.userSuggestedSearchTerms,
- });
-
- SearchPageState copyWith({
- String? searchTerm,
- bool? isSearchEnabled,
- List? searchSuggestion,
- List? userSuggestedSearchTerms,
- }) {
- return SearchPageState(
- searchTerm: searchTerm ?? this.searchTerm,
- isSearchEnabled: isSearchEnabled ?? this.isSearchEnabled,
- searchSuggestion: searchSuggestion ?? this.searchSuggestion,
- userSuggestedSearchTerms: userSuggestedSearchTerms ?? this.userSuggestedSearchTerms,
- );
- }
-
- Map toMap() {
- return {
- 'searchTerm': searchTerm,
- 'isSearchEnabled': isSearchEnabled,
- 'searchSuggestion': searchSuggestion,
- 'userSuggestedSearchTerms': userSuggestedSearchTerms,
- };
- }
-
- factory SearchPageState.fromMap(Map map) {
- return SearchPageState(
- searchTerm: map['searchTerm'] ?? '',
- isSearchEnabled: map['isSearchEnabled'] ?? false,
- searchSuggestion: List.from(map['searchSuggestion']),
- userSuggestedSearchTerms: List.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 {
SearchPageStateNotifier()
: super(
@@ -129,3 +53,14 @@ class SearchPageStateNotifier extends StateNotifier {
final searchPageStateProvider = StateNotifierProvider((ref) {
return SearchPageStateNotifier();
});
+
+final getCuratedLocationProvider = FutureProvider.autoDispose>((ref) async {
+ final SearchService _searchService = SearchService();
+
+ var curatedLocation = await _searchService.getCuratedLocation();
+ if (curatedLocation != null) {
+ return curatedLocation;
+ } else {
+ return [];
+ }
+});
diff --git a/mobile/lib/modules/search/providers/search_result_page.provider.dart b/mobile/lib/modules/search/providers/search_result_page.provider.dart
new file mode 100644
index 0000000000..a27033827d
--- /dev/null
+++ b/mobile/lib/modules/search/providers/search_result_page.provider.dart
@@ -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 {
+ 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? 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((ref) {
+ return SearchResultPageNotifier();
+});
+
+final searchResultGroupByDateTimeProvider = StateProvider((ref) {
+ var assets = ref.watch(searchResultPageProvider).searchResult;
+
+ assets.sortByCompare((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
+ return assets.groupListsBy((element) => DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)));
+});
diff --git a/mobile/lib/modules/search/services/search.service.dart b/mobile/lib/modules/search/services/search.service.dart
index b54df26291..94b47f0a66 100644
--- a/mobile/lib/modules/search/services/search.service.dart
+++ b/mobile/lib/modules/search/services/search.service.dart
@@ -1,6 +1,7 @@
import 'dart:convert';
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/services/network.service.dart';
@@ -36,4 +37,19 @@ class SearchService {
return null;
}
}
+
+ Future?> getCuratedLocation() async {
+ try {
+ var res = await _networkService.getRequest(url: "asset/allLocation");
+
+ List decodedData = jsonDecode(res.toString());
+
+ List result = List.from(decodedData.map((a) => CuratedLocation.fromMap(a)));
+
+ return result;
+ } catch (e) {
+ debugPrint("[ERROR] [getCuratedLocation] ${e.toString()}");
+ throw Error();
+ }
+ }
}
diff --git a/mobile/lib/modules/search/views/search_page.dart b/mobile/lib/modules/search/views/search_page.dart
index 22cb93657d..9c996ba145 100644
--- a/mobile/lib/modules/search/views/search_page.dart
+++ b/mobile/lib/modules/search/views/search_page.dart
@@ -1,7 +1,11 @@
import 'package:auto_route/auto_route.dart';
+import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hive_flutter/hive_flutter.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/ui/search_bar.dart';
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
@@ -15,7 +19,9 @@ class SearchPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ var box = Hive.box(userInfoBox);
final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
+ AsyncValue> curatedLocation = ref.watch(getCuratedLocationProvider);
useEffect(() {
searchFocusNode = FocusNode();
@@ -29,6 +35,53 @@ class SearchPage extends HookConsumerWidget {
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(
appBar: SearchBar(
searchFocusNode: searchFocusNode,
@@ -41,11 +94,17 @@ class SearchPage extends HookConsumerWidget {
},
child: Stack(
children: [
- const Center(
- child: Text("Start typing to search for your photos"),
- ),
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(),
],
@@ -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,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/mobile/lib/modules/search/views/search_result_page.dart b/mobile/lib/modules/search/views/search_result_page.dart
index 749e5d04cd..6f4841861d 100644
--- a/mobile/lib/modules/search/views/search_result_page.dart
+++ b/mobile/lib/modules/search/views/search_result_page.dart
@@ -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/monthly_title_text.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';
class SearchResultPage extends HookConsumerWidget {
@@ -28,7 +28,7 @@ class SearchResultPage extends HookConsumerWidget {
useEffect(() {
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();
}, []);
@@ -37,7 +37,7 @@ class SearchResultPage extends HookConsumerWidget {
searchFocusNode.unfocus();
isNewSearch.value = false;
currentSearchTerm.value = newSearchTerm;
- ref.watch(searchResultPageStateProvider.notifier).search(newSearchTerm);
+ ref.watch(searchResultPageProvider.notifier).search(newSearchTerm);
}
_buildTextField() {
@@ -99,7 +99,7 @@ class SearchResultPage extends HookConsumerWidget {
}
_buildSearchResult() {
- var searchResultPageState = ref.watch(searchResultPageStateProvider);
+ var searchResultPageState = ref.watch(searchResultPageProvider);
var assetGroupByDateTime = ref.watch(searchResultGroupByDateTimeProvider);
if (searchResultPageState.isError) {
diff --git a/mobile/lib/routing/tab_navigation_observer.dart b/mobile/lib/routing/tab_navigation_observer.dart
new file mode 100644
index 0000000000..70f6304156
--- /dev/null
+++ b/mobile/lib/routing/tab_navigation_observer.dart
@@ -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 didChangeTabRoute(TabPageRoute route, TabPageRoute previousRoute) async {
+ // Perform tasks on re-visit to SearchRoute
+ if (route.name == 'SearchRoute') {
+ // Refresh Location State
+ ref.refresh(getCuratedLocationProvider);
+ }
+ }
+}
diff --git a/mobile/test/widget_test.dart b/mobile/test/widget_test.dart
deleted file mode 100644
index 1cdcf6c5bd..0000000000
--- a/mobile/test/widget_test.dart
+++ /dev/null
@@ -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);
- });
-}
diff --git a/server/src/api-v1/asset/asset.controller.ts b/server/src/api-v1/asset/asset.controller.ts
index 8aec5ead2b..444bac7622 100644
--- a/server/src/api-v1/asset/asset.controller.ts
+++ b/server/src/api-v1/asset/asset.controller.ts
@@ -72,6 +72,11 @@ export class AssetController {
return this.assetService.serveFile(authUser, query, res, headers);
}
+ @Get('/allLocation')
+ async getCuratedLocation(@GetAuthUser() authUser: AuthUserDto) {
+ return this.assetService.getCuratedLocation(authUser);
+ }
+
@Get('/searchTerm')
async getAssetSearchTerm(@GetAuthUser() authUser: AuthUserDto) {
return this.assetService.getAssetSearchTerm(authUser);
diff --git a/server/src/api-v1/asset/asset.service.ts b/server/src/api-v1/asset/asset.service.ts
index 502980dd6e..aff091012a 100644
--- a/server/src/api-v1/asset/asset.service.ts
+++ b/server/src/api-v1/asset/asset.service.ts
@@ -303,4 +303,20 @@ export class AssetService {
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;
+ }
}