1
0
forked from Cutlery/immich

Compare commits

..

23 Commits

Author SHA1 Message Date
Alex Tran 65b865ad08 Merge branch 'main' of github.com:immich-app/immich into refactor/immich-image-provider 2024-02-13 15:31:37 -06:00
martyfuhry 9b4a770b9d refactor(mobile): Immich image provider (#7016)
* Adds image provider

* uses image provider

* wip load preview

* wip everything but activity asset thumbnail needs some help with a remote id

* Immich provider used in gallery

* First draft of the immich image provider, working nicely!

* Removed OriginalImageProvider

* Fixes for thumbnails

* feat(mobile): thumbhash support (#7028)

* feat(mobile): thumbhash support

* perf(mobile): store bmp thumbhash bytes in Isar

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>

* Uses octoimage for fade in and placeholders

* fixes thumbnails, removes unused values, adds better thumbnail size

* removes thumbhash support for now

* Forgot one thumbhash removal

* Use big thumbnail for local image on ios

* fix(mobile): Multipart image loading for iOS double swipe (#7064)

* uses local thumb first

* Multipart thumbnail

* Clean up file delete

* await file delete

* Fynn's comments, made thumbnail smaller and doesn't crash on erroring out on thumbnail

* lint

---------

Co-authored-by: Marty Fuhry <marty@fuhry.farm>
Co-authored-by: Alex <alex.tran1502@gmail.com>

* Moves http client to global private place for reuse

* Got rid of usePreview for local image providers since we always show a thumbnail anyway first

* linter

---------

Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com>
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: Marty Fuhry <marty@fuhry.farm>
2024-02-13 15:30:32 -06:00
Alex 4b3f8d1946 feat: Search filtering logic (#6968)
* commit

* controller/service/repository logic

* use enum

* openapi

* suggest people

* suggest place/camera

* cursor hover

* refactor

* Add try catch

* Remove get people with name service

* Remove deadcode

* people selection

* People placement

* sort people

* Update server/src/domain/repositories/metadata.repository.ts

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>

* pr feedback

* styling

* done

* open api

* fix test

* use string type

* remmove bad merge

* use correct type

* fix test

* fix lint

* remove unused code

* remove unused code

* pr feedback

* pr feedback

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-02-13 19:54:58 +00:00
Jason Rasmussen 0c45f51a29 fix(deps): bump oazapfts to v6 (#7093)
chore: bump to v6
2024-02-13 13:37:57 -06:00
Marty Fuhry 21ea5d8d85 linter 2024-02-13 08:57:07 -05:00
Marty Fuhry 17b6e0250a Got rid of usePreview for local image providers since we always show a thumbnail anyway first 2024-02-13 08:31:10 -05:00
Marty Fuhry a033d751b3 Moves http client to global private place for reuse 2024-02-12 18:20:31 -05:00
martyfuhry ea293dfe06 fix(mobile): Multipart image loading for iOS double swipe (#7064)
* uses local thumb first

* Multipart thumbnail

* Clean up file delete

* await file delete

* Fynn's comments, made thumbnail smaller and doesn't crash on erroring out on thumbnail

* lint

---------

Co-authored-by: Marty Fuhry <marty@fuhry.farm>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-02-12 16:09:45 -06:00
Alex aea7651c75 Use big thumbnail for local image on ios 2024-02-12 15:09:13 -06:00
Marty Fuhry 74ff8f1e56 Forgot one thumbhash removal 2024-02-12 13:48:03 -05:00
Marty Fuhry c544526400 removes thumbhash support for now 2024-02-12 13:31:01 -05:00
Marty Fuhry 2f8cb30c34 fixes thumbnails, removes unused values, adds better thumbnail size 2024-02-12 13:21:57 -05:00
Marty Fuhry 4140a66cab Uses octoimage for fade in and placeholders 2024-02-12 10:48:48 -05:00
Marty Fuhry bf20c363fd Merge branch 'refactor/immich-image-provider' of github.com:immich-app/immich into refactor/immich-image-provider 2024-02-12 09:08:19 -05:00
shenlong e270ae0017 feat(mobile): thumbhash support (#7028)
* feat(mobile): thumbhash support

* perf(mobile): store bmp thumbhash bytes in Isar

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2024-02-12 09:07:42 -05:00
Marty Fuhry e2a25742f4 Fixes for thumbnails 2024-02-11 16:45:38 -05:00
Marty Fuhry 61e32f4bf7 Removed OriginalImageProvider 2024-02-10 15:07:40 -05:00
Marty Fuhry 02075bc52e First draft of the immich image provider, working nicely! 2024-02-10 11:06:32 -05:00
Marty Fuhry 230c7dae0f Immich provider used in gallery 2024-02-10 10:09:10 -05:00
Marty Fuhry 0e672a5b24 wip everything but activity asset thumbnail needs some help with a remote id 2024-02-10 08:49:57 -05:00
Marty Fuhry c1452a359c wip load preview 2024-02-10 06:19:16 -05:00
Marty Fuhry 758b5cd6c2 uses image provider 2024-02-09 21:13:24 -05:00
Marty Fuhry 28413fedcc Adds image provider 2024-02-09 21:13:20 -05:00
51 changed files with 1698 additions and 1126 deletions
+2 -2
View File
@@ -46,8 +46,8 @@
"dev": true,
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@oazapfts/runtime": "^1.0.0",
"@types/node": "^20.11.0",
"oazapfts": "^5.1.4",
"typescript": "^5.3.3"
},
"peerDependencies": {
@@ -6111,8 +6111,8 @@
"@immich/sdk": {
"version": "file:../open-api/typescript-sdk",
"requires": {
"@oazapfts/runtime": "^1.0.0",
"@types/node": "^20.11.0",
"oazapfts": "^5.1.4",
"typescript": "^5.3.3"
}
},
+1 -1
View File
@@ -23,8 +23,8 @@
"dev": true,
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@oazapfts/runtime": "^1.0.0",
"@types/node": "^20.11.0",
"oazapfts": "^5.1.4",
"typescript": "^5.3.3"
},
"peerDependencies": {
+1 -1
View File
@@ -180,4 +180,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d
COCOAPODS: 1.11.3
COCOAPODS: 1.12.1
@@ -3,8 +3,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/datetime_extensions.dart';
import 'package:immich_mobile/modules/activities/models/activity.model.dart';
import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
class ActivityTile extends HookConsumerWidget {
@@ -106,7 +106,10 @@ class _ActivityAssetThumbnail extends StatelessWidget {
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(4)),
image: DecorationImage(
image: ImmichImage.remoteThumbnailProviderForId(assetId),
image: ImmichRemoteImageProvider(
assetId: assetId,
isThumbnail: true,
),
fit: BoxFit.cover,
),
),
@@ -45,7 +45,7 @@ class AlbumThumbnailCard extends StatelessWidget {
);
}
buildAlbumThumbnail() => ImmichImage(
buildAlbumThumbnail() => ImmichImage.thumbnail(
album.thumbnail.value,
width: cardSize,
height: cardSize,
@@ -16,7 +16,11 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
},
child: Stack(
children: [
ImmichImage(asset, width: 500, height: 500),
ImmichImage.thumbnail(
asset,
width: 500,
height: 500,
),
],
),
);
@@ -72,7 +72,7 @@ class SharingPage extends HookConsumerWidget {
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
leading: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: ImmichImage(
child: ImmichImage.thumbnail(
album.thumbnail.value,
width: 60,
height: 60,
@@ -0,0 +1,106 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:photo_manager/photo_manager.dart';
/// The local image provider for an asset
/// Only viable
class ImmichLocalImageProvider extends ImageProvider<Asset> {
final Asset asset;
ImmichLocalImageProvider({
required this.asset,
}) : assert(asset.local != null, 'Only usable when asset.local is set');
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
/// that describes the precise image to load.
@override
Future<Asset> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture(asset);
}
@override
ImageStreamCompleter loadImage(Asset key, ImageDecoderCallback decode) {
final chunkEvents = StreamController<ImageChunkEvent>();
return MultiImageStreamCompleter(
codec: _codec(key, decode, chunkEvents),
scale: 1.0,
chunkEvents: chunkEvents.stream,
informationCollector: () sync* {
yield ErrorDescription(asset.fileName);
},
);
}
// Streams in each stage of the image as we ask for it
Stream<ui.Codec> _codec(
Asset key,
ImageDecoderCallback decode,
StreamController<ImageChunkEvent> chunkEvents,
) async* {
// Load a small thumbnail
final thumbBytes = await asset.local?.thumbnailDataWithSize(
const ThumbnailSize.square(256),
quality: 80,
);
if (thumbBytes != null) {
final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes);
final codec = await decode(buffer);
yield codec;
} else {
debugPrint("Loading thumb for ${asset.fileName} failed");
}
if (asset.isImage) {
/// Using 2K thumbnail for local iOS image to avoid double swiping issue
if (Platform.isIOS) {
final largeImageBytes = await asset.local
?.thumbnailDataWithSize(const ThumbnailSize(3840, 2160));
if (largeImageBytes == null) {
throw StateError(
"Loading thumb for local photo ${asset.fileName} failed",
);
}
final buffer = await ui.ImmutableBuffer.fromUint8List(largeImageBytes);
final codec = await decode(buffer);
yield codec;
} else {
// Use the original file for Android
final File? file = await asset.local?.originFile;
if (file == null) {
throw StateError("Opening file for asset ${asset.fileName} failed");
}
try {
final buffer = await ui.ImmutableBuffer.fromFilePath(file.path);
final codec = await decode(buffer);
yield codec;
} catch (error) {
throw StateError("Loading asset ${asset.fileName} failed");
} finally {
if (Platform.isIOS) {
// Clean up this file
await file.delete();
}
}
}
}
chunkEvents.close();
}
@override
bool operator ==(Object other) {
if (other is! ImmichLocalImageProvider) return false;
if (identical(this, other)) return true;
return asset == other.asset;
}
@override
int get hashCode => asset.hashCode;
}
@@ -0,0 +1,145 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:openapi/api.dart' as api;
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
/// Our Image Provider HTTP client to make the request
final _httpClient = HttpClient()..autoUncompress = false;
/// The remote image provider
class ImmichRemoteImageProvider extends ImageProvider<String> {
/// The [Asset.remoteId] of the asset to fetch
final String assetId;
// If this is a thumbnail, we stop at loading the
// smallest version of the remote image
final bool isThumbnail;
ImmichRemoteImageProvider({
required this.assetId,
this.isThumbnail = false,
});
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
/// that describes the precise image to load.
@override
Future<String> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture('$assetId,$isThumbnail');
}
@override
ImageStreamCompleter loadImage(String key, ImageDecoderCallback decode) {
final id = key.split(',').first;
final chunkEvents = StreamController<ImageChunkEvent>();
return MultiImageStreamCompleter(
codec: _codec(id, decode, chunkEvents),
scale: 1.0,
chunkEvents: chunkEvents.stream,
);
}
/// Whether to show the original file or load a compressed version
bool get _useOriginal => Store.get(
AppSettingsEnum.loadOriginal.storeKey,
AppSettingsEnum.loadOriginal.defaultValue,
);
/// Whether to load the preview thumbnail first or not
bool get _loadPreview => Store.get(
AppSettingsEnum.loadPreview.storeKey,
AppSettingsEnum.loadPreview.defaultValue,
);
// Streams in each stage of the image as we ask for it
Stream<ui.Codec> _codec(
String key,
ImageDecoderCallback decode,
StreamController<ImageChunkEvent> chunkEvents,
) async* {
// Load a preview to the chunk events
if (_loadPreview || isThumbnail) {
final preview = getThumbnailUrlForRemoteId(
assetId,
type: api.ThumbnailFormat.WEBP,
);
yield await _loadFromUri(
Uri.parse(preview),
decode,
chunkEvents,
);
}
// Guard thumnbail rendering
if (isThumbnail) {
await chunkEvents.close();
return;
}
// Load the higher resolution version of the image
final url = getThumbnailUrlForRemoteId(
assetId,
type: api.ThumbnailFormat.JPEG,
);
final codec = await _loadFromUri(Uri.parse(url), decode, chunkEvents);
yield codec;
// Load the final remote image
if (_useOriginal) {
// Load the original image
final url = getImageUrlFromId(assetId);
final codec = await _loadFromUri(Uri.parse(url), decode, chunkEvents);
yield codec;
}
await chunkEvents.close();
}
// Loads the codec from the URI and sends the events to the [chunkEvents] stream
Future<ui.Codec> _loadFromUri(
Uri uri,
ImageDecoderCallback decode,
StreamController<ImageChunkEvent> chunkEvents,
) async {
final request = await _httpClient.getUrl(uri);
request.headers.add(
'x-immich-user-token',
Store.get(StoreKey.accessToken),
);
final response = await request.close();
// Chunks of the completed image can be shown
final data = await consolidateHttpClientResponseBytes(
response,
onBytesReceived: (cumulative, total) {
chunkEvents.add(
ImageChunkEvent(
cumulativeBytesLoaded: cumulative,
expectedTotalBytes: total,
),
);
},
);
// Decode the response
final buffer = await ui.ImmutableBuffer.fromUint8List(data);
return decode(buffer);
}
@override
bool operator ==(Object other) {
if (other is! ImmichRemoteImageProvider) return false;
if (identical(this, other)) return true;
return assetId == other.assetId;
}
@override
int get hashCode => assetId.hashCode;
}
@@ -0,0 +1,104 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart';
import 'package:openapi/api.dart' as api;
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
/// The remote image provider
class ImmichRemoteThumbnailProvider extends ImageProvider<String> {
/// The [Asset.remoteId] of the asset to fetch
final String assetId;
/// Our HTTP client to make the request
final _httpClient = HttpClient()..autoUncompress = false;
ImmichRemoteThumbnailProvider({
required this.assetId,
});
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
/// that describes the precise image to load.
@override
Future<String> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture(assetId);
}
@override
ImageStreamCompleter loadImage(String key, ImageDecoderCallback decode) {
final chunkEvents = StreamController<ImageChunkEvent>();
return MultiImageStreamCompleter(
codec: _codec(key, decode, chunkEvents),
scale: 1.0,
chunkEvents: chunkEvents.stream,
);
}
// Streams in each stage of the image as we ask for it
Stream<ui.Codec> _codec(
String key,
ImageDecoderCallback decode,
StreamController<ImageChunkEvent> chunkEvents,
) async* {
// Load a preview to the chunk events
final preview = getThumbnailUrlForRemoteId(
assetId,
type: api.ThumbnailFormat.WEBP,
);
yield await _loadFromUri(
Uri.parse(preview),
decode,
chunkEvents,
);
await chunkEvents.close();
}
// Loads the codec from the URI and sends the events to the [chunkEvents] stream
Future<ui.Codec> _loadFromUri(
Uri uri,
ImageDecoderCallback decode,
StreamController<ImageChunkEvent> chunkEvents,
) async {
final request = await _httpClient.getUrl(uri);
request.headers.add(
'x-immich-user-token',
Store.get(StoreKey.accessToken),
);
final response = await request.close();
// Chunks of the completed image can be shown
final data = await consolidateHttpClientResponseBytes(
response,
onBytesReceived: (cumulative, total) {
chunkEvents.add(
ImageChunkEvent(
cumulativeBytesLoaded: cumulative,
expectedTotalBytes: total,
),
);
},
);
// Decode the response
final buffer = await ui.ImmutableBuffer.fromUint8List(data);
return decode(buffer);
}
@override
bool operator ==(Object other) {
if (other is! ImmichRemoteImageProvider) return false;
if (identical(this, other)) return true;
return assetId == other.assetId;
}
@override
int get hashCode => assetId.hashCode;
}
@@ -25,7 +25,6 @@ import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
import 'package:immich_mobile/shared/cache/original_image_provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
@@ -41,8 +40,6 @@ import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_scale_state.da
import 'package:immich_mobile/shared/ui/photo_view/src/utils/photo_view_hero_attributes.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:isar/isar.dart';
import 'package:openapi/api.dart' show ThumbnailFormat;
@@ -78,7 +75,6 @@ class GalleryViewerPage extends HookConsumerWidget {
final isPlayingMotionVideo = useState(false);
final isPlayingVideo = useState(false);
Offset? localPosition;
final header = {"x-immich-user-token": Store.get(StoreKey.accessToken)};
final currentIndex = useState(initialIndex);
final currentAsset = loadAsset(currentIndex.value);
final isTrashEnabled =
@@ -135,53 +131,18 @@ class GalleryViewerPage extends HookConsumerWidget {
void toggleFavorite(Asset asset) =>
ref.read(assetProvider.notifier).toggleFavorite([asset]);
/// Original (large) image of a remote asset. Required asset.isRemote
ImageProvider remoteOriginalProvider(Asset asset) =>
CachedNetworkImageProvider(
getImageUrl(asset),
cacheKey: getImageCacheKey(asset),
headers: header,
);
/// Original (large) image of a local asset. Required asset.isLocal
ImageProvider localOriginalProvider(Asset asset) =>
OriginalImageProvider(asset);
ImageProvider finalImageProvider(Asset asset) {
if (ImmichImage.useLocal(asset)) {
return localOriginalProvider(asset);
} else if (isLoadOriginal.value) {
return remoteOriginalProvider(asset);
} else if (isLoadPreview.value) {
return ImmichImage.remoteThumbnailProvider(asset, jpeg, header);
}
return ImmichImage.remoteThumbnailProvider(asset, webp, header);
}
Iterable<ImageProvider> allImageProviders(Asset asset) sync* {
if (ImmichImage.useLocal(asset)) {
yield ImmichImage.localImageProvider(asset);
yield localOriginalProvider(asset);
} else {
yield ImmichImage.remoteThumbnailProvider(asset, webp, header);
if (isLoadPreview.value) {
yield ImmichImage.remoteThumbnailProvider(asset, jpeg, header);
}
if (isLoadOriginal.value) {
yield remoteOriginalProvider(asset);
}
}
}
void precacheNextImage(int index) {
void onError(Object exception, StackTrace? stackTrace) {
// swallow error silently
debugPrint('Error precaching next image: $exception, $stackTrace');
}
if (index < totalAssets && index >= 0) {
final asset = loadAsset(index);
for (final imageProvider in allImageProviders(asset)) {
precacheImage(imageProvider, context, onError: onError);
}
precacheImage(
ImmichImage.imageProvider(asset: asset),
context,
onError: onError,
);
}
}
@@ -765,6 +726,10 @@ class GalleryViewerPage extends HookConsumerWidget {
isZoomed.value = state != PhotoViewScaleState.initial;
ref.read(showControlsProvider.notifier).show = !isZoomed.value;
},
loadingBuilder: (context, event, index) => ImmichImage.thumbnail(
asset(),
fit: BoxFit.contain,
),
pageController: controller,
scrollPhysics: isZoomed.value
? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in
@@ -781,47 +746,11 @@ class GalleryViewerPage extends HookConsumerWidget {
stackIndex.value = -1;
HapticFeedback.selectionClick();
},
loadingBuilder: (context, event, index) {
final a = loadAsset(index);
if (ImmichImage.useLocal(a)) {
return Image(
image: ImmichImage.localImageProvider(a),
fit: BoxFit.contain,
);
}
// Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to achieve
// Three-Stage Loading (WEBP -> JPEG -> Original)
final webPThumbnail = CachedNetworkImage(
imageUrl: getThumbnailUrl(a, type: webp),
cacheKey: getThumbnailCacheKey(a, type: webp),
httpHeaders: header,
progressIndicatorBuilder: (_, __, ___) => const Center(
child: ImmichLoadingIndicator(),
),
fadeInDuration: const Duration(milliseconds: 0),
fit: BoxFit.contain,
errorWidget: (context, url, error) =>
const Icon(Icons.image_not_supported_outlined),
);
// loading the preview in the loadingBuilder only
// makes sense if the original is loaded in the builder
return isLoadPreview.value && isLoadOriginal.value
? CachedNetworkImage(
imageUrl: getThumbnailUrl(a, type: jpeg),
cacheKey: getThumbnailCacheKey(a, type: jpeg),
httpHeaders: header,
fit: BoxFit.contain,
fadeInDuration: const Duration(milliseconds: 0),
placeholder: (_, __) => webPThumbnail,
errorWidget: (_, __, ___) => webPThumbnail,
)
: webPThumbnail;
},
builder: (context, index) {
final a =
index == currentIndex.value ? asset() : loadAsset(index);
final ImageProvider provider = finalImageProvider(a);
final ImageProvider provider =
ImmichImage.imageProvider(asset: a);
if (a.isImage && !isPlayingMotionVideo.value) {
return PhotoViewGalleryPageOptions(
@@ -136,10 +136,8 @@ class ThumbnailImage extends StatelessWidget {
tag: isFromDto
? '${asset.remoteId}-$heroOffset'
: asset.id + heroOffset,
child: ImmichImage(
child: ImmichImage.thumbnail(
asset,
useGrayBoxPlaceholder: useGrayBoxPlaceholder,
fit: BoxFit.cover,
),
),
);
@@ -8,7 +8,6 @@ import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
class MemoryCard extends StatelessWidget {
final Asset asset;
@@ -84,8 +83,6 @@ class MemoryCard extends StatelessWidget {
fit: fit,
height: double.infinity,
width: double.infinity,
type: ThumbnailFormat.JPEG,
preferredLocalAssetSize: 2048,
),
);
} else {
@@ -97,8 +94,6 @@ class MemoryCard extends StatelessWidget {
placeholder: ImmichImage(
asset,
fit: fit,
type: ThumbnailFormat.JPEG,
preferredLocalAssetSize: 2048,
),
hideControlsTimer: const Duration(seconds: 2),
onVideoEnded: onVideoEnded,
@@ -5,7 +5,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/memories/providers/memory.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
import 'package:openapi/api.dart';
class MemoryLane extends HookConsumerWidget {
const MemoryLane({super.key});
@@ -62,7 +61,6 @@ class MemoryLane extends HookConsumerWidget {
width: 130,
height: 200,
useGrayBoxPlaceholder: true,
type: ThumbnailFormat.JPEG,
),
),
),
@@ -10,7 +10,6 @@ import 'package:immich_mobile/modules/memories/ui/memory_epilogue.dart';
import 'package:immich_mobile/modules/memories/ui/memory_progress_indicator.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
import 'package:openapi/api.dart' as api;
@RoutePage()
class MemoryPage extends HookConsumerWidget {
@@ -113,23 +112,21 @@ class MemoryPage extends HookConsumerWidget {
// Gets the thumbnail url and precaches it
final precaches = <Future<dynamic>>[];
precaches.add(
ImmichImage.precacheAsset(
asset,
precaches.addAll([
precacheImage(
ImmichImage.imageProvider(
asset: asset,
),
context,
type: api.ThumbnailFormat.WEBP,
size: 2048,
),
);
precaches.add(
ImmichImage.precacheAsset(
asset,
precacheImage(
ImmichImage.imageProvider(
asset: asset,
isThumbnail: true,
),
context,
type: api.ThumbnailFormat.JPEG,
size: 2048,
),
);
]);
await Future.wait(precaches);
}
+24 -6
View File
@@ -354,6 +354,9 @@ abstract class _$AppRouter extends RootStackRouter {
onPlaying: args.onPlaying,
onPaused: args.onPaused,
placeholder: args.placeholder,
showControls: args.showControls,
hideControlsTimer: args.hideControlsTimer,
showDownloadingIndicator: args.showDownloadingIndicator,
),
);
},
@@ -1384,11 +1387,14 @@ class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
VideoViewerRoute({
Key? key,
required Asset asset,
required bool isMotionVideo,
required void Function() onVideoEnded,
bool isMotionVideo = false,
void Function()? onVideoEnded,
void Function()? onPlaying,
void Function()? onPaused,
Widget? placeholder,
bool showControls = true,
Duration hideControlsTimer = const Duration(seconds: 5),
bool showDownloadingIndicator = true,
List<PageRouteInfo>? children,
}) : super(
VideoViewerRoute.name,
@@ -1400,6 +1406,9 @@ class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
onPlaying: onPlaying,
onPaused: onPaused,
placeholder: placeholder,
showControls: showControls,
hideControlsTimer: hideControlsTimer,
showDownloadingIndicator: showDownloadingIndicator,
),
initialChildren: children,
);
@@ -1414,11 +1423,14 @@ class VideoViewerRouteArgs {
const VideoViewerRouteArgs({
this.key,
required this.asset,
required this.isMotionVideo,
required this.onVideoEnded,
this.isMotionVideo = false,
this.onVideoEnded,
this.onPlaying,
this.onPaused,
this.placeholder,
this.showControls = true,
this.hideControlsTimer = const Duration(seconds: 5),
this.showDownloadingIndicator = true,
});
final Key? key;
@@ -1427,7 +1439,7 @@ class VideoViewerRouteArgs {
final bool isMotionVideo;
final void Function() onVideoEnded;
final void Function()? onVideoEnded;
final void Function()? onPlaying;
@@ -1435,8 +1447,14 @@ class VideoViewerRouteArgs {
final Widget? placeholder;
final bool showControls;
final Duration hideControlsTimer;
final bool showDownloadingIndicator;
@override
String toString() {
return 'VideoViewerRouteArgs{key: $key, asset: $asset, isMotionVideo: $isMotionVideo, onVideoEnded: $onVideoEnded, onPlaying: $onPlaying, onPaused: $onPaused, placeholder: $placeholder}';
return 'VideoViewerRouteArgs{key: $key, asset: $asset, isMotionVideo: $isMotionVideo, onVideoEnded: $onVideoEnded, onPlaying: $onPlaying, onPaused: $onPaused, placeholder: $placeholder, showControls: $showControls, hideControlsTimer: $hideControlsTimer, showDownloadingIndicator: $showDownloadingIndicator}';
}
}
+5 -6
View File
@@ -1,6 +1,5 @@
import 'package:flutter/painting.dart';
import 'original_image_provider.dart';
import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_local_image_provider.dart';
/// [ImageCache] that uses two caches for small and large images
/// so that a single large image does not evict all small iamges
@@ -34,7 +33,7 @@ final class CustomImageCache implements ImageCache {
@override
bool containsKey(Object key) =>
(key is OriginalImageProvider ? _large : _small).containsKey(key);
(key is ImmichLocalImageProvider ? _large : _small).containsKey(key);
@override
int get currentSize => _small.currentSize + _large.currentSize;
@@ -44,7 +43,7 @@ final class CustomImageCache implements ImageCache {
@override
bool evict(Object key, {bool includeLive = true}) =>
(key is OriginalImageProvider ? _large : _small)
(key is ImmichLocalImageProvider ? _large : _small)
.evict(key, includeLive: includeLive);
@override
@@ -60,10 +59,10 @@ final class CustomImageCache implements ImageCache {
ImageStreamCompleter Function() loader, {
ImageErrorListener? onError,
}) =>
(key is OriginalImageProvider ? _large : _small)
(key is ImmichLocalImageProvider ? _large : _small)
.putIfAbsent(key, loader, onError: onError);
@override
ImageCacheStatus statusForKey(Object key) =>
(key is OriginalImageProvider ? _large : _small).statusForKey(key);
(key is ImmichLocalImageProvider ? _large : _small).statusForKey(key);
}
-73
View File
@@ -1,73 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:immich_mobile/shared/models/asset.dart';
/// Loads the original image for local assets
@immutable
final class OriginalImageProvider extends ImageProvider<OriginalImageProvider> {
final Asset asset;
const OriginalImageProvider(this.asset);
@override
Future<OriginalImageProvider> obtainKey(ImageConfiguration configuration) =>
SynchronousFuture<OriginalImageProvider>(this);
@override
ImageStreamCompleter loadImage(
OriginalImageProvider key,
ImageDecoderCallback decode,
) =>
MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: 1.0,
informationCollector: () sync* {
yield ErrorDescription(asset.fileName);
},
);
Future<ui.Codec> _loadAsync(
OriginalImageProvider key,
ImageDecoderCallback decode,
) async {
final ui.ImmutableBuffer buffer;
if (asset.isImage) {
final File? file = await asset.local?.originFile;
if (file == null) {
throw StateError("Opening file for asset ${asset.fileName} failed");
}
try {
buffer = await ui.ImmutableBuffer.fromFilePath(file.path);
} catch (error) {
throw StateError("Loading asset ${asset.fileName} failed");
}
} else {
final thumbBytes = await asset.local?.thumbnailData;
if (thumbBytes == null) {
throw StateError("Loading thumb for video ${asset.fileName} failed");
}
buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes);
}
try {
final codec = await decode(buffer);
debugPrint("Decoded image ${asset.fileName}");
return codec;
} catch (error) {
throw StateError("Decoding asset ${asset.fileName} failed");
}
}
@override
bool operator ==(Object other) {
if (other is! OriginalImageProvider) return false;
if (identical(this, other)) return true;
return asset == other.asset;
}
@override
int get hashCode => asset.hashCode;
}
+6 -6
View File
@@ -437,17 +437,17 @@ class Asset {
"remoteId": "${remoteId ?? "N/A"}",
"localId": "${localId ?? "N/A"}",
"checksum": "$checksum",
"ownerId": $ownerId,
"ownerId": $ownerId,
"livePhotoVideoId": "${livePhotoVideoId ?? "N/A"}",
"stackCount": "$stackCount",
"stackParentId": "${stackParentId ?? "N/A"}",
"fileCreatedAt": "$fileCreatedAt",
"fileModifiedAt": "$fileModifiedAt",
"updatedAt": "$updatedAt",
"durationInSeconds": $durationInSeconds,
"fileModifiedAt": "$fileModifiedAt",
"updatedAt": "$updatedAt",
"durationInSeconds": $durationInSeconds,
"type": "$type",
"fileName": "$fileName",
"isFavorite": $isFavorite,
"fileName": "$fileName",
"isFavorite": $isFavorite,
"isRemote": $isRemote,
"storage": "$storage",
"width": ${width ?? "N/A"},
+106 -158
View File
@@ -1,16 +1,16 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_local_image_provider.dart';
import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:octo_image/octo_image.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:openapi/api.dart' as api;
import 'package:photo_manager_image_provider/photo_manager_image_provider.dart';
/// Renders an Asset using local data if available, else remote data
class ImmichImage extends StatelessWidget {
const ImmichImage(
this.asset, {
@@ -18,23 +18,89 @@ class ImmichImage extends StatelessWidget {
this.height,
this.fit = BoxFit.cover,
this.useGrayBoxPlaceholder = false,
this.useProgressIndicator = false,
this.type = api.ThumbnailFormat.WEBP,
this.preferredLocalAssetSize = 250,
this.isThumbnail = false,
this.thumbnailSize = 250,
super.key,
});
final Asset? asset;
final bool useGrayBoxPlaceholder;
final bool useProgressIndicator;
final double? width;
final double? height;
final BoxFit fit;
final api.ThumbnailFormat type;
final int preferredLocalAssetSize;
final bool isThumbnail;
final int thumbnailSize;
/// Factory constructor to use the thumbnail variant
factory ImmichImage.thumbnail(
Asset? asset, {
BoxFit fit = BoxFit.cover,
double? width,
double? height,
}) {
// Use the width and height to derive thumbnail size
final thumbnailSize = max(width ?? 250, height ?? 250).toInt();
return ImmichImage(
asset,
isThumbnail: true,
fit: fit,
width: width,
height: height,
useGrayBoxPlaceholder: true,
thumbnailSize: thumbnailSize,
);
}
// Helper function to return the image provider for the asset
// either by using the asset ID or the asset itself
/// [asset] is the Asset to request, or else use [assetId] to get a remote
/// image provider
/// Use [isThumbnail] and [thumbnailSize] if you'd like to request a thumbnail
/// The size of the square thumbnail to request. Ignored if isThumbnail
/// is not true
static ImageProvider imageProvider({
Asset? asset,
String? assetId,
bool isThumbnail = false,
int thumbnailSize = 250,
}) {
if (asset == null && assetId == null) {
throw Exception('Must supply either asset or assetId');
}
if (asset == null) {
return ImmichRemoteImageProvider(
assetId: assetId!,
isThumbnail: isThumbnail,
);
}
if (useLocal(asset) && isThumbnail) {
return AssetEntityImageProvider(
asset.local!,
isOriginal: false,
thumbnailSize: ThumbnailSize.square(thumbnailSize),
);
} else if (useLocal(asset) && !isThumbnail) {
return ImmichLocalImageProvider(
asset: asset,
);
} else {
return ImmichRemoteImageProvider(
assetId: asset.remoteId!,
isThumbnail: isThumbnail,
);
}
}
static bool useLocal(Asset asset) =>
!asset.isRemote ||
asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false);
@override
Widget build(BuildContext context) {
if (this.asset == null) {
if (asset == null) {
return Container(
decoration: const BoxDecoration(
color: Colors.grey,
@@ -48,96 +114,39 @@ class ImmichImage extends StatelessWidget {
),
);
}
final Asset asset = this.asset!;
if (useLocal(asset)) {
return Image(
image: localImageProvider(asset, size: preferredLocalAssetSize),
width: width,
height: height,
fit: fit,
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
if (wasSynchronouslyLoaded || frame != null) {
return child;
}
// Show loading if desired
return Stack(
children: [
if (useGrayBoxPlaceholder)
const SizedBox.square(
dimension: 250,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.grey),
),
),
if (useProgressIndicator)
const Center(
child: CircularProgressIndicator(),
),
],
return OctoImage(
fadeInDuration: const Duration(milliseconds: 0),
fadeOutDuration: const Duration(milliseconds: 400),
placeholderBuilder: (context) {
if (useGrayBoxPlaceholder) {
// Use the gray box placeholder
return const SizedBox.expand(
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.grey),
),
);
},
errorBuilder: (context, error, stackTrace) {
if (error is PlatformException &&
error.code == "The asset not found!") {
debugPrint(
"Asset ${asset.localId} does not exist anymore on device!",
);
} else {
debugPrint(
"Error getting thumb for assetId=${asset.localId}: $error",
);
}
return Icon(
Icons.image_not_supported_outlined,
color: context.primaryColor,
);
},
);
}
final String? accessToken = Store.get(StoreKey.accessToken);
final String thumbnailRequestUrl = getThumbnailUrl(asset, type: type);
return CachedNetworkImage(
imageUrl: thumbnailRequestUrl,
httpHeaders: {"x-immich-user-token": accessToken ?? ""},
cacheKey: getThumbnailCacheKey(asset, type: type),
}
// No placeholder
return const SizedBox();
},
image: ImmichImage.imageProvider(
asset: asset,
isThumbnail: isThumbnail,
),
width: width,
height: height,
// keeping memCacheWidth, memCacheHeight, maxWidthDiskCache and
// maxHeightDiskCache = null allows to simply store the webp thumbnail
// from the server and use it for all rendered thumbnail sizes
fit: fit,
fadeInDuration: const Duration(milliseconds: 250),
progressIndicatorBuilder: (context, url, downloadProgress) {
// Show loading if desired
return Stack(
children: [
if (useGrayBoxPlaceholder)
const SizedBox.square(
dimension: 250,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.grey),
),
),
if (useProgressIndicator)
Transform.scale(
scale: 2,
child: Center(
child: CircularProgressIndicator.adaptive(
strokeWidth: 1,
value: downloadProgress.progress,
),
),
),
],
);
},
errorWidget: (context, url, error) {
if (error is HttpExceptionWithStatus &&
error.statusCode >= 400 &&
error.statusCode < 500) {
debugPrint("Evicting thumbnail '$url' from cache: $error");
CachedNetworkImage.evictFromCache(url);
errorBuilder: (context, error, stackTrace) {
if (error is PlatformException &&
error.code == "The asset not found!") {
debugPrint(
"Asset ${asset?.localId} does not exist anymore on device!",
);
} else {
debugPrint(
"Error getting thumb for assetId=${asset?.localId}: $error",
);
}
return Icon(
Icons.image_not_supported_outlined,
@@ -146,65 +155,4 @@ class ImmichImage extends StatelessWidget {
},
);
}
static AssetEntityImageProvider localImageProvider(
Asset asset, {
int size = 250,
}) =>
AssetEntityImageProvider(
asset.local!,
isOriginal: false,
thumbnailSize: ThumbnailSize.square(size),
);
static CachedNetworkImageProvider remoteThumbnailProvider(
Asset asset,
api.ThumbnailFormat type,
Map<String, String> authHeader,
) =>
CachedNetworkImageProvider(
getThumbnailUrl(asset, type: type),
cacheKey: getThumbnailCacheKey(asset, type: type),
headers: authHeader,
);
/// TODO: refactor image providers to separate class
static CachedNetworkImageProvider remoteThumbnailProviderForId(
String assetId, {
api.ThumbnailFormat type = api.ThumbnailFormat.WEBP,
}) =>
CachedNetworkImageProvider(
getThumbnailUrlForRemoteId(assetId, type: type),
cacheKey: getThumbnailCacheKeyForRemoteId(assetId, type: type),
headers: {
"x-immich-user-token": Store.get(StoreKey.accessToken),
},
);
/// Precaches this asset for instant load the next time it is shown
static Future<void> precacheAsset(
Asset asset,
BuildContext context, {
type = api.ThumbnailFormat.WEBP,
size = 250,
}) {
if (useLocal(asset)) {
// Precache the local image
return precacheImage(
localImageProvider(asset, size: size),
context,
);
} else {
final accessToken = Store.get(StoreKey.accessToken);
// Precache the remote image since we are not using local images
return precacheImage(
remoteThumbnailProvider(asset, type, {"x-immich-user-token": accessToken}),
context,
);
}
}
static bool useLocal(Asset asset) =>
!asset.isRemote ||
asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false);
}
+5 -1
View File
@@ -56,7 +56,11 @@ String getAlbumThumbNailCacheKey(
}
String getImageUrl(final Asset asset) {
return '${Store.get(StoreKey.serverEndpoint)}/asset/file/${asset.remoteId}?isThumb=false';
return getImageUrlFromId(asset.remoteId!);
}
String getImageUrlFromId(final String id) {
return '${Store.get(StoreKey.serverEndpoint)}/asset/file/$id?isThumb=false';
}
String getImageCacheKey(final Asset asset) {
+3
View File
@@ -120,6 +120,7 @@ doc/SearchExploreResponseDto.md
doc/SearchFacetCountResponseDto.md
doc/SearchFacetResponseDto.md
doc/SearchResponseDto.md
doc/SearchSuggestionType.md
doc/ServerConfigDto.md
doc/ServerFeaturesDto.md
doc/ServerInfoApi.md
@@ -313,6 +314,7 @@ lib/model/search_explore_response_dto.dart
lib/model/search_facet_count_response_dto.dart
lib/model/search_facet_response_dto.dart
lib/model/search_response_dto.dart
lib/model/search_suggestion_type.dart
lib/model/server_config_dto.dart
lib/model/server_features_dto.dart
lib/model/server_info_response_dto.dart
@@ -485,6 +487,7 @@ test/search_explore_response_dto_test.dart
test/search_facet_count_response_dto_test.dart
test/search_facet_response_dto_test.dart
test/search_response_dto_test.dart
test/search_suggestion_type_test.dart
test/server_config_dto_test.dart
test/server_features_dto_test.dart
test/server_info_api_test.dart
+2
View File
@@ -161,6 +161,7 @@ Class | Method | HTTP request | Description
*PersonApi* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /person |
*PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} |
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |
*SearchApi* | [**getSearchSuggestions**](doc//SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions |
*SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search |
*SearchApi* | [**searchMetadata**](doc//SearchApi.md#searchmetadata) | **GET** /search/metadata |
*SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person |
@@ -315,6 +316,7 @@ Class | Method | HTTP request | Description
- [SearchFacetCountResponseDto](doc//SearchFacetCountResponseDto.md)
- [SearchFacetResponseDto](doc//SearchFacetResponseDto.md)
- [SearchResponseDto](doc//SearchResponseDto.md)
- [SearchSuggestionType](doc//SearchSuggestionType.md)
- [ServerConfigDto](doc//ServerConfigDto.md)
- [ServerFeaturesDto](doc//ServerFeaturesDto.md)
- [ServerInfoResponseDto](doc//ServerInfoResponseDto.md)
+66 -2
View File
@@ -10,6 +10,7 @@ All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore |
[**getSearchSuggestions**](SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions |
[**search**](SearchApi.md#search) | **GET** /search |
[**searchMetadata**](SearchApi.md#searchmetadata) | **GET** /search/metadata |
[**searchPerson**](SearchApi.md#searchperson) | **GET** /search/person |
@@ -67,6 +68,69 @@ This endpoint does not need any parameter.
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getSearchSuggestions**
> List<String> getSearchSuggestions(type, country, make, model, state)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = SearchApi();
final type = ; // SearchSuggestionType |
final country = country_example; // String |
final make = make_example; // String |
final model = model_example; // String |
final state = state_example; // String |
try {
final result = api_instance.getSearchSuggestions(type, country, make, model, state);
print(result);
} catch (e) {
print('Exception when calling SearchApi->getSearchSuggestions: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**type** | [**SearchSuggestionType**](.md)| |
**country** | **String**| | [optional]
**make** | **String**| | [optional]
**model** | **String**| | [optional]
**state** | **String**| | [optional]
### Return type
**List<String>**
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **search**
> SearchResponseDto search(clip, motion, page, q, query, recent, size, smart, type, withArchived)
@@ -91,7 +155,7 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = SearchApi();
final clip = true; // bool | @deprecated
final clip = true; // bool |
final motion = true; // bool |
final page = 8.14; // num |
final q = q_example; // String |
@@ -114,7 +178,7 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**clip** | **bool**| @deprecated | [optional]
**clip** | **bool**| | [optional]
**motion** | **bool**| | [optional]
**page** | **num**| | [optional]
**q** | **String**| | [optional]
+14
View File
@@ -0,0 +1,14 @@
# openapi.model.SearchSuggestionType
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+1
View File
@@ -153,6 +153,7 @@ part 'model/search_explore_response_dto.dart';
part 'model/search_facet_count_response_dto.dart';
part 'model/search_facet_response_dto.dart';
part 'model/search_response_dto.dart';
part 'model/search_suggestion_type.dart';
part 'model/server_config_dto.dart';
part 'model/server_features_dto.dart';
part 'model/server_info_response_dto.dart';
+80 -2
View File
@@ -60,11 +60,90 @@ class SearchApi {
return null;
}
/// Performs an HTTP 'GET /search/suggestions' operation and returns the [Response].
/// Parameters:
///
/// * [SearchSuggestionType] type (required):
///
/// * [String] country:
///
/// * [String] make:
///
/// * [String] model:
///
/// * [String] state:
Future<Response> getSearchSuggestionsWithHttpInfo(SearchSuggestionType type, { String? country, String? make, String? model, String? state, }) async {
// ignore: prefer_const_declarations
final path = r'/search/suggestions';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (country != null) {
queryParams.addAll(_queryParams('', 'country', country));
}
if (make != null) {
queryParams.addAll(_queryParams('', 'make', make));
}
if (model != null) {
queryParams.addAll(_queryParams('', 'model', model));
}
if (state != null) {
queryParams.addAll(_queryParams('', 'state', state));
}
queryParams.addAll(_queryParams('', 'type', type));
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [SearchSuggestionType] type (required):
///
/// * [String] country:
///
/// * [String] make:
///
/// * [String] model:
///
/// * [String] state:
Future<List<String>?> getSearchSuggestions(SearchSuggestionType type, { String? country, String? make, String? model, String? state, }) async {
final response = await getSearchSuggestionsWithHttpInfo(type, country: country, make: make, model: model, state: state, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<String>') as List)
.cast<String>()
.toList(growable: false);
}
return null;
}
/// Performs an HTTP 'GET /search' operation and returns the [Response].
/// Parameters:
///
/// * [bool] clip:
/// @deprecated
///
/// * [bool] motion:
///
@@ -142,7 +221,6 @@ class SearchApi {
/// Parameters:
///
/// * [bool] clip:
/// @deprecated
///
/// * [bool] motion:
///
+2
View File
@@ -388,6 +388,8 @@ class ApiClient {
return SearchFacetResponseDto.fromJson(value);
case 'SearchResponseDto':
return SearchResponseDto.fromJson(value);
case 'SearchSuggestionType':
return SearchSuggestionTypeTypeTransformer().decode(value);
case 'ServerConfigDto':
return ServerConfigDto.fromJson(value);
case 'ServerFeaturesDto':
+3
View File
@@ -109,6 +109,9 @@ String parameterToString(dynamic value) {
if (value is ReactionType) {
return ReactionTypeTypeTransformer().encode(value).toString();
}
if (value is SearchSuggestionType) {
return SearchSuggestionTypeTypeTransformer().encode(value).toString();
}
if (value is SharedLinkType) {
return SharedLinkTypeTypeTransformer().encode(value).toString();
}
+94
View File
@@ -0,0 +1,94 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class SearchSuggestionType {
/// Instantiate a new enum with the provided [value].
const SearchSuggestionType._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const country = SearchSuggestionType._(r'country');
static const state = SearchSuggestionType._(r'state');
static const city = SearchSuggestionType._(r'city');
static const cameraMake = SearchSuggestionType._(r'camera-make');
static const cameraModel = SearchSuggestionType._(r'camera-model');
/// List of all possible values in this [enum][SearchSuggestionType].
static const values = <SearchSuggestionType>[
country,
state,
city,
cameraMake,
cameraModel,
];
static SearchSuggestionType? fromJson(dynamic value) => SearchSuggestionTypeTypeTransformer().decode(value);
static List<SearchSuggestionType> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SearchSuggestionType>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SearchSuggestionType.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [SearchSuggestionType] to String,
/// and [decode] dynamic data back to [SearchSuggestionType].
class SearchSuggestionTypeTypeTransformer {
factory SearchSuggestionTypeTypeTransformer() => _instance ??= const SearchSuggestionTypeTypeTransformer._();
const SearchSuggestionTypeTypeTransformer._();
String encode(SearchSuggestionType data) => data.value;
/// Decodes a [dynamic value][data] to a SearchSuggestionType.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
SearchSuggestionType? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'country': return SearchSuggestionType.country;
case r'state': return SearchSuggestionType.state;
case r'city': return SearchSuggestionType.city;
case r'camera-make': return SearchSuggestionType.cameraMake;
case r'camera-model': return SearchSuggestionType.cameraModel;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [SearchSuggestionTypeTypeTransformer] instance.
static SearchSuggestionTypeTypeTransformer? _instance;
}
+5
View File
@@ -22,6 +22,11 @@ void main() {
// TODO
});
//Future<List<String>> getSearchSuggestions(SearchSuggestionType type, { String country, String make, String model, String state }) async
test('test getSearchSuggestions', () async {
// TODO
});
//Future<SearchResponseDto> search({ bool clip, bool motion, num page, String q, String query, bool recent, num size, bool smart, String type, bool withArchived }) async
test('test search', () async {
// TODO
+21
View File
@@ -0,0 +1,21 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for SearchSuggestionType
void main() {
group('test SearchSuggestionType', () {
});
}
+1 -1
View File
@@ -960,7 +960,7 @@ packages:
source: hosted
version: "0.5.0"
octo_image:
dependency: transitive
dependency: "direct main"
description:
name: octo_image
sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d"
+1
View File
@@ -56,6 +56,7 @@ dependencies:
wakelock_plus: ^1.1.4
flutter_local_notifications: ^16.3.2
timezone: ^0.9.2
octo_image: ^2.0.0
openapi:
path: openapi
+1 -1
View File
@@ -19,7 +19,7 @@ function dart {
function typescript {
rm -rf ./typescript-sdk/client
npx --yes @openapitools/openapi-generator-cli generate -g typescript-axios -i ./immich-openapi-specs.json -o ./typescript-sdk/axios-client --additional-properties=useSingleRequestParameter=true,supportsES6=true
npx --yes oazapfts@5.1.7 --optimistic --argumentStyle=object immich-openapi-specs.json typescript-sdk/fetch-client.ts
npx --yes oazapfts --optimistic --argumentStyle=object immich-openapi-specs.json typescript-sdk/fetch-client.ts
npm --prefix typescript-sdk ci && npm --prefix typescript-sdk run build
}
+86 -1
View File
@@ -4370,7 +4370,6 @@
"name": "clip",
"required": false,
"in": "query",
"description": "@deprecated",
"deprecated": true,
"schema": {
"type": "boolean"
@@ -5231,6 +5230,82 @@
]
}
},
"/search/suggestions": {
"get": {
"operationId": "getSearchSuggestions",
"parameters": [
{
"name": "country",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "make",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "model",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "state",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "type",
"required": true,
"in": "query",
"schema": {
"$ref": "#/components/schemas/SearchSuggestionType"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"type": "string"
},
"type": "array"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Search"
]
}
},
"/server-info": {
"get": {
"operationId": "getServerInfo",
@@ -9243,6 +9318,16 @@
],
"type": "object"
},
"SearchSuggestionType": {
"enum": [
"country",
"state",
"city",
"camera-make",
"camera-model"
],
"type": "string"
},
"ServerConfigDto": {
"properties": {
"externalDomain": {
+163 -3
View File
@@ -2995,6 +2995,23 @@ export interface SearchResponseDto {
*/
'assets': SearchAssetResponseDto;
}
/**
*
* @export
* @enum {string}
*/
export const SearchSuggestionType = {
Country: 'country',
State: 'state',
City: 'city',
CameraMake: 'camera-make',
CameraModel: 'camera-model'
} as const;
export type SearchSuggestionType = typeof SearchSuggestionType[keyof typeof SearchSuggestionType];
/**
*
* @export
@@ -14521,7 +14538,72 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
},
/**
*
* @param {boolean} [clip] @deprecated
* @param {SearchSuggestionType} type
* @param {string} [country]
* @param {string} [make]
* @param {string} [model]
* @param {string} [state]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getSearchSuggestions: async (type: SearchSuggestionType, country?: string, make?: string, model?: string, state?: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'type' is not null or undefined
assertParamExists('getSearchSuggestions', 'type', type)
const localVarPath = `/search/suggestions`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (country !== undefined) {
localVarQueryParameter['country'] = country;
}
if (make !== undefined) {
localVarQueryParameter['make'] = make;
}
if (model !== undefined) {
localVarQueryParameter['model'] = model;
}
if (state !== undefined) {
localVarQueryParameter['state'] = state;
}
if (type !== undefined) {
localVarQueryParameter['type'] = type;
}
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {boolean} [clip]
* @param {boolean} [motion]
* @param {number} [page]
* @param {string} [q]
@@ -15151,7 +15233,23 @@ export const SearchApiFp = function(configuration?: Configuration) {
},
/**
*
* @param {boolean} [clip] @deprecated
* @param {SearchSuggestionType} type
* @param {string} [country]
* @param {string} [make]
* @param {string} [model]
* @param {string} [state]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getSearchSuggestions(type: SearchSuggestionType, country?: string, make?: string, model?: string, state?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<string>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getSearchSuggestions(type, country, make, model, state, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['SearchApi.getSearchSuggestions']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
},
/**
*
* @param {boolean} [clip]
* @param {boolean} [motion]
* @param {number} [page]
* @param {string} [q]
@@ -15296,6 +15394,15 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
getExploreData(options?: RawAxiosRequestConfig): AxiosPromise<Array<SearchExploreResponseDto>> {
return localVarFp.getExploreData(options).then((request) => request(axios, basePath));
},
/**
*
* @param {SearchApiGetSearchSuggestionsRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getSearchSuggestions(requestParameters: SearchApiGetSearchSuggestionsRequest, options?: RawAxiosRequestConfig): AxiosPromise<Array<string>> {
return localVarFp.getSearchSuggestions(requestParameters.type, requestParameters.country, requestParameters.make, requestParameters.model, requestParameters.state, options).then((request) => request(axios, basePath));
},
/**
*
* @param {SearchApiSearchRequest} requestParameters Request parameters.
@@ -15336,6 +15443,48 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
};
};
/**
* Request parameters for getSearchSuggestions operation in SearchApi.
* @export
* @interface SearchApiGetSearchSuggestionsRequest
*/
export interface SearchApiGetSearchSuggestionsRequest {
/**
*
* @type {SearchSuggestionType}
* @memberof SearchApiGetSearchSuggestions
*/
readonly type: SearchSuggestionType
/**
*
* @type {string}
* @memberof SearchApiGetSearchSuggestions
*/
readonly country?: string
/**
*
* @type {string}
* @memberof SearchApiGetSearchSuggestions
*/
readonly make?: string
/**
*
* @type {string}
* @memberof SearchApiGetSearchSuggestions
*/
readonly model?: string
/**
*
* @type {string}
* @memberof SearchApiGetSearchSuggestions
*/
readonly state?: string
}
/**
* Request parameters for search operation in SearchApi.
* @export
@@ -15343,7 +15492,7 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
*/
export interface SearchApiSearchRequest {
/**
* @deprecated
*
* @type {boolean}
* @memberof SearchApiSearch
*/
@@ -15969,6 +16118,17 @@ export class SearchApi extends BaseAPI {
return SearchApiFp(this.configuration).getExploreData(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {SearchApiGetSearchSuggestionsRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SearchApi
*/
public getSearchSuggestions(requestParameters: SearchApiGetSearchSuggestionsRequest, options?: RawAxiosRequestConfig) {
return SearchApiFp(this.configuration).getSearchSuggestions(requestParameters.type, requestParameters.country, requestParameters.make, requestParameters.model, requestParameters.state, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {SearchApiSearchRequest} requestParameters Request parameters.
+23 -2
View File
@@ -4,8 +4,8 @@
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/
import * as Oazapfts from "oazapfts/lib/runtime";
import * as QS from "oazapfts/lib/runtime/query";
import * as Oazapfts from "@oazapfts/runtime";
import * as QS from "@oazapfts/runtime/query";
export const defaults: Oazapfts.Defaults<Oazapfts.CustomHeaders> = {
headers: {},
baseUrl: "/api",
@@ -603,6 +603,7 @@ export type SearchExploreResponseDto = {
fieldName: string;
items: SearchExploreItem[];
};
export type SearchSuggestionType = "country" | "state" | "city" | "camera-make" | "camera-model";
export type ServerInfoResponseDto = {
diskAvailable: string;
diskAvailableRaw: number;
@@ -2266,6 +2267,26 @@ export function searchSmart({ city, country, createdAfter, createdBefore, device
...opts
}));
}
export function getSearchSuggestions({ country, make, model, state, $type }: {
country?: string;
make?: string;
model?: string;
state?: string;
$type: SearchSuggestionType;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: string[];
}>(`/search/suggestions${QS.query(QS.explode({
country,
make,
model,
state,
"type": $type
}))}`, {
...opts
}));
}
export function getServerInfo(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
+5 -636
View File
@@ -9,8 +9,8 @@
"version": "1.92.1",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@oazapfts/runtime": "^1.0.0",
"@types/node": "^20.11.0",
"oazapfts": "^5.1.4",
"typescript": "^5.3.3"
},
"peerDependencies": {
@@ -22,60 +22,10 @@
}
}
},
"node_modules/@apidevtools/json-schema-ref-parser": {
"version": "9.0.6",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz",
"integrity": "sha512-M3YgsLjI0lZxvrpeGVk9Ap032W6TPQkH6pRAZz81Ac3WUNF79VQooAFnp8umjvVzUmD93NkogxEwbSce7qMsUg==",
"dev": true,
"dependencies": {
"@jsdevtools/ono": "^7.1.3",
"call-me-maybe": "^1.0.1",
"js-yaml": "^3.13.1"
}
},
"node_modules/@apidevtools/openapi-schemas": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/@apidevtools/swagger-methods": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
"dev": true
},
"node_modules/@apidevtools/swagger-parser": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.0.tgz",
"integrity": "sha512-9Kt7EuS/7WbMAUv2gSziqjvxwDbFSg3Xeyfuj5laUODX8o/k/CpsAKiQ8W7/R88eXFTMbJYg6+7uAmOWNKmwnw==",
"dev": true,
"dependencies": {
"@apidevtools/json-schema-ref-parser": "9.0.6",
"@apidevtools/openapi-schemas": "^2.1.0",
"@apidevtools/swagger-methods": "^3.0.2",
"@jsdevtools/ono": "^7.1.3",
"ajv": "^8.6.3",
"ajv-draft-04": "^1.0.0",
"call-me-maybe": "^1.0.1"
},
"peerDependencies": {
"openapi-types": ">=7"
}
},
"node_modules/@exodus/schemasafe": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz",
"integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==",
"dev": true
},
"node_modules/@jsdevtools/ono": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
"node_modules/@oazapfts/runtime": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@oazapfts/runtime/-/runtime-1.0.0.tgz",
"integrity": "sha512-1ovqeaeEvShbYge5/7ctJokpvqB0anBdfDNfU5jWstjV2/Gbe+vvcBM274Z0abM3IM0b9MmSNWYBXnJXYO8KCw==",
"dev": true
},
"node_modules/@types/node": {
@@ -87,69 +37,6 @@
"undici-types": "~5.26.4"
}
},
"node_modules/ajv": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-draft-04": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
"integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==",
"dev": true,
"peerDependencies": {
"ajv": "^8.5.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -169,44 +56,6 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/call-me-maybe": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
"dev": true
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -230,52 +79,6 @@
"node": ">=0.4.0"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"node_modules/es6-promise": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
"integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==",
"dev": true
},
"node_modules/escalade": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
},
"node_modules/fast-safe-stringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
@@ -312,55 +115,6 @@
"node": ">= 6"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/http2-client": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz",
"integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==",
"dev": true
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dev": true,
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -384,149 +138,6 @@
"node": ">= 0.6"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dev": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-fetch-h2": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz",
"integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==",
"dev": true,
"dependencies": {
"http2-client": "^1.2.5"
},
"engines": {
"node": "4.x || >=6.0.0"
}
},
"node_modules/node-readfiles": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz",
"integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==",
"dev": true,
"dependencies": {
"es6-promise": "^3.2.1"
}
},
"node_modules/oas-kit-common": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz",
"integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==",
"dev": true,
"dependencies": {
"fast-safe-stringify": "^2.0.7"
}
},
"node_modules/oas-linter": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.2.2.tgz",
"integrity": "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==",
"dev": true,
"dependencies": {
"@exodus/schemasafe": "^1.0.0-rc.2",
"should": "^13.2.1",
"yaml": "^1.10.0"
},
"funding": {
"url": "https://github.com/Mermade/oas-kit?sponsor=1"
}
},
"node_modules/oas-resolver": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.5.6.tgz",
"integrity": "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==",
"dev": true,
"dependencies": {
"node-fetch-h2": "^2.3.0",
"oas-kit-common": "^1.0.8",
"reftools": "^1.1.9",
"yaml": "^1.10.0",
"yargs": "^17.0.1"
},
"bin": {
"resolve": "resolve.js"
},
"funding": {
"url": "https://github.com/Mermade/oas-kit?sponsor=1"
}
},
"node_modules/oas-schema-walker": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz",
"integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==",
"dev": true,
"funding": {
"url": "https://github.com/Mermade/oas-kit?sponsor=1"
}
},
"node_modules/oas-validator": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-5.0.8.tgz",
"integrity": "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==",
"dev": true,
"dependencies": {
"call-me-maybe": "^1.0.1",
"oas-kit-common": "^1.0.8",
"oas-linter": "^3.2.2",
"oas-resolver": "^2.5.6",
"oas-schema-walker": "^1.1.5",
"reftools": "^1.1.9",
"should": "^13.2.1",
"yaml": "^1.10.0"
},
"funding": {
"url": "https://github.com/Mermade/oas-kit?sponsor=1"
}
},
"node_modules/oazapfts": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/oazapfts/-/oazapfts-5.1.5.tgz",
"integrity": "sha512-yEBYyX1xfUfCenL+G9wiDNzgPp8wxouuBLnS3F/yTA3lGoDlBtPIIHgC0kpP/aQo16raLLWDp2pJ+FzKNxpBLQ==",
"dev": true,
"dependencies": {
"@apidevtools/swagger-parser": "^10.1.0",
"lodash": "^4.17.21",
"minimist": "^1.2.8",
"swagger2openapi": "^7.0.8",
"typescript": "^5.3.3"
},
"bin": {
"oazapfts": "lib/codegen/cli.js"
}
},
"node_modules/openapi-types": {
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
"dev": true,
"peer": true
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -534,161 +145,6 @@
"optional": true,
"peer": true
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/reftools": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz",
"integrity": "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==",
"dev": true,
"funding": {
"url": "https://github.com/Mermade/oas-kit?sponsor=1"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/should": {
"version": "13.2.3",
"resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz",
"integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==",
"dev": true,
"dependencies": {
"should-equal": "^2.0.0",
"should-format": "^3.0.3",
"should-type": "^1.4.0",
"should-type-adaptors": "^1.0.1",
"should-util": "^1.0.0"
}
},
"node_modules/should-equal": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz",
"integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==",
"dev": true,
"dependencies": {
"should-type": "^1.4.0"
}
},
"node_modules/should-format": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz",
"integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==",
"dev": true,
"dependencies": {
"should-type": "^1.3.0",
"should-type-adaptors": "^1.0.1"
}
},
"node_modules/should-type": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz",
"integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==",
"dev": true
},
"node_modules/should-type-adaptors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz",
"integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==",
"dev": true,
"dependencies": {
"should-type": "^1.3.0",
"should-util": "^1.0.0"
}
},
"node_modules/should-util": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz",
"integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==",
"dev": true
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/swagger2openapi": {
"version": "7.0.8",
"resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-7.0.8.tgz",
"integrity": "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==",
"dev": true,
"dependencies": {
"call-me-maybe": "^1.0.1",
"node-fetch": "^2.6.1",
"node-fetch-h2": "^2.3.0",
"node-readfiles": "^0.2.0",
"oas-kit-common": "^1.0.8",
"oas-resolver": "^2.5.6",
"oas-schema-walker": "^1.1.5",
"oas-validator": "^5.0.8",
"reftools": "^1.1.9",
"yaml": "^1.10.0",
"yargs": "^17.0.1"
},
"bin": {
"boast": "boast.js",
"oas-validate": "oas-validate.js",
"swagger2openapi": "swagger2openapi.js"
},
"funding": {
"url": "https://github.com/Mermade/oas-kit?sponsor=1"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"dev": true
},
"node_modules/typescript": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
@@ -707,93 +163,6 @@
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"dependencies": {
"punycode": "^2.1.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"dev": true
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dev": true,
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"dev": true,
"engines": {
"node": ">= 6"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"engines": {
"node": ">=12"
}
}
}
}
+1 -1
View File
@@ -20,8 +20,8 @@
},
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@oazapfts/runtime": "^1.0.0",
"@types/node": "^20.11.0",
"oazapfts": "^5.1.4",
"typescript": "^5.3.3"
},
"peerDependencies": {
+1 -1
View File
@@ -145,7 +145,7 @@
"coverageDirectory": "./coverage",
"coverageThreshold": {
"./src/domain/": {
"branches": 80,
"branches": 79,
"functions": 80,
"lines": 90,
"statements": 90
@@ -39,4 +39,9 @@ export interface IMetadataRepository {
readTags(path: string): Promise<ImmichTags | null>;
writeTags(path: string, tags: Partial<Tags>): Promise<void>;
extractBinaryTag(tagName: string, path: string): Promise<Buffer>;
getCountries(userId: string): Promise<string[]>;
getStates(userId: string, country?: string): Promise<string[]>;
getCities(userId: string, country?: string, state?: string): Promise<string[]>;
getCameraMakes(userId: string, model?: string): Promise<string[]>;
getCameraModels(userId: string, make?: string): Promise<string[]>;
}
@@ -0,0 +1,33 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
export enum SearchSuggestionType {
COUNTRY = 'country',
STATE = 'state',
CITY = 'city',
CAMERA_MAKE = 'camera-make',
CAMERA_MODEL = 'camera-model',
}
export class SearchSuggestionRequestDto {
@IsEnum(SearchSuggestionType)
@IsNotEmpty()
@ApiProperty({ enumName: 'SearchSuggestionType', enum: SearchSuggestionType })
type!: SearchSuggestionType;
@IsString()
@IsOptional()
country?: string;
@IsString()
@IsOptional()
state?: string;
@IsString()
@IsOptional()
make?: string;
@IsString()
@IsOptional()
model?: string;
}
@@ -4,6 +4,7 @@ import {
authStub,
newAssetRepositoryMock,
newMachineLearningRepositoryMock,
newMetadataRepositoryMock,
newPartnerRepositoryMock,
newPersonRepositoryMock,
newSearchRepositoryMock,
@@ -14,6 +15,7 @@ import { mapAsset } from '../asset';
import {
IAssetRepository,
IMachineLearningRepository,
IMetadataRepository,
IPartnerRepository,
IPersonRepository,
ISearchRepository,
@@ -32,6 +34,7 @@ describe(SearchService.name, () => {
let personMock: jest.Mocked<IPersonRepository>;
let searchMock: jest.Mocked<ISearchRepository>;
let partnerMock: jest.Mocked<IPartnerRepository>;
let metadataMock: jest.Mocked<IMetadataRepository>;
beforeEach(() => {
assetMock = newAssetRepositoryMock();
@@ -40,7 +43,9 @@ describe(SearchService.name, () => {
personMock = newPersonRepositoryMock();
searchMock = newSearchRepositoryMock();
partnerMock = newPartnerRepositoryMock();
sut = new SearchService(configMock, machineMock, personMock, searchMock, assetMock, partnerMock);
metadataMock = newMetadataRepositoryMock();
sut = new SearchService(configMock, machineMock, personMock, searchMock, assetMock, partnerMock, metadataMock);
});
it('should work', () => {
@@ -7,6 +7,7 @@ import { PersonResponseDto } from '../person';
import {
IAssetRepository,
IMachineLearningRepository,
IMetadataRepository,
IPartnerRepository,
IPersonRepository,
ISearchRepository,
@@ -16,6 +17,7 @@ import {
} from '../repositories';
import { FeatureFlag, SystemConfigCore } from '../system-config';
import { MetadataSearchDto, SearchDto, SearchPeopleDto, SmartSearchDto } from './dto';
import { SearchSuggestionRequestDto, SearchSuggestionType } from './dto/search-suggestion.dto';
import { SearchResponseDto } from './response-dto';
@Injectable()
@@ -30,6 +32,7 @@ export class SearchService {
@Inject(ISearchRepository) private searchRepository: ISearchRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
@Inject(IMetadataRepository) private metadataRepository: IMetadataRepository,
) {
this.configCore = SystemConfigCore.create(configRepository);
}
@@ -176,4 +179,28 @@ export class SearchService {
},
};
}
async getSearchSuggestions(auth: AuthDto, dto: SearchSuggestionRequestDto): Promise<string[]> {
if (dto.type === SearchSuggestionType.COUNTRY) {
return this.metadataRepository.getCountries(auth.user.id);
}
if (dto.type === SearchSuggestionType.STATE) {
return this.metadataRepository.getStates(auth.user.id, dto.country);
}
if (dto.type === SearchSuggestionType.CITY) {
return this.metadataRepository.getCities(auth.user.id, dto.country, dto.state);
}
if (dto.type === SearchSuggestionType.CAMERA_MAKE) {
return this.metadataRepository.getCameraMakes(auth.user.id, dto.model);
}
if (dto.type === SearchSuggestionType.CAMERA_MODEL) {
return this.metadataRepository.getCameraModels(auth.user.id, dto.make);
}
return [];
}
}
@@ -9,6 +9,7 @@ import {
SearchService,
SmartSearchDto,
} from '@app/domain';
import { SearchSuggestionRequestDto } from '@app/domain/search/dto/search-suggestion.dto';
import { Controller, Get, Query } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Auth, Authenticated } from '../app.guard';
@@ -46,4 +47,9 @@ export class SearchController {
searchPerson(@Auth() auth: AuthDto, @Query() dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
return this.service.searchPerson(auth, dto);
}
@Get('suggestions')
getSearchSuggestions(@Auth() auth: AuthDto, @Query() dto: SearchSuggestionRequestDto): Promise<string[]> {
return this.service.getSearchSuggestions(auth, dto);
}
}
@@ -10,7 +10,13 @@ import {
ISystemMetadataRepository,
ReverseGeocodeResult,
} from '@app/domain';
import { GeodataAdmin1Entity, GeodataAdmin2Entity, GeodataPlacesEntity, SystemMetadataKey } from '@app/infra/entities';
import {
ExifEntity,
GeodataAdmin1Entity,
GeodataAdmin2Entity,
GeodataPlacesEntity,
SystemMetadataKey,
} from '@app/infra/entities';
import { ImmichLogger } from '@app/infra/logger';
import { Inject } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
@@ -21,12 +27,14 @@ import { createReadStream, existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import * as readLine from 'node:readline';
import { DataSource, DeepPartial, QueryRunner, Repository } from 'typeorm';
import { DummyValue, GenerateSql } from '../infra.util';
type GeoEntity = GeodataPlacesEntity | GeodataAdmin1Entity | GeodataAdmin2Entity;
type GeoEntityClass = typeof GeodataPlacesEntity | typeof GeodataAdmin1Entity | typeof GeodataAdmin2Entity;
export class MetadataRepository implements IMetadataRepository {
constructor(
@InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
@InjectRepository(GeodataPlacesEntity) private readonly geodataPlacesRepository: Repository<GeodataPlacesEntity>,
@InjectRepository(GeodataAdmin1Entity) private readonly geodataAdmin1Repository: Repository<GeodataAdmin1Entity>,
@InjectRepository(GeodataAdmin2Entity) private readonly geodataAdmin2Repository: Repository<GeodataAdmin2Entity>,
@@ -213,4 +221,106 @@ export class MetadataRepository implements IMetadataRepository {
this.logger.warn(`Error writing exif data (${path}): ${error}`);
}
}
@GenerateSql({ params: [DummyValue.UUID] })
async getCountries(userId: string): Promise<string[]> {
const entity = await this.exifRepository
.createQueryBuilder('exif')
.leftJoin('exif.asset', 'asset')
.where('asset.ownerId = :userId', { userId })
.andWhere('exif.country IS NOT NULL')
.select('exif.country')
.distinctOn(['exif.country'])
.getMany();
return entity.map((e) => e.country ?? '').filter((c) => c !== '');
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
async getStates(userId: string, country: string | undefined): Promise<string[]> {
let result: ExifEntity[] = [];
const query = this.exifRepository
.createQueryBuilder('exif')
.leftJoin('exif.asset', 'asset')
.where('asset.ownerId = :userId', { userId })
.andWhere('exif.state IS NOT NULL')
.select('exif.state')
.distinctOn(['exif.state']);
if (country) {
query.andWhere('exif.country = :country', { country });
}
result = await query.getMany();
return result.map((entity) => entity.state ?? '').filter((s) => s !== '');
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING, DummyValue.STRING] })
async getCities(userId: string, country: string | undefined, state: string | undefined): Promise<string[]> {
let result: ExifEntity[] = [];
const query = this.exifRepository
.createQueryBuilder('exif')
.leftJoin('exif.asset', 'asset')
.where('asset.ownerId = :userId', { userId })
.andWhere('exif.city IS NOT NULL')
.select('exif.city')
.distinctOn(['exif.city']);
if (country) {
query.andWhere('exif.country = :country', { country });
}
if (state) {
query.andWhere('exif.state = :state', { state });
}
result = await query.getMany();
return result.map((entity) => entity.city ?? '').filter((c) => c !== '');
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
async getCameraMakes(userId: string, model: string | undefined): Promise<string[]> {
let result: ExifEntity[] = [];
const query = this.exifRepository
.createQueryBuilder('exif')
.leftJoin('exif.asset', 'asset')
.where('asset.ownerId = :userId', { userId })
.andWhere('exif.make IS NOT NULL')
.select('exif.make')
.distinctOn(['exif.make']);
if (model) {
query.andWhere('exif.model = :model', { model });
}
result = await query.getMany();
return result.map((entity) => entity.make ?? '').filter((m) => m !== '');
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
async getCameraModels(userId: string, make: string | undefined): Promise<string[]> {
let result: ExifEntity[] = [];
const query = this.exifRepository
.createQueryBuilder('exif')
.leftJoin('exif.asset', 'asset')
.where('asset.ownerId = :userId', { userId })
.andWhere('exif.model IS NOT NULL')
.select('exif.model')
.distinctOn(['exif.model']);
if (make) {
query.andWhere('exif.make = :make', { make });
}
result = await query.getMany();
return result.map((entity) => entity.model ?? '').filter((m) => m !== '');
}
}
@@ -8,5 +8,10 @@ export const newMetadataRepositoryMock = (): jest.Mocked<IMetadataRepository> =>
readTags: jest.fn(),
writeTags: jest.fn(),
extractBinaryTag: jest.fn(),
getCameraMakes: jest.fn(),
getCameraModels: jest.fn(),
getCities: jest.fn(),
getCountries: jest.fn(),
getStates: jest.fn(),
};
};
+1 -1
View File
@@ -67,8 +67,8 @@
"version": "1.92.1",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@oazapfts/runtime": "^1.0.0",
"@types/node": "^20.11.0",
"oazapfts": "^5.1.4",
"typescript": "^5.3.3"
},
"peerDependencies": {
@@ -23,6 +23,7 @@
export let selectedOption: ComboBoxOption | undefined = undefined;
export let placeholder = '';
export const label = '';
export let noLabel = false;
let isOpen = false;
let searchQuery = '';
@@ -31,11 +32,13 @@
const dispatch = createEventDispatcher<{
select: ComboBoxOption;
click: void;
}>();
let handleClick = () => {
searchQuery = '';
isOpen = !isOpen;
dispatch('click');
};
let handleOutClick = () => {
@@ -52,7 +55,9 @@
<div class="relative" use:clickOutside on:outclick={handleOutClick}>
<button {type} class="immich-form-input text-sm text-left w-full min-h-[48px] transition-all" on:click={handleClick}
>{selectedOption?.label}
>{#if !noLabel}
{selectedOption?.label || ''}
{/if}
<div class="absolute right-0 top-0 h-full flex px-4 justify-center items-center content-between">
<Icon path={mdiUnfoldMoreHorizontal} />
</div>
@@ -60,7 +65,7 @@
{#if isOpen}
<div
transition:fly={{ y: 25, duration: 250 }}
transition:fly={{ y: -25, duration: 250 }}
class="absolute w-full top-full mt-2 bg-white dark:bg-gray-800 rounded-lg border border-gray-300 dark:border-gray-900 z-10"
>
<div class="relative border-b flex">
@@ -80,8 +85,8 @@
<button
{type}
class="block text-left w-full px-4 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 transition-all
${option.label === selectedOption?.label ? 'bg-gray-300 dark:bg-gray-600' : ''}
"
${option.label === selectedOption?.label ? 'bg-gray-300 dark:bg-gray-600' : ''}
"
class:bg-gray-300={option.label === selectedOption?.label}
on:click={() => handleSelect(option)}
>
@@ -2,6 +2,12 @@
import Button from '$lib/components/elements/buttons/button.svelte';
import { fly } from 'svelte/transition';
import Combobox, { type ComboBoxOption } from '../combobox.svelte';
import { SearchSuggestionType, api, type PersonResponseDto } from '@api';
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import { mdiArrowRight, mdiClose } from '@mdi/js';
import { handleError } from '$lib/utils/handle-error';
import { onMount } from 'svelte';
enum MediaType {
All = 'all',
@@ -9,25 +15,274 @@
Video = 'video',
}
let selectedCountry: ComboBoxOption = { label: '', value: '' };
let selectedState: ComboBoxOption = { label: '', value: '' };
let selectedCity: ComboBoxOption = { label: '', value: '' };
type SearchSuggestion = {
people: PersonResponseDto[];
country: ComboBoxOption[];
state: ComboBoxOption[];
city: ComboBoxOption[];
cameraMake: ComboBoxOption[];
cameraModel: ComboBoxOption[];
};
let mediaType: MediaType = MediaType.All;
let notInAlbum = false;
let inArchive = false;
let inFavorite = false;
type SearchParams = {
state?: string;
country?: string;
city?: string;
cameraMake?: string;
cameraModel?: string;
};
type SearchFilter = {
context?: string;
people: PersonResponseDto[];
location: {
country?: ComboBoxOption;
state?: ComboBoxOption;
city?: ComboBoxOption;
};
camera: {
make?: ComboBoxOption;
model?: ComboBoxOption;
};
dateRange: {
startDate?: Date;
endDate?: Date;
};
inArchive?: boolean;
inFavorite?: boolean;
notInAlbum?: boolean;
mediaType: MediaType;
};
let suggestions: SearchSuggestion = {
people: [],
country: [],
state: [],
city: [],
cameraMake: [],
cameraModel: [],
};
let filter: SearchFilter = {
context: undefined,
people: [],
location: {
country: undefined,
state: undefined,
city: undefined,
},
camera: {
make: undefined,
model: undefined,
},
dateRange: {
startDate: undefined,
endDate: undefined,
},
inArchive: undefined,
inFavorite: undefined,
notInAlbum: undefined,
mediaType: MediaType.All,
};
let showAllPeople = false;
$: peopleList = showAllPeople ? suggestions.people : suggestions.people.slice(0, 11);
onMount(() => {
getPeople();
});
const showSelectedPeopleFirst = () => {
suggestions.people.sort((a, _) => {
if (filter.people.some((p) => p.id === a.id)) {
return -1;
}
return 1;
});
};
const getPeople = async () => {
try {
const { data } = await api.personApi.getAllPeople({ withHidden: false });
suggestions.people = data.people;
} catch (error) {
handleError(error, 'Failed to get people');
}
};
const handlePeopleSelection = (id: string) => {
if (filter.people.some((p) => p.id === id)) {
filter.people = filter.people.filter((p) => p.id !== id);
showSelectedPeopleFirst();
return;
}
const person = suggestions.people.find((p) => p.id === id);
if (person) {
filter.people = [...filter.people, person];
showSelectedPeopleFirst();
}
};
const updateSuggestion = async (type: SearchSuggestionType, params: SearchParams) => {
if (
type === SearchSuggestionType.City ||
type === SearchSuggestionType.State ||
type === SearchSuggestionType.Country
) {
suggestions = { ...suggestions, city: [], state: [], country: [] };
}
if (type === SearchSuggestionType.CameraMake || type === SearchSuggestionType.CameraModel) {
suggestions = { ...suggestions, cameraMake: [], cameraModel: [] };
}
try {
const { data } = await api.searchApi.getSearchSuggestions({
type: type,
country: params.country,
state: params.state,
make: params.cameraMake,
model: params.cameraModel,
});
switch (type) {
case SearchSuggestionType.Country: {
for (const country of data) {
suggestions.country = [...suggestions.country, { label: country, value: country }];
}
break;
}
case SearchSuggestionType.State: {
for (const state of data) {
suggestions.state = [...suggestions.state, { label: state, value: state }];
}
break;
}
case SearchSuggestionType.City: {
for (const city of data) {
suggestions.city = [...suggestions.city, { label: city, value: city }];
}
break;
}
case SearchSuggestionType.CameraMake: {
for (const make of data) {
suggestions.cameraMake = [...suggestions.cameraMake, { label: make, value: make }];
}
break;
}
case SearchSuggestionType.CameraModel: {
for (const model of data) {
suggestions.cameraModel = [...suggestions.cameraModel, { label: model, value: model }];
}
break;
}
}
} catch (error) {
handleError(error, 'Failed to get search suggestions');
}
};
const resetForm = () => {
filter = {
context: undefined,
people: [],
location: {
country: undefined,
state: undefined,
city: undefined,
},
camera: {
make: undefined,
model: undefined,
},
dateRange: {
startDate: undefined,
endDate: undefined,
},
inArchive: undefined,
inFavorite: undefined,
notInAlbum: undefined,
mediaType: MediaType.All,
};
};
const search = () => {};
</script>
<div
transition:fly={{ y: 25, duration: 250 }}
class="absolute w-full rounded-b-3xl border border-gray-200 bg-white pb-5 shadow-2xl transition-all dark:border-gray-800 dark:bg-immich-dark-gray dark:text-gray-300 p-6"
class="absolute w-full rounded-b-3xl border border-gray-200 bg-white shadow-2xl transition-all dark:border-gray-800 dark:bg-immich-dark-gray dark:text-gray-300 px-6 pt-6 overflow-y-auto max-h-[90vh] immich-scrollbar"
>
<p class="text-xs py-2">FILTERS</p>
<hr class="py-2" />
<hr class="border-slate-300 dark:border-slate-700 py-2" />
<form id="search-filter-form" autocomplete="off">
<div class="py-3">
<form id="search-filter-form relative" autocomplete="off" class="hover:cursor-auto">
<!-- PEOPLE -->
<div id="people-selection" class="my-4">
<div class="flex justify-between place-items-center gap-6">
<div class="flex-1">
<p class="immich-form-label">PEOPLE</p>
</div>
</div>
{#if suggestions.people.length > 0}
<div class="flex gap-1 mt-4 flex-wrap max-h-[300px] overflow-y-auto immich-scrollbar transition-all">
{#each peopleList as person (person.id)}
<button
type="button"
class="w-20 text-center rounded-3xl border-2 border-transparent hover:bg-immich-gray dark:hover:bg-immich-dark-primary/20 p-2 flex-col place-items-center transition-all {filter.people.some(
(p) => p.id === person.id,
)
? 'dark:border-slate-500 border-slate-300 bg-slate-200 dark:bg-slate-800 dark:text-white'
: ''}"
on:click={() => handlePeopleSelection(person.id)}
>
<ImageThumbnail
circle
shadow
url={api.getPeopleThumbnailUrl(person.id)}
altText={person.name}
widthStyle="100px"
/>
<p class="mt-2 text-ellipsis text-sm font-medium dark:text-white">{person.name}</p>
</button>
{/each}
</div>
<div class="flex justify-center mt-2">
<Button
shadow={false}
color="text-primary"
type="button"
class="flex gap-2 place-items-center place-content-center"
on:click={() => (showAllPeople = !showAllPeople)}
>
{#if showAllPeople}
<span><Icon path={mdiClose} /></span>
Collapse
{:else}
<span><Icon path={mdiArrowRight} /></span>
See all people
{/if}
</Button>
</div>
{/if}
</div>
<hr class="border-slate-300 dark:border-slate-700" />
<!-- CONTEXT -->
<div class="my-4">
<label class="immich-form-label" for="context">CONTEXT</label>
<input
class="immich-form-input hover:cursor-text w-full mt-3"
@@ -35,9 +290,111 @@
id="context"
name="context"
placeholder="Sunrise on the beach"
bind:value={filter.context}
/>
</div>
<hr class="border-slate-300 dark:border-slate-700" />
<!-- LOCATION -->
<div id="location-selection" class="my-4">
<p class="immich-form-label">PLACE</p>
<div class="flex justify-between gap-5 mt-3">
<div class="w-full">
<p class="text-sm text-black dark:text-white">Country</p>
<Combobox
options={suggestions.country}
bind:selectedOption={filter.location.country}
placeholder="Search country..."
on:click={() => updateSuggestion(SearchSuggestionType.Country, {})}
/>
</div>
<div class="w-full">
<p class="text-sm text-black dark:text-white">State</p>
<Combobox
options={suggestions.state}
bind:selectedOption={filter.location.state}
placeholder="Search state..."
on:click={() => updateSuggestion(SearchSuggestionType.State, { country: filter.location.country?.value })}
/>
</div>
<div class="w-full">
<p class="text-sm text-black dark:text-white">City</p>
<Combobox
options={suggestions.city}
bind:selectedOption={filter.location.city}
placeholder="Search city..."
on:click={() =>
updateSuggestion(SearchSuggestionType.City, {
country: filter.location.country?.value,
state: filter.location.state?.value,
})}
/>
</div>
</div>
</div>
<hr class="border-slate-300 dark:border-slate-700" />
<!-- CAMERA MODEL -->
<div id="camera-selection" class="my-4">
<p class="immich-form-label">CAMERA</p>
<div class="flex justify-between gap-5 mt-3">
<div class="w-full">
<p class="text-sm text-black dark:text-white">Make</p>
<Combobox
options={suggestions.cameraMake}
bind:selectedOption={filter.camera.make}
placeholder="Search camera make..."
on:click={() =>
updateSuggestion(SearchSuggestionType.CameraMake, { cameraModel: filter.camera.model?.value })}
/>
</div>
<div class="w-full">
<p class="text-sm text-black dark:text-white">Model</p>
<Combobox
options={suggestions.cameraModel}
bind:selectedOption={filter.camera.model}
placeholder="Search camera model..."
on:click={() =>
updateSuggestion(SearchSuggestionType.CameraModel, { cameraMake: filter.camera.make?.value })}
/>
</div>
</div>
</div>
<hr class="border-slate-300 dark:border-slate-700" />
<!-- DATE RANGE -->
<div id="date-range-selection" class="my-4 flex justify-between gap-5">
<div class="mb-3 flex-1 mt">
<label class="immich-form-label" for="start-date">START DATE</label>
<input
class="immich-form-input w-full mt-3 hover:cursor-pointer"
type="date"
id="start-date"
name="start-date"
bind:value={filter.dateRange.startDate}
/>
</div>
<div class="mb-3 flex-1">
<label class="immich-form-label" for="end-date">END DATE</label>
<input
class="immich-form-input w-full mt-3 hover:cursor-pointer"
type="date"
id="end-date"
name="end-date"
placeholder=""
bind:value={filter.dateRange.endDate}
/>
</div>
</div>
<hr class="border-slate-300 dark:border-slate-700" />
<div class="py-3 grid grid-cols-2">
<!-- MEDIA TYPE -->
<div id="media-type-selection">
@@ -49,7 +406,7 @@
class="text-base flex place-items-center gap-1 hover:cursor-pointer text-black dark:text-white"
>
<input
bind:group={mediaType}
bind:group={filter.mediaType}
value={MediaType.All}
type="radio"
name="radio-type"
@@ -62,10 +419,10 @@
class="text-base flex place-items-center gap-1 hover:cursor-pointer text-black dark:text-white"
>
<input
bind:group={mediaType}
bind:group={filter.mediaType}
value={MediaType.Image}
type="radio"
name="radio-type"
name="media-type"
id="type-image"
/>Image</label
>
@@ -75,7 +432,7 @@
class="text-base flex place-items-center gap-1 hover:cursor-pointer text-black dark:text-white"
>
<input
bind:group={mediaType}
bind:group={filter.mediaType}
value={MediaType.Video}
type="radio"
name="radio-type"
@@ -91,108 +448,29 @@
<div class="flex gap-5 mt-3">
<label class="flex items-center mb-2">
<input type="checkbox" class="form-checkbox h-5 w-5 color" bind:checked={notInAlbum} />
<input type="checkbox" class="form-checkbox h-5 w-5 color" bind:checked={filter.notInAlbum} />
<span class="ml-2 text-sm text-black dark:text-white pt-1">Not in any album</span>
</label>
<label class="flex items-center mb-2">
<input type="checkbox" class="form-checkbox h-5 w-5 color" bind:checked={inArchive} />
<input type="checkbox" class="form-checkbox h-5 w-5 color" bind:checked={filter.inArchive} />
<span class="ml-2 text-sm text-black dark:text-white pt-1">Archive</span>
</label>
<label class="flex items-center mb-2">
<input type="checkbox" class="form-checkbox h-5 w-5 color" bind:checked={inFavorite} />
<input type="checkbox" class="form-checkbox h-5 w-5 color" bind:checked={filter.inFavorite} />
<span class="ml-2 text-sm text-black dark:text-white pt-1">Favorite</span>
</label>
</div>
</div>
</div>
<hr />
<!-- PEOPLE -->
<div id="people-selection" class="my-4">
<div class="flex justify-between place-items-center gap-6">
<div class="flex-1">
<p class="immich-form-label">PEOPLE</p>
</div>
<div class="flex-1">
<Combobox options={[]} selectedOption={selectedCountry} placeholder="Search people..." />
</div>
</div>
</div>
<hr />
<!-- LOCATION -->
<div id="location-selection" class="my-4">
<p class="immich-form-label">PLACE</p>
<div class="flex justify-between gap-5 mt-3">
<div class="w-full">
<p class="text-sm text-black dark:text-white">Country</p>
<Combobox options={[]} selectedOption={selectedCountry} placeholder="Search country..." />
</div>
<div class="w-full">
<p class="text-sm text-black dark:text-white">State</p>
<Combobox options={[]} selectedOption={selectedState} placeholder="Search state..." />
</div>
<div class="w-full">
<p class="text-sm text-black dark:text-white">City</p>
<Combobox options={[]} selectedOption={selectedCity} placeholder="Search city..." />
</div>
</div>
</div>
<hr />
<!-- CAMERA MODEL -->
<div id="camera-selection" class="my-4">
<p class="immich-form-label">CAMERA</p>
<div class="flex justify-between gap-5 mt-3">
<div class="w-full">
<p class="text-sm text-black dark:text-white">Make</p>
<Combobox options={[]} selectedOption={selectedCountry} placeholder="Search country..." />
</div>
<div class="w-full">
<p class="text-sm text-black dark:text-white">Model</p>
<Combobox options={[]} selectedOption={selectedState} placeholder="Search state..." />
</div>
</div>
</div>
<hr />
<!-- DATE RANGE -->
<div id="date-range-selection" class="my-4 flex justify-between gap-5">
<div class="mb-3 flex-1 mt">
<label class="immich-form-label" for="start-date">START DATE</label>
<input
class="immich-form-input w-full mt-3 hover:cursor-pointer"
type="date"
id="start-date"
name="start-date"
/>
</div>
<div class="mb-3 flex-1">
<label class="immich-form-label" for="end-date">END DATE</label>
<input
class="immich-form-input w-full mt-3 hover:cursor-pointer"
type="date"
id="end-date"
name="end-date"
placeholder=""
/>
</div>
</div>
<div id="button-row" class="flex justify-end gap-4 mt-5">
<Button color="gray">CLEAR ALL</Button>
<Button type="submit">SEARCH</Button>
<div
id="button-row"
class="flex justify-end gap-4 py-4 sticky bottom-0 dark:border-gray-800 dark:bg-immich-dark-gray"
>
<Button color="gray" on:click={resetForm}>CLEAR ALL</Button>
<Button type="button" on:click={search}>SEARCH</Button>
</div>
</form>
</div>