Skip to content

Commit 5e1edc2

Browse files
wuzihao051119shenlong-tanwen
authored andcommitted
feat(mobile): drift search page
1 parent 59e7754 commit 5e1edc2

File tree

16 files changed

+1306
-112
lines changed

16 files changed

+1306
-112
lines changed

mobile/analysis_options.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ custom_lint:
106106
- lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine
107107
- test/modules/utils/openapi_patching_test.dart # filename is self-explanatory...
108108
- lib/domain/services/sync_stream.service.dart # Making sure to comply with the type from database
109+
- lib/domain/services/search.service.dart
109110

110111
# refactor
111112
- lib/models/map/map_marker.model.dart
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import 'package:collection/collection.dart';
2+
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
3+
4+
class SearchResult {
5+
final List<BaseAsset> assets;
6+
final int? nextPage;
7+
8+
const SearchResult({
9+
required this.assets,
10+
this.nextPage,
11+
});
12+
13+
int get totalAssets => assets.length;
14+
15+
SearchResult copyWith({
16+
List<BaseAsset>? assets,
17+
int? nextPage,
18+
}) {
19+
return SearchResult(
20+
assets: assets ?? this.assets,
21+
nextPage: nextPage ?? this.nextPage,
22+
);
23+
}
24+
25+
@override
26+
String toString() => 'SearchResult(assets: $assets, nextPage: $nextPage)';
27+
28+
@override
29+
bool operator ==(covariant SearchResult other) {
30+
if (identical(this, other)) return true;
31+
final listEquals = const DeepCollectionEquality().equals;
32+
33+
return listEquals(other.assets, assets) && other.nextPage == nextPage;
34+
}
35+
36+
@override
37+
int get hashCode => assets.hashCode ^ nextPage.hashCode;
38+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
2+
import 'package:immich_mobile/domain/models/search_result.model.dart';
3+
import 'package:immich_mobile/extensions/string_extensions.dart';
4+
import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart';
5+
import 'package:immich_mobile/models/search/search_filter.model.dart';
6+
import 'package:logging/logging.dart';
7+
import 'package:openapi/api.dart' as api show AssetVisibility;
8+
import 'package:openapi/api.dart' hide AssetVisibility;
9+
10+
class SearchService {
11+
final _log = Logger("SearchService");
12+
final SearchApiRepository _searchApiRepository;
13+
14+
SearchService(this._searchApiRepository);
15+
16+
Future<List<String>?> getSearchSuggestions(
17+
SearchSuggestionType type, {
18+
String? country,
19+
String? state,
20+
String? make,
21+
String? model,
22+
}) async {
23+
try {
24+
return await _searchApiRepository.getSearchSuggestions(
25+
type,
26+
country: country,
27+
state: state,
28+
make: make,
29+
model: model,
30+
);
31+
} catch (e) {
32+
_log.warning("Failed to get search suggestions", e);
33+
}
34+
return [];
35+
}
36+
37+
Future<SearchResult?> search(SearchFilter filter, int page) async {
38+
try {
39+
final response = await _searchApiRepository.search(filter, page);
40+
41+
if (response == null || response.assets.items.isEmpty) {
42+
return null;
43+
}
44+
45+
return SearchResult(
46+
assets: response.assets.items.map((e) => e.toDto()).toList(),
47+
nextPage: response.assets.nextPage?.toInt(),
48+
);
49+
} catch (error, stackTrace) {
50+
_log.severe("Failed to search for assets", error, stackTrace);
51+
}
52+
return null;
53+
}
54+
}
55+
56+
extension on AssetResponseDto {
57+
RemoteAsset toDto() {
58+
return RemoteAsset(
59+
id: id,
60+
name: originalFileName,
61+
checksum: checksum,
62+
createdAt: fileCreatedAt,
63+
updatedAt: updatedAt,
64+
ownerId: ownerId,
65+
visibility: switch (visibility) {
66+
api.AssetVisibility.timeline => AssetVisibility.timeline,
67+
api.AssetVisibility.hidden => AssetVisibility.hidden,
68+
api.AssetVisibility.archive => AssetVisibility.archive,
69+
api.AssetVisibility.locked => AssetVisibility.locked,
70+
_ => AssetVisibility.timeline,
71+
},
72+
durationInSeconds: duration.toDuration()?.inSeconds ?? 0,
73+
height: exifInfo?.exifImageHeight?.toInt(),
74+
width: exifInfo?.exifImageWidth?.toInt(),
75+
isFavorite: isFavorite,
76+
livePhotoVideoId: livePhotoVideoId,
77+
thumbHash: thumbhash,
78+
localId: null,
79+
type: type.toAssetType(),
80+
);
81+
}
82+
}
83+
84+
extension on AssetTypeEnum {
85+
AssetType toAssetType() => switch (this) {
86+
AssetTypeEnum.IMAGE => AssetType.image,
87+
AssetTypeEnum.VIDEO => AssetType.video,
88+
AssetTypeEnum.AUDIO => AssetType.audio,
89+
AssetTypeEnum.OTHER => AssetType.other,
90+
_ => throw Exception('Unknown AssetType value: $this'),
91+
};
92+
}

mobile/lib/domain/services/timeline.service.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ class TimelineFactory {
6262

6363
TimelineService video(String userId) =>
6464
TimelineService(_timelineRepository.video(userId, groupBy));
65+
66+
TimelineService fromAssets(List<BaseAsset> assets) =>
67+
TimelineService(_timelineRepository.fromAssets(assets));
6568
}
6669

6770
class TimelineService {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'
2+
hide AssetVisibility;
3+
import 'package:immich_mobile/infrastructure/repositories/api.repository.dart';
4+
import 'package:immich_mobile/models/search/search_filter.model.dart';
5+
import 'package:openapi/api.dart';
6+
7+
class SearchApiRepository extends ApiRepository {
8+
final SearchApi _api;
9+
const SearchApiRepository(this._api);
10+
11+
Future<SearchResponseDto?> search(SearchFilter filter, int page) {
12+
AssetTypeEnum? type;
13+
if (filter.mediaType.index == AssetType.image.index) {
14+
type = AssetTypeEnum.IMAGE;
15+
} else if (filter.mediaType.index == AssetType.video.index) {
16+
type = AssetTypeEnum.VIDEO;
17+
}
18+
19+
if (filter.context != null && filter.context!.isNotEmpty) {
20+
return _api.searchSmart(
21+
SmartSearchDto(
22+
query: filter.context!,
23+
language: filter.language,
24+
country: filter.location.country,
25+
state: filter.location.state,
26+
city: filter.location.city,
27+
make: filter.camera.make,
28+
model: filter.camera.model,
29+
takenAfter: filter.date.takenAfter,
30+
takenBefore: filter.date.takenBefore,
31+
visibility: filter.display.isArchive
32+
? AssetVisibility.archive
33+
: AssetVisibility.timeline,
34+
isFavorite: filter.display.isFavorite ? true : null,
35+
isNotInAlbum: filter.display.isNotInAlbum ? true : null,
36+
personIds: filter.people.map((e) => e.id).toList(),
37+
type: type,
38+
page: page,
39+
size: 1000,
40+
),
41+
);
42+
}
43+
44+
return _api.searchAssets(
45+
MetadataSearchDto(
46+
originalFileName: filter.filename != null && filter.filename!.isNotEmpty
47+
? filter.filename
48+
: null,
49+
country: filter.location.country,
50+
description:
51+
filter.description != null && filter.description!.isNotEmpty
52+
? filter.description
53+
: null,
54+
state: filter.location.state,
55+
city: filter.location.city,
56+
make: filter.camera.make,
57+
model: filter.camera.model,
58+
takenAfter: filter.date.takenAfter,
59+
takenBefore: filter.date.takenBefore,
60+
visibility: filter.display.isArchive
61+
? AssetVisibility.archive
62+
: AssetVisibility.timeline,
63+
isFavorite: filter.display.isFavorite ? true : null,
64+
isNotInAlbum: filter.display.isNotInAlbum ? true : null,
65+
personIds: filter.people.map((e) => e.id).toList(),
66+
type: type,
67+
page: page,
68+
size: 1000,
69+
),
70+
);
71+
}
72+
73+
Future<List<String>?> getSearchSuggestions(
74+
SearchSuggestionType type, {
75+
String? country,
76+
String? state,
77+
String? make,
78+
String? model,
79+
}) =>
80+
_api.getSearchSuggestions(
81+
type,
82+
country: country,
83+
state: state,
84+
make: make,
85+
model: model,
86+
);
87+
}

mobile/lib/infrastructure/repositories/timeline.repository.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,12 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
251251
.get();
252252
}
253253

254+
TimelineQuery fromAssets(List<BaseAsset> assets) => (
255+
bucketSource: () => Stream.value(_generateBuckets(assets.length)),
256+
assetSource: (offset, count) =>
257+
Future.value(assets.skip(offset).take(count).toList()),
258+
);
259+
254260
TimelineQuery remote(String ownerId, GroupAssetsBy groupBy) =>
255261
_remoteQueryBuilder(
256262
filter: (row) =>

mobile/lib/pages/common/tab_shell.page.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ class TabShellPage extends ConsumerWidget {
117117
return AutoTabsRouter(
118118
routes: [
119119
const MainTimelineRoute(),
120-
SearchRoute(),
120+
DriftSearchRoute(),
121121
const DriftAlbumsRoute(),
122122
const DriftLibraryRoute(),
123123
],

0 commit comments

Comments
 (0)