Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:openapi/api.dart';

class SearchApiRepository extends ApiRepository {
final SearchApi _api;

const SearchApiRepository(this._api);

Future<SearchResponseDto?> search(SearchFilter filter, int page) {
Expand All @@ -15,10 +16,12 @@ class SearchApiRepository extends ApiRepository {
type = AssetTypeEnum.VIDEO;
}

if (filter.context != null && filter.context!.isNotEmpty) {
if ((filter.context != null && filter.context!.isNotEmpty) ||
(filter.assetId != null && filter.assetId!.isNotEmpty)) {
return _api.searchSmart(
SmartSearchDto(
query: filter.context!,
query: filter.context,
queryAssetId: filter.assetId,
language: filter.language,
country: filter.location.country,
state: filter.location.state,
Expand Down
9 changes: 8 additions & 1 deletion mobile/lib/models/search/search_filter.model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ class SearchFilter {
String? description;
String? ocr;
String? language;
String? assetId;
Set<PersonDto> people;
SearchLocationFilter location;
SearchCameraFilter camera;
Expand All @@ -193,6 +194,7 @@ class SearchFilter {
this.description,
this.ocr,
this.language,
this.assetId,
required this.people,
required this.location,
required this.camera,
Expand All @@ -205,6 +207,7 @@ class SearchFilter {
return (context == null || (context != null && context!.isEmpty)) &&
(filename == null || (filename!.isEmpty)) &&
(description == null || (description!.isEmpty)) &&
(assetId == null || (assetId!.isEmpty)) &&
(ocr == null || (ocr!.isEmpty)) &&
people.isEmpty &&
location.country == null &&
Expand All @@ -226,6 +229,7 @@ class SearchFilter {
String? description,
String? language,
String? ocr,
String? assetId,
Set<PersonDto>? people,
SearchLocationFilter? location,
SearchCameraFilter? camera,
Expand All @@ -239,6 +243,7 @@ class SearchFilter {
description: description ?? this.description,
language: language ?? this.language,
ocr: ocr ?? this.ocr,
assetId: assetId ?? this.assetId,
people: people ?? this.people,
location: location ?? this.location,
camera: camera ?? this.camera,
Expand All @@ -250,7 +255,7 @@ class SearchFilter {

@override
String toString() {
return 'SearchFilter(context: $context, filename: $filename, description: $description, language: $language, ocr: $ocr, people: $people, location: $location, camera: $camera, date: $date, display: $display, mediaType: $mediaType)';
return 'SearchFilter(context: $context, filename: $filename, description: $description, language: $language, ocr: $ocr, people: $people, location: $location, camera: $camera, date: $date, display: $display, mediaType: $mediaType, assetId: $assetId)';
}

@override
Expand All @@ -262,6 +267,7 @@ class SearchFilter {
other.description == description &&
other.language == language &&
other.ocr == ocr &&
other.assetId == assetId &&
other.people == people &&
other.location == location &&
other.camera == camera &&
Expand All @@ -277,6 +283,7 @@ class SearchFilter {
description.hashCode ^
language.hashCode ^
ocr.hashCode ^
assetId.hashCode ^
people.hashCode ^
location.hashCode ^
camera.hashCode ^
Expand Down
7 changes: 6 additions & 1 deletion mobile/lib/pages/common/tab_shell.page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
Expand Down Expand Up @@ -77,7 +78,7 @@ class _TabShellPageState extends ConsumerState<TabShellPage> {
}

return AutoTabsRouter(
routes: [const MainTimelineRoute(), DriftSearchRoute(), const DriftAlbumsRoute(), const DriftLibraryRoute()],
routes: const [MainTimelineRoute(), DriftSearchRoute(), DriftAlbumsRoute(), DriftLibraryRoute()],
duration: const Duration(milliseconds: 600),
transitionBuilder: (context, child, animation) => FadeTransition(opacity: animation, child: child),
builder: (context, child) {
Expand Down Expand Up @@ -114,6 +115,10 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
ref.invalidate(driftMemoryFutureProvider);
}

if (router.activeIndex != 1 && index == 1) {
ref.read(searchPreFilterProvider.notifier).clear();
}

// On Search page tapped
if (router.activeIndex == 1 && index == 1) {
ref.read(searchInputFocusProvider).requestFocus();
Expand Down
10 changes: 5 additions & 5 deletions mobile/lib/presentation/pages/search/drift_search.page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,14 @@ import 'package:immich_mobile/widgets/search/search_filter/search_filter_utils.d

@RoutePage()
class DriftSearchPage extends HookConsumerWidget {
const DriftSearchPage({super.key, this.preFilter});

final SearchFilter? preFilter;
const DriftSearchPage({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
final textSearchType = useState<TextSearchType>(TextSearchType.context);
final searchHintText = useState<String>('sunrise_on_the_beach'.t(context: context));
final textSearchController = useTextEditingController();
final preFilter = ref.watch(searchPreFilterProvider);
final filter = useState<SearchFilter>(
SearchFilter(
people: preFilter?.people ?? {},
Expand All @@ -49,6 +48,7 @@ class DriftSearchPage extends HookConsumerWidget {
display: preFilter?.display ?? SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false),
mediaType: preFilter?.mediaType ?? AssetType.other,
language: "${context.locale.languageCode}-${context.locale.countryCode}",
assetId: preFilter?.assetId,
),
);

Expand Down Expand Up @@ -109,8 +109,8 @@ class DriftSearchPage extends HookConsumerWidget {
Future.delayed(Duration.zero, () {
search();

if (preFilter!.location.city != null) {
locationCurrentFilterWidget.value = Text(preFilter!.location.city!, style: context.textTheme.labelLarge);
if (preFilter.location.city != null) {
locationCurrentFilterWidget.value = Text(preFilter.location.city!, style: context.textTheme.labelLarge);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ import 'package:immich_mobile/domain/services/search.service.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/providers/infrastructure/search.provider.dart';

final searchPreFilterProvider = NotifierProvider<SearchFilterProvider, SearchFilter?>(SearchFilterProvider.new);

class SearchFilterProvider extends Notifier<SearchFilter?> {
@override
SearchFilter? build() {
return null;
}

void setFilter(SearchFilter? filter) {
state = filter;
}

void clear() {
state = null;
}
}

final paginatedSearchProvider = StateNotifierProvider<PaginatedSearchNotifier, SearchResult>(
(ref) => PaginatedSearchNotifier(ref.watch(searchServiceProvider)),
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'dart:async';

import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/routing/router.dart';

class SimilarPhotosActionButton extends ConsumerWidget {
final String assetId;

const SimilarPhotosActionButton({super.key, required this.assetId});

void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
return;
}

ref.invalidate(assetViewerProvider);
ref
.read(searchPreFilterProvider.notifier)
.setFilter(
SearchFilter(
assetId: assetId,
people: {},
location: SearchLocationFilter(),
camera: SearchCameraFilter(),
date: SearchDateFilter(),
display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false),
mediaType: AssetType.image,
),
);
unawaited(context.router.popAndPush(const DriftSearchRoute()));
}

@override
Widget build(BuildContext context, WidgetRef ref) {
return BaseActionButton(
iconData: Icons.compare,
label: "view_similar_photos".t(context: context),
onPressed: () => _onTap(context, ref),
maxWidth: 100,
);
}
}
31 changes: 4 additions & 27 deletions mobile/lib/routing/router.gr.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion mobile/lib/utils/action_button.utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_al
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
Expand Down Expand Up @@ -59,7 +60,8 @@ enum ActionButtonType {
upload,
removeFromAlbum,
unstack,
likeActivity;
likeActivity,
similarPhotos;

bool shouldShow(ActionButtonContext context) {
return switch (this) {
Expand Down Expand Up @@ -123,6 +125,9 @@ enum ActionButtonType {
context.currentAlbum != null &&
context.currentAlbum!.isActivityEnabled &&
context.currentAlbum!.isShared,
ActionButtonType.similarPhotos =>
!context.isInLockedView && //
context.asset.hasRemote,
};
}

Expand All @@ -147,6 +152,7 @@ enum ActionButtonType {
),
ActionButtonType.likeActivity => const LikeActivityActionButton(),
ActionButtonType.unstack => UnStackActionButton(source: context.source),
ActionButtonType.similarPhotos => SimilarPhotosActionButton(assetId: (context.asset as RemoteAsset).id),
};
}
}
Expand Down
54 changes: 53 additions & 1 deletion mobile/test/utils/action_button_utils_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,42 @@ void main() {
});
});

group('similar photos button', () {
test('should show when not locked and has remote', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
isStacked: false,
currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline,
);

expect(ActionButtonType.similarPhotos.shouldShow(context), isTrue);
});

test('should not show when in locked view', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: true,
currentAlbum: null,
isStacked: false,
advancedTroubleshooting: false,
source: ActionSource.timeline,
);

expect(ActionButtonType.similarPhotos.shouldShow(context), isFalse);
});
});

group('trash button', () {
test('should show when owner, not locked, has remote, and trash enabled', () {
final remoteAsset = createRemoteAsset();
Expand Down Expand Up @@ -777,6 +813,8 @@ void main() {

test('should build correct widget for each button type', () {
for (final buttonType in ActionButtonType.values) {
var buttonContext = context;

if (buttonType == ActionButtonType.removeFromAlbum) {
final album = createRemoteAlbum();
final contextWithAlbum = ActionButtonContext(
Expand All @@ -792,6 +830,20 @@ void main() {
);
final widget = buttonType.buildButton(contextWithAlbum);
expect(widget, isA<Widget>());
} else if (buttonType == ActionButtonType.similarPhotos) {
final contextWithAlbum = ActionButtonContext(
asset: createRemoteAsset(),
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
final widget = buttonType.buildButton(contextWithAlbum);
expect(widget, isA<Widget>());
} else if (buttonType == ActionButtonType.unstack) {
final album = createRemoteAlbum();
final contextWithAlbum = ActionButtonContext(
Expand All @@ -808,7 +860,7 @@ void main() {
final widget = buttonType.buildButton(contextWithAlbum);
expect(widget, isA<Widget>());
} else {
final widget = buttonType.buildButton(context);
final widget = buttonType.buildButton(buttonContext);
expect(widget, isA<Widget>());
}
}
Expand Down
Loading