fix: TimelineAsset visibility (#18395)

* fix: TimelineAsset visibility

* fix enum values
This commit is contained in:
Alex 2025-05-20 10:53:34 -05:00 committed by GitHub
parent 895e0eacfe
commit bdf19ce331
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 27 additions and 120 deletions

View File

@ -554,15 +554,15 @@ class Asset {
}"""; }""";
} }
static getVisibility(AssetResponseDtoVisibilityEnum visibility) { static getVisibility(AssetVisibility visibility) {
switch (visibility) { switch (visibility) {
case AssetResponseDtoVisibilityEnum.timeline: case AssetVisibility.timeline:
return AssetVisibilityEnum.timeline; return AssetVisibilityEnum.timeline;
case AssetResponseDtoVisibilityEnum.archive: case AssetVisibility.archive:
return AssetVisibilityEnum.archive; return AssetVisibilityEnum.archive;
case AssetResponseDtoVisibilityEnum.hidden: case AssetVisibility.hidden:
return AssetVisibilityEnum.hidden; return AssetVisibilityEnum.hidden;
case AssetResponseDtoVisibilityEnum.locked: case AssetVisibility.locked:
return AssetVisibilityEnum.locked; return AssetVisibilityEnum.locked;
} }
} }

View File

@ -29,7 +29,6 @@ dynamic upgradeDto(dynamic value, String targetType) {
case 'UserResponseDto': case 'UserResponseDto':
if (value is Map) { if (value is Map) {
addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String()); addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
addDefault(value, 'visibility', AssetVisibility.timeline);
} }
break; break;
case 'AssetResponseDto': case 'AssetResponseDto':

View File

@ -133,7 +133,7 @@ class AssetResponseDto {
DateTime updatedAt; DateTime updatedAt;
AssetResponseDtoVisibilityEnum visibility; AssetVisibility visibility;
@override @override
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto && bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
@ -318,7 +318,7 @@ class AssetResponseDto {
type: AssetTypeEnum.fromJson(json[r'type'])!, type: AssetTypeEnum.fromJson(json[r'type'])!,
unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']), unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']),
updatedAt: mapDateTime(json, r'updatedAt', r'')!, updatedAt: mapDateTime(json, r'updatedAt', r'')!,
visibility: AssetResponseDtoVisibilityEnum.fromJson(json[r'visibility'])!, visibility: AssetVisibility.fromJson(json[r'visibility'])!,
); );
} }
return null; return null;
@ -389,83 +389,3 @@ class AssetResponseDto {
}; };
} }
class AssetResponseDtoVisibilityEnum {
/// Instantiate a new enum with the provided [value].
const AssetResponseDtoVisibilityEnum._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const archive = AssetResponseDtoVisibilityEnum._(r'archive');
static const timeline = AssetResponseDtoVisibilityEnum._(r'timeline');
static const hidden = AssetResponseDtoVisibilityEnum._(r'hidden');
static const locked = AssetResponseDtoVisibilityEnum._(r'locked');
/// List of all possible values in this [enum][AssetResponseDtoVisibilityEnum].
static const values = <AssetResponseDtoVisibilityEnum>[
archive,
timeline,
hidden,
locked,
];
static AssetResponseDtoVisibilityEnum? fromJson(dynamic value) => AssetResponseDtoVisibilityEnumTypeTransformer().decode(value);
static List<AssetResponseDtoVisibilityEnum> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetResponseDtoVisibilityEnum>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AssetResponseDtoVisibilityEnum.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [AssetResponseDtoVisibilityEnum] to String,
/// and [decode] dynamic data back to [AssetResponseDtoVisibilityEnum].
class AssetResponseDtoVisibilityEnumTypeTransformer {
factory AssetResponseDtoVisibilityEnumTypeTransformer() => _instance ??= const AssetResponseDtoVisibilityEnumTypeTransformer._();
const AssetResponseDtoVisibilityEnumTypeTransformer._();
String encode(AssetResponseDtoVisibilityEnum data) => data.value;
/// Decodes a [dynamic value][data] to a AssetResponseDtoVisibilityEnum.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
AssetResponseDtoVisibilityEnum? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'archive': return AssetResponseDtoVisibilityEnum.archive;
case r'timeline': return AssetResponseDtoVisibilityEnum.timeline;
case r'hidden': return AssetResponseDtoVisibilityEnum.hidden;
case r'locked': return AssetResponseDtoVisibilityEnum.locked;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [AssetResponseDtoVisibilityEnumTypeTransformer] instance.
static AssetResponseDtoVisibilityEnumTypeTransformer? _instance;
}

View File

@ -9289,13 +9289,11 @@
"type": "string" "type": "string"
}, },
"visibility": { "visibility": {
"enum": [ "allOf": [
"archive", {
"timeline", "$ref": "#/components/schemas/AssetVisibility"
"hidden", }
"locked" ]
],
"type": "string"
} }
}, },
"required": [ "required": [

View File

@ -329,7 +329,7 @@ export type AssetResponseDto = {
"type": AssetTypeEnum; "type": AssetTypeEnum;
unassignedFaces?: AssetFaceWithoutPersonResponseDto[]; unassignedFaces?: AssetFaceWithoutPersonResponseDto[];
updatedAt: string; updatedAt: string;
visibility: Visibility; visibility: AssetVisibility;
}; };
export type AlbumResponseDto = { export type AlbumResponseDto = {
albumName: string; albumName: string;
@ -3675,12 +3675,6 @@ export enum AssetTypeEnum {
Audio = "AUDIO", Audio = "AUDIO",
Other = "OTHER" Other = "OTHER"
} }
export enum Visibility {
Archive = "archive",
Timeline = "timeline",
Hidden = "hidden",
Locked = "locked"
}
export enum AssetOrder { export enum AssetOrder {
Asc = "asc", Asc = "asc",
Desc = "desc" Desc = "desc"

View File

@ -44,6 +44,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
isArchived!: boolean; isArchived!: boolean;
isTrashed!: boolean; isTrashed!: boolean;
isOffline!: boolean; isOffline!: boolean;
@ApiProperty({ enum: AssetVisibility, enumName: 'AssetVisibility' })
visibility!: AssetVisibility; visibility!: AssetVisibility;
exifInfo?: ExifResponseDto; exifInfo?: ExifResponseDto;
tags?: TagResponseDto[]; tags?: TagResponseDto[];

View File

@ -6,7 +6,7 @@
import type { TimelineAsset } from '$lib/stores/assets-store.svelte'; import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { AssetVisibility, updateAssets } from '@immich/sdk'; import { AssetVisibility, updateAssets } from '@immich/sdk';
import { mdiEyeOffOutline, mdiFolderMoveOutline } from '@mdi/js'; import { mdiLockOpenVariantOutline, mdiLockOutline } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { OnAction, PreAction } from './action'; import type { OnAction, PreAction } from './action';
@ -57,5 +57,5 @@
<MenuOption <MenuOption
onClick={() => toggleLockedVisibility()} onClick={() => toggleLockedVisibility()}
text={isLocked ? $t('move_off_locked_folder') : $t('add_to_locked_folder')} text={isLocked ? $t('move_off_locked_folder') : $t('add_to_locked_folder')}
icon={isLocked ? mdiFolderMoveOutline : mdiEyeOffOutline} icon={isLocked ? mdiLockOpenVariantOutline : mdiLockOutline}
/> />

View File

@ -29,7 +29,7 @@
import { import {
AssetJobName, AssetJobName,
AssetTypeEnum, AssetTypeEnum,
Visibility, AssetVisibility,
type AlbumResponseDto, type AlbumResponseDto,
type AssetResponseDto, type AssetResponseDto,
type PersonResponseDto, type PersonResponseDto,
@ -94,7 +94,7 @@
const sharedLink = getSharedLink(); const sharedLink = getSharedLink();
let isOwner = $derived($user && asset.ownerId === $user?.id); let isOwner = $derived($user && asset.ownerId === $user?.id);
let showDownloadButton = $derived(sharedLink ? sharedLink.allowDownload : !asset.isOffline); let showDownloadButton = $derived(sharedLink ? sharedLink.allowDownload : !asset.isOffline);
let isLocked = $derived(asset.visibility === Visibility.Locked); let isLocked = $derived(asset.visibility === AssetVisibility.Locked);
// $: showEditorButton = // $: showEditorButton =
// isOwner && // isOwner &&

View File

@ -7,7 +7,7 @@
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { AssetVisibility, updateAssets } from '@immich/sdk'; import { AssetVisibility, updateAssets } from '@immich/sdk';
import { Button } from '@immich/ui'; import { Button } from '@immich/ui';
import { mdiEyeOffOutline, mdiFolderMoveOutline } from '@mdi/js'; import { mdiLockOpenVariantOutline, mdiLockOutline } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
interface Props { interface Props {
@ -56,11 +56,11 @@
<MenuOption <MenuOption
onClick={setLockedVisibility} onClick={setLockedVisibility}
text={unlock ? $t('move_off_locked_folder') : $t('add_to_locked_folder')} text={unlock ? $t('move_off_locked_folder') : $t('add_to_locked_folder')}
icon={unlock ? mdiFolderMoveOutline : mdiEyeOffOutline} icon={unlock ? mdiLockOpenVariantOutline : mdiLockOutline}
/> />
{:else} {:else}
<Button <Button
leadingIcon={unlock ? mdiFolderMoveOutline : mdiEyeOffOutline} leadingIcon={unlock ? mdiLockOpenVariantOutline : mdiLockOutline}
disabled={loading} disabled={loading}
size="medium" size="medium"
color="secondary" color="secondary"

View File

@ -2,7 +2,7 @@ import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { getAssetRatio } from '$lib/utils/asset-utils'; import { getAssetRatio } from '$lib/utils/asset-utils';
import { AssetTypeEnum, AssetVisibility, type AssetResponseDto } from '@immich/sdk'; import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
import { memoize } from 'lodash-es'; import { memoize } from 'lodash-es';
import { DateTime, type LocaleOptions } from 'luxon'; import { DateTime, type LocaleOptions } from 'luxon';
@ -72,6 +72,7 @@ export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset):
const city = assetResponse.exifInfo?.city; const city = assetResponse.exifInfo?.city;
const country = assetResponse.exifInfo?.country; const country = assetResponse.exifInfo?.country;
const people = assetResponse.people?.map((person) => person.name) || []; const people = assetResponse.people?.map((person) => person.name) || [];
return { return {
id: assetResponse.id, id: assetResponse.id,
ownerId: assetResponse.ownerId, ownerId: assetResponse.ownerId,
@ -79,7 +80,7 @@ export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset):
thumbhash: assetResponse.thumbhash, thumbhash: assetResponse.thumbhash,
localDateTime: assetResponse.localDateTime, localDateTime: assetResponse.localDateTime,
isFavorite: assetResponse.isFavorite, isFavorite: assetResponse.isFavorite,
visibility: assetResponse.isArchived ? AssetVisibility.Archive : AssetVisibility.Timeline, visibility: assetResponse.visibility,
isTrashed: assetResponse.isTrashed, isTrashed: assetResponse.isTrashed,
isVideo: assetResponse.type == AssetTypeEnum.Video, isVideo: assetResponse.type == AssetTypeEnum.Video,
isImage: assetResponse.type == AssetTypeEnum.Image, isImage: assetResponse.type == AssetTypeEnum.Image,

View File

@ -65,7 +65,7 @@
onFilled={handleUnlockSession} onFilled={handleUnlockSession}
/> />
<Button type="button" color="secondary" onclick={() => goto(AppRoute.PHOTOS)}>Back</Button> <Button type="button" color="secondary" onclick={() => goto(AppRoute.PHOTOS)}>{$t('cancel')}</Button>
</div> </div>
</div> </div>
{:else} {:else}

View File

@ -1,12 +1,6 @@
import type { TimelineAsset } from '$lib/stores/assets-store.svelte'; import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import { import { AssetTypeEnum, AssetVisibility, type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
AssetTypeEnum,
AssetVisibility,
Visibility,
type AssetResponseDto,
type TimeBucketAssetResponseDto,
} from '@immich/sdk';
import { Sync } from 'factory.ts'; import { Sync } from 'factory.ts';
export const assetFactory = Sync.makeFactory<AssetResponseDto>({ export const assetFactory = Sync.makeFactory<AssetResponseDto>({
@ -31,7 +25,7 @@ export const assetFactory = Sync.makeFactory<AssetResponseDto>({
checksum: Sync.each(() => faker.string.alphanumeric(28)), checksum: Sync.each(() => faker.string.alphanumeric(28)),
isOffline: Sync.each(() => faker.datatype.boolean()), isOffline: Sync.each(() => faker.datatype.boolean()),
hasMetadata: Sync.each(() => faker.datatype.boolean()), hasMetadata: Sync.each(() => faker.datatype.boolean()),
visibility: Visibility.Timeline, visibility: AssetVisibility.Timeline,
}); });
export const timelineAssetFactory = Sync.makeFactory<TimelineAsset>({ export const timelineAssetFactory = Sync.makeFactory<TimelineAsset>({