mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 15:08:44 -04:00
feat: shared links custom URL (#19999)
* feat: custom url for shared links * feat: use a separate route and query param --------- Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
parent
16b14b390f
commit
9b3718120b
@ -725,6 +725,7 @@
|
|||||||
"current_server_address": "Current server address",
|
"current_server_address": "Current server address",
|
||||||
"custom_locale": "Custom Locale",
|
"custom_locale": "Custom Locale",
|
||||||
"custom_locale_description": "Format dates and numbers based on the language and the region",
|
"custom_locale_description": "Format dates and numbers based on the language and the region",
|
||||||
|
"custom_url": "Custom URL",
|
||||||
"daily_title_text_date": "E, MMM dd",
|
"daily_title_text_date": "E, MMM dd",
|
||||||
"daily_title_text_date_year": "E, MMM dd, yyyy",
|
"daily_title_text_date_year": "E, MMM dd, yyyy",
|
||||||
"dark": "Dark",
|
"dark": "Dark",
|
||||||
@ -1172,7 +1173,6 @@
|
|||||||
"light": "Light",
|
"light": "Light",
|
||||||
"like_deleted": "Like deleted",
|
"like_deleted": "Like deleted",
|
||||||
"link_motion_video": "Link motion video",
|
"link_motion_video": "Link motion video",
|
||||||
"link_options": "Link options",
|
|
||||||
"link_to_oauth": "Link to OAuth",
|
"link_to_oauth": "Link to OAuth",
|
||||||
"linked_oauth_account": "Linked OAuth account",
|
"linked_oauth_account": "Linked OAuth account",
|
||||||
"list": "List",
|
"list": "List",
|
||||||
@ -1745,6 +1745,7 @@
|
|||||||
"shared_link_clipboard_copied_massage": "Copied to clipboard",
|
"shared_link_clipboard_copied_massage": "Copied to clipboard",
|
||||||
"shared_link_clipboard_text": "Link: {link}\nPassword: {password}",
|
"shared_link_clipboard_text": "Link: {link}\nPassword: {password}",
|
||||||
"shared_link_create_error": "Error while creating shared link",
|
"shared_link_create_error": "Error while creating shared link",
|
||||||
|
"shared_link_custom_url_description": "Access this shared link with a custom URL",
|
||||||
"shared_link_edit_description_hint": "Enter the share description",
|
"shared_link_edit_description_hint": "Enter the share description",
|
||||||
"shared_link_edit_expire_after_option_day": "1 day",
|
"shared_link_edit_expire_after_option_day": "1 day",
|
||||||
"shared_link_edit_expire_after_option_days": "{count} days",
|
"shared_link_edit_expire_after_option_days": "{count} days",
|
||||||
@ -1770,6 +1771,7 @@
|
|||||||
"shared_link_info_chip_metadata": "EXIF",
|
"shared_link_info_chip_metadata": "EXIF",
|
||||||
"shared_link_manage_links": "Manage Shared links",
|
"shared_link_manage_links": "Manage Shared links",
|
||||||
"shared_link_options": "Shared link options",
|
"shared_link_options": "Shared link options",
|
||||||
|
"shared_link_password_description": "Require a password to access this shared link",
|
||||||
"shared_links": "Shared links",
|
"shared_links": "Shared links",
|
||||||
"shared_links_description": "Share photos and videos with a link",
|
"shared_links_description": "Share photos and videos with a link",
|
||||||
"shared_photos_and_videos_count": "{assetCount, plural, other {# shared photos & videos.}}",
|
"shared_photos_and_videos_count": "{assetCount, plural, other {# shared photos & videos.}}",
|
||||||
|
26
mobile/openapi/lib/api/albums_api.dart
generated
26
mobile/openapi/lib/api/albums_api.dart
generated
@ -24,7 +24,9 @@ class AlbumsApi {
|
|||||||
/// * [BulkIdsDto] bulkIdsDto (required):
|
/// * [BulkIdsDto] bulkIdsDto (required):
|
||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
Future<Response> addAssetsToAlbumWithHttpInfo(String id, BulkIdsDto bulkIdsDto, { String? key, }) async {
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
Future<Response> addAssetsToAlbumWithHttpInfo(String id, BulkIdsDto bulkIdsDto, { String? key, String? slug, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/albums/{id}/assets'
|
final apiPath = r'/albums/{id}/assets'
|
||||||
.replaceAll('{id}', id);
|
.replaceAll('{id}', id);
|
||||||
@ -39,6 +41,9 @@ class AlbumsApi {
|
|||||||
if (key != null) {
|
if (key != null) {
|
||||||
queryParams.addAll(_queryParams('', 'key', key));
|
queryParams.addAll(_queryParams('', 'key', key));
|
||||||
}
|
}
|
||||||
|
if (slug != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||||
|
}
|
||||||
|
|
||||||
const contentTypes = <String>['application/json'];
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
@ -61,8 +66,10 @@ class AlbumsApi {
|
|||||||
/// * [BulkIdsDto] bulkIdsDto (required):
|
/// * [BulkIdsDto] bulkIdsDto (required):
|
||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
Future<List<BulkIdResponseDto>?> addAssetsToAlbum(String id, BulkIdsDto bulkIdsDto, { String? key, }) async {
|
///
|
||||||
final response = await addAssetsToAlbumWithHttpInfo(id, bulkIdsDto, key: key, );
|
/// * [String] slug:
|
||||||
|
Future<List<BulkIdResponseDto>?> addAssetsToAlbum(String id, BulkIdsDto bulkIdsDto, { String? key, String? slug, }) async {
|
||||||
|
final response = await addAssetsToAlbumWithHttpInfo(id, bulkIdsDto, key: key, slug: slug, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
@ -225,8 +232,10 @@ class AlbumsApi {
|
|||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
///
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
///
|
||||||
/// * [bool] withoutAssets:
|
/// * [bool] withoutAssets:
|
||||||
Future<Response> getAlbumInfoWithHttpInfo(String id, { String? key, bool? withoutAssets, }) async {
|
Future<Response> getAlbumInfoWithHttpInfo(String id, { String? key, String? slug, bool? withoutAssets, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/albums/{id}'
|
final apiPath = r'/albums/{id}'
|
||||||
.replaceAll('{id}', id);
|
.replaceAll('{id}', id);
|
||||||
@ -241,6 +250,9 @@ class AlbumsApi {
|
|||||||
if (key != null) {
|
if (key != null) {
|
||||||
queryParams.addAll(_queryParams('', 'key', key));
|
queryParams.addAll(_queryParams('', 'key', key));
|
||||||
}
|
}
|
||||||
|
if (slug != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||||
|
}
|
||||||
if (withoutAssets != null) {
|
if (withoutAssets != null) {
|
||||||
queryParams.addAll(_queryParams('', 'withoutAssets', withoutAssets));
|
queryParams.addAll(_queryParams('', 'withoutAssets', withoutAssets));
|
||||||
}
|
}
|
||||||
@ -265,9 +277,11 @@ class AlbumsApi {
|
|||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
///
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
///
|
||||||
/// * [bool] withoutAssets:
|
/// * [bool] withoutAssets:
|
||||||
Future<AlbumResponseDto?> getAlbumInfo(String id, { String? key, bool? withoutAssets, }) async {
|
Future<AlbumResponseDto?> getAlbumInfo(String id, { String? key, String? slug, bool? withoutAssets, }) async {
|
||||||
final response = await getAlbumInfoWithHttpInfo(id, key: key, withoutAssets: withoutAssets, );
|
final response = await getAlbumInfoWithHttpInfo(id, key: key, slug: slug, withoutAssets: withoutAssets, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
|
78
mobile/openapi/lib/api/assets_api.dart
generated
78
mobile/openapi/lib/api/assets_api.dart
generated
@ -173,7 +173,9 @@ class AssetsApi {
|
|||||||
/// * [String] id (required):
|
/// * [String] id (required):
|
||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
Future<Response> downloadAssetWithHttpInfo(String id, { String? key, }) async {
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
Future<Response> downloadAssetWithHttpInfo(String id, { String? key, String? slug, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/assets/{id}/original'
|
final apiPath = r'/assets/{id}/original'
|
||||||
.replaceAll('{id}', id);
|
.replaceAll('{id}', id);
|
||||||
@ -188,6 +190,9 @@ class AssetsApi {
|
|||||||
if (key != null) {
|
if (key != null) {
|
||||||
queryParams.addAll(_queryParams('', 'key', key));
|
queryParams.addAll(_queryParams('', 'key', key));
|
||||||
}
|
}
|
||||||
|
if (slug != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||||
|
}
|
||||||
|
|
||||||
const contentTypes = <String>[];
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
@ -208,8 +213,10 @@ class AssetsApi {
|
|||||||
/// * [String] id (required):
|
/// * [String] id (required):
|
||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
Future<MultipartFile?> downloadAsset(String id, { String? key, }) async {
|
///
|
||||||
final response = await downloadAssetWithHttpInfo(id, key: key, );
|
/// * [String] slug:
|
||||||
|
Future<MultipartFile?> downloadAsset(String id, { String? key, String? slug, }) async {
|
||||||
|
final response = await downloadAssetWithHttpInfo(id, key: key, slug: slug, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
@ -289,7 +296,9 @@ class AssetsApi {
|
|||||||
/// * [String] id (required):
|
/// * [String] id (required):
|
||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
Future<Response> getAssetInfoWithHttpInfo(String id, { String? key, }) async {
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
Future<Response> getAssetInfoWithHttpInfo(String id, { String? key, String? slug, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/assets/{id}'
|
final apiPath = r'/assets/{id}'
|
||||||
.replaceAll('{id}', id);
|
.replaceAll('{id}', id);
|
||||||
@ -304,6 +313,9 @@ class AssetsApi {
|
|||||||
if (key != null) {
|
if (key != null) {
|
||||||
queryParams.addAll(_queryParams('', 'key', key));
|
queryParams.addAll(_queryParams('', 'key', key));
|
||||||
}
|
}
|
||||||
|
if (slug != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||||
|
}
|
||||||
|
|
||||||
const contentTypes = <String>[];
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
@ -324,8 +336,10 @@ class AssetsApi {
|
|||||||
/// * [String] id (required):
|
/// * [String] id (required):
|
||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
Future<AssetResponseDto?> getAssetInfo(String id, { String? key, }) async {
|
///
|
||||||
final response = await getAssetInfoWithHttpInfo(id, key: key, );
|
/// * [String] slug:
|
||||||
|
Future<AssetResponseDto?> getAssetInfo(String id, { String? key, String? slug, }) async {
|
||||||
|
final response = await getAssetInfoWithHttpInfo(id, key: key, slug: slug, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
@ -469,7 +483,9 @@ class AssetsApi {
|
|||||||
/// * [String] id (required):
|
/// * [String] id (required):
|
||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
Future<Response> playAssetVideoWithHttpInfo(String id, { String? key, }) async {
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
Future<Response> playAssetVideoWithHttpInfo(String id, { String? key, String? slug, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/assets/{id}/video/playback'
|
final apiPath = r'/assets/{id}/video/playback'
|
||||||
.replaceAll('{id}', id);
|
.replaceAll('{id}', id);
|
||||||
@ -484,6 +500,9 @@ class AssetsApi {
|
|||||||
if (key != null) {
|
if (key != null) {
|
||||||
queryParams.addAll(_queryParams('', 'key', key));
|
queryParams.addAll(_queryParams('', 'key', key));
|
||||||
}
|
}
|
||||||
|
if (slug != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||||
|
}
|
||||||
|
|
||||||
const contentTypes = <String>[];
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
@ -504,8 +523,10 @@ class AssetsApi {
|
|||||||
/// * [String] id (required):
|
/// * [String] id (required):
|
||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
Future<MultipartFile?> playAssetVideo(String id, { String? key, }) async {
|
///
|
||||||
final response = await playAssetVideoWithHttpInfo(id, key: key, );
|
/// * [String] slug:
|
||||||
|
Future<MultipartFile?> playAssetVideo(String id, { String? key, String? slug, }) async {
|
||||||
|
final response = await playAssetVideoWithHttpInfo(id, key: key, slug: slug, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
@ -541,10 +562,12 @@ class AssetsApi {
|
|||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
///
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
///
|
||||||
/// * [String] duration:
|
/// * [String] duration:
|
||||||
///
|
///
|
||||||
/// * [String] filename:
|
/// * [String] filename:
|
||||||
Future<Response> replaceAssetWithHttpInfo(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? duration, String? filename, }) async {
|
Future<Response> replaceAssetWithHttpInfo(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/assets/{id}/original'
|
final apiPath = r'/assets/{id}/original'
|
||||||
.replaceAll('{id}', id);
|
.replaceAll('{id}', id);
|
||||||
@ -559,6 +582,9 @@ class AssetsApi {
|
|||||||
if (key != null) {
|
if (key != null) {
|
||||||
queryParams.addAll(_queryParams('', 'key', key));
|
queryParams.addAll(_queryParams('', 'key', key));
|
||||||
}
|
}
|
||||||
|
if (slug != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||||
|
}
|
||||||
|
|
||||||
const contentTypes = <String>['multipart/form-data'];
|
const contentTypes = <String>['multipart/form-data'];
|
||||||
|
|
||||||
@ -628,11 +654,13 @@ class AssetsApi {
|
|||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
///
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
///
|
||||||
/// * [String] duration:
|
/// * [String] duration:
|
||||||
///
|
///
|
||||||
/// * [String] filename:
|
/// * [String] filename:
|
||||||
Future<AssetMediaResponseDto?> replaceAsset(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? duration, String? filename, }) async {
|
Future<AssetMediaResponseDto?> replaceAsset(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async {
|
||||||
final response = await replaceAssetWithHttpInfo(id, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, duration: duration, filename: filename, );
|
final response = await replaceAssetWithHttpInfo(id, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, duration: duration, filename: filename, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
@ -791,6 +819,8 @@ class AssetsApi {
|
|||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
///
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
///
|
||||||
/// * [String] xImmichChecksum:
|
/// * [String] xImmichChecksum:
|
||||||
/// sha1 checksum that can be used for duplicate detection before the file is uploaded
|
/// sha1 checksum that can be used for duplicate detection before the file is uploaded
|
||||||
///
|
///
|
||||||
@ -805,7 +835,7 @@ class AssetsApi {
|
|||||||
/// * [MultipartFile] sidecarData:
|
/// * [MultipartFile] sidecarData:
|
||||||
///
|
///
|
||||||
/// * [AssetVisibility] visibility:
|
/// * [AssetVisibility] visibility:
|
||||||
Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/assets';
|
final apiPath = r'/assets';
|
||||||
|
|
||||||
@ -819,6 +849,9 @@ class AssetsApi {
|
|||||||
if (key != null) {
|
if (key != null) {
|
||||||
queryParams.addAll(_queryParams('', 'key', key));
|
queryParams.addAll(_queryParams('', 'key', key));
|
||||||
}
|
}
|
||||||
|
if (slug != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||||
|
}
|
||||||
|
|
||||||
if (xImmichChecksum != null) {
|
if (xImmichChecksum != null) {
|
||||||
headerParams[r'x-immich-checksum'] = parameterToString(xImmichChecksum);
|
headerParams[r'x-immich-checksum'] = parameterToString(xImmichChecksum);
|
||||||
@ -903,6 +936,8 @@ class AssetsApi {
|
|||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
///
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
///
|
||||||
/// * [String] xImmichChecksum:
|
/// * [String] xImmichChecksum:
|
||||||
/// sha1 checksum that can be used for duplicate detection before the file is uploaded
|
/// sha1 checksum that can be used for duplicate detection before the file is uploaded
|
||||||
///
|
///
|
||||||
@ -917,8 +952,8 @@ class AssetsApi {
|
|||||||
/// * [MultipartFile] sidecarData:
|
/// * [MultipartFile] sidecarData:
|
||||||
///
|
///
|
||||||
/// * [AssetVisibility] visibility:
|
/// * [AssetVisibility] visibility:
|
||||||
Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
||||||
final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, visibility: visibility, );
|
final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, visibility: visibility, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
@ -940,7 +975,9 @@ class AssetsApi {
|
|||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
///
|
///
|
||||||
/// * [AssetMediaSize] size:
|
/// * [AssetMediaSize] size:
|
||||||
Future<Response> viewAssetWithHttpInfo(String id, { String? key, AssetMediaSize? size, }) async {
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
Future<Response> viewAssetWithHttpInfo(String id, { String? key, AssetMediaSize? size, String? slug, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/assets/{id}/thumbnail'
|
final apiPath = r'/assets/{id}/thumbnail'
|
||||||
.replaceAll('{id}', id);
|
.replaceAll('{id}', id);
|
||||||
@ -958,6 +995,9 @@ class AssetsApi {
|
|||||||
if (size != null) {
|
if (size != null) {
|
||||||
queryParams.addAll(_queryParams('', 'size', size));
|
queryParams.addAll(_queryParams('', 'size', size));
|
||||||
}
|
}
|
||||||
|
if (slug != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||||
|
}
|
||||||
|
|
||||||
const contentTypes = <String>[];
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
@ -980,8 +1020,10 @@ class AssetsApi {
|
|||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
///
|
///
|
||||||
/// * [AssetMediaSize] size:
|
/// * [AssetMediaSize] size:
|
||||||
Future<MultipartFile?> viewAsset(String id, { String? key, AssetMediaSize? size, }) async {
|
///
|
||||||
final response = await viewAssetWithHttpInfo(id, key: key, size: size, );
|
/// * [String] slug:
|
||||||
|
Future<MultipartFile?> viewAsset(String id, { String? key, AssetMediaSize? size, String? slug, }) async {
|
||||||
|
final response = await viewAssetWithHttpInfo(id, key: key, size: size, slug: slug, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
|
26
mobile/openapi/lib/api/download_api.dart
generated
26
mobile/openapi/lib/api/download_api.dart
generated
@ -22,7 +22,9 @@ class DownloadApi {
|
|||||||
/// * [AssetIdsDto] assetIdsDto (required):
|
/// * [AssetIdsDto] assetIdsDto (required):
|
||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
Future<Response> downloadArchiveWithHttpInfo(AssetIdsDto assetIdsDto, { String? key, }) async {
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
Future<Response> downloadArchiveWithHttpInfo(AssetIdsDto assetIdsDto, { String? key, String? slug, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/download/archive';
|
final apiPath = r'/download/archive';
|
||||||
|
|
||||||
@ -36,6 +38,9 @@ class DownloadApi {
|
|||||||
if (key != null) {
|
if (key != null) {
|
||||||
queryParams.addAll(_queryParams('', 'key', key));
|
queryParams.addAll(_queryParams('', 'key', key));
|
||||||
}
|
}
|
||||||
|
if (slug != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||||
|
}
|
||||||
|
|
||||||
const contentTypes = <String>['application/json'];
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
@ -56,8 +61,10 @@ class DownloadApi {
|
|||||||
/// * [AssetIdsDto] assetIdsDto (required):
|
/// * [AssetIdsDto] assetIdsDto (required):
|
||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
Future<MultipartFile?> downloadArchive(AssetIdsDto assetIdsDto, { String? key, }) async {
|
///
|
||||||
final response = await downloadArchiveWithHttpInfo(assetIdsDto, key: key, );
|
/// * [String] slug:
|
||||||
|
Future<MultipartFile?> downloadArchive(AssetIdsDto assetIdsDto, { String? key, String? slug, }) async {
|
||||||
|
final response = await downloadArchiveWithHttpInfo(assetIdsDto, key: key, slug: slug, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
@ -77,7 +84,9 @@ class DownloadApi {
|
|||||||
/// * [DownloadInfoDto] downloadInfoDto (required):
|
/// * [DownloadInfoDto] downloadInfoDto (required):
|
||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
Future<Response> getDownloadInfoWithHttpInfo(DownloadInfoDto downloadInfoDto, { String? key, }) async {
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
Future<Response> getDownloadInfoWithHttpInfo(DownloadInfoDto downloadInfoDto, { String? key, String? slug, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/download/info';
|
final apiPath = r'/download/info';
|
||||||
|
|
||||||
@ -91,6 +100,9 @@ class DownloadApi {
|
|||||||
if (key != null) {
|
if (key != null) {
|
||||||
queryParams.addAll(_queryParams('', 'key', key));
|
queryParams.addAll(_queryParams('', 'key', key));
|
||||||
}
|
}
|
||||||
|
if (slug != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||||
|
}
|
||||||
|
|
||||||
const contentTypes = <String>['application/json'];
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
@ -111,8 +123,10 @@ class DownloadApi {
|
|||||||
/// * [DownloadInfoDto] downloadInfoDto (required):
|
/// * [DownloadInfoDto] downloadInfoDto (required):
|
||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
Future<DownloadResponseDto?> getDownloadInfo(DownloadInfoDto downloadInfoDto, { String? key, }) async {
|
///
|
||||||
final response = await getDownloadInfoWithHttpInfo(downloadInfoDto, key: key, );
|
/// * [String] slug:
|
||||||
|
Future<DownloadResponseDto?> getDownloadInfo(DownloadInfoDto downloadInfoDto, { String? key, String? slug, }) async {
|
||||||
|
final response = await getDownloadInfoWithHttpInfo(downloadInfoDto, key: key, slug: slug, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
|
39
mobile/openapi/lib/api/shared_links_api.dart
generated
39
mobile/openapi/lib/api/shared_links_api.dart
generated
@ -24,7 +24,9 @@ class SharedLinksApi {
|
|||||||
/// * [AssetIdsDto] assetIdsDto (required):
|
/// * [AssetIdsDto] assetIdsDto (required):
|
||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
Future<Response> addSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto, { String? key, }) async {
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
Future<Response> addSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto, { String? key, String? slug, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/shared-links/{id}/assets'
|
final apiPath = r'/shared-links/{id}/assets'
|
||||||
.replaceAll('{id}', id);
|
.replaceAll('{id}', id);
|
||||||
@ -39,6 +41,9 @@ class SharedLinksApi {
|
|||||||
if (key != null) {
|
if (key != null) {
|
||||||
queryParams.addAll(_queryParams('', 'key', key));
|
queryParams.addAll(_queryParams('', 'key', key));
|
||||||
}
|
}
|
||||||
|
if (slug != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||||
|
}
|
||||||
|
|
||||||
const contentTypes = <String>['application/json'];
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
@ -61,8 +66,10 @@ class SharedLinksApi {
|
|||||||
/// * [AssetIdsDto] assetIdsDto (required):
|
/// * [AssetIdsDto] assetIdsDto (required):
|
||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
Future<List<AssetIdsResponseDto>?> addSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String? key, }) async {
|
///
|
||||||
final response = await addSharedLinkAssetsWithHttpInfo(id, assetIdsDto, key: key, );
|
/// * [String] slug:
|
||||||
|
Future<List<AssetIdsResponseDto>?> addSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String? key, String? slug, }) async {
|
||||||
|
final response = await addSharedLinkAssetsWithHttpInfo(id, assetIdsDto, key: key, slug: slug, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
@ -187,8 +194,10 @@ class SharedLinksApi {
|
|||||||
///
|
///
|
||||||
/// * [String] password:
|
/// * [String] password:
|
||||||
///
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
///
|
||||||
/// * [String] token:
|
/// * [String] token:
|
||||||
Future<Response> getMySharedLinkWithHttpInfo({ String? key, String? password, String? token, }) async {
|
Future<Response> getMySharedLinkWithHttpInfo({ String? key, String? password, String? slug, String? token, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/shared-links/me';
|
final apiPath = r'/shared-links/me';
|
||||||
|
|
||||||
@ -205,6 +214,9 @@ class SharedLinksApi {
|
|||||||
if (password != null) {
|
if (password != null) {
|
||||||
queryParams.addAll(_queryParams('', 'password', password));
|
queryParams.addAll(_queryParams('', 'password', password));
|
||||||
}
|
}
|
||||||
|
if (slug != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||||
|
}
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
queryParams.addAll(_queryParams('', 'token', token));
|
queryParams.addAll(_queryParams('', 'token', token));
|
||||||
}
|
}
|
||||||
@ -229,9 +241,11 @@ class SharedLinksApi {
|
|||||||
///
|
///
|
||||||
/// * [String] password:
|
/// * [String] password:
|
||||||
///
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
///
|
||||||
/// * [String] token:
|
/// * [String] token:
|
||||||
Future<SharedLinkResponseDto?> getMySharedLink({ String? key, String? password, String? token, }) async {
|
Future<SharedLinkResponseDto?> getMySharedLink({ String? key, String? password, String? slug, String? token, }) async {
|
||||||
final response = await getMySharedLinkWithHttpInfo( key: key, password: password, token: token, );
|
final response = await getMySharedLinkWithHttpInfo( key: key, password: password, slug: slug, token: token, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
@ -341,7 +355,9 @@ class SharedLinksApi {
|
|||||||
/// * [AssetIdsDto] assetIdsDto (required):
|
/// * [AssetIdsDto] assetIdsDto (required):
|
||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
Future<Response> removeSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto, { String? key, }) async {
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
Future<Response> removeSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto, { String? key, String? slug, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/shared-links/{id}/assets'
|
final apiPath = r'/shared-links/{id}/assets'
|
||||||
.replaceAll('{id}', id);
|
.replaceAll('{id}', id);
|
||||||
@ -356,6 +372,9 @@ class SharedLinksApi {
|
|||||||
if (key != null) {
|
if (key != null) {
|
||||||
queryParams.addAll(_queryParams('', 'key', key));
|
queryParams.addAll(_queryParams('', 'key', key));
|
||||||
}
|
}
|
||||||
|
if (slug != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||||
|
}
|
||||||
|
|
||||||
const contentTypes = <String>['application/json'];
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
@ -378,8 +397,10 @@ class SharedLinksApi {
|
|||||||
/// * [AssetIdsDto] assetIdsDto (required):
|
/// * [AssetIdsDto] assetIdsDto (required):
|
||||||
///
|
///
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
Future<List<AssetIdsResponseDto>?> removeSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String? key, }) async {
|
///
|
||||||
final response = await removeSharedLinkAssetsWithHttpInfo(id, assetIdsDto, key: key, );
|
/// * [String] slug:
|
||||||
|
Future<List<AssetIdsResponseDto>?> removeSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String? key, String? slug, }) async {
|
||||||
|
final response = await removeSharedLinkAssetsWithHttpInfo(id, assetIdsDto, key: key, slug: slug, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
|
26
mobile/openapi/lib/api/timeline_api.dart
generated
26
mobile/openapi/lib/api/timeline_api.dart
generated
@ -39,6 +39,8 @@ class TimelineApi {
|
|||||||
/// * [String] personId:
|
/// * [String] personId:
|
||||||
/// Filter assets containing a specific person (face recognition)
|
/// Filter assets containing a specific person (face recognition)
|
||||||
///
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
///
|
||||||
/// * [String] tagId:
|
/// * [String] tagId:
|
||||||
/// Filter assets with a specific tag
|
/// Filter assets with a specific tag
|
||||||
///
|
///
|
||||||
@ -53,7 +55,7 @@ class TimelineApi {
|
|||||||
///
|
///
|
||||||
/// * [bool] withStacked:
|
/// * [bool] withStacked:
|
||||||
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
|
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
|
||||||
Future<Response> getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
|
Future<Response> getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/timeline/bucket';
|
final apiPath = r'/timeline/bucket';
|
||||||
|
|
||||||
@ -82,6 +84,9 @@ class TimelineApi {
|
|||||||
if (personId != null) {
|
if (personId != null) {
|
||||||
queryParams.addAll(_queryParams('', 'personId', personId));
|
queryParams.addAll(_queryParams('', 'personId', personId));
|
||||||
}
|
}
|
||||||
|
if (slug != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||||
|
}
|
||||||
if (tagId != null) {
|
if (tagId != null) {
|
||||||
queryParams.addAll(_queryParams('', 'tagId', tagId));
|
queryParams.addAll(_queryParams('', 'tagId', tagId));
|
||||||
}
|
}
|
||||||
@ -135,6 +140,8 @@ class TimelineApi {
|
|||||||
/// * [String] personId:
|
/// * [String] personId:
|
||||||
/// Filter assets containing a specific person (face recognition)
|
/// Filter assets containing a specific person (face recognition)
|
||||||
///
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
///
|
||||||
/// * [String] tagId:
|
/// * [String] tagId:
|
||||||
/// Filter assets with a specific tag
|
/// Filter assets with a specific tag
|
||||||
///
|
///
|
||||||
@ -149,8 +156,8 @@ class TimelineApi {
|
|||||||
///
|
///
|
||||||
/// * [bool] withStacked:
|
/// * [bool] withStacked:
|
||||||
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
|
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
|
||||||
Future<TimeBucketAssetResponseDto?> getTimeBucket(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
|
Future<TimeBucketAssetResponseDto?> getTimeBucket(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
|
||||||
final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, );
|
final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
@ -184,6 +191,8 @@ class TimelineApi {
|
|||||||
/// * [String] personId:
|
/// * [String] personId:
|
||||||
/// Filter assets containing a specific person (face recognition)
|
/// Filter assets containing a specific person (face recognition)
|
||||||
///
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
///
|
||||||
/// * [String] tagId:
|
/// * [String] tagId:
|
||||||
/// Filter assets with a specific tag
|
/// Filter assets with a specific tag
|
||||||
///
|
///
|
||||||
@ -198,7 +207,7 @@ class TimelineApi {
|
|||||||
///
|
///
|
||||||
/// * [bool] withStacked:
|
/// * [bool] withStacked:
|
||||||
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
|
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
|
||||||
Future<Response> getTimeBucketsWithHttpInfo({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
|
Future<Response> getTimeBucketsWithHttpInfo({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/timeline/buckets';
|
final apiPath = r'/timeline/buckets';
|
||||||
|
|
||||||
@ -227,6 +236,9 @@ class TimelineApi {
|
|||||||
if (personId != null) {
|
if (personId != null) {
|
||||||
queryParams.addAll(_queryParams('', 'personId', personId));
|
queryParams.addAll(_queryParams('', 'personId', personId));
|
||||||
}
|
}
|
||||||
|
if (slug != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||||
|
}
|
||||||
if (tagId != null) {
|
if (tagId != null) {
|
||||||
queryParams.addAll(_queryParams('', 'tagId', tagId));
|
queryParams.addAll(_queryParams('', 'tagId', tagId));
|
||||||
}
|
}
|
||||||
@ -276,6 +288,8 @@ class TimelineApi {
|
|||||||
/// * [String] personId:
|
/// * [String] personId:
|
||||||
/// Filter assets containing a specific person (face recognition)
|
/// Filter assets containing a specific person (face recognition)
|
||||||
///
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
///
|
||||||
/// * [String] tagId:
|
/// * [String] tagId:
|
||||||
/// Filter assets with a specific tag
|
/// Filter assets with a specific tag
|
||||||
///
|
///
|
||||||
@ -290,8 +304,8 @@ class TimelineApi {
|
|||||||
///
|
///
|
||||||
/// * [bool] withStacked:
|
/// * [bool] withStacked:
|
||||||
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
|
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
|
||||||
Future<List<TimeBucketsResponseDto>?> getTimeBuckets({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
|
Future<List<TimeBucketsResponseDto>?> getTimeBuckets({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
|
||||||
final response = await getTimeBucketsWithHttpInfo( albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, );
|
final response = await getTimeBucketsWithHttpInfo( albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
|
25
mobile/openapi/lib/model/shared_link_create_dto.dart
generated
25
mobile/openapi/lib/model/shared_link_create_dto.dart
generated
@ -21,6 +21,7 @@ class SharedLinkCreateDto {
|
|||||||
this.expiresAt,
|
this.expiresAt,
|
||||||
this.password,
|
this.password,
|
||||||
this.showMetadata = true,
|
this.showMetadata = true,
|
||||||
|
this.slug,
|
||||||
required this.type,
|
required this.type,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -44,26 +45,16 @@ class SharedLinkCreateDto {
|
|||||||
|
|
||||||
List<String> assetIds;
|
List<String> assetIds;
|
||||||
|
|
||||||
///
|
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
|
||||||
/// does not include a default value (using the "default:" property), however, the generated
|
|
||||||
/// source code must fall back to having a nullable type.
|
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
|
||||||
///
|
|
||||||
String? description;
|
String? description;
|
||||||
|
|
||||||
DateTime? expiresAt;
|
DateTime? expiresAt;
|
||||||
|
|
||||||
///
|
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
|
||||||
/// does not include a default value (using the "default:" property), however, the generated
|
|
||||||
/// source code must fall back to having a nullable type.
|
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
|
||||||
///
|
|
||||||
String? password;
|
String? password;
|
||||||
|
|
||||||
bool showMetadata;
|
bool showMetadata;
|
||||||
|
|
||||||
|
String? slug;
|
||||||
|
|
||||||
SharedLinkType type;
|
SharedLinkType type;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -76,6 +67,7 @@ class SharedLinkCreateDto {
|
|||||||
other.expiresAt == expiresAt &&
|
other.expiresAt == expiresAt &&
|
||||||
other.password == password &&
|
other.password == password &&
|
||||||
other.showMetadata == showMetadata &&
|
other.showMetadata == showMetadata &&
|
||||||
|
other.slug == slug &&
|
||||||
other.type == type;
|
other.type == type;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -89,10 +81,11 @@ class SharedLinkCreateDto {
|
|||||||
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
||||||
(password == null ? 0 : password!.hashCode) +
|
(password == null ? 0 : password!.hashCode) +
|
||||||
(showMetadata.hashCode) +
|
(showMetadata.hashCode) +
|
||||||
|
(slug == null ? 0 : slug!.hashCode) +
|
||||||
(type.hashCode);
|
(type.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SharedLinkCreateDto[albumId=$albumId, allowDownload=$allowDownload, allowUpload=$allowUpload, assetIds=$assetIds, description=$description, expiresAt=$expiresAt, password=$password, showMetadata=$showMetadata, type=$type]';
|
String toString() => 'SharedLinkCreateDto[albumId=$albumId, allowDownload=$allowDownload, allowUpload=$allowUpload, assetIds=$assetIds, description=$description, expiresAt=$expiresAt, password=$password, showMetadata=$showMetadata, slug=$slug, type=$type]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -124,6 +117,11 @@ class SharedLinkCreateDto {
|
|||||||
// json[r'password'] = null;
|
// json[r'password'] = null;
|
||||||
}
|
}
|
||||||
json[r'showMetadata'] = this.showMetadata;
|
json[r'showMetadata'] = this.showMetadata;
|
||||||
|
if (this.slug != null) {
|
||||||
|
json[r'slug'] = this.slug;
|
||||||
|
} else {
|
||||||
|
// json[r'slug'] = null;
|
||||||
|
}
|
||||||
json[r'type'] = this.type;
|
json[r'type'] = this.type;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
@ -147,6 +145,7 @@ class SharedLinkCreateDto {
|
|||||||
expiresAt: mapDateTime(json, r'expiresAt', r''),
|
expiresAt: mapDateTime(json, r'expiresAt', r''),
|
||||||
password: mapValueOfType<String>(json, r'password'),
|
password: mapValueOfType<String>(json, r'password'),
|
||||||
showMetadata: mapValueOfType<bool>(json, r'showMetadata') ?? true,
|
showMetadata: mapValueOfType<bool>(json, r'showMetadata') ?? true,
|
||||||
|
slug: mapValueOfType<String>(json, r'slug'),
|
||||||
type: SharedLinkType.fromJson(json[r'type'])!,
|
type: SharedLinkType.fromJson(json[r'type'])!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
29
mobile/openapi/lib/model/shared_link_edit_dto.dart
generated
29
mobile/openapi/lib/model/shared_link_edit_dto.dart
generated
@ -20,6 +20,7 @@ class SharedLinkEditDto {
|
|||||||
this.expiresAt,
|
this.expiresAt,
|
||||||
this.password,
|
this.password,
|
||||||
this.showMetadata,
|
this.showMetadata,
|
||||||
|
this.slug,
|
||||||
});
|
});
|
||||||
|
|
||||||
///
|
///
|
||||||
@ -47,22 +48,10 @@ class SharedLinkEditDto {
|
|||||||
///
|
///
|
||||||
bool? changeExpiryTime;
|
bool? changeExpiryTime;
|
||||||
|
|
||||||
///
|
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
|
||||||
/// does not include a default value (using the "default:" property), however, the generated
|
|
||||||
/// source code must fall back to having a nullable type.
|
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
|
||||||
///
|
|
||||||
String? description;
|
String? description;
|
||||||
|
|
||||||
DateTime? expiresAt;
|
DateTime? expiresAt;
|
||||||
|
|
||||||
///
|
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
|
||||||
/// does not include a default value (using the "default:" property), however, the generated
|
|
||||||
/// source code must fall back to having a nullable type.
|
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
|
||||||
///
|
|
||||||
String? password;
|
String? password;
|
||||||
|
|
||||||
///
|
///
|
||||||
@ -73,6 +62,8 @@ class SharedLinkEditDto {
|
|||||||
///
|
///
|
||||||
bool? showMetadata;
|
bool? showMetadata;
|
||||||
|
|
||||||
|
String? slug;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is SharedLinkEditDto &&
|
bool operator ==(Object other) => identical(this, other) || other is SharedLinkEditDto &&
|
||||||
other.allowDownload == allowDownload &&
|
other.allowDownload == allowDownload &&
|
||||||
@ -81,7 +72,8 @@ class SharedLinkEditDto {
|
|||||||
other.description == description &&
|
other.description == description &&
|
||||||
other.expiresAt == expiresAt &&
|
other.expiresAt == expiresAt &&
|
||||||
other.password == password &&
|
other.password == password &&
|
||||||
other.showMetadata == showMetadata;
|
other.showMetadata == showMetadata &&
|
||||||
|
other.slug == slug;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
@ -92,10 +84,11 @@ class SharedLinkEditDto {
|
|||||||
(description == null ? 0 : description!.hashCode) +
|
(description == null ? 0 : description!.hashCode) +
|
||||||
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
||||||
(password == null ? 0 : password!.hashCode) +
|
(password == null ? 0 : password!.hashCode) +
|
||||||
(showMetadata == null ? 0 : showMetadata!.hashCode);
|
(showMetadata == null ? 0 : showMetadata!.hashCode) +
|
||||||
|
(slug == null ? 0 : slug!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SharedLinkEditDto[allowDownload=$allowDownload, allowUpload=$allowUpload, changeExpiryTime=$changeExpiryTime, description=$description, expiresAt=$expiresAt, password=$password, showMetadata=$showMetadata]';
|
String toString() => 'SharedLinkEditDto[allowDownload=$allowDownload, allowUpload=$allowUpload, changeExpiryTime=$changeExpiryTime, description=$description, expiresAt=$expiresAt, password=$password, showMetadata=$showMetadata, slug=$slug]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -134,6 +127,11 @@ class SharedLinkEditDto {
|
|||||||
} else {
|
} else {
|
||||||
// json[r'showMetadata'] = null;
|
// json[r'showMetadata'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.slug != null) {
|
||||||
|
json[r'slug'] = this.slug;
|
||||||
|
} else {
|
||||||
|
// json[r'slug'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,6 +151,7 @@ class SharedLinkEditDto {
|
|||||||
expiresAt: mapDateTime(json, r'expiresAt', r''),
|
expiresAt: mapDateTime(json, r'expiresAt', r''),
|
||||||
password: mapValueOfType<String>(json, r'password'),
|
password: mapValueOfType<String>(json, r'password'),
|
||||||
showMetadata: mapValueOfType<bool>(json, r'showMetadata'),
|
showMetadata: mapValueOfType<bool>(json, r'showMetadata'),
|
||||||
|
slug: mapValueOfType<String>(json, r'slug'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -24,6 +24,7 @@ class SharedLinkResponseDto {
|
|||||||
required this.key,
|
required this.key,
|
||||||
required this.password,
|
required this.password,
|
||||||
required this.showMetadata,
|
required this.showMetadata,
|
||||||
|
required this.slug,
|
||||||
this.token,
|
this.token,
|
||||||
required this.type,
|
required this.type,
|
||||||
required this.userId,
|
required this.userId,
|
||||||
@ -57,6 +58,8 @@ class SharedLinkResponseDto {
|
|||||||
|
|
||||||
bool showMetadata;
|
bool showMetadata;
|
||||||
|
|
||||||
|
String? slug;
|
||||||
|
|
||||||
String? token;
|
String? token;
|
||||||
|
|
||||||
SharedLinkType type;
|
SharedLinkType type;
|
||||||
@ -76,6 +79,7 @@ class SharedLinkResponseDto {
|
|||||||
other.key == key &&
|
other.key == key &&
|
||||||
other.password == password &&
|
other.password == password &&
|
||||||
other.showMetadata == showMetadata &&
|
other.showMetadata == showMetadata &&
|
||||||
|
other.slug == slug &&
|
||||||
other.token == token &&
|
other.token == token &&
|
||||||
other.type == type &&
|
other.type == type &&
|
||||||
other.userId == userId;
|
other.userId == userId;
|
||||||
@ -94,12 +98,13 @@ class SharedLinkResponseDto {
|
|||||||
(key.hashCode) +
|
(key.hashCode) +
|
||||||
(password == null ? 0 : password!.hashCode) +
|
(password == null ? 0 : password!.hashCode) +
|
||||||
(showMetadata.hashCode) +
|
(showMetadata.hashCode) +
|
||||||
|
(slug == null ? 0 : slug!.hashCode) +
|
||||||
(token == null ? 0 : token!.hashCode) +
|
(token == null ? 0 : token!.hashCode) +
|
||||||
(type.hashCode) +
|
(type.hashCode) +
|
||||||
(userId.hashCode);
|
(userId.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SharedLinkResponseDto[album=$album, allowDownload=$allowDownload, allowUpload=$allowUpload, assets=$assets, createdAt=$createdAt, description=$description, expiresAt=$expiresAt, id=$id, key=$key, password=$password, showMetadata=$showMetadata, token=$token, type=$type, userId=$userId]';
|
String toString() => 'SharedLinkResponseDto[album=$album, allowDownload=$allowDownload, allowUpload=$allowUpload, assets=$assets, createdAt=$createdAt, description=$description, expiresAt=$expiresAt, id=$id, key=$key, password=$password, showMetadata=$showMetadata, slug=$slug, token=$token, type=$type, userId=$userId]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -130,6 +135,11 @@ class SharedLinkResponseDto {
|
|||||||
// json[r'password'] = null;
|
// json[r'password'] = null;
|
||||||
}
|
}
|
||||||
json[r'showMetadata'] = this.showMetadata;
|
json[r'showMetadata'] = this.showMetadata;
|
||||||
|
if (this.slug != null) {
|
||||||
|
json[r'slug'] = this.slug;
|
||||||
|
} else {
|
||||||
|
// json[r'slug'] = null;
|
||||||
|
}
|
||||||
if (this.token != null) {
|
if (this.token != null) {
|
||||||
json[r'token'] = this.token;
|
json[r'token'] = this.token;
|
||||||
} else {
|
} else {
|
||||||
@ -160,6 +170,7 @@ class SharedLinkResponseDto {
|
|||||||
key: mapValueOfType<String>(json, r'key')!,
|
key: mapValueOfType<String>(json, r'key')!,
|
||||||
password: mapValueOfType<String>(json, r'password'),
|
password: mapValueOfType<String>(json, r'password'),
|
||||||
showMetadata: mapValueOfType<bool>(json, r'showMetadata')!,
|
showMetadata: mapValueOfType<bool>(json, r'showMetadata')!,
|
||||||
|
slug: mapValueOfType<String>(json, r'slug'),
|
||||||
token: mapValueOfType<String>(json, r'token'),
|
token: mapValueOfType<String>(json, r'token'),
|
||||||
type: SharedLinkType.fromJson(json[r'type'])!,
|
type: SharedLinkType.fromJson(json[r'type'])!,
|
||||||
userId: mapValueOfType<String>(json, r'userId')!,
|
userId: mapValueOfType<String>(json, r'userId')!,
|
||||||
@ -220,6 +231,7 @@ class SharedLinkResponseDto {
|
|||||||
'key',
|
'key',
|
||||||
'password',
|
'password',
|
||||||
'showMetadata',
|
'showMetadata',
|
||||||
|
'slug',
|
||||||
'type',
|
'type',
|
||||||
'userId',
|
'userId',
|
||||||
};
|
};
|
||||||
|
@ -956,6 +956,14 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "slug",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "withoutAssets",
|
"name": "withoutAssets",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -1116,6 +1124,14 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "slug",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
@ -1550,6 +1566,14 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "slug",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "x-immich-checksum",
|
"name": "x-immich-checksum",
|
||||||
"in": "header",
|
"in": "header",
|
||||||
@ -1929,6 +1953,14 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "slug",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@ -2029,6 +2061,14 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "slug",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@ -2079,6 +2119,14 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "slug",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
@ -2151,6 +2199,14 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/AssetMediaSize"
|
"$ref": "#/components/schemas/AssetMediaSize"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "slug",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@ -2202,6 +2258,14 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "slug",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@ -2605,6 +2669,14 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "slug",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
@ -2657,6 +2729,14 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "slug",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
@ -6217,6 +6297,14 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "slug",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "token",
|
"name": "token",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -6399,6 +6487,14 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "slug",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
@ -6460,6 +6556,14 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "slug",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
@ -7730,6 +7834,14 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "slug",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "tagId",
|
"name": "tagId",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -7875,6 +7987,14 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "slug",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "tagId",
|
"name": "tagId",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -13027,6 +13147,7 @@
|
|||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"expiresAt": {
|
"expiresAt": {
|
||||||
@ -13036,12 +13157,17 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"showMetadata": {
|
"showMetadata": {
|
||||||
"default": true,
|
"default": true,
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"slug": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
@ -13068,6 +13194,7 @@
|
|||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"expiresAt": {
|
"expiresAt": {
|
||||||
@ -13076,10 +13203,15 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"showMetadata": {
|
"showMetadata": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
@ -13127,6 +13259,10 @@
|
|||||||
"showMetadata": {
|
"showMetadata": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"slug": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"token": {
|
"token": {
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -13153,6 +13289,7 @@
|
|||||||
"key",
|
"key",
|
||||||
"password",
|
"password",
|
||||||
"showMetadata",
|
"showMetadata",
|
||||||
|
"slug",
|
||||||
"type",
|
"type",
|
||||||
"userId"
|
"userId"
|
||||||
],
|
],
|
||||||
|
@ -1199,6 +1199,7 @@ export type SharedLinkResponseDto = {
|
|||||||
key: string;
|
key: string;
|
||||||
password: string | null;
|
password: string | null;
|
||||||
showMetadata: boolean;
|
showMetadata: boolean;
|
||||||
|
slug: string | null;
|
||||||
token?: string | null;
|
token?: string | null;
|
||||||
"type": SharedLinkType;
|
"type": SharedLinkType;
|
||||||
userId: string;
|
userId: string;
|
||||||
@ -1208,10 +1209,11 @@ export type SharedLinkCreateDto = {
|
|||||||
allowDownload?: boolean;
|
allowDownload?: boolean;
|
||||||
allowUpload?: boolean;
|
allowUpload?: boolean;
|
||||||
assetIds?: string[];
|
assetIds?: string[];
|
||||||
description?: string;
|
description?: string | null;
|
||||||
expiresAt?: string | null;
|
expiresAt?: string | null;
|
||||||
password?: string;
|
password?: string | null;
|
||||||
showMetadata?: boolean;
|
showMetadata?: boolean;
|
||||||
|
slug?: string | null;
|
||||||
"type": SharedLinkType;
|
"type": SharedLinkType;
|
||||||
};
|
};
|
||||||
export type SharedLinkEditDto = {
|
export type SharedLinkEditDto = {
|
||||||
@ -1221,10 +1223,11 @@ export type SharedLinkEditDto = {
|
|||||||
Setting this flag and not sending expiryAt is considered as null instead.
|
Setting this flag and not sending expiryAt is considered as null instead.
|
||||||
Clients that can send null values can ignore this. */
|
Clients that can send null values can ignore this. */
|
||||||
changeExpiryTime?: boolean;
|
changeExpiryTime?: boolean;
|
||||||
description?: string;
|
description?: string | null;
|
||||||
expiresAt?: string | null;
|
expiresAt?: string | null;
|
||||||
password?: string;
|
password?: string | null;
|
||||||
showMetadata?: boolean;
|
showMetadata?: boolean;
|
||||||
|
slug?: string | null;
|
||||||
};
|
};
|
||||||
export type AssetIdsResponseDto = {
|
export type AssetIdsResponseDto = {
|
||||||
assetId: string;
|
assetId: string;
|
||||||
@ -1821,9 +1824,10 @@ export function deleteAlbum({ id }: {
|
|||||||
method: "DELETE"
|
method: "DELETE"
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
export function getAlbumInfo({ id, key, withoutAssets }: {
|
export function getAlbumInfo({ id, key, slug, withoutAssets }: {
|
||||||
id: string;
|
id: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
|
slug?: string;
|
||||||
withoutAssets?: boolean;
|
withoutAssets?: boolean;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
@ -1831,6 +1835,7 @@ export function getAlbumInfo({ id, key, withoutAssets }: {
|
|||||||
data: AlbumResponseDto;
|
data: AlbumResponseDto;
|
||||||
}>(`/albums/${encodeURIComponent(id)}${QS.query(QS.explode({
|
}>(`/albums/${encodeURIComponent(id)}${QS.query(QS.explode({
|
||||||
key,
|
key,
|
||||||
|
slug,
|
||||||
withoutAssets
|
withoutAssets
|
||||||
}))}`, {
|
}))}`, {
|
||||||
...opts
|
...opts
|
||||||
@ -1862,16 +1867,18 @@ export function removeAssetFromAlbum({ id, bulkIdsDto }: {
|
|||||||
body: bulkIdsDto
|
body: bulkIdsDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function addAssetsToAlbum({ id, key, bulkIdsDto }: {
|
export function addAssetsToAlbum({ id, key, slug, bulkIdsDto }: {
|
||||||
id: string;
|
id: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
|
slug?: string;
|
||||||
bulkIdsDto: BulkIdsDto;
|
bulkIdsDto: BulkIdsDto;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: BulkIdResponseDto[];
|
data: BulkIdResponseDto[];
|
||||||
}>(`/albums/${encodeURIComponent(id)}/assets${QS.query(QS.explode({
|
}>(`/albums/${encodeURIComponent(id)}/assets${QS.query(QS.explode({
|
||||||
key
|
key,
|
||||||
|
slug
|
||||||
}))}`, oazapfts.json({
|
}))}`, oazapfts.json({
|
||||||
...opts,
|
...opts,
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
@ -1971,8 +1978,9 @@ export function deleteAssets({ assetBulkDeleteDto }: {
|
|||||||
body: assetBulkDeleteDto
|
body: assetBulkDeleteDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function uploadAsset({ key, xImmichChecksum, assetMediaCreateDto }: {
|
export function uploadAsset({ key, slug, xImmichChecksum, assetMediaCreateDto }: {
|
||||||
key?: string;
|
key?: string;
|
||||||
|
slug?: string;
|
||||||
xImmichChecksum?: string;
|
xImmichChecksum?: string;
|
||||||
assetMediaCreateDto: AssetMediaCreateDto;
|
assetMediaCreateDto: AssetMediaCreateDto;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
@ -1980,7 +1988,8 @@ export function uploadAsset({ key, xImmichChecksum, assetMediaCreateDto }: {
|
|||||||
status: 201;
|
status: 201;
|
||||||
data: AssetMediaResponseDto;
|
data: AssetMediaResponseDto;
|
||||||
}>(`/assets${QS.query(QS.explode({
|
}>(`/assets${QS.query(QS.explode({
|
||||||
key
|
key,
|
||||||
|
slug
|
||||||
}))}`, oazapfts.multipart({
|
}))}`, oazapfts.multipart({
|
||||||
...opts,
|
...opts,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -2082,15 +2091,17 @@ export function getAssetStatistics({ isFavorite, isTrashed, visibility }: {
|
|||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
export function getAssetInfo({ id, key }: {
|
export function getAssetInfo({ id, key, slug }: {
|
||||||
id: string;
|
id: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
|
slug?: string;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: AssetResponseDto;
|
data: AssetResponseDto;
|
||||||
}>(`/assets/${encodeURIComponent(id)}${QS.query(QS.explode({
|
}>(`/assets/${encodeURIComponent(id)}${QS.query(QS.explode({
|
||||||
key
|
key,
|
||||||
|
slug
|
||||||
}))}`, {
|
}))}`, {
|
||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
@ -2108,15 +2119,17 @@ export function updateAsset({ id, updateAssetDto }: {
|
|||||||
body: updateAssetDto
|
body: updateAssetDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function downloadAsset({ id, key }: {
|
export function downloadAsset({ id, key, slug }: {
|
||||||
id: string;
|
id: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
|
slug?: string;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchBlob<{
|
return oazapfts.ok(oazapfts.fetchBlob<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: Blob;
|
data: Blob;
|
||||||
}>(`/assets/${encodeURIComponent(id)}/original${QS.query(QS.explode({
|
}>(`/assets/${encodeURIComponent(id)}/original${QS.query(QS.explode({
|
||||||
key
|
key,
|
||||||
|
slug
|
||||||
}))}`, {
|
}))}`, {
|
||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
@ -2124,46 +2137,52 @@ export function downloadAsset({ id, key }: {
|
|||||||
/**
|
/**
|
||||||
* replaceAsset
|
* replaceAsset
|
||||||
*/
|
*/
|
||||||
export function replaceAsset({ id, key, assetMediaReplaceDto }: {
|
export function replaceAsset({ id, key, slug, assetMediaReplaceDto }: {
|
||||||
id: string;
|
id: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
|
slug?: string;
|
||||||
assetMediaReplaceDto: AssetMediaReplaceDto;
|
assetMediaReplaceDto: AssetMediaReplaceDto;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: AssetMediaResponseDto;
|
data: AssetMediaResponseDto;
|
||||||
}>(`/assets/${encodeURIComponent(id)}/original${QS.query(QS.explode({
|
}>(`/assets/${encodeURIComponent(id)}/original${QS.query(QS.explode({
|
||||||
key
|
key,
|
||||||
|
slug
|
||||||
}))}`, oazapfts.multipart({
|
}))}`, oazapfts.multipart({
|
||||||
...opts,
|
...opts,
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: assetMediaReplaceDto
|
body: assetMediaReplaceDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function viewAsset({ id, key, size }: {
|
export function viewAsset({ id, key, size, slug }: {
|
||||||
id: string;
|
id: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
size?: AssetMediaSize;
|
size?: AssetMediaSize;
|
||||||
|
slug?: string;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchBlob<{
|
return oazapfts.ok(oazapfts.fetchBlob<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: Blob;
|
data: Blob;
|
||||||
}>(`/assets/${encodeURIComponent(id)}/thumbnail${QS.query(QS.explode({
|
}>(`/assets/${encodeURIComponent(id)}/thumbnail${QS.query(QS.explode({
|
||||||
key,
|
key,
|
||||||
size
|
size,
|
||||||
|
slug
|
||||||
}))}`, {
|
}))}`, {
|
||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
export function playAssetVideo({ id, key }: {
|
export function playAssetVideo({ id, key, slug }: {
|
||||||
id: string;
|
id: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
|
slug?: string;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchBlob<{
|
return oazapfts.ok(oazapfts.fetchBlob<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: Blob;
|
data: Blob;
|
||||||
}>(`/assets/${encodeURIComponent(id)}/video/playback${QS.query(QS.explode({
|
}>(`/assets/${encodeURIComponent(id)}/video/playback${QS.query(QS.explode({
|
||||||
key
|
key,
|
||||||
|
slug
|
||||||
}))}`, {
|
}))}`, {
|
||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
@ -2272,30 +2291,34 @@ export function validateAccessToken(opts?: Oazapfts.RequestOpts) {
|
|||||||
method: "POST"
|
method: "POST"
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
export function downloadArchive({ key, assetIdsDto }: {
|
export function downloadArchive({ key, slug, assetIdsDto }: {
|
||||||
key?: string;
|
key?: string;
|
||||||
|
slug?: string;
|
||||||
assetIdsDto: AssetIdsDto;
|
assetIdsDto: AssetIdsDto;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchBlob<{
|
return oazapfts.ok(oazapfts.fetchBlob<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: Blob;
|
data: Blob;
|
||||||
}>(`/download/archive${QS.query(QS.explode({
|
}>(`/download/archive${QS.query(QS.explode({
|
||||||
key
|
key,
|
||||||
|
slug
|
||||||
}))}`, oazapfts.json({
|
}))}`, oazapfts.json({
|
||||||
...opts,
|
...opts,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: assetIdsDto
|
body: assetIdsDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function getDownloadInfo({ key, downloadInfoDto }: {
|
export function getDownloadInfo({ key, slug, downloadInfoDto }: {
|
||||||
key?: string;
|
key?: string;
|
||||||
|
slug?: string;
|
||||||
downloadInfoDto: DownloadInfoDto;
|
downloadInfoDto: DownloadInfoDto;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 201;
|
status: 201;
|
||||||
data: DownloadResponseDto;
|
data: DownloadResponseDto;
|
||||||
}>(`/download/info${QS.query(QS.explode({
|
}>(`/download/info${QS.query(QS.explode({
|
||||||
key
|
key,
|
||||||
|
slug
|
||||||
}))}`, oazapfts.json({
|
}))}`, oazapfts.json({
|
||||||
...opts,
|
...opts,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -3230,9 +3253,10 @@ export function createSharedLink({ sharedLinkCreateDto }: {
|
|||||||
body: sharedLinkCreateDto
|
body: sharedLinkCreateDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function getMySharedLink({ key, password, token }: {
|
export function getMySharedLink({ key, password, slug, token }: {
|
||||||
key?: string;
|
key?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
slug?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
@ -3241,6 +3265,7 @@ export function getMySharedLink({ key, password, token }: {
|
|||||||
}>(`/shared-links/me${QS.query(QS.explode({
|
}>(`/shared-links/me${QS.query(QS.explode({
|
||||||
key,
|
key,
|
||||||
password,
|
password,
|
||||||
|
slug,
|
||||||
token
|
token
|
||||||
}))}`, {
|
}))}`, {
|
||||||
...opts
|
...opts
|
||||||
@ -3277,32 +3302,36 @@ export function updateSharedLink({ id, sharedLinkEditDto }: {
|
|||||||
body: sharedLinkEditDto
|
body: sharedLinkEditDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function removeSharedLinkAssets({ id, key, assetIdsDto }: {
|
export function removeSharedLinkAssets({ id, key, slug, assetIdsDto }: {
|
||||||
id: string;
|
id: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
|
slug?: string;
|
||||||
assetIdsDto: AssetIdsDto;
|
assetIdsDto: AssetIdsDto;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: AssetIdsResponseDto[];
|
data: AssetIdsResponseDto[];
|
||||||
}>(`/shared-links/${encodeURIComponent(id)}/assets${QS.query(QS.explode({
|
}>(`/shared-links/${encodeURIComponent(id)}/assets${QS.query(QS.explode({
|
||||||
key
|
key,
|
||||||
|
slug
|
||||||
}))}`, oazapfts.json({
|
}))}`, oazapfts.json({
|
||||||
...opts,
|
...opts,
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
body: assetIdsDto
|
body: assetIdsDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function addSharedLinkAssets({ id, key, assetIdsDto }: {
|
export function addSharedLinkAssets({ id, key, slug, assetIdsDto }: {
|
||||||
id: string;
|
id: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
|
slug?: string;
|
||||||
assetIdsDto: AssetIdsDto;
|
assetIdsDto: AssetIdsDto;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: AssetIdsResponseDto[];
|
data: AssetIdsResponseDto[];
|
||||||
}>(`/shared-links/${encodeURIComponent(id)}/assets${QS.query(QS.explode({
|
}>(`/shared-links/${encodeURIComponent(id)}/assets${QS.query(QS.explode({
|
||||||
key
|
key,
|
||||||
|
slug
|
||||||
}))}`, oazapfts.json({
|
}))}`, oazapfts.json({
|
||||||
...opts,
|
...opts,
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
@ -3611,13 +3640,14 @@ export function tagAssets({ id, bulkIdsDto }: {
|
|||||||
body: bulkIdsDto
|
body: bulkIdsDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, personId, tagId, timeBucket, userId, visibility, withPartners, withStacked }: {
|
export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, personId, slug, tagId, timeBucket, userId, visibility, withPartners, withStacked }: {
|
||||||
albumId?: string;
|
albumId?: string;
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
isTrashed?: boolean;
|
isTrashed?: boolean;
|
||||||
key?: string;
|
key?: string;
|
||||||
order?: AssetOrder;
|
order?: AssetOrder;
|
||||||
personId?: string;
|
personId?: string;
|
||||||
|
slug?: string;
|
||||||
tagId?: string;
|
tagId?: string;
|
||||||
timeBucket: string;
|
timeBucket: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
@ -3635,6 +3665,7 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, pers
|
|||||||
key,
|
key,
|
||||||
order,
|
order,
|
||||||
personId,
|
personId,
|
||||||
|
slug,
|
||||||
tagId,
|
tagId,
|
||||||
timeBucket,
|
timeBucket,
|
||||||
userId,
|
userId,
|
||||||
@ -3645,13 +3676,14 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, pers
|
|||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, personId, tagId, userId, visibility, withPartners, withStacked }: {
|
export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, personId, slug, tagId, userId, visibility, withPartners, withStacked }: {
|
||||||
albumId?: string;
|
albumId?: string;
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
isTrashed?: boolean;
|
isTrashed?: boolean;
|
||||||
key?: string;
|
key?: string;
|
||||||
order?: AssetOrder;
|
order?: AssetOrder;
|
||||||
personId?: string;
|
personId?: string;
|
||||||
|
slug?: string;
|
||||||
tagId?: string;
|
tagId?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
visibility?: AssetVisibility;
|
visibility?: AssetVisibility;
|
||||||
@ -3668,6 +3700,7 @@ export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, per
|
|||||||
key,
|
key,
|
||||||
order,
|
order,
|
||||||
personId,
|
personId,
|
||||||
|
slug,
|
||||||
tagId,
|
tagId,
|
||||||
userId,
|
userId,
|
||||||
visibility,
|
visibility,
|
||||||
|
@ -192,6 +192,7 @@ export type SharedLink = {
|
|||||||
showExif: boolean;
|
showExif: boolean;
|
||||||
type: SharedLinkType;
|
type: SharedLinkType;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
slug: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Album = Selectable<AlbumTable> & {
|
export type Album = Selectable<AlbumTable> & {
|
||||||
|
@ -22,13 +22,17 @@ export class SharedLinkCreateDto {
|
|||||||
@ValidateUUID({ optional: true })
|
@ValidateUUID({ optional: true })
|
||||||
albumId?: string;
|
albumId?: string;
|
||||||
|
|
||||||
|
@Optional({ nullable: true, emptyToNull: true })
|
||||||
@IsString()
|
@IsString()
|
||||||
@Optional()
|
description?: string | null;
|
||||||
description?: string;
|
|
||||||
|
|
||||||
|
@Optional({ nullable: true, emptyToNull: true })
|
||||||
@IsString()
|
@IsString()
|
||||||
@Optional()
|
password?: string | null;
|
||||||
password?: string;
|
|
||||||
|
@Optional({ nullable: true, emptyToNull: true })
|
||||||
|
@IsString()
|
||||||
|
slug?: string | null;
|
||||||
|
|
||||||
@ValidateDate({ optional: true, nullable: true })
|
@ValidateDate({ optional: true, nullable: true })
|
||||||
expiresAt?: Date | null = null;
|
expiresAt?: Date | null = null;
|
||||||
@ -44,16 +48,22 @@ export class SharedLinkCreateDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SharedLinkEditDto {
|
export class SharedLinkEditDto {
|
||||||
@Optional()
|
@Optional({ nullable: true, emptyToNull: true })
|
||||||
description?: string;
|
@IsString()
|
||||||
|
description?: string | null;
|
||||||
|
|
||||||
@Optional()
|
@Optional({ nullable: true, emptyToNull: true })
|
||||||
password?: string;
|
@IsString()
|
||||||
|
password?: string | null;
|
||||||
|
|
||||||
|
@Optional({ nullable: true, emptyToNull: true })
|
||||||
|
@IsString()
|
||||||
|
slug?: string | null;
|
||||||
|
|
||||||
@Optional({ nullable: true })
|
@Optional({ nullable: true })
|
||||||
expiresAt?: Date | null;
|
expiresAt?: Date | null;
|
||||||
|
|
||||||
@Optional()
|
@ValidateBoolean({ optional: true })
|
||||||
allowUpload?: boolean;
|
allowUpload?: boolean;
|
||||||
|
|
||||||
@ValidateBoolean({ optional: true })
|
@ValidateBoolean({ optional: true })
|
||||||
@ -99,6 +109,8 @@ export class SharedLinkResponseDto {
|
|||||||
|
|
||||||
allowDownload!: boolean;
|
allowDownload!: boolean;
|
||||||
showMetadata!: boolean;
|
showMetadata!: boolean;
|
||||||
|
|
||||||
|
slug!: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapSharedLink(sharedLink: SharedLink): SharedLinkResponseDto {
|
export function mapSharedLink(sharedLink: SharedLink): SharedLinkResponseDto {
|
||||||
@ -118,6 +130,7 @@ export function mapSharedLink(sharedLink: SharedLink): SharedLinkResponseDto {
|
|||||||
allowUpload: sharedLink.allowUpload,
|
allowUpload: sharedLink.allowUpload,
|
||||||
allowDownload: sharedLink.allowDownload,
|
allowDownload: sharedLink.allowDownload,
|
||||||
showMetadata: sharedLink.showExif,
|
showMetadata: sharedLink.showExif,
|
||||||
|
slug: sharedLink.slug,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,5 +154,6 @@ export function mapSharedLinkWithoutMetadata(sharedLink: SharedLink): SharedLink
|
|||||||
allowUpload: sharedLink.allowUpload,
|
allowUpload: sharedLink.allowUpload,
|
||||||
allowDownload: sharedLink.allowDownload,
|
allowDownload: sharedLink.allowDownload,
|
||||||
showMetadata: sharedLink.showExif,
|
showMetadata: sharedLink.showExif,
|
||||||
|
slug: sharedLink.slug,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -17,12 +17,14 @@ export enum ImmichHeader {
|
|||||||
UserToken = 'x-immich-user-token',
|
UserToken = 'x-immich-user-token',
|
||||||
SessionToken = 'x-immich-session-token',
|
SessionToken = 'x-immich-session-token',
|
||||||
SharedLinkKey = 'x-immich-share-key',
|
SharedLinkKey = 'x-immich-share-key',
|
||||||
|
SharedLinkSlug = 'x-immich-share-slug',
|
||||||
Checksum = 'x-immich-checksum',
|
Checksum = 'x-immich-checksum',
|
||||||
Cid = 'x-immich-cid',
|
Cid = 'x-immich-cid',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ImmichQuery {
|
export enum ImmichQuery {
|
||||||
SharedLinkKey = 'key',
|
SharedLinkKey = 'key',
|
||||||
|
SharedLinkSlug = 'slug',
|
||||||
ApiKey = 'apiKey',
|
ApiKey = 'apiKey',
|
||||||
SessionKey = 'sessionKey',
|
SessionKey = 'sessionKey',
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,10 @@ export const Authenticated = (options?: AuthenticatedOptions): MethodDecorator =
|
|||||||
];
|
];
|
||||||
|
|
||||||
if ((options as SharedLinkRoute)?.sharedLink) {
|
if ((options as SharedLinkRoute)?.sharedLink) {
|
||||||
decorators.push(ApiQuery({ name: ImmichQuery.SharedLinkKey, type: String, required: false }));
|
decorators.push(
|
||||||
|
ApiQuery({ name: ImmichQuery.SharedLinkKey, type: String, required: false }),
|
||||||
|
ApiQuery({ name: ImmichQuery.SharedLinkSlug, type: String, required: false }),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return applyDecorators(...decorators);
|
return applyDecorators(...decorators);
|
||||||
|
@ -188,9 +188,47 @@ from
|
|||||||
"shared_link"
|
"shared_link"
|
||||||
left join "album" on "album"."id" = "shared_link"."albumId"
|
left join "album" on "album"."id" = "shared_link"."albumId"
|
||||||
where
|
where
|
||||||
"shared_link"."key" = $1
|
"album"."deletedAt" is null
|
||||||
and "album"."deletedAt" is null
|
|
||||||
and (
|
and (
|
||||||
"shared_link"."type" = $2
|
"shared_link"."type" = $1
|
||||||
or "album"."id" is not null
|
or "album"."id" is not null
|
||||||
)
|
)
|
||||||
|
and "shared_link"."key" = $2
|
||||||
|
|
||||||
|
-- SharedLinkRepository.getBySlug
|
||||||
|
select
|
||||||
|
"shared_link"."id",
|
||||||
|
"shared_link"."userId",
|
||||||
|
"shared_link"."expiresAt",
|
||||||
|
"shared_link"."showExif",
|
||||||
|
"shared_link"."allowUpload",
|
||||||
|
"shared_link"."allowDownload",
|
||||||
|
"shared_link"."password",
|
||||||
|
(
|
||||||
|
select
|
||||||
|
to_json(obj)
|
||||||
|
from
|
||||||
|
(
|
||||||
|
select
|
||||||
|
"user"."id",
|
||||||
|
"user"."name",
|
||||||
|
"user"."email",
|
||||||
|
"user"."isAdmin",
|
||||||
|
"user"."quotaUsageInBytes",
|
||||||
|
"user"."quotaSizeInBytes"
|
||||||
|
from
|
||||||
|
"user"
|
||||||
|
where
|
||||||
|
"user"."id" = "shared_link"."userId"
|
||||||
|
) as obj
|
||||||
|
) as "user"
|
||||||
|
from
|
||||||
|
"shared_link"
|
||||||
|
left join "album" on "album"."id" = "shared_link"."albumId"
|
||||||
|
where
|
||||||
|
"album"."deletedAt" is null
|
||||||
|
and (
|
||||||
|
"shared_link"."type" = $1
|
||||||
|
or "album"."id" is not null
|
||||||
|
)
|
||||||
|
and "shared_link"."slug" = $2
|
||||||
|
@ -173,10 +173,18 @@ export class SharedLinkRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.BUFFER] })
|
@GenerateSql({ params: [DummyValue.BUFFER] })
|
||||||
async getByKey(key: Buffer) {
|
getByKey(key: Buffer) {
|
||||||
|
return this.authBuilder().where('shared_link.key', '=', key).executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.BUFFER] })
|
||||||
|
getBySlug(slug: string) {
|
||||||
|
return this.authBuilder().where('shared_link.slug', '=', slug).executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
private authBuilder() {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('shared_link')
|
.selectFrom('shared_link')
|
||||||
.where('shared_link.key', '=', key)
|
|
||||||
.leftJoin('album', 'album.id', 'shared_link.albumId')
|
.leftJoin('album', 'album.id', 'shared_link.albumId')
|
||||||
.where('album.deletedAt', 'is', null)
|
.where('album.deletedAt', 'is', null)
|
||||||
.select((eb) => [
|
.select((eb) => [
|
||||||
@ -185,8 +193,7 @@ export class SharedLinkRepository {
|
|||||||
eb.selectFrom('user').select(columns.authUser).whereRef('user.id', '=', 'shared_link.userId'),
|
eb.selectFrom('user').select(columns.authUser).whereRef('user.id', '=', 'shared_link.userId'),
|
||||||
).as('user'),
|
).as('user'),
|
||||||
])
|
])
|
||||||
.where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.Individual), eb('album.id', 'is not', null)]))
|
.where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.Individual), eb('album.id', 'is not', null)]));
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(entity: Insertable<SharedLinkTable> & { assetIds?: string[] }) {
|
async create(entity: Insertable<SharedLinkTable> & { assetIds?: string[] }) {
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "shared_link" ADD "slug" character varying;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "shared_link" ADD CONSTRAINT "shared_link_slug_uq" UNIQUE ("slug");`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "shared_link" DROP CONSTRAINT "shared_link_slug_uq";`.execute(db);
|
||||||
|
await sql`ALTER TABLE "shared_link" DROP COLUMN "slug";`.execute(db);
|
||||||
|
}
|
@ -48,4 +48,7 @@ export class SharedLinkTable {
|
|||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
@Column({ type: 'character varying', nullable: true })
|
||||||
password!: string | null;
|
password!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'character varying', nullable: true, unique: true })
|
||||||
|
slug!: string | null;
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ export class ApiService {
|
|||||||
if (shareMatches) {
|
if (shareMatches) {
|
||||||
try {
|
try {
|
||||||
const key = shareMatches[1];
|
const key = shareMatches[1];
|
||||||
const auth = await this.authService.validateSharedLink(key);
|
const auth = await this.authService.validateSharedLinkKey(key);
|
||||||
const meta = await this.sharedLinkService.getMetadataTags(
|
const meta = await this.sharedLinkService.getMetadataTags(
|
||||||
auth,
|
auth,
|
||||||
request.host ? `${request.protocol}://${request.host}` : undefined,
|
request.host ? `${request.protocol}://${request.host}` : undefined,
|
||||||
|
@ -322,15 +322,18 @@ describe(AuthService.name, () => {
|
|||||||
mocks.sharedLink.getByKey.mockResolvedValue(sharedLink);
|
mocks.sharedLink.getByKey.mockResolvedValue(sharedLink);
|
||||||
mocks.user.get.mockResolvedValue(user);
|
mocks.user.get.mockResolvedValue(user);
|
||||||
|
|
||||||
|
const buffer = sharedLink.key;
|
||||||
|
const key = buffer.toString('base64url');
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.authenticate({
|
sut.authenticate({
|
||||||
headers: { 'x-immich-share-key': sharedLink.key.toString('base64url') },
|
headers: { 'x-immich-share-key': key },
|
||||||
queryParams: {},
|
queryParams: {},
|
||||||
metadata: { adminRoute: false, sharedLinkRoute: true, uri: 'test' },
|
metadata: { adminRoute: false, sharedLinkRoute: true, uri: 'test' },
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual({ user, sharedLink });
|
).resolves.toEqual({ user, sharedLink });
|
||||||
|
|
||||||
expect(mocks.sharedLink.getByKey).toHaveBeenCalledWith(sharedLink.key);
|
expect(mocks.sharedLink.getByKey).toHaveBeenCalledWith(buffer);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should accept a hex key', async () => {
|
it('should accept a hex key', async () => {
|
||||||
@ -340,15 +343,50 @@ describe(AuthService.name, () => {
|
|||||||
mocks.sharedLink.getByKey.mockResolvedValue(sharedLink);
|
mocks.sharedLink.getByKey.mockResolvedValue(sharedLink);
|
||||||
mocks.user.get.mockResolvedValue(user);
|
mocks.user.get.mockResolvedValue(user);
|
||||||
|
|
||||||
|
const buffer = sharedLink.key;
|
||||||
|
const key = buffer.toString('hex');
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.authenticate({
|
sut.authenticate({
|
||||||
headers: { 'x-immich-share-key': sharedLink.key.toString('hex') },
|
headers: { 'x-immich-share-key': key },
|
||||||
queryParams: {},
|
queryParams: {},
|
||||||
metadata: { adminRoute: false, sharedLinkRoute: true, uri: 'test' },
|
metadata: { adminRoute: false, sharedLinkRoute: true, uri: 'test' },
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual({ user, sharedLink });
|
).resolves.toEqual({ user, sharedLink });
|
||||||
|
|
||||||
expect(mocks.sharedLink.getByKey).toHaveBeenCalledWith(sharedLink.key);
|
expect(mocks.sharedLink.getByKey).toHaveBeenCalledWith(buffer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validate - shared link slug', () => {
|
||||||
|
it('should not accept a non-existent slug', async () => {
|
||||||
|
mocks.sharedLink.getBySlug.mockResolvedValue(void 0);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.authenticate({
|
||||||
|
headers: { 'x-immich-share-slug': 'slug' },
|
||||||
|
queryParams: {},
|
||||||
|
metadata: { adminRoute: false, sharedLinkRoute: true, uri: 'test' },
|
||||||
|
}),
|
||||||
|
).rejects.toBeInstanceOf(UnauthorizedException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept a valid slug', async () => {
|
||||||
|
const user = factory.userAdmin();
|
||||||
|
const sharedLink = { ...sharedLinkStub.valid, slug: 'slug-123', user } as any;
|
||||||
|
|
||||||
|
mocks.sharedLink.getBySlug.mockResolvedValue(sharedLink);
|
||||||
|
mocks.user.get.mockResolvedValue(user);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.authenticate({
|
||||||
|
headers: { 'x-immich-share-slug': 'slug-123' },
|
||||||
|
queryParams: {},
|
||||||
|
metadata: { adminRoute: false, sharedLinkRoute: true, uri: 'test' },
|
||||||
|
}),
|
||||||
|
).resolves.toEqual({ user, sharedLink });
|
||||||
|
|
||||||
|
expect(mocks.sharedLink.getBySlug).toHaveBeenCalledWith('slug-123');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import { IncomingHttpHeaders } from 'node:http';
|
|||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
|
import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { UserAdmin } from 'src/database';
|
import { AuthSharedLink, AuthUser, UserAdmin } from 'src/database';
|
||||||
import {
|
import {
|
||||||
AuthDto,
|
AuthDto,
|
||||||
AuthStatusResponseDto,
|
AuthStatusResponseDto,
|
||||||
@ -196,6 +196,7 @@ export class AuthService extends BaseService {
|
|||||||
|
|
||||||
private async validate({ headers, queryParams }: Omit<ValidateRequest, 'metadata'>): Promise<AuthDto> {
|
private async validate({ headers, queryParams }: Omit<ValidateRequest, 'metadata'>): Promise<AuthDto> {
|
||||||
const shareKey = (headers[ImmichHeader.SharedLinkKey] || queryParams[ImmichQuery.SharedLinkKey]) as string;
|
const shareKey = (headers[ImmichHeader.SharedLinkKey] || queryParams[ImmichQuery.SharedLinkKey]) as string;
|
||||||
|
const shareSlug = (headers[ImmichHeader.SharedLinkSlug] || queryParams[ImmichQuery.SharedLinkSlug]) as string;
|
||||||
const session = (headers[ImmichHeader.UserToken] ||
|
const session = (headers[ImmichHeader.UserToken] ||
|
||||||
headers[ImmichHeader.SessionToken] ||
|
headers[ImmichHeader.SessionToken] ||
|
||||||
queryParams[ImmichQuery.SessionKey] ||
|
queryParams[ImmichQuery.SessionKey] ||
|
||||||
@ -204,7 +205,11 @@ export class AuthService extends BaseService {
|
|||||||
const apiKey = (headers[ImmichHeader.ApiKey] || queryParams[ImmichQuery.ApiKey]) as string;
|
const apiKey = (headers[ImmichHeader.ApiKey] || queryParams[ImmichQuery.ApiKey]) as string;
|
||||||
|
|
||||||
if (shareKey) {
|
if (shareKey) {
|
||||||
return this.validateSharedLink(shareKey);
|
return this.validateSharedLinkKey(shareKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shareSlug) {
|
||||||
|
return this.validateSharedLinkSlug(shareSlug);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
@ -403,18 +408,33 @@ export class AuthService extends BaseService {
|
|||||||
return cookies[ImmichCookie.OAuthCodeVerifier] || null;
|
return cookies[ImmichCookie.OAuthCodeVerifier] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateSharedLink(key: string | string[]): Promise<AuthDto> {
|
async validateSharedLinkKey(key: string | string[]): Promise<AuthDto> {
|
||||||
key = Array.isArray(key) ? key[0] : key;
|
key = Array.isArray(key) ? key[0] : key;
|
||||||
|
|
||||||
const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url');
|
const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url');
|
||||||
const sharedLink = await this.sharedLinkRepository.getByKey(bytes);
|
const sharedLink = await this.sharedLinkRepository.getByKey(bytes);
|
||||||
if (sharedLink?.user && (!sharedLink.expiresAt || new Date(sharedLink.expiresAt) > new Date())) {
|
if (!this.isValidSharedLink(sharedLink)) {
|
||||||
return {
|
throw new UnauthorizedException('Invalid share key');
|
||||||
user: sharedLink.user,
|
|
||||||
sharedLink,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
throw new UnauthorizedException('Invalid share key');
|
|
||||||
|
return { user: sharedLink.user, sharedLink };
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateSharedLinkSlug(slug: string | string[]): Promise<AuthDto> {
|
||||||
|
slug = Array.isArray(slug) ? slug[0] : slug;
|
||||||
|
|
||||||
|
const sharedLink = await this.sharedLinkRepository.getBySlug(slug);
|
||||||
|
if (!this.isValidSharedLink(sharedLink)) {
|
||||||
|
throw new UnauthorizedException('Invalid share slug');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user: sharedLink.user, sharedLink };
|
||||||
|
}
|
||||||
|
|
||||||
|
private isValidSharedLink(
|
||||||
|
sharedLink?: AuthSharedLink & { user: AuthUser | null },
|
||||||
|
): sharedLink is AuthSharedLink & { user: AuthUser } {
|
||||||
|
return !!sharedLink?.user && (!sharedLink.expiresAt || new Date(sharedLink.expiresAt) > new Date());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async validateApiKey(key: string): Promise<AuthDto> {
|
private async validateApiKey(key: string): Promise<AuthDto> {
|
||||||
|
@ -136,6 +136,7 @@ describe(SharedLinkService.name, () => {
|
|||||||
allowUpload: true,
|
allowUpload: true,
|
||||||
description: null,
|
description: null,
|
||||||
expiresAt: null,
|
expiresAt: null,
|
||||||
|
slug: null,
|
||||||
showExif: true,
|
showExif: true,
|
||||||
key: Buffer.from('random-bytes', 'utf8'),
|
key: Buffer.from('random-bytes', 'utf8'),
|
||||||
});
|
});
|
||||||
@ -163,6 +164,7 @@ describe(SharedLinkService.name, () => {
|
|||||||
userId: authStub.admin.user.id,
|
userId: authStub.admin.user.id,
|
||||||
albumId: null,
|
albumId: null,
|
||||||
allowDownload: true,
|
allowDownload: true,
|
||||||
|
slug: null,
|
||||||
allowUpload: true,
|
allowUpload: true,
|
||||||
assetIds: [assetStub.image.id],
|
assetIds: [assetStub.image.id],
|
||||||
description: null,
|
description: null,
|
||||||
@ -199,6 +201,7 @@ describe(SharedLinkService.name, () => {
|
|||||||
description: null,
|
description: null,
|
||||||
expiresAt: null,
|
expiresAt: null,
|
||||||
showExif: false,
|
showExif: false,
|
||||||
|
slug: null,
|
||||||
key: Buffer.from('random-bytes', 'utf8'),
|
key: Buffer.from('random-bytes', 'utf8'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -223,6 +226,7 @@ describe(SharedLinkService.name, () => {
|
|||||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
|
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
|
||||||
expect(mocks.sharedLink.update).toHaveBeenCalledWith({
|
expect(mocks.sharedLink.update).toHaveBeenCalledWith({
|
||||||
id: sharedLinkStub.valid.id,
|
id: sharedLinkStub.valid.id,
|
||||||
|
slug: null,
|
||||||
userId: authStub.user1.user.id,
|
userId: authStub.user1.user.id,
|
||||||
allowDownload: false,
|
allowDownload: false,
|
||||||
});
|
});
|
||||||
@ -277,6 +281,7 @@ describe(SharedLinkService.name, () => {
|
|||||||
expect(mocks.sharedLink.update).toHaveBeenCalled();
|
expect(mocks.sharedLink.update).toHaveBeenCalled();
|
||||||
expect(mocks.sharedLink.update).toHaveBeenCalledWith({
|
expect(mocks.sharedLink.update).toHaveBeenCalledWith({
|
||||||
...sharedLinkStub.individual,
|
...sharedLinkStub.individual,
|
||||||
|
slug: null,
|
||||||
assetIds: ['asset-3'],
|
assetIds: ['asset-3'],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { BadRequestException, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common';
|
import { BadRequestException, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { PostgresError } from 'postgres';
|
||||||
import { SharedLink } from 'src/database';
|
import { SharedLink } from 'src/database';
|
||||||
import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
|
import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AssetIdsDto } from 'src/dtos/asset.dto';
|
import { AssetIdsDto } from 'src/dtos/asset.dto';
|
||||||
@ -64,36 +65,53 @@ export class SharedLinkService extends BaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sharedLink = await this.sharedLinkRepository.create({
|
try {
|
||||||
key: this.cryptoRepository.randomBytes(50),
|
const sharedLink = await this.sharedLinkRepository.create({
|
||||||
userId: auth.user.id,
|
key: this.cryptoRepository.randomBytes(50),
|
||||||
type: dto.type,
|
userId: auth.user.id,
|
||||||
albumId: dto.albumId || null,
|
type: dto.type,
|
||||||
assetIds: dto.assetIds,
|
albumId: dto.albumId || null,
|
||||||
description: dto.description || null,
|
assetIds: dto.assetIds,
|
||||||
password: dto.password,
|
description: dto.description || null,
|
||||||
expiresAt: dto.expiresAt || null,
|
password: dto.password,
|
||||||
allowUpload: dto.allowUpload ?? true,
|
expiresAt: dto.expiresAt || null,
|
||||||
allowDownload: dto.showMetadata === false ? false : (dto.allowDownload ?? true),
|
allowUpload: dto.allowUpload ?? true,
|
||||||
showExif: dto.showMetadata ?? true,
|
allowDownload: dto.showMetadata === false ? false : (dto.allowDownload ?? true),
|
||||||
});
|
showExif: dto.showMetadata ?? true,
|
||||||
|
slug: dto.slug || null,
|
||||||
|
});
|
||||||
|
|
||||||
return this.mapToSharedLink(sharedLink, { withExif: true });
|
return this.mapToSharedLink(sharedLink, { withExif: true });
|
||||||
|
} catch (error) {
|
||||||
|
this.handleError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleError(error: unknown): never {
|
||||||
|
if ((error as PostgresError).constraint_name === 'shared_link_slug_uq') {
|
||||||
|
throw new BadRequestException('Shared link with this slug already exists');
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(auth: AuthDto, id: string, dto: SharedLinkEditDto) {
|
async update(auth: AuthDto, id: string, dto: SharedLinkEditDto) {
|
||||||
await this.findOrFail(auth.user.id, id);
|
await this.findOrFail(auth.user.id, id);
|
||||||
const sharedLink = await this.sharedLinkRepository.update({
|
try {
|
||||||
id,
|
const sharedLink = await this.sharedLinkRepository.update({
|
||||||
userId: auth.user.id,
|
id,
|
||||||
description: dto.description,
|
userId: auth.user.id,
|
||||||
password: dto.password,
|
description: dto.description,
|
||||||
expiresAt: dto.changeExpiryTime && !dto.expiresAt ? null : dto.expiresAt,
|
password: dto.password,
|
||||||
allowUpload: dto.allowUpload,
|
expiresAt: dto.changeExpiryTime && !dto.expiresAt ? null : dto.expiresAt,
|
||||||
allowDownload: dto.allowDownload,
|
allowUpload: dto.allowUpload,
|
||||||
showExif: dto.showMetadata,
|
allowDownload: dto.allowDownload,
|
||||||
});
|
showExif: dto.showMetadata,
|
||||||
return this.mapToSharedLink(sharedLink, { withExif: true });
|
slug: dto.slug || null,
|
||||||
|
});
|
||||||
|
return this.mapToSharedLink(sharedLink, { withExif: true });
|
||||||
|
} catch (error) {
|
||||||
|
this.handleError(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(auth: AuthDto, id: string): Promise<void> {
|
async remove(auth: AuthDto, id: string): Promise<void> {
|
||||||
|
8
server/test/fixtures/shared-link.stub.ts
vendored
8
server/test/fixtures/shared-link.stub.ts
vendored
@ -118,6 +118,7 @@ export const sharedLinkStub = {
|
|||||||
description: null,
|
description: null,
|
||||||
assets: [assetStub.image],
|
assets: [assetStub.image],
|
||||||
password: 'password',
|
password: 'password',
|
||||||
|
slug: null,
|
||||||
}),
|
}),
|
||||||
valid: Object.freeze({
|
valid: Object.freeze({
|
||||||
id: '123',
|
id: '123',
|
||||||
@ -135,6 +136,7 @@ export const sharedLinkStub = {
|
|||||||
password: null,
|
password: null,
|
||||||
assets: [] as MapAsset[],
|
assets: [] as MapAsset[],
|
||||||
album: null,
|
album: null,
|
||||||
|
slug: null,
|
||||||
}),
|
}),
|
||||||
expired: Object.freeze({
|
expired: Object.freeze({
|
||||||
id: '123',
|
id: '123',
|
||||||
@ -152,6 +154,7 @@ export const sharedLinkStub = {
|
|||||||
albumId: null,
|
albumId: null,
|
||||||
assets: [] as MapAsset[],
|
assets: [] as MapAsset[],
|
||||||
album: null,
|
album: null,
|
||||||
|
slug: null,
|
||||||
}),
|
}),
|
||||||
readonlyNoExif: Object.freeze({
|
readonlyNoExif: Object.freeze({
|
||||||
id: '123',
|
id: '123',
|
||||||
@ -166,6 +169,7 @@ export const sharedLinkStub = {
|
|||||||
description: null,
|
description: null,
|
||||||
password: null,
|
password: null,
|
||||||
assets: [],
|
assets: [],
|
||||||
|
slug: null,
|
||||||
albumId: 'album-123',
|
albumId: 'album-123',
|
||||||
album: {
|
album: {
|
||||||
id: 'album-123',
|
id: 'album-123',
|
||||||
@ -266,6 +270,7 @@ export const sharedLinkStub = {
|
|||||||
allowUpload: true,
|
allowUpload: true,
|
||||||
allowDownload: true,
|
allowDownload: true,
|
||||||
showExif: true,
|
showExif: true,
|
||||||
|
slug: null,
|
||||||
description: null,
|
description: null,
|
||||||
password: 'password',
|
password: 'password',
|
||||||
assets: [],
|
assets: [],
|
||||||
@ -288,6 +293,7 @@ export const sharedLinkResponseStub = {
|
|||||||
showMetadata: true,
|
showMetadata: true,
|
||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
userId: 'admin_id',
|
userId: 'admin_id',
|
||||||
|
slug: null,
|
||||||
}),
|
}),
|
||||||
expired: Object.freeze<SharedLinkResponseDto>({
|
expired: Object.freeze<SharedLinkResponseDto>({
|
||||||
album: undefined,
|
album: undefined,
|
||||||
@ -303,6 +309,7 @@ export const sharedLinkResponseStub = {
|
|||||||
showMetadata: true,
|
showMetadata: true,
|
||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
userId: 'admin_id',
|
userId: 'admin_id',
|
||||||
|
slug: null,
|
||||||
}),
|
}),
|
||||||
readonlyNoMetadata: Object.freeze<SharedLinkResponseDto>({
|
readonlyNoMetadata: Object.freeze<SharedLinkResponseDto>({
|
||||||
id: '123',
|
id: '123',
|
||||||
@ -316,6 +323,7 @@ export const sharedLinkResponseStub = {
|
|||||||
allowUpload: false,
|
allowUpload: false,
|
||||||
allowDownload: false,
|
allowDownload: false,
|
||||||
showMetadata: false,
|
showMetadata: false,
|
||||||
|
slug: null,
|
||||||
album: { ...albumResponse, startDate: assetResponse.localDateTime, endDate: assetResponse.localDateTime },
|
album: { ...albumResponse, startDate: assetResponse.localDateTime, endDate: assetResponse.localDateTime },
|
||||||
assets: [{ ...assetResponseWithoutMetadata, exifInfo: undefined }],
|
assets: [{ ...assetResponseWithoutMetadata, exifInfo: undefined }],
|
||||||
}),
|
}),
|
||||||
|
@ -369,7 +369,7 @@
|
|||||||
|
|
||||||
if (sharedLink) {
|
if (sharedLink) {
|
||||||
handleSharedLinkCreated(albumToShare);
|
handleSharedLinkCreated(albumToShare);
|
||||||
await modalManager.show(QrCodeModal, { title: $t('view_link'), value: makeSharedLinkUrl(sharedLink.key) });
|
await modalManager.show(QrCodeModal, { title: $t('view_link'), value: makeSharedLinkUrl(sharedLink) });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
let { asset, menuItem = false }: Props = $props();
|
let { asset, menuItem = false }: Props = $props();
|
||||||
|
|
||||||
const onDownloadFile = async () => downloadFile(await getAssetInfo({ id: asset.id, key: authManager.key }));
|
const onDownloadFile = async () => downloadFile(await getAssetInfo({ ...authManager.params, id: asset.id }));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:document use:shortcut={{ shortcut: { key: 'd', shift: true }, onShortcut: onDownloadFile }} />
|
<svelte:document use:shortcut={{ shortcut: { key: 'd', shift: true }, onShortcut: onDownloadFile }} />
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
const sharedLink = await modalManager.show(SharedLinkCreateModal, { assetIds: [asset.id] });
|
const sharedLink = await modalManager.show(SharedLinkCreateModal, { assetIds: [asset.id] });
|
||||||
|
|
||||||
if (sharedLink) {
|
if (sharedLink) {
|
||||||
await modalManager.show(QrCodeModal, { title: $t('view_link'), value: makeSharedLinkUrl(sharedLink.key) });
|
await modalManager.show(QrCodeModal, { title: $t('view_link'), value: makeSharedLinkUrl(sharedLink) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -111,7 +111,7 @@
|
|||||||
let zoomToggle = $state(() => void 0);
|
let zoomToggle = $state(() => void 0);
|
||||||
|
|
||||||
const refreshStack = async () => {
|
const refreshStack = async () => {
|
||||||
if (authManager.key) {
|
if (authManager.isSharedLink) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,7 +191,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleGetAllAlbums = async () => {
|
const handleGetAllAlbums = async () => {
|
||||||
if (authManager.key) {
|
if (authManager.isSharedLink) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !authManager.key && $preferences?.ratings.enabled}
|
{#if !authManager.isSharedLink && $preferences?.ratings.enabled}
|
||||||
<section class="px-4 pt-2">
|
<section class="px-4 pt-2">
|
||||||
<StarRating {rating} readOnly={!isOwner} onRating={(rating) => handlePromiseError(handleChangeRating(rating))} />
|
<StarRating {rating} readOnly={!isOwner} onRating={(rating) => handlePromiseError(handleChangeRating(rating))} />
|
||||||
</section>
|
</section>
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
<svelte:document use:shortcut={{ shortcut: { key: 't' }, onShortcut: handleAddTag }} />
|
<svelte:document use:shortcut={{ shortcut: { key: 't' }, onShortcut: handleAddTag }} />
|
||||||
|
|
||||||
{#if isOwner && !authManager.key}
|
{#if isOwner && !authManager.isSharedLink}
|
||||||
<section class="px-4 mt-4">
|
<section class="px-4 mt-4">
|
||||||
<div class="flex h-10 w-full items-center justify-between text-sm">
|
<div class="flex h-10 w-full items-center justify-between text-sm">
|
||||||
<h2>{$t('tags').toUpperCase()}</h2>
|
<h2>{$t('tags').toUpperCase()}</h2>
|
||||||
|
@ -85,7 +85,7 @@
|
|||||||
|
|
||||||
const handleNewAsset = async (newAsset: AssetResponseDto) => {
|
const handleNewAsset = async (newAsset: AssetResponseDto) => {
|
||||||
// TODO: check if reloading asset data is necessary
|
// TODO: check if reloading asset data is necessary
|
||||||
if (newAsset.id && !authManager.key) {
|
if (newAsset.id && !authManager.isSharedLink) {
|
||||||
const data = await getAssetInfo({ id: asset.id });
|
const data = await getAssetInfo({ id: asset.id });
|
||||||
people = data?.people || [];
|
people = data?.people || [];
|
||||||
unassignedFaces = data?.unassignedFaces || [];
|
unassignedFaces = data?.unassignedFaces || [];
|
||||||
@ -195,7 +195,7 @@
|
|||||||
<DetailPanelDescription {asset} {isOwner} />
|
<DetailPanelDescription {asset} {isOwner} />
|
||||||
<DetailPanelRating {asset} {isOwner} />
|
<DetailPanelRating {asset} {isOwner} />
|
||||||
|
|
||||||
{#if !authManager.key && isOwner}
|
{#if !authManager.isSharedLink && isOwner}
|
||||||
<section class="px-4 pt-4 text-sm">
|
<section class="px-4 pt-4 text-sm">
|
||||||
<div class="flex h-10 w-full items-center justify-between">
|
<div class="flex h-10 w-full items-center justify-between">
|
||||||
<h2>{$t('people').toUpperCase()}</h2>
|
<h2>{$t('people').toUpperCase()}</h2>
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
const { asset }: Props = $props();
|
const { asset }: Props = $props();
|
||||||
|
|
||||||
const loadAssetData = async (id: string) => {
|
const loadAssetData = async (id: string) => {
|
||||||
const data = await viewAsset({ id, size: AssetMediaSize.Preview, key: authManager.key });
|
const data = await viewAsset({ ...authManager.params, id, size: AssetMediaSize.Preview });
|
||||||
return URL.createObjectURL(data);
|
return URL.createObjectURL(data);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -270,13 +270,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Favorite asset star -->
|
<!-- Favorite asset star -->
|
||||||
{#if !authManager.key && asset.isFavorite}
|
{#if !authManager.isSharedLink && asset.isFavorite}
|
||||||
<div class="absolute bottom-2 start-2">
|
<div class="absolute bottom-2 start-2">
|
||||||
<Icon path={mdiHeart} size="24" class="text-white" />
|
<Icon path={mdiHeart} size="24" class="text-white" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !authManager.key && showArchiveIcon && asset.visibility === AssetVisibility.Archive}
|
{#if !authManager.isSharedLink && showArchiveIcon && asset.visibility === AssetVisibility.Archive}
|
||||||
<div class={['absolute start-2', asset.isFavorite ? 'bottom-10' : 'bottom-2']}>
|
<div class={['absolute start-2', asset.isFavorite ? 'bottom-10' : 'bottom-2']}>
|
||||||
<Icon path={mdiArchiveArrowDownOutline} size="24" class="text-white" />
|
<Icon path={mdiArchiveArrowDownOutline} size="24" class="text-white" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -69,7 +69,7 @@
|
|||||||
let paused = $state(false);
|
let paused = $state(false);
|
||||||
let current = $state<MemoryAsset | undefined>(undefined);
|
let current = $state<MemoryAsset | undefined>(undefined);
|
||||||
let currentMemoryAssetFull = $derived.by(async () =>
|
let currentMemoryAssetFull = $derived.by(async () =>
|
||||||
current?.asset ? await getAssetInfo({ id: current.asset.id, key: authManager.key }) : undefined,
|
current?.asset ? await getAssetInfo({ ...authManager.params, id: current.asset.id }) : undefined,
|
||||||
);
|
);
|
||||||
let currentTimelineAssets = $derived(current?.memory.assets.map((asset) => toTimelineAsset(asset)) || []);
|
let currentTimelineAssets = $derived(current?.memory.assets.map((asset) => toTimelineAsset(asset)) || []);
|
||||||
|
|
||||||
|
14
web/src/lib/components/pages/SharedLinkErrorPage.svelte
Normal file
14
web/src/lib/components/pages/SharedLinkErrorPage.svelte
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/state';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Oops! Error - Immich</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<section class="flex flex-col px-4 h-dvh w-dvw place-content-center place-items-center">
|
||||||
|
<h1 class="py-10 text-4xl text-immich-primary dark:text-immich-dark-primary">Page not found :/</h1>
|
||||||
|
{#if page.error?.message}
|
||||||
|
<h2 class="text-xl text-immich-fg dark:text-immich-dark-fg">{page.error.message}</h2>
|
||||||
|
{/if}
|
||||||
|
</section>
|
108
web/src/lib/components/pages/SharedLinkPage.svelte
Normal file
108
web/src/lib/components/pages/SharedLinkPage.svelte
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import AlbumViewer from '$lib/components/album-page/album-viewer.svelte';
|
||||||
|
import IndividualSharedViewer from '$lib/components/share-page/individual-shared-viewer.svelte';
|
||||||
|
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||||
|
import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte';
|
||||||
|
import PasswordField from '$lib/components/shared-components/password-field.svelte';
|
||||||
|
import ThemeButton from '$lib/components/shared-components/theme-button.svelte';
|
||||||
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
|
import { user } from '$lib/stores/user.store';
|
||||||
|
import { setSharedLink } from '$lib/utils';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { navigate } from '$lib/utils/navigation';
|
||||||
|
import { getMySharedLink, SharedLinkType, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
|
||||||
|
import { Button } from '@immich/ui';
|
||||||
|
import { tick } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: {
|
||||||
|
meta: {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
sharedLink?: SharedLinkResponseDto;
|
||||||
|
key?: string;
|
||||||
|
slug?: string;
|
||||||
|
asset?: AssetResponseDto;
|
||||||
|
passwordRequired?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data }: Props = $props();
|
||||||
|
|
||||||
|
let { gridScrollTarget } = assetViewingStore;
|
||||||
|
let { sharedLink, passwordRequired, key, slug, meta } = $state(data);
|
||||||
|
let { title, description } = $state(meta);
|
||||||
|
let isOwned = $derived($user ? $user.id === sharedLink?.userId : false);
|
||||||
|
let password = $state('');
|
||||||
|
|
||||||
|
const handlePasswordSubmit = async () => {
|
||||||
|
try {
|
||||||
|
sharedLink = await getMySharedLink({ password, key, slug });
|
||||||
|
setSharedLink(sharedLink);
|
||||||
|
passwordRequired = false;
|
||||||
|
title = (sharedLink.album ? sharedLink.album.albumName : $t('public_share')) + ' - Immich';
|
||||||
|
description =
|
||||||
|
sharedLink.description ||
|
||||||
|
$t('shared_photos_and_videos_count', { values: { assetCount: sharedLink.assets.length } });
|
||||||
|
await tick();
|
||||||
|
await navigate(
|
||||||
|
{ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget },
|
||||||
|
{ forceNavigate: true, replaceState: true },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.unable_to_get_shared_link'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onsubmit = async (event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
await handlePasswordSubmit();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{title}</title>
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
</svelte:head>
|
||||||
|
{#if passwordRequired}
|
||||||
|
<main
|
||||||
|
class="relative h-dvh overflow-hidden px-6 max-md:pt-(--navbar-height-md) pt-(--navbar-height) sm:px-12 md:px-24 lg:px-40"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center justify-center mt-20">
|
||||||
|
<div class="text-2xl font-bold text-immich-primary dark:text-immich-dark-primary">{$t('password_required')}</div>
|
||||||
|
<div class="mt-4 text-lg text-immich-primary dark:text-immich-dark-primary">
|
||||||
|
{$t('sharing_enter_password')}
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<form class="flex gap-x-2" novalidate {onsubmit}>
|
||||||
|
<PasswordField autocomplete="off" bind:password placeholder="Password" />
|
||||||
|
<Button type="submit">{$t('submit')}</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<header>
|
||||||
|
<ControlAppBar showBackButton={false}>
|
||||||
|
{#snippet leading()}
|
||||||
|
<ImmichLogoSmallLink />
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet trailing()}
|
||||||
|
<ThemeButton />
|
||||||
|
{/snippet}
|
||||||
|
</ControlAppBar>
|
||||||
|
</header>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !passwordRequired && sharedLink?.type == SharedLinkType.Album}
|
||||||
|
<AlbumViewer {sharedLink} />
|
||||||
|
{/if}
|
||||||
|
{#if !passwordRequired && sharedLink?.type == SharedLinkType.Individual}
|
||||||
|
<div class="immich-scrollbar">
|
||||||
|
<IndividualSharedViewer {sharedLink} {isOwned} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
@ -15,7 +15,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (sharedLink) {
|
if (sharedLink) {
|
||||||
await modalManager.show(QrCodeModal, { title: $t('view_link'), value: makeSharedLinkUrl(sharedLink.key) });
|
await modalManager.show(QrCodeModal, { title: $t('view_link'), value: makeSharedLinkUrl(sharedLink) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -4,11 +4,11 @@
|
|||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { downloadArchive, downloadFile } from '$lib/utils/asset-utils';
|
import { downloadArchive, downloadFile } from '$lib/utils/asset-utils';
|
||||||
import { getAssetInfo } from '@immich/sdk';
|
import { getAssetInfo } from '@immich/sdk';
|
||||||
|
import { IconButton } from '@immich/ui';
|
||||||
import { mdiCloudDownloadOutline, mdiFileDownloadOutline, mdiFolderDownloadOutline } from '@mdi/js';
|
import { mdiCloudDownloadOutline, mdiFileDownloadOutline, mdiFolderDownloadOutline } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
import { IconButton } from '@immich/ui';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
filename?: string;
|
filename?: string;
|
||||||
@ -23,7 +23,7 @@
|
|||||||
const assets = [...getAssets()];
|
const assets = [...getAssets()];
|
||||||
if (assets.length === 1) {
|
if (assets.length === 1) {
|
||||||
clearSelect();
|
clearSelect();
|
||||||
let asset = await getAssetInfo({ id: assets[0].id, key: authManager.key });
|
let asset = await getAssetInfo({ ...authManager.params, id: assets[0].id });
|
||||||
await downloadFile(asset);
|
await downloadFile(asset);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getAssetControlContext } from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
import { getAssetControlContext } from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import type { OnLink, OnUnlink } from '$lib/utils/actions';
|
import type { OnLink, OnUnlink } from '$lib/utils/actions';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { getAssetInfo, updateAsset } from '@immich/sdk';
|
import { getAssetInfo, updateAsset } from '@immich/sdk';
|
||||||
|
import { IconButton } from '@immich/ui';
|
||||||
import { mdiLinkOff, mdiMotionPlayOutline, mdiTimerSand } from '@mdi/js';
|
import { mdiLinkOff, mdiMotionPlayOutline, mdiTimerSand } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||||
import { IconButton } from '@immich/ui';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onLink: OnLink;
|
onLink: OnLink;
|
||||||
@ -59,7 +59,7 @@
|
|||||||
try {
|
try {
|
||||||
loading = true;
|
loading = true;
|
||||||
const stillResponse = await updateAsset({ id: still.id, updateAssetDto: { livePhotoVideoId: null } });
|
const stillResponse = await updateAsset({ id: still.id, updateAssetDto: { livePhotoVideoId: null } });
|
||||||
const motionResponse = await getAssetInfo({ id: motionId, key: authManager.key });
|
const motionResponse = await getAssetInfo({ ...authManager.params, id: motionId });
|
||||||
onUnlink({ still: toTimelineAsset(stillResponse), motion: toTimelineAsset(motionResponse) });
|
onUnlink({ still: toTimelineAsset(stillResponse), motion: toTimelineAsset(motionResponse) });
|
||||||
clearSelect();
|
clearSelect();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -29,11 +29,11 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const results = await removeSharedLinkAssets({
|
const results = await removeSharedLinkAssets({
|
||||||
|
...authManager.params,
|
||||||
id: sharedLink.id,
|
id: sharedLink.id,
|
||||||
assetIdsDto: {
|
assetIdsDto: {
|
||||||
assetIds: [...getAssets()].map((asset) => asset.id),
|
assetIds: [...getAssets()].map((asset) => asset.id),
|
||||||
},
|
},
|
||||||
key: authManager.key,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
|
@ -443,7 +443,7 @@
|
|||||||
|
|
||||||
if (laterAsset) {
|
if (laterAsset) {
|
||||||
const preloadAsset = await timelineManager.getLaterAsset(laterAsset);
|
const preloadAsset = await timelineManager.getLaterAsset(laterAsset);
|
||||||
const asset = await getAssetInfo({ id: laterAsset.id, key: authManager.key });
|
const asset = await getAssetInfo({ ...authManager.params, id: laterAsset.id });
|
||||||
assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []);
|
assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []);
|
||||||
await navigate({ targetRoute: 'current', assetId: laterAsset.id });
|
await navigate({ targetRoute: 'current', assetId: laterAsset.id });
|
||||||
}
|
}
|
||||||
@ -458,7 +458,7 @@
|
|||||||
|
|
||||||
if (earlierAsset) {
|
if (earlierAsset) {
|
||||||
const preloadAsset = await timelineManager.getEarlierAsset(earlierAsset);
|
const preloadAsset = await timelineManager.getEarlierAsset(earlierAsset);
|
||||||
const asset = await getAssetInfo({ id: earlierAsset.id, key: authManager.key });
|
const asset = await getAssetInfo({ ...authManager.params, id: earlierAsset.id });
|
||||||
assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []);
|
assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []);
|
||||||
await navigate({ targetRoute: 'current', assetId: earlierAsset.id });
|
await navigate({ targetRoute: 'current', assetId: earlierAsset.id });
|
||||||
}
|
}
|
||||||
@ -471,7 +471,7 @@
|
|||||||
const randomAsset = await timelineManager.getRandomAsset();
|
const randomAsset = await timelineManager.getRandomAsset();
|
||||||
|
|
||||||
if (randomAsset) {
|
if (randomAsset) {
|
||||||
const asset = await getAssetInfo({ id: randomAsset.id, key: authManager.key });
|
const asset = await getAssetInfo({ ...authManager.params, id: randomAsset.id });
|
||||||
assetViewingStore.setAsset(asset);
|
assetViewingStore.setAsset(asset);
|
||||||
await navigate({ targetRoute: 'current', assetId: randomAsset.id });
|
await navigate({ targetRoute: 'current', assetId: randomAsset.id });
|
||||||
return asset;
|
return asset;
|
||||||
@ -869,7 +869,7 @@
|
|||||||
style:margin-right={(usingMobileDevice ? 0 : scrubberWidth) + 'px'}
|
style:margin-right={(usingMobileDevice ? 0 : scrubberWidth) + 'px'}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
bind:clientHeight={timelineManager.viewportHeight}
|
bind:clientHeight={timelineManager.viewportHeight}
|
||||||
bind:clientWidth={null, (v) => ((timelineManager.viewportWidth = v), updateSlidingWindow())}
|
bind:clientWidth={null, (v: number) => ((timelineManager.viewportWidth = v), updateSlidingWindow())}
|
||||||
bind:this={element}
|
bind:this={element}
|
||||||
onscroll={() => (handleTimelineScroll(), updateSlidingWindow(), updateIsScrolling())}
|
onscroll={() => (handleTimelineScroll(), updateSlidingWindow(), updateIsScrolling())}
|
||||||
>
|
>
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte';
|
import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte';
|
||||||
import { AppRoute, AssetAction } from '$lib/constants';
|
import { AppRoute, AssetAction } from '$lib/constants';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
|
||||||
import type { Viewport } from '$lib/managers/timeline-manager/types';
|
import type { Viewport } from '$lib/managers/timeline-manager/types';
|
||||||
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import { cancelMultiselect, downloadArchive } from '$lib/utils/asset-utils';
|
import { cancelMultiselect, downloadArchive } from '$lib/utils/asset-utils';
|
||||||
@ -13,6 +13,7 @@
|
|||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { addSharedLinkAssets, getAssetInfo, type SharedLinkResponseDto } from '@immich/sdk';
|
import { addSharedLinkAssets, getAssetInfo, type SharedLinkResponseDto } from '@immich/sdk';
|
||||||
|
import { IconButton } from '@immich/ui';
|
||||||
import { mdiArrowLeft, mdiFileImagePlusOutline, mdiFolderDownloadOutline, mdiSelectAll } from '@mdi/js';
|
import { mdiArrowLeft, mdiFileImagePlusOutline, mdiFolderDownloadOutline, mdiSelectAll } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import AssetViewer from '../asset-viewer/asset-viewer.svelte';
|
import AssetViewer from '../asset-viewer/asset-viewer.svelte';
|
||||||
@ -22,7 +23,6 @@
|
|||||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||||
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
|
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||||
import { IconButton } from '@immich/ui';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
sharedLink: SharedLinkResponseDto;
|
sharedLink: SharedLinkResponseDto;
|
||||||
@ -54,11 +54,11 @@
|
|||||||
? openFileUploadDialog()
|
? openFileUploadDialog()
|
||||||
: fileUploadHandler({ files }));
|
: fileUploadHandler({ files }));
|
||||||
const data = await addSharedLinkAssets({
|
const data = await addSharedLinkAssets({
|
||||||
|
...authManager.params,
|
||||||
id: sharedLink.id,
|
id: sharedLink.id,
|
||||||
assetIdsDto: {
|
assetIdsDto: {
|
||||||
assetIds: results.filter((id) => !!id) as string[],
|
assetIds: results.filter((id) => !!id) as string[],
|
||||||
},
|
},
|
||||||
key: authManager.key,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const added = data.filter((item) => item.success).length;
|
const added = data.filter((item) => item.success).length;
|
||||||
@ -145,7 +145,7 @@
|
|||||||
<GalleryViewer {assets} {assetInteraction} {viewport} />
|
<GalleryViewer {assets} {assetInteraction} {viewport} />
|
||||||
</section>
|
</section>
|
||||||
{:else if assets.length === 1}
|
{:else if assets.length === 1}
|
||||||
{#await getAssetInfo({ id: assets[0].id, key: authManager.key }) then asset}
|
{#await getAssetInfo({ ...authManager.params, id: assets[0].id }) then asset}
|
||||||
<AssetViewer
|
<AssetViewer
|
||||||
{asset}
|
{asset}
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
|
@ -126,7 +126,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const filesArray: File[] = Array.from<File>(files);
|
const filesArray: File[] = Array.from<File>(files);
|
||||||
if (authManager.key) {
|
if (authManager.isSharedLink) {
|
||||||
dragAndDropFilesStore.set({ isDragging: true, files: filesArray });
|
dragAndDropFilesStore.set({ isDragging: true, files: filesArray });
|
||||||
} else {
|
} else {
|
||||||
await fileUploadHandler({ files: filesArray, albumId, isLockedAssets: isInLockedFolder });
|
await fileUploadHandler({ files: filesArray, albumId, isLockedAssets: isInLockedFolder });
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
let { link, menuItem = false }: Props = $props();
|
let { link, menuItem = false }: Props = $props();
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
await copyToClipboard(makeSharedLinkUrl(link.key));
|
await copyToClipboard(makeSharedLinkUrl(link));
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Badge from '$lib/components/elements/badge.svelte';
|
import Badge from '$lib/components/elements/badge.svelte';
|
||||||
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||||
|
import SharedLinkCopy from '$lib/components/sharedlinks-page/actions/shared-link-copy.svelte';
|
||||||
|
import SharedLinkDelete from '$lib/components/sharedlinks-page/actions/shared-link-delete.svelte';
|
||||||
|
import SharedLinkEdit from '$lib/components/sharedlinks-page/actions/shared-link-edit.svelte';
|
||||||
import ShareCover from '$lib/components/sharedlinks-page/covers/share-cover.svelte';
|
import ShareCover from '$lib/components/sharedlinks-page/covers/share-cover.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
|
import { SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
|
||||||
|
import { mdiDotsVertical } from '@mdi/js';
|
||||||
import { DateTime, type ToRelativeUnit } from 'luxon';
|
import { DateTime, type ToRelativeUnit } from 'luxon';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import SharedLinkDelete from '$lib/components/sharedlinks-page/actions/shared-link-delete.svelte';
|
|
||||||
import SharedLinkEdit from '$lib/components/sharedlinks-page/actions/shared-link-edit.svelte';
|
|
||||||
import SharedLinkCopy from '$lib/components/sharedlinks-page/actions/shared-link-copy.svelte';
|
|
||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
|
||||||
import { mdiDotsVertical } from '@mdi/js';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
link: SharedLinkResponseDto;
|
link: SharedLinkResponseDto;
|
||||||
@ -91,6 +91,9 @@
|
|||||||
{#if link.password}
|
{#if link.password}
|
||||||
<Badge rounded="full"><span class="text-xs px-1">{$t('password')}</span></Badge>
|
<Badge rounded="full"><span class="text-xs px-1">{$t('password')}</span></Badge>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if link.slug}
|
||||||
|
<Badge rounded="full"><span class="text-xs px-1">{$t('custom_url')}</span></Badge>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</svelte:element>
|
</svelte:element>
|
||||||
|
@ -6,7 +6,8 @@ import { isSharedLinkRoute } from '$lib/utils/navigation';
|
|||||||
import { logout } from '@immich/sdk';
|
import { logout } from '@immich/sdk';
|
||||||
|
|
||||||
class AuthManager {
|
class AuthManager {
|
||||||
key = $derived(isSharedLinkRoute(page.route?.id) ? page.params.key : undefined);
|
isSharedLink = $derived(isSharedLinkRoute(page.route?.id));
|
||||||
|
params = $derived(this.isSharedLink ? { key: page.params.key, slug: page.params.slug } : {});
|
||||||
|
|
||||||
async logout() {
|
async logout() {
|
||||||
let redirectUri;
|
let redirectUri;
|
||||||
|
@ -18,12 +18,11 @@ export async function loadFromTimeBuckets(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const timeBucket = toISOYearMonthUTC(monthGroup.yearMonth);
|
const timeBucket = toISOYearMonthUTC(monthGroup.yearMonth);
|
||||||
const key = authManager.key;
|
|
||||||
const bucketResponse = await getTimeBucket(
|
const bucketResponse = await getTimeBucket(
|
||||||
{
|
{
|
||||||
|
...authManager.params,
|
||||||
...options,
|
...options,
|
||||||
timeBucket,
|
timeBucket,
|
||||||
key,
|
|
||||||
},
|
},
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
@ -35,9 +34,9 @@ export async function loadFromTimeBuckets(
|
|||||||
if (options.timelineAlbumId) {
|
if (options.timelineAlbumId) {
|
||||||
const albumAssets = await getTimeBucket(
|
const albumAssets = await getTimeBucket(
|
||||||
{
|
{
|
||||||
|
...authManager.params,
|
||||||
albumId: options.timelineAlbumId,
|
albumId: options.timelineAlbumId,
|
||||||
timeBucket,
|
timeBucket,
|
||||||
key,
|
|
||||||
},
|
},
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
|
@ -288,8 +288,8 @@ export class TimelineManager {
|
|||||||
|
|
||||||
async #initializeMonthGroups() {
|
async #initializeMonthGroups() {
|
||||||
const timebuckets = await getTimeBuckets({
|
const timebuckets = await getTimeBuckets({
|
||||||
|
...authManager.params,
|
||||||
...this.#options,
|
...this.#options,
|
||||||
key: authManager.key,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.months = timebuckets.map((timeBucket) => {
|
this.months = timebuckets.map((timeBucket) => {
|
||||||
@ -423,7 +423,7 @@ export class TimelineManager {
|
|||||||
if (monthGroup) {
|
if (monthGroup) {
|
||||||
return monthGroup;
|
return monthGroup;
|
||||||
}
|
}
|
||||||
const asset = toTimelineAsset(await getAssetInfo({ id, key: authManager.key }));
|
const asset = toTimelineAsset(await getAssetInfo({ ...authManager.params, id }));
|
||||||
if (!asset || this.isExcluded(asset)) {
|
if (!asset || this.isExcluded(asset)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
let sharedLinkUrl = $state('');
|
let sharedLinkUrl = $state('');
|
||||||
const handleViewQrCode = (sharedLink: SharedLinkResponseDto) => {
|
const handleViewQrCode = (sharedLink: SharedLinkResponseDto) => {
|
||||||
sharedLinkUrl = makeSharedLinkUrl(sharedLink.key);
|
sharedLinkUrl = makeSharedLinkUrl(sharedLink);
|
||||||
};
|
};
|
||||||
|
|
||||||
const roleOptions: Array<{ title: string; value: AlbumUserRole | 'none'; icon?: string }> = [
|
const roleOptions: Array<{ title: string; value: AlbumUserRole | 'none'; icon?: string }> = [
|
||||||
|
@ -1,16 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||||
import { SettingInputFieldType } from '$lib/constants';
|
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { SharedLinkType, createSharedLink, updateSharedLink, type SharedLinkResponseDto } from '@immich/sdk';
|
import { SharedLinkType, createSharedLink, updateSharedLink, type SharedLinkResponseDto } from '@immich/sdk';
|
||||||
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
|
import { Button, Field, Input, Modal, ModalBody, ModalFooter, PasswordInput, Switch, Text } from '@immich/ui';
|
||||||
import { mdiLink } from '@mdi/js';
|
import { mdiLink } from '@mdi/js';
|
||||||
import { DateTime, Duration } from 'luxon';
|
import { DateTime, Duration } from 'luxon';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { NotificationType, notificationController } from '../components/shared-components/notification/notification';
|
import { NotificationType, notificationController } from '../components/shared-components/notification/notification';
|
||||||
import SettingInputField from '../components/shared-components/settings/setting-input-field.svelte';
|
|
||||||
import SettingSwitch from '../components/shared-components/settings/setting-switch.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: (sharedLink?: SharedLinkResponseDto) => void;
|
onClose: (sharedLink?: SharedLinkResponseDto) => void;
|
||||||
@ -28,8 +25,8 @@
|
|||||||
let showMetadata = $state(true);
|
let showMetadata = $state(true);
|
||||||
let expirationOption: number = $state(0);
|
let expirationOption: number = $state(0);
|
||||||
let password = $state('');
|
let password = $state('');
|
||||||
|
let slug = $state('');
|
||||||
let shouldChangeExpirationTime = $state(false);
|
let shouldChangeExpirationTime = $state(false);
|
||||||
let enablePassword = $state(false);
|
|
||||||
|
|
||||||
const expirationOptions: [number, Intl.RelativeTimeFormatUnit][] = [
|
const expirationOptions: [number, Intl.RelativeTimeFormatUnit][] = [
|
||||||
[30, 'minutes'],
|
[30, 'minutes'],
|
||||||
@ -63,17 +60,15 @@
|
|||||||
if (editingLink.description) {
|
if (editingLink.description) {
|
||||||
description = editingLink.description;
|
description = editingLink.description;
|
||||||
}
|
}
|
||||||
if (editingLink.password) {
|
|
||||||
password = editingLink.password;
|
password = editingLink.password ?? '';
|
||||||
}
|
slug = editingLink.slug ?? '';
|
||||||
allowUpload = editingLink.allowUpload;
|
allowUpload = editingLink.allowUpload;
|
||||||
allowDownload = editingLink.allowDownload;
|
allowDownload = editingLink.allowDownload;
|
||||||
showMetadata = editingLink.showMetadata;
|
showMetadata = editingLink.showMetadata;
|
||||||
|
|
||||||
albumId = editingLink.album?.id;
|
albumId = editingLink.album?.id;
|
||||||
assetIds = editingLink.assets.map(({ id }) => id);
|
assetIds = editingLink.assets.map(({ id }) => id);
|
||||||
|
|
||||||
enablePassword = !!editingLink.password;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCreateSharedLink = async () => {
|
const handleCreateSharedLink = async () => {
|
||||||
@ -91,6 +86,7 @@
|
|||||||
password,
|
password,
|
||||||
allowDownload,
|
allowDownload,
|
||||||
showMetadata,
|
showMetadata,
|
||||||
|
slug,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
onClose(data);
|
onClose(data);
|
||||||
@ -111,11 +107,12 @@
|
|||||||
id: editingLink.id,
|
id: editingLink.id,
|
||||||
sharedLinkEditDto: {
|
sharedLinkEditDto: {
|
||||||
description,
|
description,
|
||||||
password: enablePassword ? password : '',
|
password: password ?? null,
|
||||||
expiresAt: shouldChangeExpirationTime ? expirationDate : undefined,
|
expiresAt: shouldChangeExpirationTime ? expirationDate : undefined,
|
||||||
allowUpload,
|
allowUpload,
|
||||||
allowDownload,
|
allowDownload,
|
||||||
showMetadata,
|
showMetadata,
|
||||||
|
slug: slug.trim() ?? null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -165,63 +162,51 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="mb-2 mt-4">
|
<div class="flex flex-col gap-4 mt-4">
|
||||||
<p class="text-xs">{$t('link_options').toUpperCase()}</p>
|
<div>
|
||||||
</div>
|
<Field label={$t('custom_url')} description={$t('shared_link_custom_url_description')}>
|
||||||
<div class="rounded-lg bg-gray-100 p-4 dark:bg-black/40 overflow-y-auto">
|
<Input bind:value={slug} placeholder="immich-10000" />
|
||||||
<div class="flex flex-col">
|
</Field>
|
||||||
<div class="mb-2">
|
{#if slug}
|
||||||
<SettingInputField
|
<Text size="tiny" color="muted" class="pt-2">/s/{encodeURIComponent(slug)}</Text>
|
||||||
inputType={SettingInputFieldType.TEXT}
|
|
||||||
label={$t('description')}
|
|
||||||
bind:value={description}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-2">
|
|
||||||
<SettingInputField
|
|
||||||
inputType={SettingInputFieldType.TEXT}
|
|
||||||
label={$t('password')}
|
|
||||||
bind:value={password}
|
|
||||||
disabled={!enablePassword}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="my-3">
|
|
||||||
<SettingSwitch bind:checked={enablePassword} title={$t('require_password')} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="my-3">
|
|
||||||
<SettingSwitch bind:checked={showMetadata} title={$t('show_metadata')} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="my-3">
|
|
||||||
<SettingSwitch
|
|
||||||
bind:checked={allowDownload}
|
|
||||||
title={$t('allow_public_user_to_download')}
|
|
||||||
disabled={!showMetadata}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="my-3">
|
|
||||||
<SettingSwitch bind:checked={allowUpload} title={$t('allow_public_user_to_upload')} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if editingLink}
|
|
||||||
<div class="my-3">
|
|
||||||
<SettingSwitch bind:checked={shouldChangeExpirationTime} title={$t('change_expiration_time')} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
<div class="mt-3">
|
|
||||||
<SettingSelect
|
|
||||||
bind:value={expirationOption}
|
|
||||||
options={expiredDateOptions}
|
|
||||||
label={$t('expire_after')}
|
|
||||||
disabled={editingLink && !shouldChangeExpirationTime}
|
|
||||||
number={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Field label={$t('password')} description={$t('shared_link_password_description')}>
|
||||||
|
<PasswordInput bind:value={password} />
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label={$t('description')}>
|
||||||
|
<Input bind:value={description} />
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<SettingSelect
|
||||||
|
bind:value={expirationOption}
|
||||||
|
options={expiredDateOptions}
|
||||||
|
label={$t('expire_after')}
|
||||||
|
disabled={editingLink && !shouldChangeExpirationTime}
|
||||||
|
number={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Field label={$t('show_metadata')}>
|
||||||
|
<Switch bind:checked={showMetadata} />
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label={$t('allow_public_user_to_download')} disabled={!showMetadata}>
|
||||||
|
<Switch bind:checked={allowDownload} />
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label={$t('allow_public_user_to_upload')}>
|
||||||
|
<Switch bind:checked={allowUpload} />
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
{#if editingLink}
|
||||||
|
<Field label={$t('change_expiration_time')}>
|
||||||
|
<Switch bind:checked={shouldChangeExpirationTime} />
|
||||||
|
</Field>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ function createAssetViewingStore() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setAssetId = async (id: string): Promise<AssetResponseDto> => {
|
const setAssetId = async (id: string): Promise<AssetResponseDto> => {
|
||||||
const asset = await getAssetInfo({ id, key: authManager.key });
|
const asset = await getAssetInfo({ ...authManager.params, id });
|
||||||
setAsset(asset);
|
setAsset(asset);
|
||||||
return asset;
|
return asset;
|
||||||
};
|
};
|
||||||
|
@ -184,7 +184,7 @@ export const getAssetOriginalUrl = (options: string | AssetUrlOptions) => {
|
|||||||
options = { id: options };
|
options = { id: options };
|
||||||
}
|
}
|
||||||
const { id, cacheKey } = options;
|
const { id, cacheKey } = options;
|
||||||
return createUrl(getAssetOriginalPath(id), { key: authManager.key, c: cacheKey });
|
return createUrl(getAssetOriginalPath(id), { ...authManager.params, c: cacheKey });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAssetThumbnailUrl = (options: string | (AssetUrlOptions & { size?: AssetMediaSize })) => {
|
export const getAssetThumbnailUrl = (options: string | (AssetUrlOptions & { size?: AssetMediaSize })) => {
|
||||||
@ -192,7 +192,7 @@ export const getAssetThumbnailUrl = (options: string | (AssetUrlOptions & { size
|
|||||||
options = { id: options };
|
options = { id: options };
|
||||||
}
|
}
|
||||||
const { id, size, cacheKey } = options;
|
const { id, size, cacheKey } = options;
|
||||||
return createUrl(getAssetThumbnailPath(id), { size, key: authManager.key, c: cacheKey });
|
return createUrl(getAssetThumbnailPath(id), { ...authManager.params, size, c: cacheKey });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAssetPlaybackUrl = (options: string | AssetUrlOptions) => {
|
export const getAssetPlaybackUrl = (options: string | AssetUrlOptions) => {
|
||||||
@ -200,7 +200,7 @@ export const getAssetPlaybackUrl = (options: string | AssetUrlOptions) => {
|
|||||||
options = { id: options };
|
options = { id: options };
|
||||||
}
|
}
|
||||||
const { id, cacheKey } = options;
|
const { id, cacheKey } = options;
|
||||||
return createUrl(getAssetPlaybackPath(id), { key: authManager.key, c: cacheKey });
|
return createUrl(getAssetPlaybackPath(id), { ...authManager.params, c: cacheKey });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProfileImageUrl = (user: UserResponseDto) =>
|
export const getProfileImageUrl = (user: UserResponseDto) =>
|
||||||
@ -257,8 +257,9 @@ export const copyToClipboard = async (secret: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const makeSharedLinkUrl = (key: string) => {
|
export const makeSharedLinkUrl = (sharedLink: SharedLinkResponseDto) => {
|
||||||
return new URL(`share/${key}`, get(serverConfig).externalDomain || globalThis.location.origin).href;
|
const path = sharedLink.slug ? `s/${sharedLink.slug}` : `share/${sharedLink.key}`;
|
||||||
|
return new URL(path, get(serverConfig).externalDomain || globalThis.location.origin).href;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const oauth = {
|
export const oauth = {
|
||||||
|
@ -13,6 +13,7 @@ import { downloadRequest, withError } from '$lib/utils';
|
|||||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
|
import { asQueryString } from '$lib/utils/shared-links';
|
||||||
import {
|
import {
|
||||||
addAssetsToAlbum as addAssets,
|
addAssetsToAlbum as addAssets,
|
||||||
AssetVisibility,
|
AssetVisibility,
|
||||||
@ -42,11 +43,11 @@ import { handleError } from './handle-error';
|
|||||||
|
|
||||||
export const addAssetsToAlbum = async (albumId: string, assetIds: string[], showNotification = true) => {
|
export const addAssetsToAlbum = async (albumId: string, assetIds: string[], showNotification = true) => {
|
||||||
const result = await addAssets({
|
const result = await addAssets({
|
||||||
|
...authManager.params,
|
||||||
id: albumId,
|
id: albumId,
|
||||||
bulkIdsDto: {
|
bulkIdsDto: {
|
||||||
ids: assetIds,
|
ids: assetIds,
|
||||||
},
|
},
|
||||||
key: authManager.key,
|
|
||||||
});
|
});
|
||||||
const count = result.filter(({ success }) => success).length;
|
const count = result.filter(({ success }) => success).length;
|
||||||
const duplicateErrorCount = result.filter(({ error }) => error === 'duplicate').length;
|
const duplicateErrorCount = result.filter(({ error }) => error === 'duplicate').length;
|
||||||
@ -155,7 +156,7 @@ export const downloadArchive = async (fileName: string, options: Omit<DownloadIn
|
|||||||
const $preferences = get<UserPreferencesResponseDto | undefined>(preferences);
|
const $preferences = get<UserPreferencesResponseDto | undefined>(preferences);
|
||||||
const dto = { ...options, archiveSize: $preferences?.download.archiveSize };
|
const dto = { ...options, archiveSize: $preferences?.download.archiveSize };
|
||||||
|
|
||||||
const [error, downloadInfo] = await withError(() => getDownloadInfo({ downloadInfoDto: dto, key: authManager.key }));
|
const [error, downloadInfo] = await withError(() => getDownloadInfo({ ...authManager.params, downloadInfoDto: dto }));
|
||||||
if (error) {
|
if (error) {
|
||||||
const $t = get(t);
|
const $t = get(t);
|
||||||
handleError(error, $t('errors.unable_to_download_files'));
|
handleError(error, $t('errors.unable_to_download_files'));
|
||||||
@ -170,7 +171,7 @@ export const downloadArchive = async (fileName: string, options: Omit<DownloadIn
|
|||||||
const archive = downloadInfo.archives[index];
|
const archive = downloadInfo.archives[index];
|
||||||
const suffix = downloadInfo.archives.length > 1 ? `+${index + 1}` : '';
|
const suffix = downloadInfo.archives.length > 1 ? `+${index + 1}` : '';
|
||||||
const archiveName = fileName.replace('.zip', `${suffix}-${DateTime.now().toFormat('yyyyLLdd_HHmmss')}.zip`);
|
const archiveName = fileName.replace('.zip', `${suffix}-${DateTime.now().toFormat('yyyyLLdd_HHmmss')}.zip`);
|
||||||
const key = authManager.key;
|
const queryParams = asQueryString(authManager.params);
|
||||||
|
|
||||||
let downloadKey = `${archiveName} `;
|
let downloadKey = `${archiveName} `;
|
||||||
if (downloadInfo.archives.length > 1) {
|
if (downloadInfo.archives.length > 1) {
|
||||||
@ -184,7 +185,7 @@ export const downloadArchive = async (fileName: string, options: Omit<DownloadIn
|
|||||||
// TODO use sdk once it supports progress events
|
// TODO use sdk once it supports progress events
|
||||||
const { data } = await downloadRequest({
|
const { data } = await downloadRequest({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: getBaseUrl() + '/download/archive' + (key ? `?key=${key}` : ''),
|
url: getBaseUrl() + '/download/archive' + (queryParams ? `?${queryParams}` : ''),
|
||||||
data: { assetIds: archive.assetIds },
|
data: { assetIds: archive.assetIds },
|
||||||
signal: abort.signal,
|
signal: abort.signal,
|
||||||
onDownloadProgress: (event) => downloadManager.update(downloadKey, event.loaded),
|
onDownloadProgress: (event) => downloadManager.update(downloadKey, event.loaded),
|
||||||
@ -217,7 +218,7 @@ export const downloadFile = async (asset: AssetResponseDto) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (asset.livePhotoVideoId) {
|
if (asset.livePhotoVideoId) {
|
||||||
const motionAsset = await getAssetInfo({ id: asset.livePhotoVideoId, key: authManager.key });
|
const motionAsset = await getAssetInfo({ ...authManager.params, id: asset.livePhotoVideoId });
|
||||||
if (!isAndroidMotionVideo(motionAsset) || get(preferences)?.download.includeEmbeddedVideos) {
|
if (!isAndroidMotionVideo(motionAsset) || get(preferences)?.download.includeEmbeddedVideos) {
|
||||||
assets.push({
|
assets.push({
|
||||||
filename: motionAsset.originalFileName,
|
filename: motionAsset.originalFileName,
|
||||||
@ -227,16 +228,16 @@ export const downloadFile = async (asset: AssetResponseDto) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const queryParams = asQueryString(authManager.params);
|
||||||
|
|
||||||
for (const { filename, id } of assets) {
|
for (const { filename, id } of assets) {
|
||||||
try {
|
try {
|
||||||
const key = authManager.key;
|
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
message: $t('downloading_asset_filename', { values: { filename: asset.originalFileName } }),
|
message: $t('downloading_asset_filename', { values: { filename: asset.originalFileName } }),
|
||||||
});
|
});
|
||||||
|
|
||||||
downloadUrl(getBaseUrl() + `/assets/${id}/original` + (key ? `?key=${key}` : ''), filename);
|
downloadUrl(getBaseUrl() + `/assets/${id}/original` + (queryParams ? `?${queryParams}` : ''), filename);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('errors.error_downloading', { values: { filename } }));
|
handleError(error, $t('errors.error_downloading', { values: { filename } }));
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import { uploadAssetsStore } from '$lib/stores/upload';
|
|||||||
import { uploadRequest } from '$lib/utils';
|
import { uploadRequest } from '$lib/utils';
|
||||||
import { addAssetsToAlbum } from '$lib/utils/asset-utils';
|
import { addAssetsToAlbum } from '$lib/utils/asset-utils';
|
||||||
import { ExecutorQueue } from '$lib/utils/executor-queue';
|
import { ExecutorQueue } from '$lib/utils/executor-queue';
|
||||||
|
import { asQueryString } from '$lib/utils/shared-links';
|
||||||
import {
|
import {
|
||||||
Action,
|
Action,
|
||||||
AssetMediaStatus,
|
AssetMediaStatus,
|
||||||
@ -152,8 +153,7 @@ async function fileUploader({
|
|||||||
}
|
}
|
||||||
|
|
||||||
let responseData: { id: string; status: AssetMediaStatus; isTrashed?: boolean } | undefined;
|
let responseData: { id: string; status: AssetMediaStatus; isTrashed?: boolean } | undefined;
|
||||||
const key = authManager.key;
|
if (crypto?.subtle?.digest && !authManager.isSharedLink) {
|
||||||
if (crypto?.subtle?.digest && !key) {
|
|
||||||
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_hashing') });
|
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_hashing') });
|
||||||
await tick();
|
await tick();
|
||||||
try {
|
try {
|
||||||
@ -179,10 +179,12 @@ async function fileUploader({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!responseData) {
|
if (!responseData) {
|
||||||
|
const queryParams = asQueryString(authManager.params);
|
||||||
|
|
||||||
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_uploading') });
|
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_uploading') });
|
||||||
if (replaceAssetId) {
|
if (replaceAssetId) {
|
||||||
const response = await uploadRequest<AssetMediaResponseDto>({
|
const response = await uploadRequest<AssetMediaResponseDto>({
|
||||||
url: getBaseUrl() + getAssetOriginalPath(replaceAssetId) + (key ? `?key=${key}` : ''),
|
url: getBaseUrl() + getAssetOriginalPath(replaceAssetId) + (queryParams ? `?${queryParams}` : ''),
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
data: formData,
|
data: formData,
|
||||||
onUploadProgress: (event) => uploadAssetsStore.updateProgress(deviceAssetId, event.loaded, event.total),
|
onUploadProgress: (event) => uploadAssetsStore.updateProgress(deviceAssetId, event.loaded, event.total),
|
||||||
@ -190,7 +192,7 @@ async function fileUploader({
|
|||||||
responseData = response.data;
|
responseData = response.data;
|
||||||
} else {
|
} else {
|
||||||
const response = await uploadRequest<AssetMediaResponseDto>({
|
const response = await uploadRequest<AssetMediaResponseDto>({
|
||||||
url: getBaseUrl() + '/assets' + (key ? `?key=${key}` : ''),
|
url: getBaseUrl() + '/assets' + (queryParams ? `?${queryParams}` : ''),
|
||||||
data: formData,
|
data: formData,
|
||||||
onUploadProgress: (event) => uploadAssetsStore.updateProgress(deviceAssetId, event.loaded, event.total),
|
onUploadProgress: (event) => uploadAssetsStore.updateProgress(deviceAssetId, event.loaded, event.total),
|
||||||
});
|
});
|
||||||
|
@ -13,7 +13,8 @@ export const isExternalUrl = (url: string): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isPhotosRoute = (route?: string | null) => !!route?.startsWith('/(user)/photos/[[assetId=id]]');
|
export const isPhotosRoute = (route?: string | null) => !!route?.startsWith('/(user)/photos/[[assetId=id]]');
|
||||||
export const isSharedLinkRoute = (route?: string | null) => !!route?.startsWith('/(user)/share/[key]');
|
export const isSharedLinkRoute = (route?: string | null) =>
|
||||||
|
!!route?.startsWith('/(user)/share/[key]') || !!route?.startsWith('/(user)/s/[slug]');
|
||||||
export const isSearchRoute = (route?: string | null) => !!route?.startsWith('/(user)/search');
|
export const isSearchRoute = (route?: string | null) => !!route?.startsWith('/(user)/search');
|
||||||
export const isAlbumsRoute = (route?: string | null) => !!route?.startsWith('/(user)/albums/[albumId=id]');
|
export const isAlbumsRoute = (route?: string | null) => !!route?.startsWith('/(user)/albums/[albumId=id]');
|
||||||
export const isPeopleRoute = (route?: string | null) => !!route?.startsWith('/(user)/people/[personId]');
|
export const isPeopleRoute = (route?: string | null) => !!route?.startsWith('/(user)/people/[personId]');
|
||||||
|
58
web/src/lib/utils/shared-links.ts
Normal file
58
web/src/lib/utils/shared-links.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { getAssetThumbnailUrl, setSharedLink } from '$lib/utils';
|
||||||
|
import { authenticate } from '$lib/utils/auth';
|
||||||
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
|
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
||||||
|
import { getMySharedLink, isHttpError } from '@immich/sdk';
|
||||||
|
|
||||||
|
export const asQueryString = ({ slug, key }: { slug?: string; key?: string }) => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (slug) {
|
||||||
|
params.set('slug', slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key) {
|
||||||
|
params.set('key', key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return params.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadSharedLink = async ({ url, params }: { url: URL; params: { key?: string; slug?: string } }) => {
|
||||||
|
const { key, slug } = params;
|
||||||
|
await authenticate(url, { public: true });
|
||||||
|
|
||||||
|
const common = { key, slug };
|
||||||
|
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [sharedLink, asset] = await Promise.all([getMySharedLink({ key, slug }), getAssetInfoFromParam(params)]);
|
||||||
|
setSharedLink(sharedLink);
|
||||||
|
const assetCount = sharedLink.assets.length;
|
||||||
|
const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
|
||||||
|
const assetPath = assetId ? getAssetThumbnailUrl(assetId) : '/feature-panel.png';
|
||||||
|
|
||||||
|
return {
|
||||||
|
...common,
|
||||||
|
sharedLink,
|
||||||
|
asset,
|
||||||
|
meta: {
|
||||||
|
title: sharedLink.album ? sharedLink.album.albumName : $t('public_share'),
|
||||||
|
description: sharedLink.description || $t('shared_photos_and_videos_count', { values: { assetCount } }),
|
||||||
|
imageUrl: assetPath,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (isHttpError(error) && error.data.message === 'Invalid password') {
|
||||||
|
return {
|
||||||
|
...common,
|
||||||
|
passwordRequired: true,
|
||||||
|
meta: {
|
||||||
|
title: $t('password_required'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
@ -402,9 +402,8 @@
|
|||||||
|
|
||||||
const handleShareLink = async () => {
|
const handleShareLink = async () => {
|
||||||
const sharedLink = await modalManager.show(SharedLinkCreateModal, { albumId: album.id });
|
const sharedLink = await modalManager.show(SharedLinkCreateModal, { albumId: album.id });
|
||||||
|
|
||||||
if (sharedLink) {
|
if (sharedLink) {
|
||||||
await modalManager.show(QrCodeModal, { title: $t('view_link'), value: makeSharedLinkUrl(sharedLink.key) });
|
await modalManager.show(QrCodeModal, { title: $t('view_link'), value: makeSharedLinkUrl(sharedLink) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
5
web/src/routes/(user)/s/[slug]/+error.svelte
Normal file
5
web/src/routes/(user)/s/[slug]/+error.svelte
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import SharedLinkErrorPage from '$lib/components/pages/SharedLinkErrorPage.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SharedLinkErrorPage />
|
@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import SharedLinkPage from '$lib/components/pages/SharedLinkPage.svelte';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: PageData;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { data }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SharedLinkPage {data} />
|
@ -0,0 +1,4 @@
|
|||||||
|
import { loadSharedLink } from '$lib/utils/shared-links';
|
||||||
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
|
export const load = (async ({ params, url }) => loadSharedLink({ params, url })) satisfies PageLoad;
|
@ -1,14 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/state';
|
import SharedLinkErrorPage from '$lib/components/pages/SharedLinkErrorPage.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<SharedLinkErrorPage />
|
||||||
<title>Oops! Error - Immich</title>
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<section class="flex flex-col px-4 h-dvh w-dvw place-content-center place-items-center">
|
|
||||||
<h1 class="py-10 text-4xl text-immich-primary dark:text-immich-dark-primary">Page not found :/</h1>
|
|
||||||
{#if page.error?.message}
|
|
||||||
<h2 class="text-xl text-immich-fg dark:text-immich-dark-fg">{page.error.message}</h2>
|
|
||||||
{/if}
|
|
||||||
</section>
|
|
||||||
|
@ -1,97 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import AlbumViewer from '$lib/components/album-page/album-viewer.svelte';
|
import SharedLinkPage from '$lib/components/pages/SharedLinkPage.svelte';
|
||||||
import IndividualSharedViewer from '$lib/components/share-page/individual-shared-viewer.svelte';
|
|
||||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
|
||||||
import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte';
|
|
||||||
import PasswordField from '$lib/components/shared-components/password-field.svelte';
|
|
||||||
import ThemeButton from '$lib/components/shared-components/theme-button.svelte';
|
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
|
||||||
import { user } from '$lib/stores/user.store';
|
|
||||||
import { setSharedLink } from '$lib/utils';
|
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
|
||||||
import { navigate } from '$lib/utils/navigation';
|
|
||||||
import { getMySharedLink, SharedLinkType } from '@immich/sdk';
|
|
||||||
import { Button } from '@immich/ui';
|
|
||||||
import { tick } from 'svelte';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
interface Props {
|
type Props = {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
}
|
};
|
||||||
|
|
||||||
let { data }: Props = $props();
|
let { data }: Props = $props();
|
||||||
|
|
||||||
let { gridScrollTarget } = assetViewingStore;
|
|
||||||
let { sharedLink, passwordRequired, sharedLinkKey: key, meta } = $state(data);
|
|
||||||
let { title, description } = $state(meta);
|
|
||||||
let isOwned = $derived($user ? $user.id === sharedLink?.userId : false);
|
|
||||||
let password = $state('');
|
|
||||||
|
|
||||||
const handlePasswordSubmit = async () => {
|
|
||||||
try {
|
|
||||||
sharedLink = await getMySharedLink({ password, key });
|
|
||||||
setSharedLink(sharedLink);
|
|
||||||
passwordRequired = false;
|
|
||||||
title = (sharedLink.album ? sharedLink.album.albumName : $t('public_share')) + ' - Immich';
|
|
||||||
description =
|
|
||||||
sharedLink.description ||
|
|
||||||
$t('shared_photos_and_videos_count', { values: { assetCount: sharedLink.assets.length } });
|
|
||||||
await tick();
|
|
||||||
await navigate(
|
|
||||||
{ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget },
|
|
||||||
{ forceNavigate: true, replaceState: true },
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.unable_to_get_shared_link'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onsubmit = async (event: Event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
await handlePasswordSubmit();
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<SharedLinkPage {data} />
|
||||||
<title>{title}</title>
|
|
||||||
<meta name="description" content={description} />
|
|
||||||
</svelte:head>
|
|
||||||
{#if passwordRequired}
|
|
||||||
<main
|
|
||||||
class="relative h-dvh overflow-hidden px-6 max-md:pt-(--navbar-height-md) pt-(--navbar-height) sm:px-12 md:px-24 lg:px-40"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col items-center justify-center mt-20">
|
|
||||||
<div class="text-2xl font-bold text-immich-primary dark:text-immich-dark-primary">{$t('password_required')}</div>
|
|
||||||
<div class="mt-4 text-lg text-immich-primary dark:text-immich-dark-primary">
|
|
||||||
{$t('sharing_enter_password')}
|
|
||||||
</div>
|
|
||||||
<div class="mt-4">
|
|
||||||
<form class="flex gap-x-2" novalidate {onsubmit}>
|
|
||||||
<PasswordField autocomplete="off" bind:password placeholder="Password" />
|
|
||||||
<Button type="submit">{$t('submit')}</Button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<header>
|
|
||||||
<ControlAppBar showBackButton={false}>
|
|
||||||
{#snippet leading()}
|
|
||||||
<ImmichLogoSmallLink />
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
{#snippet trailing()}
|
|
||||||
<ThemeButton />
|
|
||||||
{/snippet}
|
|
||||||
</ControlAppBar>
|
|
||||||
</header>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if !passwordRequired && sharedLink?.type == SharedLinkType.Album}
|
|
||||||
<AlbumViewer {sharedLink} />
|
|
||||||
{/if}
|
|
||||||
{#if !passwordRequired && sharedLink?.type == SharedLinkType.Individual}
|
|
||||||
<div class="immich-scrollbar">
|
|
||||||
<IndividualSharedViewer {sharedLink} {isOwned} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
@ -1,44 +1,4 @@
|
|||||||
import { getAssetThumbnailUrl, setSharedLink } from '$lib/utils';
|
import { loadSharedLink } from '$lib/utils/shared-links';
|
||||||
import { authenticate } from '$lib/utils/auth';
|
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
|
||||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
|
||||||
import { getMySharedLink, isHttpError } from '@immich/sdk';
|
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async ({ params, url }) => {
|
export const load = (async ({ params, url }) => loadSharedLink({ params, url })) satisfies PageLoad;
|
||||||
const { key } = params;
|
|
||||||
await authenticate(url, { public: true });
|
|
||||||
|
|
||||||
const $t = await getFormatter();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [sharedLink, asset] = await Promise.all([getMySharedLink({ key }), getAssetInfoFromParam(params)]);
|
|
||||||
setSharedLink(sharedLink);
|
|
||||||
const assetCount = sharedLink.assets.length;
|
|
||||||
const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
|
|
||||||
const assetPath = assetId ? getAssetThumbnailUrl(assetId) : '/feature-panel.png';
|
|
||||||
|
|
||||||
return {
|
|
||||||
sharedLink,
|
|
||||||
sharedLinkKey: key,
|
|
||||||
asset,
|
|
||||||
meta: {
|
|
||||||
title: sharedLink.album ? sharedLink.album.albumName : $t('public_share'),
|
|
||||||
description: sharedLink.description || $t('shared_photos_and_videos_count', { values: { assetCount } }),
|
|
||||||
imageUrl: assetPath,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
if (isHttpError(error) && error.data.message === 'Invalid password') {
|
|
||||||
return {
|
|
||||||
passwordRequired: true,
|
|
||||||
sharedLinkKey: key,
|
|
||||||
meta: {
|
|
||||||
title: $t('password_required'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}) satisfies PageLoad;
|
|
||||||
|
@ -16,4 +16,5 @@ export const sharedLinkFactory = Sync.makeFactory<SharedLinkResponseDto>({
|
|||||||
allowUpload: Sync.each(() => faker.datatype.boolean()),
|
allowUpload: Sync.each(() => faker.datatype.boolean()),
|
||||||
allowDownload: Sync.each(() => faker.datatype.boolean()),
|
allowDownload: Sync.each(() => faker.datatype.boolean()),
|
||||||
showMetadata: Sync.each(() => faker.datatype.boolean()),
|
showMetadata: Sync.each(() => faker.datatype.boolean()),
|
||||||
|
slug: null,
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user