mirror of
https://github.com/immich-app/immich.git
synced 2024-12-28 06:31:58 +00: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
15 changed files with 434 additions and 165 deletions
25
README.md
25
README.md
|
@ -1,13 +1,28 @@
|
|||
<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>
|
||||
|
||||
# Immich
|
||||
|
||||
| Android Build | iOS Build | Server Docker Build |
|
||||
| --- | --- | --- |
|
||||
| [![Build Status](<https://immichci.little-home.net/app/rest/builds/buildType:(id:Immich_BuildAndroidAndGetArtifact)/statusIcon>)](https://immichci.little-home.net/viewType.html?buildTypeId=Immich_BuildAndroidAndGetArtifact&guest=1) | [![Build Status](<https://immichci.little-home.net/app/rest/builds/buildType:(id:Immich_BuildAndroidAndGetArtifact)/statusIcon>)](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)
|
||||
|
|
|
@ -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<ImmichApp> with WidgetsBindingObserv
|
|||
),
|
||||
),
|
||||
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 '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<ImmichAsset> 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<ImmichAsset>? 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<String, dynamic> map) {
|
||||
return SearchresultPageState(
|
||||
factory SearchResultPageState.fromMap(Map<String, dynamic> 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<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: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<String> searchSuggestion;
|
||||
final List<String> userSuggestedSearchTerms;
|
||||
|
||||
SearchPageState({
|
||||
required this.searchTerm,
|
||||
required this.isSearchEnabled,
|
||||
required this.searchSuggestion,
|
||||
required this.userSuggestedSearchTerms,
|
||||
});
|
||||
|
||||
SearchPageState copyWith({
|
||||
String? searchTerm,
|
||||
bool? isSearchEnabled,
|
||||
List<String>? searchSuggestion,
|
||||
List<String>? userSuggestedSearchTerms,
|
||||
}) {
|
||||
return SearchPageState(
|
||||
searchTerm: searchTerm ?? this.searchTerm,
|
||||
isSearchEnabled: isSearchEnabled ?? this.isSearchEnabled,
|
||||
searchSuggestion: searchSuggestion ?? this.searchSuggestion,
|
||||
userSuggestedSearchTerms: userSuggestedSearchTerms ?? this.userSuggestedSearchTerms,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'searchTerm': searchTerm,
|
||||
'isSearchEnabled': isSearchEnabled,
|
||||
'searchSuggestion': searchSuggestion,
|
||||
'userSuggestedSearchTerms': userSuggestedSearchTerms,
|
||||
};
|
||||
}
|
||||
|
||||
factory SearchPageState.fromMap(Map<String, dynamic> map) {
|
||||
return SearchPageState(
|
||||
searchTerm: map['searchTerm'] ?? '',
|
||||
isSearchEnabled: map['isSearchEnabled'] ?? false,
|
||||
searchSuggestion: List<String>.from(map['searchSuggestion']),
|
||||
userSuggestedSearchTerms: List<String>.from(map['userSuggestedSearchTerms']),
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory SearchPageState.fromJson(String source) => SearchPageState.fromMap(json.decode(source));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SearchPageState(searchTerm: $searchTerm, isSearchEnabled: $isSearchEnabled, searchSuggestion: $searchSuggestion, userSuggestedSearchTerms: $userSuggestedSearchTerms)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
final listEquals = const DeepCollectionEquality().equals;
|
||||
|
||||
return other is SearchPageState &&
|
||||
other.searchTerm == searchTerm &&
|
||||
other.isSearchEnabled == isSearchEnabled &&
|
||||
listEquals(other.searchSuggestion, searchSuggestion) &&
|
||||
listEquals(other.userSuggestedSearchTerms, userSuggestedSearchTerms);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return searchTerm.hashCode ^
|
||||
isSearchEnabled.hashCode ^
|
||||
searchSuggestion.hashCode ^
|
||||
userSuggestedSearchTerms.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
|
||||
SearchPageStateNotifier()
|
||||
: super(
|
||||
|
@ -129,3 +53,14 @@ class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
|
|||
final searchPageStateProvider = StateNotifierProvider<SearchPageStateNotifier, SearchPageState>((ref) {
|
||||
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 '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<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: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<List<CuratedLocation>> 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
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);
|
||||
}
|
||||
|
||||
@Get('/allLocation')
|
||||
async getCuratedLocation(@GetAuthUser() authUser: AuthUserDto) {
|
||||
return this.assetService.getCuratedLocation(authUser);
|
||||
}
|
||||
|
||||
@Get('/searchTerm')
|
||||
async getAssetSearchTerm(@GetAuthUser() authUser: AuthUserDto) {
|
||||
return this.assetService.getAssetSearchTerm(authUser);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue