mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
chore: tailwindcss v4 and z-war clean up (#18358)
* chore: styling tweak * replace full-screen-modal, update docs * scrubber * fix: control app bar in memory viewer * face lift * pr feedback * clean up
This commit is contained in:
parent
2431e04a09
commit
c8641d24f6
@ -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
|
||||
|
||||
|
24
web/package-lock.json
generated
24
web/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -1,5 +1,6 @@
|
||||
@import 'tailwindcss';
|
||||
@import '@immich/ui/theme/default.css';
|
||||
/* @import '/usr/ui/dist/theme/default.css'; */
|
||||
|
||||
@config '../tailwind.config.js';
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
||||
|
@ -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]">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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)}
|
||||
|
@ -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}
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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(
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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', {
|
||||
|
Loading…
x
Reference in New Issue
Block a user