diff --git a/docs/docs/developer/setup.md b/docs/docs/developer/setup.md index 76106803e8..5e821e9b9b 100644 --- a/docs/docs/developer/setup.md +++ b/docs/docs/developer/setup.md @@ -75,11 +75,12 @@ npm run dev To see local changes to `@immich/ui` in Immich, do the following: 1. Install `@immich/ui` as a sibling to `immich/`, for example `/home/user/immich` and `/home/user/ui` -1. Build the `@immich/ui` project via `npm run build` -1. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yaml` file (`../../ui:/usr/ui`) -1. Uncomment the corresponding alias in the `web/vite.config.js` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui')`) -1. Start up the stack via `make dev` -1. After making changes in `@immich/ui`, rebuild it (`npm run build`) +2. Build the `@immich/ui` project via `npm run build` +3. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yaml` file (`../../ui:/usr/ui`) +4. Uncomment the corresponding alias in the `web/vite.config.js` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui')`) +5. Uncomment the import statement in `web/src/app.css` file `@import '/usr/ui/dist/theme/default.css';` and comment out `@import '@immich/ui/theme/default.css';` +6. Start up the stack via `make dev` +7. After making changes in `@immich/ui`, rebuild it (`npm run build`) ### Mobile app diff --git a/web/package-lock.json b/web/package-lock.json index cc29dd6856..bde201c176 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.22.0", + "@immich/ui": "^0.22.1", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", @@ -1341,15 +1341,15 @@ "link": true }, "node_modules/@immich/ui": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.22.0.tgz", - "integrity": "sha512-bBx9hPy7/VECZPcEiBGty6Lu9jmD4vJf6VL2ud+LHLQcpZebv4FVFZzzVFf7ctBwooYJWTEfWZTPNgAo0rbQtQ==", + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.22.1.tgz", + "integrity": "sha512-/QdqctBit+eX8QZgTL4PlgS7l6/NCGXeDjR6kQNLOVBPhbjkmtwqsvZ+RymYClcHAEhutXOKRhnlQU9mNLC/SA==", "license": "GNU Affero General Public License version 3", "dependencies": { "@mdi/js": "^7.4.47", "bits-ui": "^1.0.0-next.46", "tailwind-merge": "^2.5.4", - "tailwind-variants": "^0.3.0" + "tailwind-variants": "^1.0.0" }, "peerDependencies": { "svelte": "^5.0.0" @@ -9479,12 +9479,12 @@ } }, "node_modules/tailwind-variants": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-0.3.1.tgz", - "integrity": "sha512-krn67M3FpPwElg4FsZrOQd0U26o7UDH/QOkK8RNaiCCrr052f6YJPBUfNKnPo/s/xRzNPtv1Mldlxsg8Tb46BQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-1.0.0.tgz", + "integrity": "sha512-2WSbv4ulEEyuBKomOunut65D8UZwxrHoRfYnxGcQNnHqlSCp2+B7Yz2W+yrNDrxRodOXtGD/1oCcKGNBnUqMqA==", "license": "MIT", "dependencies": { - "tailwind-merge": "2.5.4" + "tailwind-merge": "3.0.2" }, "engines": { "node": ">=16.x", @@ -9495,9 +9495,9 @@ } }, "node_modules/tailwind-variants/node_modules/tailwind-merge": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz", - "integrity": "sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.2.tgz", + "integrity": "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==", "license": "MIT", "funding": { "type": "github", diff --git a/web/package.json b/web/package.json index d352afe45b..99df56b7f0 100644 --- a/web/package.json +++ b/web/package.json @@ -28,7 +28,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.22.0", + "@immich/ui": "^0.22.1", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", diff --git a/web/src/app.css b/web/src/app.css index 6160af1b8e..96639845b5 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -1,5 +1,6 @@ @import 'tailwindcss'; @import '@immich/ui/theme/default.css'; +/* @import '/usr/ui/dist/theme/default.css'; */ @config '../tailwind.config.js'; diff --git a/web/src/lib/components/admin-page/server-stats/stats-card.svelte b/web/src/lib/components/admin-page/server-stats/stats-card.svelte index aa18c6f3ae..6250a8ef7f 100644 --- a/web/src/lib/components/admin-page/server-stats/stats-card.svelte +++ b/web/src/lib/components/admin-page/server-stats/stats-card.svelte @@ -30,7 +30,7 @@ <div class="relative mx-auto font-mono text-2xl font-semibold"> <span class="text-gray-400 dark:text-gray-600">{zeros()}</span><span>{value}</span> {#if unit} - <Code color="muted" class="absolute -top-5 end-2 font-light">{unit}</Code> + <Code color="muted" class="absolute -top-5 end-1 font-light">{unit}</Code> {/if} </div> </div> diff --git a/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte b/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte index a294eb1768..8e85664750 100644 --- a/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte +++ b/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte @@ -1,13 +1,12 @@ <script lang="ts"> import Icon from '$lib/components/elements/icon.svelte'; import FormatMessage from '$lib/components/i18n/format-message.svelte'; - import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte'; import SettingTextarea from '$lib/components/shared-components/settings/setting-textarea.svelte'; import { handleError } from '$lib/utils/handle-error'; import { type SystemConfigDto, type SystemConfigTemplateEmailsDto, getNotificationTemplateAdmin } from '@immich/sdk'; - import { Button } from '@immich/ui'; + import { Button, Modal, ModalBody } from '@immich/ui'; import { mdiEyeOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; import { fade } from 'svelte/transition'; @@ -112,15 +111,17 @@ </div> {#if htmlPreview} - <FullScreenModal title={$t('admin.template_email_preview')} onClose={closePreviewModal} width="wide"> - <div style="position:relative; width:100%; height:90vh; overflow: hidden"> - <iframe - title={$t('admin.template_email_preview')} - srcdoc={htmlPreview} - style="width: 100%; height: 100%; border: none; overflow:hidden; position: absolute; top: 0; left: 0;" - ></iframe> - </div> - </FullScreenModal> + <Modal title={$t('admin.template_email_preview')} onClose={closePreviewModal} size="medium"> + <ModalBody> + <div style="position:relative; width:100%; height:90vh; overflow: hidden"> + <iframe + title={$t('admin.template_email_preview')} + srcdoc={htmlPreview} + style="width: 100%; height: 100%; border: none; overflow:hidden; position: absolute; top: 0; left: 0;" + ></iframe> + </div> + </ModalBody> + </Modal> {/if} </form> </div> diff --git a/web/src/lib/components/album-page/album-map.svelte b/web/src/lib/components/album-page/album-map.svelte index 497422daa2..fd96cf8b64 100644 --- a/web/src/lib/components/album-page/album-map.svelte +++ b/web/src/lib/components/album-page/album-map.svelte @@ -1,7 +1,6 @@ <script lang="ts"> import { clickOutside } from '$lib/actions/click-outside'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; - import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import type Map from '$lib/components/shared-components/map/map.svelte'; import Portal from '$lib/components/shared-components/portal/portal.svelte'; import { timeToLoadTheMap } from '$lib/constants'; @@ -11,7 +10,7 @@ import { delay } from '$lib/utils/asset-utils'; import { navigate } from '$lib/utils/navigation'; import { getAlbumInfo, type AlbumResponseDto, type MapMarkerResponseDto } from '@immich/sdk'; - import { LoadingSpinner } from '@immich/ui'; + import { LoadingSpinner, Modal, ModalBody } from '@immich/ui'; import { mdiMapOutline } from '@mdi/js'; import { onDestroy, onMount } from 'svelte'; import { t } from 'svelte-i18n'; @@ -112,31 +111,33 @@ {#if albumMapViewManager.isInMapView} <div use:clickOutside={{ onOutclick: closeMap }}> - <FullScreenModal title={$t('map')} width="wide" onClose={closeMap}> - <div class="flex flex-col w-full h-full gap-2"> - <div class="h-[500px] min-h-[300px] w-full"> - {#await import('../shared-components/map/map.svelte')} - {#await delay(timeToLoadTheMap) then} - <!-- show the loading spinner only if loading the map takes too much time --> - <div class="flex items-center justify-center h-full w-full"> - <LoadingSpinner /> - </div> + <Modal title={$t('map')} size="medium" onClose={closeMap}> + <ModalBody> + <div class="flex flex-col w-full h-full gap-2 border border-gray-300 dark:border-light rounded-2xl"> + <div class="h-[500px] min-h-[300px] w-full"> + {#await import('../shared-components/map/map.svelte')} + {#await delay(timeToLoadTheMap) then} + <!-- show the loading spinner only if loading the map takes too much time --> + <div class="flex items-center justify-center h-full w-full"> + <LoadingSpinner /> + </div> + {/await} + {:then { default: Map }} + <Map + bind:this={mapElement} + center={undefined} + {zoom} + clickable={false} + bind:mapMarkers + onSelect={onViewAssets} + showSettings={false} + rounded + /> {/await} - {:then { default: Map }} - <Map - bind:this={mapElement} - center={undefined} - {zoom} - clickable={false} - bind:mapMarkers - onSelect={onViewAssets} - showSettings={false} - rounded - /> - {/await} + </div> </div> - </div> - </FullScreenModal> + </ModalBody> + </Modal> </div> <Portal target="body"> diff --git a/web/src/lib/components/album-page/album-options.svelte b/web/src/lib/components/album-page/album-options.svelte index d63de9bdee..9fbcf4e2ad 100644 --- a/web/src/lib/components/album-page/album-options.svelte +++ b/web/src/lib/components/album-page/album-options.svelte @@ -2,7 +2,6 @@ import Icon from '$lib/components/elements/icon.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 FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; import ConfirmModal from '$lib/modals/ConfirmModal.svelte'; @@ -16,6 +15,7 @@ type AlbumResponseDto, type UserResponseDto, } from '@immich/sdk'; + import { Modal, ModalBody } from '@immich/ui'; import { mdiArrowDownThin, mdiArrowUpThin, mdiDotsVertical, mdiPlus } from '@mdi/js'; import { findKey } from 'lodash-es'; import { t } from 'svelte-i18n'; @@ -115,79 +115,81 @@ </script> {#if !selectedRemoveUser} - <FullScreenModal title={$t('options')} {onClose}> - <div class="items-center justify-center"> - <div class="py-2"> - <h2 class="text-gray text-sm mb-2">{$t('settings').toUpperCase()}</h2> - <div class="grid p-2 gap-y-2"> - {#if order} - <SettingDropdown - title={$t('display_order')} - options={Object.values(options)} - selectedOption={options[order]} - onToggle={handleToggle} + <Modal title={$t('options')} {onClose} size="small"> + <ModalBody> + <div class="items-center justify-center"> + <div class="py-2"> + <h2 class="text-gray text-sm mb-2">{$t('settings').toUpperCase()}</h2> + <div class="grid p-2 gap-y-2"> + {#if order} + <SettingDropdown + title={$t('display_order')} + options={Object.values(options)} + selectedOption={options[order]} + onToggle={handleToggle} + /> + {/if} + <SettingSwitch + title={$t('comments_and_likes')} + subtitle={$t('let_others_respond')} + checked={album.isActivityEnabled} + onToggle={onToggleEnabledActivity} /> - {/if} - <SettingSwitch - title={$t('comments_and_likes')} - subtitle={$t('let_others_respond')} - checked={album.isActivityEnabled} - onToggle={onToggleEnabledActivity} - /> - </div> - </div> - <div class="py-2"> - <div class="text-gray text-sm mb-3">{$t('people').toUpperCase()}</div> - <div class="p-2"> - <button type="button" class="flex items-center gap-2" onclick={onShowSelectSharedUser}> - <div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center"> - <div><Icon path={mdiPlus} size="25" /></div> - </div> - <div>{$t('invite_people')}</div> - </button> - - <div class="flex items-center gap-2 py-2 mt-2"> - <div> - <UserAvatar {user} size="md" /> - </div> - <div class="w-full">{user.name}</div> - <div>{$t('owner')}</div> </div> + </div> + <div class="py-2"> + <div class="text-gray text-sm mb-3">{$t('people').toUpperCase()}</div> + <div class="p-2"> + <button type="button" class="flex items-center gap-2" onclick={onShowSelectSharedUser}> + <div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center"> + <div><Icon path={mdiPlus} size="25" /></div> + </div> + <div>{$t('invite_people')}</div> + </button> - {#each album.albumUsers as { user, role } (user.id)} - <div class="flex items-center gap-2 py-2"> + <div class="flex items-center gap-2 py-2 mt-2"> <div> <UserAvatar {user} size="md" /> </div> <div class="w-full">{user.name}</div> - {#if role === AlbumUserRole.Viewer} - {$t('role_viewer')} - {:else} - {$t('role_editor')} - {/if} - {#if user.id !== album.ownerId} - <ButtonContextMenu icon={mdiDotsVertical} size="20" title={$t('options')}> - {#if role === AlbumUserRole.Viewer} - <MenuOption - onClick={() => handleUpdateSharedUserRole(user, AlbumUserRole.Editor)} - text={$t('allow_edits')} - /> - {:else} - <MenuOption - onClick={() => handleUpdateSharedUserRole(user, AlbumUserRole.Viewer)} - text={$t('disallow_edits')} - /> - {/if} - <!-- Allow deletion for non-owners --> - <MenuOption onClick={() => handleMenuRemove(user)} text={$t('remove')} /> - </ButtonContextMenu> - {/if} + <div>{$t('owner')}</div> </div> - {/each} + + {#each album.albumUsers as { user, role } (user.id)} + <div class="flex items-center gap-2 py-2"> + <div> + <UserAvatar {user} size="md" /> + </div> + <div class="w-full">{user.name}</div> + {#if role === AlbumUserRole.Viewer} + {$t('role_viewer')} + {:else} + {$t('role_editor')} + {/if} + {#if user.id !== album.ownerId} + <ButtonContextMenu icon={mdiDotsVertical} size="20" title={$t('options')}> + {#if role === AlbumUserRole.Viewer} + <MenuOption + onClick={() => handleUpdateSharedUserRole(user, AlbumUserRole.Editor)} + text={$t('allow_edits')} + /> + {:else} + <MenuOption + onClick={() => handleUpdateSharedUserRole(user, AlbumUserRole.Viewer)} + text={$t('disallow_edits')} + /> + {/if} + <!-- Allow deletion for non-owners --> + <MenuOption onClick={() => handleMenuRemove(user)} text={$t('remove')} /> + </ButtonContextMenu> + {/if} + </div> + {/each} + </div> </div> </div> - </div> - </FullScreenModal> + </ModalBody> + </Modal> {/if} {#if selectedRemoveUser} diff --git a/web/src/lib/components/album-page/album-title.svelte b/web/src/lib/components/album-page/album-title.svelte index f5b1a4fa1e..f2559c86e5 100644 --- a/web/src/lib/components/album-page/album-title.svelte +++ b/web/src/lib/components/album-page/album-title.svelte @@ -40,7 +40,7 @@ onblur={handleUpdateName} class="w-[99%] mb-2 border-b-2 border-transparent text-2xl md:text-4xl lg:text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned ? 'hover:border-gray-400' - : 'hover:border-transparent'} focus:border-b-2 focus:border-immich-primary focus:outline-none bg-light dark:focus:border-immich-dark-primary dark:focus:bg-immich-dark-gray" + : 'hover:border-transparent'} focus:border-b-2 focus:border-immich-primary focus:outline-none bg-light dark:focus:border-immich-dark-primary dark:focus:bg-immich-dark-gray placeholder:text-primary/90" type="text" bind:value={newAlbumName} disabled={!isOwned} diff --git a/web/src/lib/components/asset-viewer/detail-panel-tags.svelte b/web/src/lib/components/asset-viewer/detail-panel-tags.svelte index eee7a7c0b6..5da6324528 100644 --- a/web/src/lib/components/asset-viewer/detail-panel-tags.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel-tags.svelte @@ -50,7 +50,7 @@ {#each tags as tag (tag.id)} <div class="flex group transition-all"> <a - class="inline-block h-min whitespace-nowrap ps-3 pe-1 group-hover:ps-3 py-1 text-center align-baseline leading-none text-gray-100 dark:text-immich-dark-gray bg-immich-primary dark:bg-immich-dark-primary roudned-s-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all" + class="inline-block h-min whitespace-nowrap ps-3 pe-1 group-hover:ps-3 py-1 text-center align-baseline leading-none text-gray-100 dark:text-immich-dark-gray bg-immich-primary dark:bg-immich-dark-primary rounded-s-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all" href={encodeURI(`${AppRoute.TAGS}/?path=${tag.value}`)} > <p class="text-sm"> diff --git a/web/src/lib/components/forms/edit-album-form.svelte b/web/src/lib/components/forms/edit-album-form.svelte index e269f0d8c0..32e1e422e7 100644 --- a/web/src/lib/components/forms/edit-album-form.svelte +++ b/web/src/lib/components/forms/edit-album-form.svelte @@ -1,9 +1,8 @@ <script lang="ts"> import AlbumCover from '$lib/components/album-page/album-cover.svelte'; - import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import { handleError } from '$lib/utils/handle-error'; import { updateAlbumInfo, type AlbumResponseDto } from '@immich/sdk'; - import { Button } from '@immich/ui'; + import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui'; import { mdiRenameOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -47,29 +46,33 @@ }; </script> -<FullScreenModal icon={mdiRenameOutline} title={$t('edit_album')} width="wide" {onClose}> - <form {onsubmit} autocomplete="off" id="edit-album-form"> - <div class="flex items-center"> - <div class="hidden sm:flex"> - <AlbumCover {album} class="h-[200px] w-[200px] m-4 shadow-lg" /> - </div> - - <div class="grow"> - <div class="m-4 flex flex-col gap-2"> - <label class="immich-form-label" for="name">{$t('name')}</label> - <input class="immich-form-input" id="name" type="text" bind:value={albumName} /> +<Modal icon={mdiRenameOutline} title={$t('edit_album')} size="medium" {onClose}> + <ModalBody> + <form {onsubmit} autocomplete="off" id="edit-album-form"> + <div class="flex items-center"> + <div class="hidden sm:flex"> + <AlbumCover {album} class="h-[200px] w-[200px] m-4 shadow-lg" /> </div> - <div class="m-4 flex flex-col gap-2"> - <label class="immich-form-label" for="description">{$t('description')}</label> - <textarea class="immich-form-input" id="description" bind:value={description}></textarea> + <div class="grow"> + <div class="m-4 flex flex-col gap-2"> + <label class="immich-form-label" for="name">{$t('name')}</label> + <input class="immich-form-input" id="name" type="text" bind:value={albumName} /> + </div> + + <div class="m-4 flex flex-col gap-2"> + <label class="immich-form-label" for="description">{$t('description')}</label> + <textarea class="immich-form-input" id="description" bind:value={description}></textarea> + </div> </div> </div> + </form> + </ModalBody> + + <ModalFooter> + <div class="flex gap-2 w-full"> + <Button shape="round" color="secondary" fullWidth onclick={() => onCancel?.()}>{$t('cancel')}</Button> + <Button shape="round" type="submit" fullWidth disabled={isSubmitting} form="edit-album-form">{$t('save')}</Button> </div> - </form> - - {#snippet stickyBottom()} - <Button shape="round" color="secondary" fullWidth onclick={() => onCancel?.()}>{$t('cancel')}</Button> - <Button shape="round" type="submit" fullWidth disabled={isSubmitting} form="edit-album-form">{$t('save')}</Button> - {/snippet} -</FullScreenModal> + </ModalFooter> +</Modal> diff --git a/web/src/lib/components/forms/library-exclusion-pattern-form.svelte b/web/src/lib/components/forms/library-exclusion-pattern-form.svelte index d6ce4bcda0..e069e5c7a2 100644 --- a/web/src/lib/components/forms/library-exclusion-pattern-form.svelte +++ b/web/src/lib/components/forms/library-exclusion-pattern-form.svelte @@ -1,9 +1,8 @@ <script lang="ts"> - import { Button } from '@immich/ui'; + import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui'; import { mdiFolderRemove } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; - import FullScreenModal from '../shared-components/full-screen-modal.svelte'; interface Props { exclusionPattern: string; @@ -42,37 +41,40 @@ }; </script> -<FullScreenModal title={$t('add_exclusion_pattern')} icon={mdiFolderRemove} onClose={onCancel}> - <form {onsubmit} autocomplete="off" id="add-exclusion-pattern-form"> - <p class="py-5 text-sm"> - {$t('admin.exclusion_pattern_description')} - <br /><br /> - {$t('admin.add_exclusion_pattern_description')} - </p> - <div class="my-4 flex flex-col gap-2"> - <label class="immich-form-label" for="exclusionPattern">{$t('pattern')}</label> - <input - class="immich-form-input" - id="exclusionPattern" - name="exclusionPattern" - type="text" - bind:value={exclusionPattern} - /> - </div> - <div class="mt-8 flex w-full gap-4"> - {#if isDuplicate} - <p class="text-red-500 text-sm">{$t('errors.exclusion_pattern_already_exists')}</p> +<Modal size="small" title={$t('add_exclusion_pattern')} icon={mdiFolderRemove} onClose={onCancel}> + <ModalBody> + <form {onsubmit} autocomplete="off" id="add-exclusion-pattern-form"> + <p class="py-5 text-sm"> + {$t('admin.exclusion_pattern_description')} + <br /><br /> + {$t('admin.add_exclusion_pattern_description')} + </p> + <div class="my-4 flex flex-col gap-2"> + <label class="immich-form-label" for="exclusionPattern">{$t('pattern')}</label> + <input + class="immich-form-input" + id="exclusionPattern" + name="exclusionPattern" + type="text" + bind:value={exclusionPattern} + /> + </div> + <div class="mt-8 flex w-full gap-4"> + {#if isDuplicate} + <p class="text-red-500 text-sm">{$t('errors.exclusion_pattern_already_exists')}</p> + {/if} + </div> + </form> + </ModalBody> + <ModalFooter> + <div class="flex gap-2 w-full"> + <Button shape="round" color="secondary" fullWidth onclick={onCancel}>{$t('cancel')}</Button> + {#if isEditing} + <Button shape="round" color="danger" fullWidth onclick={onDelete}>{$t('delete')}</Button> {/if} + <Button shape="round" type="submit" disabled={!canSubmit} fullWidth form="add-exclusion-pattern-form" + >{submitText}</Button + > </div> - </form> - - {#snippet stickyBottom()} - <Button shape="round" color="secondary" fullWidth onclick={onCancel}>{$t('cancel')}</Button> - {#if isEditing} - <Button shape="round" color="danger" fullWidth onclick={onDelete}>{$t('delete')}</Button> - {/if} - <Button shape="round" type="submit" disabled={!canSubmit} fullWidth form="add-exclusion-pattern-form" - >{submitText}</Button - > - {/snippet} -</FullScreenModal> + </ModalFooter> +</Modal> diff --git a/web/src/lib/components/forms/library-import-path-form.svelte b/web/src/lib/components/forms/library-import-path-form.svelte index 512b8919e3..ee2a273708 100644 --- a/web/src/lib/components/forms/library-import-path-form.svelte +++ b/web/src/lib/components/forms/library-import-path-form.svelte @@ -1,6 +1,5 @@ <script lang="ts"> - import { Button } from '@immich/ui'; - import FullScreenModal from '../shared-components/full-screen-modal.svelte'; + import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui'; import { mdiFolderSync } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; @@ -46,29 +45,33 @@ }; </script> -<FullScreenModal {title} icon={mdiFolderSync} onClose={onCancel}> - <form {onsubmit} autocomplete="off" id="library-import-path-form"> - <p class="py-5 text-sm">{$t('admin.library_import_path_description')}</p> +<Modal {title} icon={mdiFolderSync} onClose={onCancel} size="small"> + <ModalBody> + <form {onsubmit} autocomplete="off" id="library-import-path-form"> + <p class="py-5 text-sm">{$t('admin.library_import_path_description')}</p> - <div class="my-4 flex flex-col gap-2"> - <label class="immich-form-label" for="path">{$t('path')}</label> - <input class="immich-form-input" id="path" name="path" type="text" bind:value={importPath} /> - </div> + <div class="my-4 flex flex-col gap-2"> + <label class="immich-form-label" for="path">{$t('path')}</label> + <input class="immich-form-input" id="path" name="path" type="text" bind:value={importPath} /> + </div> - <div class="mt-8 flex w-full gap-4"> - {#if isDuplicate} - <p class="text-red-500 text-sm">{$t('errors.import_path_already_exists')}</p> + <div class="mt-8 flex w-full gap-4"> + {#if isDuplicate} + <p class="text-red-500 text-sm">{$t('errors.import_path_already_exists')}</p> + {/if} + </div> + </form> + </ModalBody> + + <ModalFooter> + <div class="flex gap-2 w-full"> + <Button shape="round" color="secondary" fullWidth onclick={onCancel}>{cancelText}</Button> + {#if isEditing} + <Button shape="round" color="danger" fullWidth onclick={onDelete}>{$t('delete')}</Button> {/if} + <Button shape="round" type="submit" disabled={!canSubmit} fullWidth form="library-import-path-form" + >{submitText}</Button + > </div> - </form> - - {#snippet stickyBottom()} - <Button shape="round" color="secondary" fullWidth onclick={onCancel}>{cancelText}</Button> - {#if isEditing} - <Button shape="round" color="danger" fullWidth onclick={onDelete}>{$t('delete')}</Button> - {/if} - <Button shape="round" type="submit" disabled={!canSubmit} fullWidth form="library-import-path-form" - >{submitText}</Button - > - {/snippet} -</FullScreenModal> + </ModalFooter> +</Modal> diff --git a/web/src/lib/components/forms/library-rename-form.svelte b/web/src/lib/components/forms/library-rename-form.svelte index af60bdc2be..0c04c77da1 100644 --- a/web/src/lib/components/forms/library-rename-form.svelte +++ b/web/src/lib/components/forms/library-rename-form.svelte @@ -1,7 +1,6 @@ <script lang="ts"> - import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import type { LibraryResponseDto } from '@immich/sdk'; - import { Button, Field, Input } from '@immich/ui'; + import { Button, Field, Input, Modal, ModalBody, ModalFooter } from '@immich/ui'; import { mdiRenameOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -21,15 +20,19 @@ }; </script> -<form {onsubmit} autocomplete="off"> - <FullScreenModal icon={mdiRenameOutline} title={$t('rename')} onClose={onCancel}> - <Field label={$t('name')}> - <Input bind:value={newName} /> - </Field> +<Modal icon={mdiRenameOutline} title={$t('rename')} onClose={onCancel} size="small"> + <ModalBody> + <form {onsubmit} autocomplete="off" id="rename-library-form"> + <Field label={$t('name')}> + <Input bind:value={newName} /> + </Field> + </form> + </ModalBody> - {#snippet stickyBottom()} + <ModalFooter> + <div class="flex gap-2 w-full"> <Button shape="round" fullWidth color="secondary" onclick={onCancel}>{$t('cancel')}</Button> - <Button shape="round" fullWidth type="submit">{$t('save')}</Button> - {/snippet} - </FullScreenModal> -</form> + <Button shape="round" fullWidth type="submit" form="rename-library-form">{$t('save')}</Button> + </div> + </ModalFooter> +</Modal> diff --git a/web/src/lib/components/forms/library-user-picker-form.svelte b/web/src/lib/components/forms/library-user-picker-form.svelte index 673fe7d5e9..43b3eb69f1 100644 --- a/web/src/lib/components/forms/library-user-picker-form.svelte +++ b/web/src/lib/components/forms/library-user-picker-form.svelte @@ -2,11 +2,10 @@ import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte'; import { user } from '$lib/stores/user.store'; import { searchUsersAdmin } from '@immich/sdk'; - import { Button } from '@immich/ui'; + import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui'; import { mdiFolderSync } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; - import FullScreenModal from '../shared-components/full-screen-modal.svelte'; interface Props { onCancel: () => void; @@ -30,15 +29,19 @@ }; </script> -<FullScreenModal title={$t('select_library_owner')} icon={mdiFolderSync} onClose={onCancel}> - <form {onsubmit} autocomplete="off" id="select-library-owner-form"> - <p class="p-5 text-sm">{$t('admin.note_cannot_be_changed_later')}</p> +<Modal title={$t('select_library_owner')} icon={mdiFolderSync} onClose={onCancel} size="small"> + <ModalBody> + <form {onsubmit} autocomplete="off" id="select-library-owner-form"> + <p class="p-5 text-sm">{$t('admin.note_cannot_be_changed_later')}</p> - <SettingSelect bind:value={ownerId} options={userOptions} name="user" /> - </form> + <SettingSelect bind:value={ownerId} options={userOptions} name="user" /> + </form> + </ModalBody> - {#snippet stickyBottom()} - <Button shape="round" color="secondary" fullWidth onclick={onCancel}>{$t('cancel')}</Button> - <Button shape="round" type="submit" fullWidth form="select-library-owner-form">{$t('create')}</Button> - {/snippet} -</FullScreenModal> + <ModalFooter> + <div class="flex gap-2 w-full"> + <Button shape="round" color="secondary" fullWidth onclick={onCancel}>{$t('cancel')}</Button> + <Button shape="round" type="submit" fullWidth form="select-library-owner-form">{$t('create')}</Button> + </div> + </ModalFooter> +</Modal> diff --git a/web/src/lib/components/forms/tag-asset-form.svelte b/web/src/lib/components/forms/tag-asset-form.svelte index c757ff5335..a6e0928ece 100644 --- a/web/src/lib/components/forms/tag-asset-form.svelte +++ b/web/src/lib/components/forms/tag-asset-form.svelte @@ -1,13 +1,12 @@ <script lang="ts"> import Icon from '$lib/components/elements/icon.svelte'; import { getAllTags, upsertTags, type TagResponseDto } from '@immich/sdk'; - import { Button } from '@immich/ui'; + import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui'; import { mdiClose, mdiTag } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; import { SvelteSet } from 'svelte/reactivity'; import Combobox, { type ComboBoxOption } from '../shared-components/combobox.svelte'; - import FullScreenModal from '../shared-components/full-screen-modal.svelte'; interface Props { onTag: (tagIds: string[]) => void; @@ -52,48 +51,52 @@ }; </script> -<FullScreenModal title={$t('tag_assets')} icon={mdiTag} onClose={onCancel}> - <form {onsubmit} autocomplete="off" id="create-tag-form"> - <div class="my-4 flex flex-col gap-2"> - <Combobox - onSelect={handleSelect} - label={$t('tag')} - {allowCreate} - defaultFirstOption - options={allTags.map((tag) => ({ id: tag.id, label: tag.value, value: tag.id }))} - placeholder={$t('search_tags')} - /> +<Modal size="small" title={$t('tag_assets')} icon={mdiTag} onClose={onCancel}> + <ModalBody> + <form {onsubmit} autocomplete="off" id="create-tag-form"> + <div class="my-4 flex flex-col gap-2"> + <Combobox + onSelect={handleSelect} + label={$t('tag')} + {allowCreate} + defaultFirstOption + options={allTags.map((tag) => ({ id: tag.id, label: tag.value, value: tag.id }))} + placeholder={$t('search_tags')} + /> + </div> + </form> + + <section class="flex flex-wrap pt-2 gap-1"> + {#each selectedIds as tagId (tagId)} + {@const tag = tagMap[tagId]} + {#if tag} + <div class="flex group transition-all"> + <span + class="inline-block h-min whitespace-nowrap ps-3 pe-1 group-hover:ps-3 py-1 text-center align-baseline leading-none text-gray-100 dark:text-immich-dark-gray bg-immich-primary dark:bg-immich-dark-primary roudned-s-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all" + > + <p class="text-sm"> + {tag.value} + </p> + </span> + + <button + type="button" + class="text-gray-100 dark:text-immich-dark-gray bg-immich-primary/95 dark:bg-immich-dark-primary/95 rounded-e-full place-items-center place-content-center pe-2 ps-1 py-1 hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all" + title="Remove tag" + onclick={() => handleRemove(tagId)} + > + <Icon path={mdiClose} /> + </button> + </div> + {/if} + {/each} + </section> + </ModalBody> + + <ModalFooter> + <div class="flex w-full gap-2"> + <Button shape="round" fullWidth color="secondary" onclick={onCancel}>{$t('cancel')}</Button> + <Button type="submit" shape="round" fullWidth form="create-tag-form" {disabled}>{$t('tag_assets')}</Button> </div> - </form> - - <section class="flex flex-wrap pt-2 gap-1"> - {#each selectedIds as tagId (tagId)} - {@const tag = tagMap[tagId]} - {#if tag} - <div class="flex group transition-all"> - <span - class="inline-block h-min whitespace-nowrap ps-3 pe-1 group-hover:ps-3 py-1 text-center align-baseline leading-none text-gray-100 dark:text-immich-dark-gray bg-immich-primary dark:bg-immich-dark-primary roudned-s-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all" - > - <p class="text-sm"> - {tag.value} - </p> - </span> - - <button - type="button" - class="text-gray-100 dark:text-immich-dark-gray bg-immich-primary/95 dark:bg-immich-dark-primary/95 rounded-e-full place-items-center place-content-center pe-2 ps-1 py-1 hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all" - title="Remove tag" - onclick={() => handleRemove(tagId)} - > - <Icon path={mdiClose} /> - </button> - </div> - {/if} - {/each} - </section> - - {#snippet stickyBottom()} - <Button shape="round" fullWidth color="secondary" onclick={onCancel}>{$t('cancel')}</Button> - <Button type="submit" shape="round" fullWidth form="create-tag-form" {disabled}>{$t('tag_assets')}</Button> - {/snippet} -</FullScreenModal> + </ModalFooter> +</Modal> diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte index cace59e9d6..2dd25491a8 100644 --- a/web/src/lib/components/memory-page/memory-viewer.svelte +++ b/web/src/lib/components/memory-page/memory-viewer.svelte @@ -314,8 +314,9 @@ /> {#if assetInteraction.selectionActive} - <div class="sticky top-0"> + <div class="sticky top-0 z-1"> <AssetSelectControlBar + forceDark assets={assetInteraction.selectedAssets} clearSelect={() => cancelMultiselect(assetInteraction)} > @@ -605,6 +606,7 @@ </section> {/if} </section> + {#if current} <!-- GALLERY VIEWER --> <section class="bg-immich-dark-gray p-4"> diff --git a/web/src/lib/components/photos-page/asset-select-control-bar.svelte b/web/src/lib/components/photos-page/asset-select-control-bar.svelte index 17b06c0e7e..b21693bc51 100644 --- a/web/src/lib/components/photos-page/asset-select-control-bar.svelte +++ b/web/src/lib/components/photos-page/asset-select-control-bar.svelte @@ -24,9 +24,10 @@ clearSelect: () => void; ownerId?: string | undefined; children?: Snippet; + forceDark?: boolean; } - let { assets, clearSelect, ownerId = undefined, children }: Props = $props(); + let { assets, clearSelect, ownerId = undefined, children, forceDark }: Props = $props(); setContext({ getAssets: () => assets, @@ -35,9 +36,11 @@ }); </script> -<ControlAppBar onClose={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md"> +<ControlAppBar onClose={clearSelect} {forceDark} backIcon={mdiClose} tailwindClasses="bg-white shadow-md"> {#snippet leading()} - <div class="font-medium text-immich-primary dark:text-immich-dark-primary"> + <div + class="font-medium {forceDark ? 'text-immich-dark-primary' : 'text-immich-primary dark:text-immich-dark-primary'}" + > <p class="block sm:hidden">{assets.length}</p> <p class="hidden sm:block">{$t('selected_count', { values: { count: assets.length } })}</p> </div> diff --git a/web/src/lib/components/shared-components/album-selection/album-selection-modal.svelte b/web/src/lib/components/shared-components/album-selection/album-selection-modal.svelte index 3bc0161236..ab763546af 100644 --- a/web/src/lib/components/shared-components/album-selection/album-selection-modal.svelte +++ b/web/src/lib/components/shared-components/album-selection/album-selection-modal.svelte @@ -5,9 +5,9 @@ AlbumModalRowType, isSelectableRowType, } from '$lib/components/shared-components/album-selection/album-selection-utils'; - import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import { albumViewSettings } from '$lib/stores/preferences.store'; import { type AlbumResponseDto, getAllAlbums } from '@immich/sdk'; + import { Modal, ModalBody } from '@immich/ui'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; import AlbumListItem from '../../asset-viewer/album-list-item.svelte'; @@ -80,49 +80,51 @@ const handleAlbumClick = (album: AlbumResponseDto) => () => onAlbumClick(album); </script> -<FullScreenModal title={shared ? $t('add_to_shared_album') : $t('add_to_album')} {onClose}> - <div class="mb-2 flex max-h-[400px] flex-col"> - {#if loading} - <!-- eslint-disable-next-line svelte/require-each-key --> - {#each { length: 3 } as _} - <div class="flex animate-pulse gap-4 px-6 py-2"> - <div class="h-12 w-12 rounded-xl bg-slate-200"></div> - <div class="flex flex-col items-start justify-center gap-2"> - <span class="h-4 w-36 animate-pulse bg-slate-200"></span> - <div class="flex animate-pulse gap-1"> - <span class="h-3 w-8 bg-slate-200"></span> - <span class="h-3 w-20 bg-slate-200"></span> +<Modal title={shared ? $t('add_to_shared_album') : $t('add_to_album')} {onClose} size="small"> + <ModalBody> + <div class="mb-2 flex max-h-[400px] flex-col"> + {#if loading} + <!-- eslint-disable-next-line svelte/require-each-key --> + {#each { length: 3 } as _} + <div class="flex animate-pulse gap-4 px-6 py-2"> + <div class="h-12 w-12 rounded-xl bg-slate-200"></div> + <div class="flex flex-col items-start justify-center gap-2"> + <span class="h-4 w-36 animate-pulse bg-slate-200"></span> + <div class="flex animate-pulse gap-1"> + <span class="h-3 w-8 bg-slate-200"></span> + <span class="h-3 w-20 bg-slate-200"></span> + </div> </div> </div> - </div> - {/each} - {:else} - <input - class="border-b-4 border-immich-bg px-6 py-2 text-2xl focus:border-immich-primary dark:border-immich-dark-gray dark:focus:border-immich-dark-primary" - placeholder={$t('search')} - {onkeydown} - bind:value={search} - use:initInput - /> - <div class="immich-scrollbar overflow-y-auto"> - <!-- eslint-disable-next-line svelte/require-each-key --> - {#each albumModalRows as row} - {#if row.type === AlbumModalRowType.NEW_ALBUM} - <NewAlbumListItem selected={row.selected || false} {onNewAlbum} searchQuery={search} /> - {:else if row.type === AlbumModalRowType.SECTION} - <p class="px-5 py-3 text-xs">{row.text}</p> - {:else if row.type === AlbumModalRowType.MESSAGE} - <p class="px-5 py-1 text-sm">{row.text}</p> - {:else if row.type === AlbumModalRowType.ALBUM_ITEM && row.album} - <AlbumListItem - album={row.album} - selected={row.selected || false} - searchQuery={search} - onAlbumClick={handleAlbumClick(row.album)} - /> - {/if} {/each} - </div> - {/if} - </div> -</FullScreenModal> + {:else} + <input + class="border-b-4 border-immich-bg px-6 py-2 text-2xl focus:border-immich-primary dark:border-immich-dark-gray dark:focus:border-immich-dark-primary" + placeholder={$t('search')} + {onkeydown} + bind:value={search} + use:initInput + /> + <div class="immich-scrollbar overflow-y-auto"> + <!-- eslint-disable-next-line svelte/require-each-key --> + {#each albumModalRows as row} + {#if row.type === AlbumModalRowType.NEW_ALBUM} + <NewAlbumListItem selected={row.selected || false} {onNewAlbum} searchQuery={search} /> + {:else if row.type === AlbumModalRowType.SECTION} + <p class="px-5 py-3 text-xs">{row.text}</p> + {:else if row.type === AlbumModalRowType.MESSAGE} + <p class="px-5 py-1 text-sm">{row.text}</p> + {:else if row.type === AlbumModalRowType.ALBUM_ITEM && row.album} + <AlbumListItem + album={row.album} + selected={row.selected || false} + searchQuery={search} + onAlbumClick={handleAlbumClick(row.album)} + /> + {/if} + {/each} + </div> + {/if} + </div> + </ModalBody> +</Modal> diff --git a/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte b/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte index 593baafc7c..9e5d5d2391 100644 --- a/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte +++ b/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte @@ -38,6 +38,10 @@ buttonClass?: string | undefined; hideContent?: boolean; children?: Snippet; + offset?: { + x: number; + y: number; + }; } & HTMLAttributes<HTMLDivElement>; let { @@ -51,6 +55,7 @@ buttonClass = undefined, hideContent = false, children, + offset, ...restProps }: Props = $props(); @@ -186,13 +191,14 @@ ]} > <ContextMenu - {...contextMenuPosition} {direction} ariaActiveDescendant={$selectedIdStore} ariaLabelledBy={buttonId} bind:menuElement={menuContainer} id={menuId} isVisible={isOpen} + x={contextMenuPosition.x - (offset?.x ?? 0)} + y={contextMenuPosition.y + (offset?.y ?? 0)} > {@render children?.()} </ContextMenu> diff --git a/web/src/lib/components/shared-components/control-app-bar.svelte b/web/src/lib/components/shared-components/control-app-bar.svelte index 0476ba6bfd..6830dfefa0 100644 --- a/web/src/lib/components/shared-components/control-app-bar.svelte +++ b/web/src/lib/components/shared-components/control-app-bar.svelte @@ -66,7 +66,7 @@ let buttonClass = $derived(forceDark ? 'hover:text-immich-dark-gray' : undefined); </script> -<div in:fly={{ y: 10, duration: 200 }} class="absolute top-0 w-full bg-transparent z-1"> +<div in:fly={{ y: 10, duration: 200 }} class="absolute top-0 w-full bg-transparent"> <nav id="asset-selection-app-bar" class={[ @@ -77,7 +77,7 @@ appBarBorder, 'mx-2 my-2 place-items-center rounded-lg p-2 max-md:p-0 transition-all', tailwindClasses, - forceDark ? 'bg-immich-dark-gray text-white' : 'bg-subtle dark:bg-immich-dark-gray', + forceDark ? 'bg-immich-dark-gray! text-white' : 'bg-subtle dark:bg-immich-dark-gray', ]} > <div class="flex place-items-center sm:gap-6 justify-self-start dark:text-immich-dark-fg"> diff --git a/web/src/lib/components/shared-components/empty-placeholder.svelte b/web/src/lib/components/shared-components/empty-placeholder.svelte index 63c30a0c4a..ae7f9aab6a 100644 --- a/web/src/lib/components/shared-components/empty-placeholder.svelte +++ b/web/src/lib/components/shared-components/empty-placeholder.svelte @@ -29,5 +29,5 @@ {#if title} <h2 class="text-xl font-medium my-4">{title}</h2> {/if} - <p class="text-immich-text-gray-500 dark:text-immich-dark-fg font-light">{text}</p> + <p class="text-immich-text-gray-500 dark:text-immich-dark-fg font-light text-center">{text}</p> </svelte:element> diff --git a/web/src/lib/components/shared-components/full-screen-modal.svelte b/web/src/lib/components/shared-components/full-screen-modal.svelte deleted file mode 100644 index 39ba62ea80..0000000000 --- a/web/src/lib/components/shared-components/full-screen-modal.svelte +++ /dev/null @@ -1,107 +0,0 @@ -<script lang="ts"> - import { clickOutside } from '$lib/actions/click-outside'; - import { focusTrap } from '$lib/actions/focus-trap'; - import ModalHeader from '$lib/components/shared-components/modal-header.svelte'; - import { generateId } from '$lib/utils/generate-id'; - import type { Snippet } from 'svelte'; - import { fade } from 'svelte/transition'; - - interface Props { - onClose: () => void; - title: string; - /** - * If true, the logo will be displayed next to the modal title. - */ - showLogo?: boolean; - /** - * Optional icon to display next to the modal title, if `showLogo` is false. - */ - icon?: string | undefined; - /** - * Sets the width of the modal. - * - * - `wide`: 48rem - * - `narrow`: 28rem - * - `auto`: fits the width of the modal content, up to a maximum of 32rem - */ - width?: 'extra-wide' | 'wide' | 'narrow' | 'auto'; - stickyBottom?: Snippet; - children?: Snippet; - } - - let { - onClose, - title, - showLogo = false, - icon = undefined, - width = 'narrow', - stickyBottom, - children, - }: Props = $props(); - - /** - * Unique identifier for the modal. - */ - let id: string = generateId(); - - let titleId = $derived(`${id}-title`); - let isStickyBottom = $derived(!!stickyBottom); - - let modalWidth = $state<string>(); - - $effect(() => { - switch (width) { - case 'extra-wide': { - modalWidth = 'w-4xl'; - break; - } - - case 'wide': { - modalWidth = 'w-3xl'; - break; - } - - case 'narrow': { - modalWidth = 'w-md'; - break; - } - - default: { - modalWidth = 'sm:max-w-4xl'; - } - } - }); -</script> - -<section - role="presentation" - in:fade={{ duration: 100 }} - out:fade={{ duration: 100 }} - class="fixed start-0 top-0 flex h-dvh w-dvw place-content-center place-items-center bg-black/40" - onkeydown={(event) => { - event.stopPropagation(); - }} - use:focusTrap -> - <div - class="flex flex-col max-h-[min(95dvh,60rem)] max-w-[95vw] {modalWidth} overflow-hidden rounded-3xl bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg pt-3 pb-4" - use:clickOutside={{ onOutclick: onClose, onEscape: onClose }} - tabindex="-1" - aria-modal="true" - aria-labelledby={titleId} - > - <div class="immich-scrollbar overflow-y-auto pt-1" class:pb-4={isStickyBottom}> - <ModalHeader id={titleId} {title} {showLogo} {icon} {onClose} /> - <div class="px-5 pt-0 mb-5"> - {@render children?.()} - </div> - </div> - {#if isStickyBottom} - <div - class="flex flex-col sm:flex-row justify-end w-full gap-2 sm:gap-4 sticky pt-4 px-5 bg-immich-bg dark:bg-immich-dark-gray border-t border-gray-200 dark:border-gray-500" - > - {@render stickyBottom?.()} - </div> - {/if} - </div> -</section> diff --git a/web/src/lib/components/shared-components/immich-logo.svelte b/web/src/lib/components/shared-components/immich-logo.svelte index 4a96cfd632..a57f367964 100644 --- a/web/src/lib/components/shared-components/immich-logo.svelte +++ b/web/src/lib/components/shared-components/immich-logo.svelte @@ -14,7 +14,7 @@ <svg {viewBox} class={cssClass}> <title>{$t('immich_logo')}</title> {#if !noText} - <g class="st0 dark:fill-[#accbfa]"> + <g class="st0 dark:fill-[#accbfa] fill-[#4251b0]"> <path d="M268.73,63.18c6.34,0,11.52,5.18,11.52,11.35c0,6.34-5.18,11.35-11.52,11.35s-11.69-5.01-11.69-11.35 C257.04,68.36,262.39,63.18,268.73,63.18z M258.88,122.45c0-3.01-0.67-7.85-0.67-10.68c0-6.01,4.67-10.68,10.52-10.68 @@ -94,9 +94,6 @@ </svg> <style> - .st0 { - fill: #4251b0; - } .st1 { fill: #fa2921; } diff --git a/web/src/lib/components/shared-components/navigation-bar/notification-panel.svelte b/web/src/lib/components/shared-components/navigation-bar/notification-panel.svelte index 04ce019d9f..f76f187ad9 100644 --- a/web/src/lib/components/shared-components/navigation-bar/notification-panel.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/notification-panel.svelte @@ -39,7 +39,7 @@ in:fade={{ duration: 100 }} out:fade={{ duration: 100 }} id="notification-panel" - class="absolute right-[25px] top-[70px] z-1 w-[min(360px,100vw-50px)] rounded-3xl bg-gray-100 border border-gray-200 shadow-lg dark:border dark:border-immich-dark-gray dark:bg-immich-dark-gray text-light" + class="absolute right-[25px] top-[70px] z-1 w-[min(360px,100vw-50px)] rounded-3xl bg-gray-100 border border-gray-200 shadow-lg dark:border dark:border-light dark:bg-immich-dark-gray text-light px-2" use:focusTrap > <Stack class="max-h-[500px]"> diff --git a/web/src/lib/components/shared-components/profile-image-cropper.svelte b/web/src/lib/components/shared-components/profile-image-cropper.svelte index c2a66d1565..6e55d94f7f 100644 --- a/web/src/lib/components/shared-components/profile-image-cropper.svelte +++ b/web/src/lib/components/shared-components/profile-image-cropper.svelte @@ -1,9 +1,8 @@ <script lang="ts"> - import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import { user } from '$lib/stores/user.store'; import { handleError } from '$lib/utils/handle-error'; import { createProfileImage, type AssetResponseDto } from '@immich/sdk'; - import { Button } from '@immich/ui'; + import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui'; import domtoimage from 'dom-to-image'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; @@ -89,16 +88,17 @@ }; </script> -<FullScreenModal title={$t('set_profile_picture')} width="auto" {onClose}> - <div class="flex place-items-center items-center justify-center"> - <div - class="relative flex aspect-square w-[250px] overflow-hidden rounded-full border-4 border-immich-primary bg-immich-dark-primary dark:border-immich-dark-primary dark:bg-immich-primary" - > - <PhotoViewer bind:element={imgElement} {asset} /> +<Modal size="small" title={$t('set_profile_picture')} {onClose}> + <ModalBody> + <div class="flex place-items-center items-center justify-center"> + <div + class="relative flex aspect-square w-[250px] overflow-hidden rounded-full border-4 border-immich-primary bg-immich-dark-primary dark:border-immich-dark-primary dark:bg-immich-primary" + > + <PhotoViewer bind:element={imgElement} {asset} /> + </div> </div> - </div> - - {#snippet stickyBottom()} + </ModalBody> + <ModalFooter> <Button fullWidth shape="round" onclick={handleSetProfilePicture}>{$t('set_as_profile_picture')}</Button> - {/snippet} -</FullScreenModal> + </ModalFooter> +</Modal> diff --git a/web/src/lib/components/shared-components/scrubber/scrubber.svelte b/web/src/lib/components/shared-components/scrubber/scrubber.svelte index e36e07c4b0..26a2e3f143 100644 --- a/web/src/lib/components/shared-components/scrubber/scrubber.svelte +++ b/web/src/lib/components/shared-components/scrubber/scrubber.svelte @@ -464,7 +464,7 @@ class={[ { 'border-b-2': isDragging }, { 'rounded-bl-md': !isDragging }, - 'bg-light truncate opacity-85 pointer-events-none absolute end-0 min-w-20 max-w-64 w-fit rounded-ss-md border-immich-primary py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:text-immich-dark-fg', + 'bg-light truncate opacity-85 pointer-events-none absolute end-0 min-w-20 max-w-64 w-fit rounded-ss-md border-immich-primary py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:text-immich-dark-fg z-1', ]} style:top="{hoverY + 2}px" > @@ -506,7 +506,7 @@ {#if assetStore.scrolling && scrollHoverLabel && !isHover} <p transition:fade={{ duration: 200 }} - class="truncate pointer-events-none absolute end-0 bottom-0 min-w-20 max-w-64 w-fit rounded-tl-md border-b-2 border-immich-primary bg-subtle/70 py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:text-immich-dark-fg" + class="truncate pointer-events-none absolute end-0 bottom-0 min-w-20 max-w-64 w-fit rounded-tl-md border-b-2 border-immich-primary bg-subtle/90 z-1 py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:text-immich-dark-fg" > {scrollHoverLabel} </p> diff --git a/web/src/lib/components/shared-components/settings/setting-accordion.svelte b/web/src/lib/components/shared-components/settings/setting-accordion.svelte index f48d14ea30..422f4bdded 100755 --- a/web/src/lib/components/shared-components/settings/setting-accordion.svelte +++ b/web/src/lib/components/shared-components/settings/setting-accordion.svelte @@ -64,8 +64,8 @@ </script> <div - class="border rounded-2xl my-4 px-6 py-4 transition-all {isOpen - ? 'border-primary/40 dark:border-primary/50 shadow-md' + class="border-2 rounded-2xl border-primary/20 my-4 px-6 py-4 transition-all {isOpen + ? 'border-primary/60 shadow-md' : ''}" bind:this={accordionElement} > diff --git a/web/src/lib/components/shared-components/version-announcement-box.svelte b/web/src/lib/components/shared-components/version-announcement-box.svelte index 7586328490..236c3ed559 100644 --- a/web/src/lib/components/shared-components/version-announcement-box.svelte +++ b/web/src/lib/components/shared-components/version-announcement-box.svelte @@ -2,9 +2,8 @@ import FormatMessage from '$lib/components/i18n/format-message.svelte'; import { websocketStore } from '$lib/stores/websocket'; import type { ServerVersionResponseDto } from '@immich/sdk'; - import { Button } from '@immich/ui'; + import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui'; import { t } from 'svelte-i18n'; - import FullScreenModal from './full-screen-modal.svelte'; let showModal = $state(false); @@ -39,33 +38,38 @@ </script> {#if showModal} - <FullScreenModal title="🎉 {$t('new_version_available')}" onClose={() => (showModal = false)}> - <div> - <FormatMessage key="version_announcement_message"> - {#snippet children({ tag, message })} - {#if tag === 'link'} - <span class="font-medium underline"> - <a href="https://github.com/immich-app/immich/releases/latest" target="_blank" rel="noopener noreferrer"> - {message} - </a> - </span> - {:else if tag === 'code'} - <code>{message}</code> - {/if} - {/snippet} - </FormatMessage> - </div> + <Modal size="small" title="🎉 {$t('new_version_available')}" onClose={() => (showModal = false)} icon={false}> + <ModalBody> + <div> + <FormatMessage key="version_announcement_message"> + {#snippet children({ tag, message })} + {#if tag === 'link'} + <span class="font-medium underline"> + <a + href="https://github.com/immich-app/immich/releases/latest" + target="_blank" + rel="noopener noreferrer" + > + {message} + </a> + </span> + {:else if tag === 'code'} + <code>{message}</code> + {/if} + {/snippet} + </FormatMessage> + </div> - <div class="mt-4 font-medium">{$t('version_announcement_closing')}</div> + <div class="mt-4 font-medium">{$t('version_announcement_closing')}</div> - <div class="font-sm mt-8"> - <code>{$t('server_version')}: {serverVersion}</code> - <br /> - <code>{$t('latest_version')}: {releaseVersion}</code> - </div> - - {#snippet stickyBottom()} + <div class="font-sm mt-8"> + <code>{$t('server_version')}: {serverVersion}</code> + <br /> + <code>{$t('latest_version')}: {releaseVersion}</code> + </div> + </ModalBody> + <ModalFooter> <Button fullWidth shape="round" onclick={onAcknowledge}>{$t('acknowledge')}</Button> - {/snippet} - </FullScreenModal> + </ModalFooter> + </Modal> {/if} diff --git a/web/src/lib/components/slideshow-settings.svelte b/web/src/lib/components/slideshow-settings.svelte index c30d2cfb09..0e0019e3eb 100644 --- a/web/src/lib/components/slideshow-settings.svelte +++ b/web/src/lib/components/slideshow-settings.svelte @@ -1,9 +1,8 @@ <script lang="ts"> - import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import { SettingInputFieldType } from '$lib/constants'; - import { Button } from '@immich/ui'; + import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui'; import { mdiArrowDownThin, mdiArrowUpThin, @@ -65,37 +64,40 @@ }; </script> -<FullScreenModal title={$t('slideshow_settings')} onClose={() => onClose()}> - <div class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary"> - <SettingDropdown - title={$t('direction')} - options={Object.values(navigationOptions)} - selectedOption={navigationOptions[tempSlideshowNavigation]} - onToggle={(option) => { - tempSlideshowNavigation = handleToggle(option, navigationOptions) || tempSlideshowNavigation; - }} - /> - <SettingDropdown - title={$t('look')} - options={Object.values(lookOptions)} - selectedOption={lookOptions[tempSlideshowLook]} - onToggle={(option) => { - tempSlideshowLook = handleToggle(option, lookOptions) || tempSlideshowLook; - }} - /> - <SettingSwitch title={$t('show_progress_bar')} bind:checked={tempShowProgressBar} /> - <SettingSwitch title={$t('show_slideshow_transition')} bind:checked={tempSlideshowTransition} /> - <SettingInputField - inputType={SettingInputFieldType.NUMBER} - label={$t('duration')} - description={$t('admin.slideshow_duration_description')} - min={1} - bind:value={tempSlideshowDelay} - /> - </div> - - {#snippet stickyBottom()} - <Button color="secondary" shape="round" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button> - <Button fullWidth color="primary" shape="round" onclick={applyChanges}>{$t('confirm')}</Button> - {/snippet} -</FullScreenModal> +<Modal size="small" title={$t('slideshow_settings')} onClose={() => onClose()}> + <ModalBody> + <div class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary"> + <SettingDropdown + title={$t('direction')} + options={Object.values(navigationOptions)} + selectedOption={navigationOptions[tempSlideshowNavigation]} + onToggle={(option) => { + tempSlideshowNavigation = handleToggle(option, navigationOptions) || tempSlideshowNavigation; + }} + /> + <SettingDropdown + title={$t('look')} + options={Object.values(lookOptions)} + selectedOption={lookOptions[tempSlideshowLook]} + onToggle={(option) => { + tempSlideshowLook = handleToggle(option, lookOptions) || tempSlideshowLook; + }} + /> + <SettingSwitch title={$t('show_progress_bar')} bind:checked={tempShowProgressBar} /> + <SettingSwitch title={$t('show_slideshow_transition')} bind:checked={tempSlideshowTransition} /> + <SettingInputField + inputType={SettingInputFieldType.NUMBER} + label={$t('duration')} + description={$t('admin.slideshow_duration_description')} + min={1} + bind:value={tempSlideshowDelay} + /> + </div> + </ModalBody> + <ModalFooter> + <div class="flex gap-2 w-full"> + <Button color="secondary" shape="round" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button> + <Button fullWidth color="primary" shape="round" onclick={applyChanges}>{$t('confirm')}</Button> + </div> + </ModalFooter> +</Modal> diff --git a/web/src/lib/components/user-settings-page/PinCodeInput.svelte b/web/src/lib/components/user-settings-page/PinCodeInput.svelte index 01de7b3563..0b0e499445 100644 --- a/web/src/lib/components/user-settings-page/PinCodeInput.svelte +++ b/web/src/lib/components/user-settings-page/PinCodeInput.svelte @@ -126,7 +126,7 @@ maxlength="1" bind:this={pinCodeInputElements[index]} id="pin-code-{index}" - class="h-12 w-10 rounded-xl border-2 border-suble dark:border-gray-700 bg-transparent text-center text-lg font-medium focus:border-immich-primary focus:ring-primary dark:focus:border-primary font-mono bg-white dark:bg-light" + class="h-12 w-10 rounded-xl border-2 border-suble dark:border-gray-700 text-center text-lg font-medium focus:border-immich-primary focus:ring-primary dark:focus:border-primary font-mono bg-white dark:bg-light" bind:value={pinValues[index]} onkeydown={handleKeydown} oninput={(event) => handleInput(event, index)} diff --git a/web/src/lib/modals/AlbumShareModal.svelte b/web/src/lib/modals/AlbumShareModal.svelte index 30777e7430..5bff5ab560 100644 --- a/web/src/lib/modals/AlbumShareModal.svelte +++ b/web/src/lib/modals/AlbumShareModal.svelte @@ -2,7 +2,6 @@ import AlbumSharedLink from '$lib/components/album-page/album-shared-link.svelte'; import Dropdown from '$lib/components/elements/dropdown.svelte'; import Icon from '$lib/components/elements/icon.svelte'; - import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import { AppRoute } from '$lib/constants'; import QrCodeModal from '$lib/modals/QrCodeModal.svelte'; import { makeSharedLinkUrl } from '$lib/utils'; @@ -15,7 +14,7 @@ type SharedLinkResponseDto, type UserResponseDto, } from '@immich/sdk'; - import { Button, Link, Stack, Text } from '@immich/ui'; + import { Button, Link, Modal, ModalBody, Stack, Text } from '@immich/ui'; import { mdiCheck, mdiEye, mdiLink, mdiPencil } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; @@ -76,63 +75,22 @@ {#if sharedLinkUrl} <QrCodeModal title={$t('view_link')} onClose={() => (sharedLinkUrl = '')} value={sharedLinkUrl} /> {:else} - <FullScreenModal title={$t('share')} showLogo {onClose}> - {#if Object.keys(selectedUsers).length > 0} - <div class="mb-2 py-2 sticky"> - <p class="text-xs font-medium">{$t('selected')}</p> - <div class="my-2"> - {#each Object.values(selectedUsers) as { user } (user.id)} - {#key user.id} - <div class="flex place-items-center gap-4 p-4"> - <div - class="flex h-10 w-10 items-center justify-center rounded-full border bg-immich-dark-success text-3xl text-white dark:border-immich-dark-gray dark:bg-immich-dark-success" - > - <Icon path={mdiCheck} size={24} /> - </div> + <Modal size="small" title={$t('share')} {onClose}> + <ModalBody> + {#if Object.keys(selectedUsers).length > 0} + <div class="mb-2 py-2 sticky"> + <p class="text-xs font-medium">{$t('selected')}</p> + <div class="my-2"> + {#each Object.values(selectedUsers) as { user } (user.id)} + {#key user.id} + <div class="flex place-items-center gap-4 p-4"> + <div + class="flex h-10 w-10 items-center justify-center rounded-full border bg-immich-dark-success text-3xl text-white dark:border-immich-dark-gray dark:bg-immich-dark-success" + > + <Icon path={mdiCheck} size={24} /> + </div> - <!-- <UserAvatar {user} size="md" /> --> - <div class="text-start grow"> - <p class="text-immich-fg dark:text-immich-dark-fg"> - {user.name} - </p> - <p class="text-xs"> - {user.email} - </p> - </div> - - <Dropdown - title={$t('role')} - options={roleOptions} - render={({ title, icon }) => ({ title, icon })} - onSelect={({ value }) => handleChangeRole(user, value)} - /> - </div> - {/key} - {/each} - </div> - </div> - {/if} - - {#if users.length + Object.keys(selectedUsers).length === 0} - <p class="p-5 text-sm"> - {$t('album_share_no_users')} - </p> - {/if} - - <div class="immich-scrollbar max-h-[500px] overflow-y-auto"> - {#if users.length > 0 && users.length !== Object.keys(selectedUsers).length} - <Text>{$t('users')}</Text> - - <div class="my-2"> - {#each users as user (user.id)} - {#if !Object.keys(selectedUsers).includes(user.id)} - <div class="flex place-items-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"> - <button - type="button" - onclick={() => handleToggle(user)} - class="flex w-full place-items-center gap-4 p-4" - > - <UserAvatar {user} size="md" /> + <!-- <UserAvatar {user} size="md" /> --> <div class="text-start grow"> <p class="text-immich-fg dark:text-immich-dark-fg"> {user.name} @@ -141,53 +99,96 @@ {user.email} </p> </div> - </button> - </div> - {/if} - {/each} + + <Dropdown + title={$t('role')} + options={roleOptions} + render={({ title, icon }) => ({ title, icon })} + onSelect={({ value }) => handleChangeRole(user, value)} + /> + </div> + {/key} + {/each} + </div> </div> {/if} - </div> - {#if users.length > 0} - <div class="py-3"> - <Button - size="small" - fullWidth - shape="round" - disabled={Object.keys(selectedUsers).length === 0} - onclick={() => - onClose({ - action: 'sharedUsers', - data: Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })), - })}>{$t('add')}</Button - > + {#if users.length + Object.keys(selectedUsers).length === 0} + <p class="p-5 text-sm"> + {$t('album_share_no_users')} + </p> + {/if} + + <div class="immich-scrollbar max-h-[500px] overflow-y-auto"> + {#if users.length > 0 && users.length !== Object.keys(selectedUsers).length} + <Text>{$t('users')}</Text> + + <div class="my-2"> + {#each users as user (user.id)} + {#if !Object.keys(selectedUsers).includes(user.id)} + <div class="flex place-items-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"> + <button + type="button" + onclick={() => handleToggle(user)} + class="flex w-full place-items-center gap-4 p-4" + > + <UserAvatar {user} size="md" /> + <div class="text-start grow"> + <p class="text-immich-fg dark:text-immich-dark-fg"> + {user.name} + </p> + <p class="text-xs"> + {user.email} + </p> + </div> + </button> + </div> + {/if} + {/each} + </div> + {/if} </div> - {/if} - <hr class="my-4" /> - - <Stack gap={6}> - {#if sharedLinks.length > 0} - <div class="flex justify-between items-center"> - <Text>{$t('shared_links')}</Text> - <Link href={AppRoute.SHARED_LINKS} class="text-sm">{$t('view_all')}</Link> + {#if users.length > 0} + <div class="py-3"> + <Button + size="small" + fullWidth + shape="round" + disabled={Object.keys(selectedUsers).length === 0} + onclick={() => + onClose({ + action: 'sharedUsers', + data: Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })), + })}>{$t('add')}</Button + > </div> - - <Stack gap={4}> - {#each sharedLinks as sharedLink (sharedLink.id)} - <AlbumSharedLink {album} {sharedLink} onViewQrCode={() => handleViewQrCode(sharedLink)} /> - {/each} - </Stack> {/if} - <Button - leadingIcon={mdiLink} - size="small" - shape="round" - fullWidth - onclick={() => onClose({ action: 'sharedLink' })}>{$t('create_link')}</Button - > - </Stack> - </FullScreenModal> + <hr class="my-4" /> + + <Stack gap={6}> + {#if sharedLinks.length > 0} + <div class="flex justify-between items-center"> + <Text>{$t('shared_links')}</Text> + <Link href={AppRoute.SHARED_LINKS} class="text-sm">{$t('view_all')}</Link> + </div> + + <Stack gap={4}> + {#each sharedLinks as sharedLink (sharedLink.id)} + <AlbumSharedLink {album} {sharedLink} onViewQrCode={() => handleViewQrCode(sharedLink)} /> + {/each} + </Stack> + {/if} + + <Button + leadingIcon={mdiLink} + size="small" + shape="round" + fullWidth + onclick={() => onClose({ action: 'sharedLink' })}>{$t('create_link')}</Button + > + </Stack> + </ModalBody> + </Modal> {/if} diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 8eba11fcfd..7fcc70ae25 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -453,150 +453,6 @@ <div class="flex overflow-hidden" use:scrollMemoryClearer={{ routeStartsWith: AppRoute.ALBUMS }}> <div class="relative w-full shrink"> - {#if assetInteraction.selectionActive} - <AssetSelectControlBar - assets={assetInteraction.selectedAssets} - clearSelect={() => assetInteraction.clearMultiselect()} - > - <CreateSharedLink /> - <SelectAllAssets {assetStore} {assetInteraction} /> - <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}> - <AddToAlbum /> - <AddToAlbum shared /> - </ButtonContextMenu> - {#if assetInteraction.isAllUserOwned} - <FavoriteAction - removeFavorite={assetInteraction.isAllFavorite} - onFavorite={(ids, isFavorite) => - assetStore.updateAssetOperation(ids, (asset) => { - asset.isFavorite = isFavorite; - return { remove: false }; - })} - ></FavoriteAction> - {/if} - <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> - <DownloadAction menuItem filename="{album.albumName}.zip" /> - {#if assetInteraction.isAllUserOwned} - <ChangeDate menuItem /> - <ChangeDescription menuItem /> - <ChangeLocation menuItem /> - {#if assetInteraction.selectedAssets.length === 1} - <MenuOption - text={$t('set_as_album_cover')} - icon={mdiImageOutline} - onClick={() => updateThumbnailUsingCurrentSelection()} - /> - {/if} - <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} /> - {/if} - - {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} - <TagAction menuItem /> - {/if} - - {#if isOwned || assetInteraction.isAllUserOwned} - <RemoveFromAlbum menuItem bind:album onRemove={handleRemoveAssets} /> - {/if} - {#if assetInteraction.isAllUserOwned} - <DeleteAssets menuItem onAssetDelete={handleRemoveAssets} /> - {/if} - </ButtonContextMenu> - </AssetSelectControlBar> - {:else} - {#if viewMode === AlbumPageViewMode.VIEW} - <ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(backUrl)}> - {#snippet trailing()} - {#if isEditor} - <CircleIconButton - title={$t('add_photos')} - onclick={async () => { - assetStore.suspendTransitions = true; - viewMode = AlbumPageViewMode.SELECT_ASSETS; - oldAt = { at: $gridScrollTarget?.at }; - await navigate( - { targetRoute: 'current', assetId: null, assetGridRouteSearchParams: { at: null } }, - { replaceState: true }, - ); - }} - icon={mdiImagePlusOutline} - /> - {/if} - - {#if isOwned} - <CircleIconButton title={$t('share')} onclick={handleShare} icon={mdiShareVariantOutline} /> - {/if} - - <AlbumMap {album} /> - - {#if album.assetCount > 0} - <CircleIconButton title={$t('slideshow')} onclick={handleStartSlideshow} icon={mdiPresentationPlay} /> - <CircleIconButton title={$t('download')} onclick={handleDownloadAlbum} icon={mdiFolderDownloadOutline} /> - {/if} - - {#if isOwned} - <ButtonContextMenu icon={mdiDotsVertical} title={$t('album_options')}> - {#if album.assetCount > 0} - <MenuOption - icon={mdiImageOutline} - text={$t('select_album_cover')} - onClick={() => (viewMode = AlbumPageViewMode.SELECT_THUMBNAIL)} - /> - <MenuOption - icon={mdiCogOutline} - text={$t('options')} - onClick={() => (viewMode = AlbumPageViewMode.OPTIONS)} - /> - {/if} - - <MenuOption icon={mdiDeleteOutline} text={$t('delete_album')} onClick={() => handleRemoveAlbum()} /> - </ButtonContextMenu> - {/if} - - {#if isCreatingSharedAlbum && album.albumUsers.length === 0} - <Button size="small" disabled={album.assetCount === 0} onclick={handleShare}> - {$t('share')} - </Button> - {/if} - {/snippet} - </ControlAppBar> - {/if} - - {#if viewMode === AlbumPageViewMode.SELECT_ASSETS} - <ControlAppBar onClose={handleCloseSelectAssets}> - {#snippet leading()} - <p class="text-lg dark:text-immich-dark-fg"> - {#if !timelineInteraction.selectionActive} - {$t('add_to_album')} - {:else} - {$t('selected_count', { values: { count: timelineInteraction.selectedAssets.length } })} - {/if} - </p> - {/snippet} - - {#snippet trailing()} - <button - type="button" - onclick={handleSelectFromComputer} - class="rounded-lg px-6 py-2 text-sm font-medium text-immich-primary transition-all hover:bg-immich-primary/10 dark:text-immich-dark-primary dark:hover:bg-immich-dark-primary/25" - > - {$t('select_from_computer')} - </button> - <Button size="small" disabled={!timelineInteraction.selectionActive} onclick={handleAddAssets} - >{$t('done')}</Button - > - {/snippet} - </ControlAppBar> - {/if} - - {#if viewMode === AlbumPageViewMode.SELECT_THUMBNAIL} - <ControlAppBar onClose={() => (viewMode = AlbumPageViewMode.VIEW)}> - {#snippet leading()} - {$t('select_album_cover')} - {/snippet} - </ControlAppBar> - {/if} - {/if} - <main class="relative h-dvh overflow-hidden px-6 max-md:pt-(--navbar-height-md) pt-(--navbar-height)"> <AssetGrid enableRouting={viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : true} @@ -685,7 +541,7 @@ <button type="button" onclick={() => (viewMode = AlbumPageViewMode.SELECT_ASSETS)} - class="mt-5 bg-subtle flex w-full place-items-center gap-6 rounded-md border px-8 py-8 text-immich-fg transition-all hover:bg-gray-100 hover:text-immich-primary dark:border-none dark:text-immich-dark-fg dark:hover:text-immich-dark-primary" + class="mt-5 bg-subtle flex w-full place-items-center gap-6 rounded-2xl border px-8 py-8 text-immich-fg transition-all hover:bg-gray-100 dark:hover:bg-gray-500/20 hover:text-immich-primary dark:border-none dark:text-immich-dark-fg dark:hover:text-immich-dark-primary" > <span class="text-text-immich-primary dark:text-immich-dark-primary" ><Icon path={mdiPlus} size="24" /> @@ -710,6 +566,150 @@ </div> {/if} </main> + + {#if assetInteraction.selectionActive} + <AssetSelectControlBar + assets={assetInteraction.selectedAssets} + clearSelect={() => assetInteraction.clearMultiselect()} + > + <CreateSharedLink /> + <SelectAllAssets {assetStore} {assetInteraction} /> + <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}> + <AddToAlbum /> + <AddToAlbum shared /> + </ButtonContextMenu> + {#if assetInteraction.isAllUserOwned} + <FavoriteAction + removeFavorite={assetInteraction.isAllFavorite} + onFavorite={(ids, isFavorite) => + assetStore.updateAssetOperation(ids, (asset) => { + asset.isFavorite = isFavorite; + return { remove: false }; + })} + ></FavoriteAction> + {/if} + <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')} offset={{ x: 175, y: 25 }}> + <DownloadAction menuItem filename="{album.albumName}.zip" /> + {#if assetInteraction.isAllUserOwned} + <ChangeDate menuItem /> + <ChangeDescription menuItem /> + <ChangeLocation menuItem /> + {#if assetInteraction.selectedAssets.length === 1} + <MenuOption + text={$t('set_as_album_cover')} + icon={mdiImageOutline} + onClick={() => updateThumbnailUsingCurrentSelection()} + /> + {/if} + <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} /> + {/if} + + {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} + <TagAction menuItem /> + {/if} + + {#if isOwned || assetInteraction.isAllUserOwned} + <RemoveFromAlbum menuItem bind:album onRemove={handleRemoveAssets} /> + {/if} + {#if assetInteraction.isAllUserOwned} + <DeleteAssets menuItem onAssetDelete={handleRemoveAssets} /> + {/if} + </ButtonContextMenu> + </AssetSelectControlBar> + {:else} + {#if viewMode === AlbumPageViewMode.VIEW} + <ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(backUrl)}> + {#snippet trailing()} + {#if isEditor} + <CircleIconButton + title={$t('add_photos')} + onclick={async () => { + assetStore.suspendTransitions = true; + viewMode = AlbumPageViewMode.SELECT_ASSETS; + oldAt = { at: $gridScrollTarget?.at }; + await navigate( + { targetRoute: 'current', assetId: null, assetGridRouteSearchParams: { at: null } }, + { replaceState: true }, + ); + }} + icon={mdiImagePlusOutline} + /> + {/if} + + {#if isOwned} + <CircleIconButton title={$t('share')} onclick={handleShare} icon={mdiShareVariantOutline} /> + {/if} + + <AlbumMap {album} /> + + {#if album.assetCount > 0} + <CircleIconButton title={$t('slideshow')} onclick={handleStartSlideshow} icon={mdiPresentationPlay} /> + <CircleIconButton title={$t('download')} onclick={handleDownloadAlbum} icon={mdiFolderDownloadOutline} /> + {/if} + + {#if isOwned} + <ButtonContextMenu icon={mdiDotsVertical} title={$t('album_options')} offset={{ x: 175, y: 25 }}> + {#if album.assetCount > 0} + <MenuOption + icon={mdiImageOutline} + text={$t('select_album_cover')} + onClick={() => (viewMode = AlbumPageViewMode.SELECT_THUMBNAIL)} + /> + <MenuOption + icon={mdiCogOutline} + text={$t('options')} + onClick={() => (viewMode = AlbumPageViewMode.OPTIONS)} + /> + {/if} + + <MenuOption icon={mdiDeleteOutline} text={$t('delete_album')} onClick={() => handleRemoveAlbum()} /> + </ButtonContextMenu> + {/if} + + {#if isCreatingSharedAlbum && album.albumUsers.length === 0} + <Button size="small" disabled={album.assetCount === 0} onclick={handleShare}> + {$t('share')} + </Button> + {/if} + {/snippet} + </ControlAppBar> + {/if} + + {#if viewMode === AlbumPageViewMode.SELECT_ASSETS} + <ControlAppBar onClose={handleCloseSelectAssets}> + {#snippet leading()} + <p class="text-lg dark:text-immich-dark-fg"> + {#if !timelineInteraction.selectionActive} + {$t('add_to_album')} + {:else} + {$t('selected_count', { values: { count: timelineInteraction.selectedAssets.length } })} + {/if} + </p> + {/snippet} + + {#snippet trailing()} + <button + type="button" + onclick={handleSelectFromComputer} + class="rounded-lg px-6 py-2 text-sm font-medium text-immich-primary transition-all hover:bg-immich-primary/10 dark:text-immich-dark-primary dark:hover:bg-immich-dark-primary/25" + > + {$t('select_from_computer')} + </button> + <Button size="small" disabled={!timelineInteraction.selectionActive} onclick={handleAddAssets} + >{$t('done')}</Button + > + {/snippet} + </ControlAppBar> + {/if} + + {#if viewMode === AlbumPageViewMode.SELECT_THUMBNAIL} + <ControlAppBar onClose={() => (viewMode = AlbumPageViewMode.VIEW)}> + {#snippet leading()} + {$t('select_album_cover')} + {/snippet} + </ControlAppBar> + {/if} + {/if} </div> {#if album.albumUsers.length > 0 && album && isShowActivity && $user && !$showAssetViewer} <div class="flex"> diff --git a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte index 5353724591..0ee796ee0a 100644 --- a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -40,6 +40,20 @@ }; </script> +<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}> + <AssetGrid + enableRouting={true} + {assetStore} + {assetInteraction} + removeAction={AssetAction.UNARCHIVE} + onEscape={handleEscape} + > + {#snippet empty()} + <EmptyPlaceholder text={$t('no_archived_assets_message')} /> + {/snippet} + </AssetGrid> +</UserPageLayout> + {#if assetInteraction.selectionActive} <AssetSelectControlBar assets={assetInteraction.selectedAssets} @@ -73,17 +87,3 @@ </ButtonContextMenu> </AssetSelectControlBar> {/if} - -<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}> - <AssetGrid - enableRouting={true} - {assetStore} - {assetInteraction} - removeAction={AssetAction.UNARCHIVE} - onEscape={handleEscape} - > - {#snippet empty()} - <EmptyPlaceholder text={$t('no_archived_assets_message')} /> - {/snippet} - </AssetGrid> -</UserPageLayout> diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte index 7f02bff8e0..b67c4fb6b7 100644 --- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -44,6 +44,21 @@ }; </script> +<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}> + <AssetGrid + enableRouting={true} + withStacked={true} + {assetStore} + {assetInteraction} + removeAction={AssetAction.UNFAVORITE} + onEscape={handleEscape} + > + {#snippet empty()} + <EmptyPlaceholder text={$t('no_favorites_message')} /> + {/snippet} + </AssetGrid> +</UserPageLayout> + <!-- Multiselection mode app bar --> {#if assetInteraction.selectionActive} <AssetSelectControlBar @@ -74,18 +89,3 @@ </ButtonContextMenu> </AssetSelectControlBar> {/if} - -<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}> - <AssetGrid - enableRouting={true} - withStacked={true} - {assetStore} - {assetInteraction} - removeAction={AssetAction.UNFAVORITE} - onEscape={handleEscape} - > - {#snippet empty()} - <EmptyPlaceholder text={$t('no_favorites_message')} /> - {/snippet} - </AssetGrid> -</UserPageLayout> diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte index 0de8206193..a0ec649fea 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -90,6 +90,44 @@ }; </script> +<UserPageLayout title={data.meta.title}> + {#snippet sidebar()} + <Sidebar> + <SkipLink target={`#${headerId}`} text={$t('skip_to_folders')} breakpoint="md" /> + <section> + <div class="text-xs ps-4 mb-2 dark:text-white">{$t('explorer').toUpperCase()}</div> + <div class="h-full"> + <TreeItems + icons={{ default: mdiFolderOutline, active: mdiFolder }} + items={tree} + active={currentPath} + getLink={getLinkForPath} + /> + </div> + </section> + </Sidebar> + {/snippet} + + <Breadcrumbs {pathSegments} icon={mdiFolderHome} title={$t('folders')} getLink={getLinkForPath} /> + + <section class="mt-2 h-[calc(100%-(--spacing(20)))] overflow-auto immich-scrollbar"> + <TreeItemThumbnails items={currentTreeItems} icon={mdiFolder} onClick={handleNavigateToFolder} /> + + <!-- Assets --> + {#if data.pathAssets && data.pathAssets.length > 0} + <div bind:clientHeight={viewport.height} bind:clientWidth={viewport.width} class="mt-2"> + <GalleryViewer + assets={data.pathAssets} + {assetInteraction} + {viewport} + showAssetName={true} + pageHeaderOffset={54} + /> + </div> + {/if} + </section> +</UserPageLayout> + {#if assetInteraction.selectionActive} <div class="fixed top-0 start-0 w-full"> <AssetSelectControlBar @@ -132,41 +170,3 @@ </AssetSelectControlBar> </div> {/if} - -<UserPageLayout title={data.meta.title}> - {#snippet sidebar()} - <Sidebar> - <SkipLink target={`#${headerId}`} text={$t('skip_to_folders')} breakpoint="md" /> - <section> - <div class="text-xs ps-4 mb-2 dark:text-white">{$t('explorer').toUpperCase()}</div> - <div class="h-full"> - <TreeItems - icons={{ default: mdiFolderOutline, active: mdiFolder }} - items={tree} - active={currentPath} - getLink={getLinkForPath} - /> - </div> - </section> - </Sidebar> - {/snippet} - - <Breadcrumbs {pathSegments} icon={mdiFolderHome} title={$t('folders')} getLink={getLinkForPath} /> - - <section class="mt-2 h-[calc(100%-(--spacing(20)))] overflow-auto immich-scrollbar"> - <TreeItemThumbnails items={currentTreeItems} icon={mdiFolder} onClick={handleNavigateToFolder} /> - - <!-- Assets --> - {#if data.pathAssets && data.pathAssets.length > 0} - <div bind:clientHeight={viewport.height} bind:clientWidth={viewport.width} class="mt-2"> - <GalleryViewer - assets={data.pathAssets} - {assetInteraction} - {viewport} - showAssetName={true} - pageHeaderOffset={54} - /> - </div> - {/if} - </section> -</UserPageLayout> diff --git a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte index 9c41a7fe59..32b20ab1dd 100644 --- a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -51,26 +51,9 @@ }; </script> -<!-- Multi-selection mode app bar --> -{#if assetInteraction.selectionActive} - <AssetSelectControlBar - assets={assetInteraction.selectedAssets} - clearSelect={() => assetInteraction.clearMultiselect()} - > - <SelectAllAssets withText {assetStore} {assetInteraction} /> - <SetVisibilityAction unlock onVisibilitySet={handleMoveOffLockedFolder} /> - <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> - <DownloadAction menuItem /> - <ChangeDate menuItem /> - <ChangeLocation menuItem /> - <DeleteAssets menuItem force onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} /> - </ButtonContextMenu> - </AssetSelectControlBar> -{/if} - <UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}> {#snippet buttons()} - <Button size="small" variant="filled" color="warning" leadingIcon={mdiLockOutline} onclick={handleLock}> + <Button size="small" variant="ghost" color="primary" leadingIcon={mdiLockOutline} onclick={handleLock}> {$t('lock')} </Button> {/snippet} @@ -87,3 +70,20 @@ {/snippet} </AssetGrid> </UserPageLayout> + +<!-- Multi-selection mode app bar --> +{#if assetInteraction.selectionActive} + <AssetSelectControlBar + assets={assetInteraction.selectedAssets} + clearSelect={() => assetInteraction.clearMultiselect()} + > + <SelectAllAssets withText {assetStore} {assetInteraction} /> + <SetVisibilityAction unlock onVisibilitySet={handleMoveOffLockedFolder} /> + <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> + <DownloadAction menuItem /> + <ChangeDate menuItem /> + <ChangeLocation menuItem /> + <DeleteAssets menuItem force onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} /> + </ButtonContextMenu> + </AssetSelectControlBar> +{/if} diff --git a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index c921d6a7e9..48d830d78c 100644 --- a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -43,6 +43,8 @@ </script> <main class="grid h-dvh pt-18"> + <AssetGrid enableRouting={true} {assetStore} {assetInteraction} onEscape={handleEscape} /> + {#if assetInteraction.selectionActive} <AssetSelectControlBar assets={assetInteraction.selectedAssets} @@ -64,5 +66,4 @@ {/snippet} </ControlAppBar> {/if} - <AssetGrid enableRouting={true} {assetStore} {assetInteraction} onEscape={handleEscape} /> </main> diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 66c8ef0af4..6e47d74413 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -421,11 +421,11 @@ class="flex flex-col justify-center text-start px-4 text-immich-primary dark:text-immich-dark-primary" > <p class="w-40 sm:w-72 font-medium truncate">{person.name || $t('add_a_name')}</p> - <p class="text-sm text-gray-500 dark:text-immich-gray"> + <p class="text-sm text-gray-500 dark:text-gray-400"> {$t('assets_count', { values: { count: numberOfAssets } })} </p> {#if person.birthDate} - <p class="text-sm text-gray-500 dark:text-immich-gray"> + <p class="text-sm text-gray-500 dark:text-gray-400"> {$t('person_birthdate', { values: { date: DateTime.fromISO(person.birthDate).toLocaleString( diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte index c8329dee32..c897ed4791 100644 --- a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -4,7 +4,6 @@ import SkipLink from '$lib/components/elements/buttons/skip-link.svelte'; import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte'; import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; - import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import { notificationController, NotificationType, @@ -20,7 +19,7 @@ import { AssetStore } from '$lib/stores/assets-store.svelte'; import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils'; import { deleteTag, getAllTags, updateTag, upsertTags, type TagResponseDto } from '@immich/sdk'; - import { Button, HStack, Text } from '@immich/ui'; + import { Button, HStack, Modal, ModalBody, ModalFooter, Text } from '@immich/ui'; import { mdiPencil, mdiPlus, mdiTag, mdiTagMultiple, mdiTrashCanOutline } from '@mdi/js'; import { onDestroy } from 'svelte'; import { t } from 'svelte-i18n'; @@ -192,47 +191,55 @@ </UserPageLayout> {#if isNewOpen} - <FullScreenModal title={$t('create_tag')} icon={mdiTag} onClose={handleCancel}> - <div class="text-immich-primary dark:text-immich-dark-primary"> - <p class="text-sm dark:text-immich-dark-fg"> - {$t('create_tag_description')} - </p> - </div> - - <form {onsubmit} autocomplete="off" id="create-tag-form"> - <div class="my-4 flex flex-col gap-2"> - <SettingInputField - inputType={SettingInputFieldType.TEXT} - label={$t('tag').toUpperCase()} - bind:value={newTagValue} - required={true} - autofocus={true} - /> + <Modal size="small" title={$t('create_tag')} icon={mdiTag} onClose={handleCancel}> + <ModalBody> + <div class="text-immich-primary dark:text-immich-dark-primary"> + <p class="text-sm dark:text-immich-dark-fg"> + {$t('create_tag_description')} + </p> </div> - </form> - {#snippet stickyBottom()} - <Button color="secondary" fullWidth shape="round" onclick={() => handleCancel()}>{$t('cancel')}</Button> - <Button type="submit" fullWidth shape="round" form="create-tag-form">{$t('create')}</Button> - {/snippet} - </FullScreenModal> + <form {onsubmit} autocomplete="off" id="create-tag-form"> + <div class="my-4 flex flex-col gap-2"> + <SettingInputField + inputType={SettingInputFieldType.TEXT} + label={$t('tag').toUpperCase()} + bind:value={newTagValue} + required={true} + autofocus={true} + /> + </div> + </form> + </ModalBody> + + <ModalFooter> + <div class="flex w-full gap-2"> + <Button color="secondary" fullWidth shape="round" onclick={() => handleCancel()}>{$t('cancel')}</Button> + <Button type="submit" fullWidth shape="round" form="create-tag-form">{$t('create')}</Button> + </div> + </ModalFooter> + </Modal> {/if} {#if isEditOpen} - <FullScreenModal title={$t('edit_tag')} icon={mdiTag} onClose={handleCancel}> - <form {onsubmit} autocomplete="off" id="edit-tag-form"> - <div class="my-4 flex flex-col gap-2"> - <SettingInputField - inputType={SettingInputFieldType.COLOR} - label={$t('color').toUpperCase()} - bind:value={newTagColor} - /> - </div> - </form> + <Modal title={$t('edit_tag')} icon={mdiTag} onClose={handleCancel}> + <ModalBody> + <form {onsubmit} autocomplete="off" id="edit-tag-form"> + <div class="my-4 flex flex-col gap-2"> + <SettingInputField + inputType={SettingInputFieldType.COLOR} + label={$t('color').toUpperCase()} + bind:value={newTagColor} + /> + </div> + </form> + </ModalBody> - {#snippet stickyBottom()} - <Button color="secondary" fullWidth shape="round" onclick={() => handleCancel()}>{$t('cancel')}</Button> - <Button type="submit" fullWidth shape="round" form="edit-tag-form">{$t('save')}</Button> - {/snippet} - </FullScreenModal> + <ModalFooter> + <div class="flex w-full gap-2"> + <Button color="secondary" fullWidth shape="round" onclick={() => handleCancel()}>{$t('cancel')}</Button> + <Button type="submit" fullWidth shape="round" form="edit-tag-form">{$t('save')}</Button> + </div> + </ModalFooter> + </Modal> {/if} diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte index 3750b239d5..36ff2ec266 100644 --- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -90,17 +90,6 @@ }; </script> -{#if assetInteraction.selectionActive} - <AssetSelectControlBar - assets={assetInteraction.selectedAssets} - clearSelect={() => assetInteraction.clearMultiselect()} - > - <SelectAllAssets {assetStore} {assetInteraction} /> - <DeleteAssets force onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} /> - <RestoreAssets onRestore={(assetIds) => assetStore.removeAssets(assetIds)} /> - </AssetSelectControlBar> -{/if} - {#if $featureFlags.loaded && $featureFlags.trash} <UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}> {#snippet buttons()} @@ -138,3 +127,14 @@ </AssetGrid> </UserPageLayout> {/if} + +{#if assetInteraction.selectionActive} + <AssetSelectControlBar + assets={assetInteraction.selectedAssets} + clearSelect={() => assetInteraction.clearMultiselect()} + > + <SelectAllAssets {assetStore} {assetInteraction} /> + <DeleteAssets force onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} /> + <RestoreAssets onRestore={(assetIds) => assetStore.removeAssets(assetIds)} /> + </AssetSelectControlBar> +{/if} diff --git a/web/src/routes/admin/users/[id]/+page.svelte b/web/src/routes/admin/users/[id]/+page.svelte index 6b0b0234fb..a33f2e9ec5 100644 --- a/web/src/routes/admin/users/[id]/+page.svelte +++ b/web/src/routes/admin/users/[id]/+page.svelte @@ -249,47 +249,49 @@ <div> <Card color="secondary"> <CardHeader> - <div class="flex items-center gap-2"> + <div class="flex items-center gap-2 px-4 py-2 text-primary"> <Icon icon={mdiAccountOutline} size="1.5rem" /> <CardTitle>{$t('profile')}</CardTitle> </div> </CardHeader> <CardBody> - <Stack gap={2}> - <div> - <Heading tag="h3" size="tiny">{$t('name')}</Heading> - <Text>{user.name}</Text> - </div> - <div> - <Heading tag="h3" size="tiny">{$t('email')}</Heading> - <Text>{user.email}</Text> - </div> - <div> - <Heading tag="h3" size="tiny">{$t('created_at')}</Heading> - <Text>{user.createdAt}</Text> - </div> - <div> - <Heading tag="h3" size="tiny">{$t('updated_at')}</Heading> - <Text>{user.updatedAt}</Text> - </div> - <div> - <Heading tag="h3" size="tiny">{$t('id')}</Heading> - <Code>{user.id}</Code> - </div> - </Stack> + <div class="px-4 pb-7"> + <Stack gap={2}> + <div> + <Heading tag="h3" size="tiny">{$t('name')}</Heading> + <Text>{user.name}</Text> + </div> + <div> + <Heading tag="h3" size="tiny">{$t('email')}</Heading> + <Text>{user.email}</Text> + </div> + <div> + <Heading tag="h3" size="tiny">{$t('created_at')}</Heading> + <Text>{user.createdAt}</Text> + </div> + <div> + <Heading tag="h3" size="tiny">{$t('updated_at')}</Heading> + <Text>{user.updatedAt}</Text> + </div> + <div> + <Heading tag="h3" size="tiny">{$t('id')}</Heading> + <Code>{user.id}</Code> + </div> + </Stack> + </div> </CardBody> </Card> </div> <Card color="secondary"> <CardHeader> - <div class="flex items-center gap-2"> + <div class="flex items-center gap-2 px-4 py-2 text-primary"> <Icon icon={mdiFeatureSearchOutline} size="1.5rem" /> <CardTitle>{$t('features')}</CardTitle> </div> </CardHeader> <CardBody> - <div> - <Stack gap={2}> + <div class="px-4 pb-4"> + <Stack gap={3}> <Field readOnly label={$t('email_notifications')}> <Switch checked={userPreferences.emailNotifications.enabled} color="primary" /> </Field> @@ -320,13 +322,13 @@ </Card> <Card color="secondary"> <CardHeader> - <div class="flex items-center gap-2"> + <div class="flex items-center gap-2 px-4 py-2 text-primary"> <Icon icon={mdiChartPieOutline} size="1.5rem" /> <CardTitle>{$t('storage_quota')}</CardTitle> </div> </CardHeader> <CardBody> - <div> + <div class="px-4 pb-4"> {#if user.quotaSizeInBytes !== null && user.quotaSizeInBytes >= 0} <Text> {$t('storage_usage', {