diff --git a/README.md b/README.md index 291aaac465..9c6c9f6d1f 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,28 @@

- + License: MIT + Star on Github + + Android Build + + + iOS Build + + + Build Status + + +
+
+
+
+ +

+ +

# 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; + } }