refactor: album page (#25140)

This commit is contained in:
Daniel Dietzler
2026-01-08 16:27:20 -06:00
committed by GitHub
parent 191401f2f1
commit 520b825511
7 changed files with 142 additions and 159 deletions

View File

@@ -463,7 +463,7 @@ test.describe('Timeline', () => {
}); });
changes.albumAdditions.push(...requestJson.ids); changes.albumAdditions.push(...requestJson.ids);
}); });
await page.getByText('Done').click(); await page.getByText('Add assets').click();
await expect(put).resolves.toEqual({ await expect(put).resolves.toEqual({
ids: [ ids: [
'c077ea7b-cfa1-45e4-8554-f86c00ee5658', 'c077ea7b-cfa1-45e4-8554-f86c00ee5658',

View File

@@ -18,6 +18,7 @@
"add_a_title": "Add a title", "add_a_title": "Add a title",
"add_action": "Add action", "add_action": "Add action",
"add_action_description": "Click to add an action to perform", "add_action_description": "Click to add an action to perform",
"add_assets": "Add assets",
"add_birthday": "Add a birthday", "add_birthday": "Add a birthday",
"add_endpoint": "Add endpoint", "add_endpoint": "Add endpoint",
"add_exclusion_pattern": "Add exclusion pattern", "add_exclusion_pattern": "Add exclusion pattern",
@@ -478,6 +479,7 @@
"album_summary": "Album summary", "album_summary": "Album summary",
"album_updated": "Album updated", "album_updated": "Album updated",
"album_updated_setting_description": "Receive an email notification when a shared album has new assets", "album_updated_setting_description": "Receive an email notification when a shared album has new assets",
"album_upload_assets": "Upload assets from your computer and add to album",
"album_user_left": "Left {album}", "album_user_left": "Left {album}",
"album_user_removed": "Removed {user}", "album_user_removed": "Removed {user}",
"album_viewer_appbar_delete_confirm": "Are you sure you want to delete this album from your account?", "album_viewer_appbar_delete_confirm": "Are you sure you want to delete this album from your account?",

View File

@@ -6,7 +6,6 @@
import RightClickContextMenu from '$lib/components/shared-components/context-menu/right-click-context-menu.svelte'; import RightClickContextMenu from '$lib/components/shared-components/context-menu/right-click-context-menu.svelte';
import AlbumEditModal from '$lib/modals/AlbumEditModal.svelte'; import AlbumEditModal from '$lib/modals/AlbumEditModal.svelte';
import AlbumShareModal from '$lib/modals/AlbumShareModal.svelte'; import AlbumShareModal from '$lib/modals/AlbumShareModal.svelte';
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
import { handleDeleteAlbum, handleDownloadAlbum } from '$lib/services/album.service'; import { handleDeleteAlbum, handleDownloadAlbum } from '$lib/services/album.service';
import { import {
AlbumFilter, AlbumFilter,
@@ -21,14 +20,8 @@
import { userInteraction } from '$lib/stores/user.svelte'; import { userInteraction } from '$lib/stores/user.svelte';
import { getSelectedAlbumGroupOption, sortAlbums, stringToSortOrder, type AlbumGroup } from '$lib/utils/album-utils'; import { getSelectedAlbumGroupOption, sortAlbums, stringToSortOrder, type AlbumGroup } from '$lib/utils/album-utils';
import type { ContextMenuPosition } from '$lib/utils/context-menu'; import type { ContextMenuPosition } from '$lib/utils/context-menu';
import { handleError } from '$lib/utils/handle-error';
import { normalizeSearchString } from '$lib/utils/string-utils'; import { normalizeSearchString } from '$lib/utils/string-utils';
import { import { type AlbumResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
addUsersToAlbum,
type AlbumResponseDto,
type AlbumUserAddDto,
type SharedLinkResponseDto,
} from '@immich/sdk';
import { modalManager } from '@immich/ui'; import { modalManager } from '@immich/ui';
import { mdiDeleteOutline, mdiDownload, mdiRenameOutline, mdiShareVariantOutline } from '@mdi/js'; import { mdiDeleteOutline, mdiDownload, mdiRenameOutline, mdiShareVariantOutline } from '@mdi/js';
import { groupBy } from 'lodash-es'; import { groupBy } from 'lodash-es';
@@ -205,18 +198,7 @@
} }
case 'share': { case 'share': {
const result = await modalManager.show(AlbumShareModal, { album: selectedAlbum }); await modalManager.show(AlbumShareModal, { album: selectedAlbum });
switch (result?.action) {
case 'sharedUsers': {
await handleAddUsers(selectedAlbum, result.data);
break;
}
case 'sharedLink': {
await modalManager.show(SharedLinkCreateModal, { albumId: selectedAlbum.id });
break;
}
}
break; break;
} }
@@ -251,20 +233,6 @@
sharedAlbums = findAndUpdate(sharedAlbums, album); sharedAlbums = findAndUpdate(sharedAlbums, album);
}; };
const handleAddUsers = async (album: AlbumResponseDto, albumUsers: AlbumUserAddDto[]) => {
try {
const updatedAlbum = await addUsersToAlbum({
id: album.id,
addUsersDto: {
albumUsers,
},
});
onUpdate(updatedAlbum);
} catch (error) {
handleError(error, $t('errors.unable_to_add_album_users'));
}
};
const onAlbumUpdate = (album: AlbumResponseDto) => { const onAlbumUpdate = (album: AlbumResponseDto) => {
onUpdate(album); onUpdate(album);
userInteraction.recentAlbums = findAndUpdate(userInteraction.recentAlbums || [], album); userInteraction.recentAlbums = findAndUpdate(userInteraction.recentAlbums || [], album);

View File

@@ -33,8 +33,10 @@ export type Events = {
AssetUpdate: [AssetResponseDto]; AssetUpdate: [AssetResponseDto];
AssetReplace: [{ oldAssetId: string; newAssetId: string }]; AssetReplace: [{ oldAssetId: string; newAssetId: string }];
AlbumAddAssets: [];
AlbumUpdate: [AlbumResponseDto]; AlbumUpdate: [AlbumResponseDto];
AlbumDelete: [AlbumResponseDto]; AlbumDelete: [AlbumResponseDto];
AlbumShare: [];
PersonUpdate: [PersonResponseDto]; PersonUpdate: [PersonResponseDto];

View File

@@ -2,16 +2,17 @@
import AlbumSharedLink from '$lib/components/album-page/album-shared-link.svelte'; import AlbumSharedLink from '$lib/components/album-page/album-shared-link.svelte';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import Dropdown from '$lib/elements/Dropdown.svelte'; import Dropdown from '$lib/elements/Dropdown.svelte';
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
import { handleAddUsersToAlbum } from '$lib/services/album.service';
import { import {
AlbumUserRole, AlbumUserRole,
getAllSharedLinks, getAllSharedLinks,
searchUsers, searchUsers,
type AlbumResponseDto, type AlbumResponseDto,
type AlbumUserAddDto,
type SharedLinkResponseDto, type SharedLinkResponseDto,
type UserResponseDto, type UserResponseDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { Button, Icon, Link, Modal, ModalBody, Stack, Text } from '@immich/ui'; import { Button, Icon, Link, Modal, ModalBody, modalManager, Stack, Text } from '@immich/ui';
import { mdiCheck, mdiEye, mdiLink, mdiPencil } from '@mdi/js'; import { mdiCheck, mdiEye, mdiLink, mdiPencil } from '@mdi/js';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@@ -19,7 +20,7 @@
interface Props { interface Props {
album: AlbumResponseDto; album: AlbumResponseDto;
onClose: (result?: { action: 'sharedLink' } | { action: 'sharedUsers'; data: AlbumUserAddDto[] }) => void; onClose: () => void;
} }
let { album, onClose }: Props = $props(); let { album, onClose }: Props = $props();
@@ -62,6 +63,21 @@
selectedUsers[user.id].role = role; selectedUsers[user.id].role = role;
} }
}; };
const onShareUser = async () => {
const success = await handleAddUsersToAlbum(
album,
Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })),
);
if (success) {
onClose();
}
};
const onShareLink = () => {
void modalManager.show(SharedLinkCreateModal, { albumId: album.id });
onClose();
};
</script> </script>
<Modal size="small" title={$t('share')} {onClose}> <Modal size="small" title={$t('share')} {onClose}>
@@ -145,12 +161,10 @@
fullWidth fullWidth
shape="round" shape="round"
disabled={Object.keys(selectedUsers).length === 0} disabled={Object.keys(selectedUsers).length === 0}
onclick={() => onclick={onShareUser}
onClose({
action: 'sharedUsers',
data: Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })),
})}>{$t('add')}</Button
> >
{$t('add')}
</Button>
</div> </div>
{/if} {/if}
@@ -170,13 +184,9 @@
</Stack> </Stack>
{/if} {/if}
<Button <Button leadingIcon={mdiLink} size="small" shape="round" fullWidth onclick={onShareLink}>
leadingIcon={mdiLink} {$t('create_link')}
size="small" </Button>
shape="round"
fullWidth
onclick={() => onClose({ action: 'sharedLink' })}>{$t('create_link')}</Button
>
</Stack> </Stack>
</ModalBody> </ModalBody>
</Modal> </Modal>

View File

@@ -2,11 +2,89 @@ import { goto } from '$app/navigation';
import ToastAction from '$lib/components/ToastAction.svelte'; import ToastAction from '$lib/components/ToastAction.svelte';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { eventManager } from '$lib/managers/event-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import AlbumShareModal from '$lib/modals/AlbumShareModal.svelte';
import { user } from '$lib/stores/user.store';
import { downloadArchive } from '$lib/utils/asset-utils'; import { downloadArchive } from '$lib/utils/asset-utils';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n'; import { getFormatter } from '$lib/utils/i18n';
import { deleteAlbum, updateAlbumInfo, type AlbumResponseDto, type UpdateAlbumDto } from '@immich/sdk'; import {
import { modalManager, toastManager } from '@immich/ui'; addAssetsToAlbum,
addUsersToAlbum,
deleteAlbum,
updateAlbumInfo,
type AlbumResponseDto,
type AlbumUserAddDto,
type UpdateAlbumDto,
} from '@immich/sdk';
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
import { mdiPlusBoxOutline, mdiShareVariantOutline, mdiUpload } from '@mdi/js';
import { type MessageFormatter } from 'svelte-i18n';
import { get } from 'svelte/store';
export const getAlbumActions = ($t: MessageFormatter, album: AlbumResponseDto) => {
const isOwned = get(user).id === album.ownerId;
const Share: ActionItem = {
title: $t('share'),
type: $t('command'),
icon: mdiShareVariantOutline,
$if: () => isOwned,
onAction: () => modalManager.show(AlbumShareModal, { album }),
};
return { Share };
};
export const getAlbumAssetsActions = ($t: MessageFormatter, album: AlbumResponseDto, assets: TimelineAsset[]) => {
const AddAssets: ActionItem = {
title: $t('add_assets'),
type: $t('command'),
icon: mdiPlusBoxOutline,
$if: () => assets.length > 0,
onAction: () => addAssets(album, assets),
};
const Upload: ActionItem = {
title: $t('select_from_computer'),
description: $t('album_upload_assets'),
type: $t('command'),
icon: mdiUpload,
onAction: () => void openFileUploadDialog({ albumId: album.id }),
};
return { AddAssets, Upload };
};
const addAssets = async (album: AlbumResponseDto, assets: TimelineAsset[]) => {
const $t = await getFormatter();
const assetIds = assets.map(({ id }) => id);
try {
const results = await addAssetsToAlbum({ id: album.id, bulkIdsDto: { ids: assetIds } });
const count = results.filter(({ success }) => success).length;
toastManager.success($t('assets_added_count', { values: { count } }));
eventManager.emit('AlbumAddAssets');
} catch (error) {
handleError(error, $t('errors.error_adding_assets_to_album'));
}
};
export const handleAddUsersToAlbum = async (album: AlbumResponseDto, albumUsers: AlbumUserAddDto[]) => {
const $t = await getFormatter();
try {
await addUsersToAlbum({ id: album.id, addUsersDto: { albumUsers } });
eventManager.emit('AlbumShare');
return true;
} catch (error) {
handleError(error, $t('errors.error_adding_users_to_album'));
}
return false;
};
export const handleUpdateAlbum = async ({ id }: { id: string }, dto: UpdateAlbumDto) => { export const handleUpdateAlbum = async ({ id }: { id: string }, dto: UpdateAlbumDto) => {
const $t = await getFormatter(); const $t = await getFormatter();

View File

@@ -8,6 +8,7 @@
import AlbumTitle from '$lib/components/album-page/album-title.svelte'; import AlbumTitle from '$lib/components/album-page/album-title.svelte';
import ActivityStatus from '$lib/components/asset-viewer/activity-status.svelte'; import ActivityStatus from '$lib/components/asset-viewer/activity-status.svelte';
import ActivityViewer from '$lib/components/asset-viewer/activity-viewer.svelte'; import ActivityViewer from '$lib/components/asset-viewer/activity-viewer.svelte';
import HeaderActionButton from '$lib/components/HeaderActionButton.svelte';
import OnEvents from '$lib/components/OnEvents.svelte'; import OnEvents from '$lib/components/OnEvents.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
@@ -38,7 +39,12 @@
import AlbumShareModal from '$lib/modals/AlbumShareModal.svelte'; import AlbumShareModal from '$lib/modals/AlbumShareModal.svelte';
import AlbumUsersModal from '$lib/modals/AlbumUsersModal.svelte'; import AlbumUsersModal from '$lib/modals/AlbumUsersModal.svelte';
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte'; import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
import { handleDeleteAlbum, handleDownloadAlbum } from '$lib/services/album.service'; import {
getAlbumActions,
getAlbumAssetsActions,
handleDeleteAlbum,
handleDownloadAlbum,
} from '$lib/services/album.service';
import { getGlobalActions } from '$lib/services/app.service'; import { getGlobalActions } from '$lib/services/app.service';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
@@ -46,7 +52,6 @@
import { preferences, user } from '$lib/stores/user.store'; import { preferences, user } from '$lib/stores/user.store';
import { handlePromiseError } from '$lib/utils'; import { handlePromiseError } from '$lib/utils';
import { cancelMultiselect } from '$lib/utils/asset-utils'; import { cancelMultiselect } from '$lib/utils/asset-utils';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { import {
isAlbumsRoute, isAlbumsRoute,
@@ -59,14 +64,11 @@
AlbumUserRole, AlbumUserRole,
AssetOrder, AssetOrder,
AssetVisibility, AssetVisibility,
addAssetsToAlbum,
addUsersToAlbum,
getAlbumInfo, getAlbumInfo,
updateAlbumInfo, updateAlbumInfo,
type AlbumResponseDto, type AlbumResponseDto,
type AlbumUserAddDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { Button, Icon, IconButton, modalManager, toastManager } from '@immich/ui'; import { CommandPaletteDefaultProvider, Icon, IconButton, modalManager, toastManager } from '@immich/ui';
import { import {
mdiAccountEye, mdiAccountEye,
mdiAccountEyeOutline, mdiAccountEyeOutline,
@@ -80,8 +82,6 @@
mdiLink, mdiLink,
mdiPlus, mdiPlus,
mdiPresentationPlay, mdiPresentationPlay,
mdiShareVariantOutline,
mdiUpload,
} from '@mdi/js'; } from '@mdi/js';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@@ -101,7 +101,6 @@
let backUrl: string = $state(AppRoute.ALBUMS); let backUrl: string = $state(AppRoute.ALBUMS);
let viewMode: AlbumPageViewMode = $state(AlbumPageViewMode.VIEW); let viewMode: AlbumPageViewMode = $state(AlbumPageViewMode.VIEW);
let isCreatingSharedAlbum = $state(false);
let albumOrder: AssetOrder | undefined = $state(data.album.order); let albumOrder: AssetOrder | undefined = $state(data.album.order);
let timelineManager = $state<TimelineManager>() as TimelineManager; let timelineManager = $state<TimelineManager>() as TimelineManager;
@@ -124,9 +123,7 @@
backUrl = url || AppRoute.ALBUMS; backUrl = url || AppRoute.ALBUMS;
if (backUrl === AppRoute.SHARING && album.albumUsers.length === 0 && !album.hasSharedLink) { if (backUrl === AppRoute.SHARED_LINKS) {
isCreatingSharedAlbum = true;
} else if (backUrl === AppRoute.SHARED_LINKS) {
backUrl = history.state?.backUrl || AppRoute.ALBUMS; backUrl = history.state?.backUrl || AppRoute.ALBUMS;
} }
}); });
@@ -177,26 +174,6 @@
const refreshAlbum = async () => { const refreshAlbum = async () => {
album = await getAlbumInfo({ id: album.id, withoutAssets: true }); album = await getAlbumInfo({ id: album.id, withoutAssets: true });
}; };
const handleAddAssets = async () => {
const assetIds = timelineInteraction.selectedAssets.map((asset) => asset.id);
try {
const results = await addAssetsToAlbum({
id: album.id,
bulkIdsDto: { ids: assetIds },
});
const count = results.filter(({ success }) => success).length;
toastManager.success($t('assets_added_count', { values: { count } }));
await refreshAlbum();
timelineInteraction.clearMultiselect();
await setModeToView();
} catch (error) {
handleError(error, $t('errors.error_adding_assets_to_album'));
}
};
const setModeToView = async () => { const setModeToView = async () => {
timelineManager.suspendTransitions = true; timelineManager.suspendTransitions = true;
@@ -213,28 +190,6 @@
await setModeToView(); await setModeToView();
}; };
const handleSelectFromComputer = async () => {
await openFileUploadDialog({ albumId: album.id });
timelineInteraction.clearMultiselect();
await setModeToView();
};
const handleAddUsers = async (albumUsers: AlbumUserAddDto[]) => {
try {
await addUsersToAlbum({
id: album.id,
addUsersDto: {
albumUsers,
},
});
await refreshAlbum();
viewMode = AlbumPageViewMode.VIEW;
} catch (error) {
handleError(error, $t('errors.error_adding_users_to_album'));
}
};
const handleSetVisibility = (assetIds: string[]) => { const handleSetVisibility = (assetIds: string[]) => {
timelineManager.removeAssets(assetIds); timelineManager.removeAssets(assetIds);
assetInteraction.clearMultiselect(); assetInteraction.clearMultiselect();
@@ -353,22 +308,6 @@
viewMode === AlbumPageViewMode.SELECT_ASSETS ? timelineInteraction : assetInteraction, viewMode === AlbumPageViewMode.SELECT_ASSETS ? timelineInteraction : assetInteraction,
); );
const handleShare = async () => {
const result = await modalManager.show(AlbumShareModal, { album });
switch (result?.action) {
case 'sharedLink': {
await handleShareLink();
return;
}
case 'sharedUsers': {
await handleAddUsers(result.data);
return;
}
}
};
const onSharedLinkCreate = async () => { const onSharedLinkCreate = async () => {
await refreshAlbum(); await refreshAlbum();
}; };
@@ -380,10 +319,6 @@
} }
}; };
const handleShareLink = async () => {
await modalManager.show(SharedLinkCreateModal, { albumId: album.id });
};
const handleEditUsers = async () => { const handleEditUsers = async () => {
const changed = await modalManager.show(AlbumUsersModal, { album }); const changed = await modalManager.show(AlbumUsersModal, { album });
@@ -405,7 +340,7 @@
break; break;
} }
case 'shareUser': { case 'shareUser': {
await handleShare(); await modalManager.show(AlbumShareModal, { album });
break; break;
} }
case 'refreshAlbum': { case 'refreshAlbum': {
@@ -415,10 +350,24 @@
} }
}; };
const onAlbumAddAssets = async () => {
await refreshAlbum();
timelineInteraction.clearMultiselect();
await setModeToView();
};
const onAlbumShare = async () => {
await refreshAlbum();
await setModeToView();
};
const { Cast } = $derived(getGlobalActions($t)); const { Cast } = $derived(getGlobalActions($t));
const { Share } = $derived(getAlbumActions($t, album));
const { AddAssets, Upload } = $derived(getAlbumAssetsActions($t, album, timelineInteraction.selectedAssets));
</script> </script>
<OnEvents {onSharedLinkCreate} {onAlbumDelete} /> <OnEvents {onSharedLinkCreate} {onAlbumDelete} {onAlbumAddAssets} {onAlbumShare} />
<CommandPaletteDefaultProvider name={$t('album')} actions={[AddAssets, Upload]} />
<div class="flex overflow-hidden" use:scrollMemoryClearer={{ routeStartsWith: AppRoute.ALBUMS }}> <div class="flex overflow-hidden" use:scrollMemoryClearer={{ routeStartsWith: AppRoute.ALBUMS }}>
<div class="relative w-full shrink"> <div class="relative w-full shrink">
@@ -463,7 +412,7 @@
size="medium" size="medium"
shape="round" shape="round"
icon={mdiLink} icon={mdiLink}
onclick={handleShareLink} onclick={() => modalManager.show(SharedLinkCreateModal, { albumId: album.id })}
/> />
{/if} {/if}
@@ -491,16 +440,7 @@
/> />
{/if} {/if}
{#if isOwned} <ActionButton action={Share} />
<IconButton
shape="round"
color="secondary"
size="medium"
icon={mdiPlus}
onclick={handleShare}
aria-label={$t('add_more_users')}
/>
{/if}
</div> </div>
{/if} {/if}
<!-- ALBUM DESCRIPTION --> <!-- ALBUM DESCRIPTION -->
@@ -616,16 +556,7 @@
/> />
{/if} {/if}
{#if isOwned} <ActionButton action={Share} />
<IconButton
shape="round"
variant="ghost"
color="secondary"
aria-label={$t('share')}
onclick={handleShare}
icon={mdiShareVariantOutline}
/>
{/if}
{#if featureFlagsManager.value.map} {#if featureFlagsManager.value.map}
<AlbumMap {album} /> <AlbumMap {album} />
@@ -682,12 +613,6 @@
{/if} {/if}
</ButtonContextMenu> </ButtonContextMenu>
{/if} {/if}
{#if isCreatingSharedAlbum && album.albumUsers.length === 0}
<Button size="small" disabled={album.assetCount === 0} onclick={handleShare}>
{$t('share')}
</Button>
{/if}
{/snippet} {/snippet}
</ControlAppBar> </ControlAppBar>
{/if} {/if}
@@ -705,10 +630,8 @@
{/snippet} {/snippet}
{#snippet trailing()} {#snippet trailing()}
<Button variant="ghost" leadingIcon={mdiUpload} onclick={handleSelectFromComputer} <HeaderActionButton action={Upload} />
>{$t('select_from_computer')}</Button <HeaderActionButton action={AddAssets} />
>
<Button disabled={!timelineInteraction.selectionActive} onclick={handleAddAssets}>{$t('done')}</Button>
{/snippet} {/snippet}
</ControlAppBar> </ControlAppBar>
{/if} {/if}