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:
Jed-Giblin 2025-07-28 14:16:55 -04:00 committed by GitHub
parent 16b14b390f
commit 9b3718120b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 947 additions and 432 deletions

View File

@ -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.}}",

View File

@ -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));
} }

View File

@ -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));
} }

View File

@ -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));
} }

View File

@ -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));
} }

View File

@ -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));
} }

View File

@ -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'])!,
); );
} }

View File

@ -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;

View File

@ -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',
}; };

View File

@ -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"
], ],

View File

@ -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,

View File

@ -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> & {

View File

@ -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,
}; };
} }

View File

@ -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',
} }

View File

@ -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);

View File

@ -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

View File

@ -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[] }) {

View File

@ -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);
}

View File

@ -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;
} }

View File

@ -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,

View File

@ -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');
}); });
}); });

View File

@ -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> {

View File

@ -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'],
}); });
}); });

View File

@ -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> {

View File

@ -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 }],
}), }),

View File

@ -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;
} }

View File

@ -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 }} />

View File

@ -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>

View File

@ -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;
} }

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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)) || []);

View 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>

View 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}

View File

@ -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>

View File

@ -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;
} }

View File

@ -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) {

View File

@ -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) {

View File

@ -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())}
> >

View File

@ -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}

View File

@ -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 });

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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 },
); );

View File

@ -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;
} }

View File

@ -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 }> = [

View File

@ -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>

View File

@ -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;
}; };

View File

@ -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 = {

View File

@ -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 } }));
} }

View File

@ -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),
}); });

View File

@ -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]');

View 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;
}
};

View File

@ -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) });
} }
}; };

View File

@ -0,0 +1,5 @@
<script lang="ts">
import SharedLinkErrorPage from '$lib/components/pages/SharedLinkErrorPage.svelte';
</script>
<SharedLinkErrorPage />

View File

@ -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} />

View File

@ -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;

View File

@ -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>

View File

@ -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}

View File

@ -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;

View File

@ -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,
}); });