1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-01 08:31:59 +00:00

refactor(web): descriptions (#6517)

* refactor: reusable autogrow

* fix: remove useless autogrow

* fix: correct size for album description

* fix: format

* fix: move to own file

* refactor: album description

* refactor: asset description

* simplify

* fix: style when no description provided

* fix: switching assets

* feat: update description with ctrl + enter

* fix: variable name

* fix: styling

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
martin 2024-01-22 05:47:55 +01:00 committed by GitHub
parent 95cfe22866
commit 3845fec280
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 97 additions and 124 deletions

View file

@ -1,49 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { AlbumResponseDto } from '@api';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import Button from '../elements/buttons/button.svelte';
const dispatch = createEventDispatcher<{
close: void;
save: string;
}>();
export let album: AlbumResponseDto;
let description = album.description;
const handleCancel = () => dispatch('close');
const handleSubmit = () => dispatch('save', description);
</script>
<FullScreenModal on:clickOutside={handleCancel} on:escape={handleCancel}>
<div
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
>
<div
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Edit description</h1>
</div>
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">Description</label>
<!-- svelte-ignore a11y-autofocus -->
<textarea
class="immich-form-input focus:outline-none"
id="name"
name="name"
rows="5"
bind:value={description}
autofocus
/>
</div>
<div class="mt-8 flex w-full gap-4 px-4">
<Button color="gray" fullwidth on:click={handleCancel}>Cancel</Button>
<Button type="submit" fullwidth>Ok</Button>
</div>
</form>
</div>
</FullScreenModal>

View file

@ -19,6 +19,7 @@
import { NotificationType, notificationController } from '../shared-components/notification/notification'; import { NotificationType, notificationController } from '../shared-components/notification/notification';
import { getAssetType } from '$lib/utils/asset-utils'; import { getAssetType } from '$lib/utils/asset-utils';
import * as luxon from 'luxon'; import * as luxon from 'luxon';
import { autoGrowHeight } from '$lib/utils/autogrow';
const units: Intl.RelativeTimeFormatUnit[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second']; const units: Intl.RelativeTimeFormatUnit[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'];
@ -98,11 +99,6 @@
} }
}; };
const autoGrow = () => {
textArea.style.height = '5px';
textArea.style.height = textArea.scrollHeight + 'px';
};
const timeOptions = { const timeOptions = {
year: 'numeric', year: 'numeric',
month: '2-digit', month: '2-digit',
@ -293,7 +289,7 @@
bind:this={textArea} bind:this={textArea}
bind:value={message} bind:value={message}
placeholder={disabled ? 'Comments are disabled' : 'Say something'} placeholder={disabled ? 'Comments are disabled' : 'Say something'}
on:input={autoGrow} on:input={() => autoGrowHeight(textArea)}
on:keypress={handleEnter} on:keypress={handleEnter}
class="h-[18px] {disabled class="h-[18px] {disabled
? 'cursor-not-allowed' ? 'cursor-not-allowed'

View file

@ -31,14 +31,17 @@
import ChangeLocation from '../shared-components/change-location.svelte'; import ChangeLocation from '../shared-components/change-location.svelte';
import { handleError } from '../../utils/handle-error'; import { handleError } from '../../utils/handle-error';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { autoGrowHeight } from '$lib/utils/autogrow';
import { clickOutside } from '$lib/utils/click-outside';
export let asset: AssetResponseDto; export let asset: AssetResponseDto;
export let albums: AlbumResponseDto[] = []; export let albums: AlbumResponseDto[] = [];
export let albumId: string | null = null; export let albumId: string | null = null;
let showAssetPath = false; let showAssetPath = false;
let textarea: HTMLTextAreaElement; let textArea: HTMLTextAreaElement;
let description: string; let description: string;
let originalDescription: string;
let showEditFaces = false; let showEditFaces = false;
let previousId: string; let previousId: string;
@ -61,10 +64,10 @@
if (newAsset.id && !api.isSharedLink) { if (newAsset.id && !api.isSharedLink) {
const { data } = await api.assetApi.getAssetById({ id: asset.id }); const { data } = await api.assetApi.getAssetById({ id: asset.id });
people = data?.people || []; people = data?.people || [];
description = data.exifInfo?.description || ''; description = data.exifInfo?.description || '';
textarea.value = description;
autoGrowHeight();
} }
originalDescription = description;
}; };
$: handleNewAsset(asset); $: handleNewAsset(asset);
@ -99,6 +102,19 @@
closeViewer: void; closeViewer: void;
}>(); }>();
const handleKeypress = async (event: KeyboardEvent) => {
if (event.target !== textArea) {
return;
}
const ctrl = event.ctrlKey;
switch (event.key) {
case 'Enter':
if (ctrl && event.target === textArea) {
handleFocusOut();
}
}
};
const getMegapixel = (width: number, height: number): number | undefined => { const getMegapixel = (width: number, height: number): number | undefined => {
const megapixel = Math.round((height * width) / 1_000_000); const megapixel = Math.round((height * width) / 1_000_000);
@ -112,21 +128,21 @@
const handleRefreshPeople = async () => { const handleRefreshPeople = async () => {
await api.assetApi.getAssetById({ id: asset.id }).then((res) => { await api.assetApi.getAssetById({ id: asset.id }).then((res) => {
people = res.data?.people || []; people = res.data?.people || [];
textarea.value = res.data?.exifInfo?.description || ''; textArea.value = res.data?.exifInfo?.description || '';
}); });
showEditFaces = false; showEditFaces = false;
}; };
const autoGrowHeight = () => {
textarea.style.height = 'auto';
textarea.style.height = `${textarea.scrollHeight}px`;
};
const handleFocusIn = () => { const handleFocusIn = () => {
dispatch('descriptionFocusIn'); dispatch('descriptionFocusIn');
}; };
const handleFocusOut = async () => { const handleFocusOut = async () => {
textArea.blur();
if (description === originalDescription) {
return;
}
originalDescription = description;
dispatch('descriptionFocusOut'); dispatch('descriptionFocusOut');
try { try {
await api.assetApi.updateAsset({ await api.assetApi.updateAsset({
@ -134,7 +150,7 @@
updateAssetDto: { description }, updateAssetDto: { description },
}); });
} catch (error) { } catch (error) {
console.error(error); handleError(error, 'Cannot update the description');
} }
}; };
@ -170,6 +186,8 @@
} }
</script> </script>
<svelte:window on:keydown={handleKeypress} />
<section class="relative p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg"> <section class="relative p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
<div class="flex place-items-center gap-2"> <div class="flex place-items-center gap-2">
<button <button
@ -196,22 +214,26 @@
</section> </section>
{/if} {/if}
<section class="mx-4 mt-10" style:display={!isOwner && description === '' ? 'none' : 'block'}> {#if isOwner || description !== ''}
{#if !isOwner || api.isSharedLink} <section class="px-4 mt-10">
<span class="break-words">{description}</span> {#key asset.id}
{:else} <textarea
<textarea disabled={!isOwner || api.isSharedLink}
bind:this={textarea} bind:this={textArea}
class="max-h-[500px] class="max-h-[500px]
w-full resize-none overflow-hidden border-b border-gray-500 bg-transparent text-base text-black outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:text-white dark:focus:border-immich-dark-primary" w-full resize-none overflow-hidden border-b border-gray-500 bg-transparent text-base text-black outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:text-white dark:focus:border-immich-dark-primary"
placeholder={!isOwner ? '' : 'Add a description'} placeholder={!isOwner ? '' : 'Add a description'}
on:focusin={handleFocusIn} on:focusin={handleFocusIn}
on:focusout={handleFocusOut} on:focusout={handleFocusOut}
on:input={autoGrowHeight} on:input={() => autoGrowHeight(textArea)}
bind:value={description} bind:value={description}
/> use:autoGrowHeight
{/if} use:clickOutside
</section> on:outclick={handleFocusOut}
/>
{/key}
</section>
{/if}
{#if !api.isSharedLink && people.length > 0} {#if !api.isSharedLink && people.length > 0}
<section class="px-4 py-4 text-sm"> <section class="px-4 py-4 text-sm">
@ -315,7 +337,9 @@
</div> </div>
</div> </div>
{:else} {:else}
<p class="text-sm">DETAILS</p> <div class="flex h-10 w-full items-center justify-between text-sm">
<h2>DETAILS</h2>
</div>
{/if} {/if}
{#if asset.exifInfo?.dateTimeOriginal && !asset.isReadOnly} {#if asset.exifInfo?.dateTimeOriginal && !asset.isReadOnly}

View file

@ -0,0 +1,5 @@
export const autoGrowHeight = (textarea: HTMLTextAreaElement, height = 'auto') => {
textarea.scrollHeight;
textarea.style.height = height;
textarea.style.height = `${textarea.scrollHeight}px`;
};

View file

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { afterNavigate, goto } from '$app/navigation'; import { afterNavigate, goto } from '$app/navigation';
import EditDescriptionModal from '$lib/components/album-page/edit-description-modal.svelte';
import ShareInfoModal from '$lib/components/album-page/share-info-modal.svelte'; import ShareInfoModal from '$lib/components/album-page/share-info-modal.svelte';
import UserSelectionModal from '$lib/components/album-page/user-selection-modal.svelte'; import UserSelectionModal from '$lib/components/album-page/user-selection-modal.svelte';
import Button from '$lib/components/elements/buttons/button.svelte'; import Button from '$lib/components/elements/buttons/button.svelte';
@ -60,6 +59,7 @@
import AlbumOptions from '$lib/components/album-page/album-options.svelte'; import AlbumOptions from '$lib/components/album-page/album-options.svelte';
import UpdatePanel from '$lib/components/shared-components/update-panel.svelte'; import UpdatePanel from '$lib/components/shared-components/update-panel.svelte';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { autoGrowHeight } from '$lib/utils/autogrow';
export let data: PageData; export let data: PageData;
@ -67,6 +67,7 @@
let { slideshowState, slideshowShuffle } = slideshowStore; let { slideshowState, slideshowShuffle } = slideshowStore;
let album = data.album; let album = data.album;
let description = album.description;
$: album = data.album; $: album = data.album;
@ -91,7 +92,6 @@
let backUrl: string = AppRoute.ALBUMS; let backUrl: string = AppRoute.ALBUMS;
let viewMode = ViewMode.VIEW; let viewMode = ViewMode.VIEW;
let titleInput: HTMLInputElement; let titleInput: HTMLInputElement;
let isEditingDescription = false;
let isCreatingSharedAlbum = false; let isCreatingSharedAlbum = false;
let currentAlbumName = album.albumName; let currentAlbumName = album.albumName;
let contextMenuPosition: { x: number; y: number } = { x: 0, y: 0 }; let contextMenuPosition: { x: number; y: number } = { x: 0, y: 0 };
@ -100,7 +100,7 @@
let reactions: ActivityResponseDto[] = []; let reactions: ActivityResponseDto[] = [];
let globalWidth: number; let globalWidth: number;
let assetGridWidth: number; let assetGridWidth: number;
let textarea: HTMLTextAreaElement; let textArea: HTMLTextAreaElement;
const assetStore = new AssetStore({ albumId: album.id }); const assetStore = new AssetStore({ albumId: album.id });
const assetInteractionStore = createAssetInteractionStore(); const assetInteractionStore = createAssetInteractionStore();
@ -123,12 +123,6 @@
$: showActivityStatus = $: showActivityStatus =
album.sharedUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0); album.sharedUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0);
$: {
if (textarea) {
textarea.value = album.description;
autoGrowHeight();
}
}
$: afterNavigate(({ from }) => { $: afterNavigate(({ from }) => {
assetViewingStore.showAssetViewer(false); assetViewingStore.showAssetViewer(false);
@ -149,13 +143,6 @@
} }
}); });
const autoGrowHeight = () => {
// little hack so that the height of the text area is correctly initialized
textarea.scrollHeight;
textarea.style.height = '5px';
textarea.style.height = `${textarea.scrollHeight}px`;
};
const handleToggleEnableActivity = async () => { const handleToggleEnableActivity = async () => {
try { try {
const { data } = await api.albumApi.updateAlbumInfo({ const { data } = await api.albumApi.updateAlbumInfo({
@ -231,6 +218,19 @@
} }
}); });
const handleKeypress = async (event: KeyboardEvent) => {
if (event.target !== textArea) {
return;
}
const ctrl = event.ctrlKey;
switch (event.key) {
case 'Enter':
if (ctrl && event.target === textArea) {
textArea.blur();
}
}
};
const handleStartSlideshow = async () => { const handleStartSlideshow = async () => {
const asset = $slideshowShuffle ? await assetStore.getRandomAsset() : assetStore.assets[0]; const asset = $slideshowShuffle ? await assetStore.getRandomAsset() : assetStore.assets[0];
if (asset) { if (asset) {
@ -252,6 +252,14 @@
handleCloseSelectAssets(); handleCloseSelectAssets();
return; return;
} }
if (viewMode === ViewMode.LINK_SHARING) {
viewMode = ViewMode.VIEW;
return;
}
if (viewMode === ViewMode.OPTIONS) {
viewMode = ViewMode.VIEW;
return;
}
if ($showAssetViewer) { if ($showAssetViewer) {
return; return;
} }
@ -426,7 +434,10 @@
} }
}; };
const handleUpdateDescription = async (description: string) => { const handleUpdateDescription = async () => {
if (album.description === description) {
return;
}
try { try {
await api.albumApi.updateAlbumInfo({ await api.albumApi.updateAlbumInfo({
id: album.id, id: album.id,
@ -436,13 +447,14 @@
}); });
album.description = description; album.description = description;
isEditingDescription = false;
} catch (error) { } catch (error) {
handleError(error, 'Error updating album description'); handleError(error, 'Error updating album description');
} }
}; };
</script> </script>
<svelte:window on:keydown={handleKeypress} />
<div class="flex overflow-hidden" bind:clientWidth={globalWidth}> <div class="flex overflow-hidden" bind:clientWidth={globalWidth}>
<div class="relative w-full shrink"> <div class="relative w-full shrink">
{#if $isMultiSelectState} {#if $isMultiSelectState}
@ -640,24 +652,17 @@
{/if} {/if}
</div> </div>
{/if} {/if}
<!-- ALBUM DESCRIPTION --> <!-- ALBUM DESCRIPTION -->
{#if isOwned || album.description} <textarea
<button class="w-full resize-none overflow-hidden text-black dark:text-white border-b-2 border-transparent border-gray-500 bg-transparent text-base outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:focus:border-immich-dark-primary hover:border-gray-400"
class="mb-12 mt-6 w-full border-b-2 border-transparent pb-2 text-left text-lg font-medium transition-colors hover:border-b-2 dark:text-gray-300" bind:this={textArea}
on:click={() => (isEditingDescription = true)} bind:value={description}
class:hover:border-gray-400={isOwned} disabled={!isOwned}
disabled={!isOwned} on:input={() => autoGrowHeight(textArea)}
title="Edit description" on:focusout={handleUpdateDescription}
> use:autoGrowHeight
<textarea placeholder="Add description"
class="w-full bg-transparent resize-none overflow-hidden outline-none" />
bind:this={textarea}
bind:value={album.description}
placeholder="Add description"
/>
</button>
{/if}
</section> </section>
{/if} {/if}
@ -763,14 +768,6 @@
/> />
{/if} {/if}
{#if isEditingDescription}
<EditDescriptionModal
{album}
on:close={() => (isEditingDescription = false)}
on:save={({ detail: description }) => handleUpdateDescription(description)}
/>
{/if}
<UpdatePanel {assetStore} /> <UpdatePanel {assetStore} />
<style> <style>