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:
Alex 2022-03-16 10:19:31 -05:00 committed by GitHub
parent 348d395b21
commit 8c7080eaef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 434 additions and 165 deletions

View 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;
}
}

View file

@ -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;
}
}

View file

@ -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)));
});

View file

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

View file

@ -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)));
});

View file

@ -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();
}
}
}

View file

@ -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,
),
),
),
),
],
),
),
),
);
}
}

View file

@ -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) {