Merge remote-tracking branch 'origin/main' into lighter_buckets_web
This commit is contained in:
@@ -58,6 +58,8 @@ export default typescriptEslint.config(
|
||||
},
|
||||
},
|
||||
|
||||
ignores: ['**/service-worker/**'],
|
||||
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
|
||||
14
web/package-lock.json
generated
14
web/package-lock.json
generated
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "1.132.1",
|
||||
"version": "1.132.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich-web",
|
||||
"version": "1.132.1",
|
||||
"version": "1.132.3",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@immich/ui": "^0.17.3",
|
||||
"@immich/ui": "^0.18.1",
|
||||
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@photo-sphere-viewer/core": "^5.11.5",
|
||||
@@ -82,7 +82,7 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.132.1",
|
||||
"version": "1.132.3",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
@@ -1320,9 +1320,9 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@immich/ui": {
|
||||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.17.4.tgz",
|
||||
"integrity": "sha512-a6M7Fxno5fwY5A0kxdluS8r+A4L6xZhSTKMW8c8hoFhQHvbBTHAsGFKQF3GOEQLOlUuvsS2Lt7dMevBlAPgo/A==",
|
||||
"version": "0.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.18.1.tgz",
|
||||
"integrity": "sha512-XWWO6OTfH3MektyxCn0hWefZyOGyWwwx/2zHinuShpxTHSyfveJ4mOkFP8DkyMz0dnvJ1EfdkPBMkld3y5R/Hw==",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@mdi/js": "^7.4.47",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "1.132.1",
|
||||
"version": "1.132.3",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -27,7 +27,7 @@
|
||||
"dependencies": {
|
||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@immich/ui": "^0.17.3",
|
||||
"@immich/ui": "^0.18.1",
|
||||
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@photo-sphere-viewer/core": "^5.11.5",
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
let isIdle = $derived(!queueStatus.isActive && !queueStatus.isPaused);
|
||||
let multipleButtons = $derived(allText || refreshText);
|
||||
|
||||
const commonClasses = 'flex place-items-center justify-between w-full py-2 sm:py-4 pr-4 pl-6';
|
||||
const commonClasses = 'flex place-items-center justify-between w-full py-2 sm:py-4 pe-4 ps-6';
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -110,7 +110,7 @@
|
||||
|
||||
<div class="mt-2 flex w-full max-w-md flex-col sm:flex-row">
|
||||
<div
|
||||
class="{commonClasses} rounded-t-lg bg-immich-primary text-white dark:bg-immich-dark-primary dark:text-immich-dark-gray sm:rounded-l-lg sm:rounded-r-none"
|
||||
class="{commonClasses} rounded-t-lg bg-immich-primary text-white dark:bg-immich-dark-primary dark:text-immich-dark-gray sm:rounded-s-lg sm:rounded-e-none"
|
||||
>
|
||||
<p>{$t('active')}</p>
|
||||
<p class="text-2xl">
|
||||
@@ -119,7 +119,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="{commonClasses} flex-row-reverse rounded-b-lg bg-gray-200 text-immich-dark-bg dark:bg-gray-700 dark:text-immich-gray sm:rounded-l-none sm:rounded-r-lg"
|
||||
class="{commonClasses} flex-row-reverse rounded-b-lg bg-gray-200 text-immich-dark-bg dark:bg-gray-700 dark:text-immich-gray sm:rounded-s-none sm:rounded-e-lg"
|
||||
>
|
||||
<p class="text-2xl">
|
||||
{waitingCount.toLocaleString($locale)}
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
<span class="text-[#DCDADA] dark:text-[#525252]">{zeros(statsUsage)}</span><span
|
||||
class="text-immich-primary dark:text-immich-dark-primary">{statsUsage}</span
|
||||
>
|
||||
<span class="my-auto ml-2 text-center text-base font-light text-gray-400">{statsUsageUnit}</span>
|
||||
<span class="my-auto ms-2 text-center text-base font-light text-gray-400">{statsUsageUnit}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,7 +88,7 @@
|
||||
|
||||
<div>
|
||||
<p class="text-sm dark:text-immich-dark-fg">{$t('user_usage_detail').toUpperCase()}</p>
|
||||
<table class="mt-5 w-full text-left">
|
||||
<table class="mt-5 w-full text-start">
|
||||
<thead
|
||||
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
|
||||
>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
class="text-immich-primary dark:text-immich-dark-primary">{value}</span
|
||||
>
|
||||
{#if unit}
|
||||
<span class="absolute -top-5 right-2 text-base font-light text-gray-400">{unit}</span>
|
||||
<span class="absolute -top-5 end-2 text-base font-light text-gray-400">{unit}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -76,13 +76,13 @@
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" onsubmit={(e) => e.preventDefault()}>
|
||||
<div class="ml-4 mt-4 flex flex-col">
|
||||
<div class="ms-4 mt-4 flex flex-col">
|
||||
<SettingAccordion
|
||||
key="oauth"
|
||||
title={$t('admin.oauth_settings')}
|
||||
subtitle={$t('admin.oauth_settings_description')}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
<FormatMessage key="admin.oauth_settings_more_details">
|
||||
{#snippet children({ message })}
|
||||
@@ -243,8 +243,8 @@
|
||||
title={$t('admin.password_settings')}
|
||||
subtitle={$t('admin.password_settings_description')}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ml-4 mt-4 flex flex-col">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col">
|
||||
<SettingSwitch
|
||||
title={$t('admin.password_enable_description')}
|
||||
{disabled}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" {onsubmit}>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.backup_database_enable_description')}
|
||||
{disabled}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" {onsubmit}>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
<Icon path={mdiHelpCircleOutline} class="inline" size="15" />
|
||||
<FormatMessage key="admin.transcoding_codecs_learn_more">
|
||||
@@ -70,7 +70,7 @@
|
||||
title={$t('admin.transcoding_policy')}
|
||||
subtitle={$t('admin.transcoding_policy_description')}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSelect
|
||||
label={$t('admin.transcoding_transcode_policy')}
|
||||
{disabled}
|
||||
@@ -159,7 +159,7 @@
|
||||
title={$t('admin.transcoding_encoding_options')}
|
||||
subtitle={$t('admin.transcoding_encoding_options_description')}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSelect
|
||||
label={$t('admin.transcoding_video_codec')}
|
||||
{disabled}
|
||||
@@ -302,7 +302,7 @@
|
||||
title={$t('admin.transcoding_hardware_acceleration')}
|
||||
subtitle={$t('admin.transcoding_hardware_acceleration_description')}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSelect
|
||||
label={$t('admin.transcoding_acceleration_api')}
|
||||
{disabled}
|
||||
@@ -376,7 +376,7 @@
|
||||
title={$t('advanced')}
|
||||
subtitle={$t('admin.transcoding_advanced_options_description')}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.transcoding_max_b_frames')}
|
||||
@@ -407,7 +407,7 @@
|
||||
</SettingAccordion>
|
||||
</div>
|
||||
|
||||
<div class="ml-4">
|
||||
<div class="ms-4">
|
||||
<SettingButtonsRow
|
||||
onReset={(options) => onReset({ ...options, configKeys: ['ffmpeg'] })}
|
||||
onSave={() => onSave({ ffmpeg: config.ffmpeg })}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" {onsubmit}>
|
||||
<div class="ml-4 mt-4">
|
||||
<div class="ms-4 mt-4">
|
||||
<SettingAccordion
|
||||
key="thumbnail-settings"
|
||||
title={$t('admin.image_thumbnail_title')}
|
||||
@@ -195,7 +195,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-4 mt-4">
|
||||
<div class="ms-4 mt-4">
|
||||
<SettingButtonsRow
|
||||
onReset={(options) => onReset({ ...options, configKeys: ['image'] })}
|
||||
onSave={() => onSave({ image: config.image })}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" {onsubmit}>
|
||||
{#each jobNames as jobName (jobName)}
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
{#if isSystemConfigJobDto(jobName)}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
@@ -71,7 +71,7 @@
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="ml-4">
|
||||
<div class="ms-4">
|
||||
<SettingButtonsRow
|
||||
onReset={(options) => onReset({ ...options, configKeys: ['job'] })}
|
||||
onSave={() => onSave({ job: config.job })}
|
||||
|
||||
@@ -47,14 +47,14 @@
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" {onsubmit}>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingAccordion
|
||||
key="library-watching"
|
||||
title={$t('admin.library_watching_settings')}
|
||||
subtitle={$t('admin.library_watching_settings_description')}
|
||||
isOpen={openByDefault}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.library_watching_enable_description')}
|
||||
{disabled}
|
||||
@@ -69,7 +69,7 @@
|
||||
subtitle={$t('admin.library_scanning_description')}
|
||||
isOpen={openByDefault}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.library_scanning_enable_description')}
|
||||
{disabled}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" {onsubmit}>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.logging_enable_description')}
|
||||
{disabled}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
{#if config.machineLearning.urls.length > 1}
|
||||
<CircleIconButton
|
||||
size="24"
|
||||
class="ml-2"
|
||||
class="ms-2"
|
||||
padding="2"
|
||||
color="red"
|
||||
title=""
|
||||
@@ -88,7 +88,7 @@
|
||||
title={$t('admin.machine_learning_smart_search')}
|
||||
subtitle={$t('admin.machine_learning_smart_search_description')}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.machine_learning_smart_search_enabled')}
|
||||
subtitle={$t('admin.machine_learning_smart_search_enabled_description')}
|
||||
@@ -124,7 +124,7 @@
|
||||
title={$t('admin.machine_learning_duplicate_detection')}
|
||||
subtitle={$t('admin.machine_learning_duplicate_detection_setting_description')}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.machine_learning_duplicate_detection_enabled')}
|
||||
subtitle={$t('admin.machine_learning_duplicate_detection_enabled_description')}
|
||||
@@ -154,7 +154,7 @@
|
||||
title={$t('admin.machine_learning_facial_recognition')}
|
||||
subtitle={$t('admin.machine_learning_facial_recognition_description')}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.machine_learning_facial_recognition_setting')}
|
||||
subtitle={$t('admin.machine_learning_facial_recognition_setting_description')}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<form autocomplete="off" {onsubmit}>
|
||||
<div class="flex flex-col gap-4">
|
||||
<SettingAccordion key="map" title={$t('admin.map_settings')} subtitle={$t('admin.map_settings_description')}>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.map_enable_description')}
|
||||
subtitle={$t('admin.map_implications')}
|
||||
@@ -78,7 +78,7 @@
|
||||
</FormatMessage>
|
||||
</p>
|
||||
{/snippet}
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.map_reverse_geocoding_enable_description')}
|
||||
{disabled}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<div class="mt-2">
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" {onsubmit} class="mx-4 mt-4">
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.metadata_faces_import_setting')}
|
||||
subtitle={$t('admin.metadata_faces_import_setting_description')}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" {onsubmit}>
|
||||
<div class="ml-4 mt-4">
|
||||
<div class="ms-4 mt-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.version_check_enabled_description')}
|
||||
subtitle={$t('admin.version_check_implications')}
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<form autocomplete="off" {onsubmit} class="mt-4">
|
||||
<div class="flex flex-col gap-4">
|
||||
<SettingAccordion key="email" title={$t('email')} subtitle={$t('admin.notification_email_setting_description')}>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.notification_enable_email_notifications')}
|
||||
{disabled}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" {onsubmit}>
|
||||
<div class="mt-4 ml-4">
|
||||
<div class="mt-4 ms-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.server_external_domain_settings')}
|
||||
@@ -52,7 +52,7 @@
|
||||
bind:checked={config.server.publicUsers}
|
||||
/>
|
||||
|
||||
<div class="ml-4">
|
||||
<div class="ms-4">
|
||||
<SettingButtonsRow
|
||||
onReset={(options) => onReset({ ...options, configKeys: ['server'] })}
|
||||
onSave={() => onSave({ server: config.server })}
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
</p>
|
||||
</div>
|
||||
{#await getTemplateOptions() then}
|
||||
<div id="directory-path-builder" class="flex flex-col gap-4 {minified ? '' : 'ml-4 mt-4'}">
|
||||
<div id="directory-path-builder" class="flex flex-col gap-4 {minified ? '' : 'ms-4 mt-4'}">
|
||||
<SettingSwitch
|
||||
title={$t('admin.storage_template_enable_description')}
|
||||
{disabled}
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
title={$t('admin.template_email_settings')}
|
||||
subtitle={$t('admin.template_settings_description')}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
<FormatMessage key="admin.template_email_if_empty">
|
||||
{$t('admin.template_email_if_empty')}
|
||||
@@ -102,7 +102,7 @@
|
||||
onclick={() => getTemplate(templateName, config.templates.email[templateKey])}
|
||||
title={$t('admin.template_email_preview')}
|
||||
>
|
||||
<Icon path={mdiEyeOutline} class="mr-1" />
|
||||
<Icon path={mdiEyeOutline} class="me-1" />
|
||||
{$t('admin.template_email_preview')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" {onsubmit}>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingTextarea
|
||||
{disabled}
|
||||
label={$t('admin.theme_custom_css_settings')}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" {onsubmit}>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch title={$t('admin.trash_enabled_description')} {disabled} bind:checked={config.trash.enabled} />
|
||||
|
||||
<hr />
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" onsubmit={(e) => e.preventDefault()}>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
min={1}
|
||||
@@ -35,7 +35,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="ml-4">
|
||||
<div class="ms-4">
|
||||
<SettingButtonsRow
|
||||
onReset={(options) => onReset({ ...options, configKeys: ['user'] })}
|
||||
onSave={() => onSave({ user: config.user })}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => toggleAlbumGroupCollapsing(group.id)}
|
||||
class="w-full text-left mt-2 pt-2 pr-2 pb-2 rounded-md transition-colors cursor-pointer dark:text-immich-dark-fg hover:text-immich-primary dark:hover:text-immich-dark-primary hover:bg-immich-gray dark:hover:bg-immich-dark-gray"
|
||||
class="w-full text-start mt-2 pt-2 pe-2 pb-2 rounded-md transition-colors cursor-pointer dark:text-immich-dark-fg hover:text-immich-primary dark:hover:text-immich-dark-primary hover:bg-immich-gray dark:hover:bg-immich-dark-gray"
|
||||
aria-expanded={!isCollapsed}
|
||||
>
|
||||
<Icon
|
||||
@@ -57,7 +57,7 @@
|
||||
class="inline-block -mt-2.5 transition-all duration-[250ms] {iconRotation}"
|
||||
/>
|
||||
<span class="font-bold text-3xl text-black dark:text-white">{group.name}</span>
|
||||
<span class="ml-1.5">({$t('albums_count', { values: { count: albums.length } })})</span>
|
||||
<span class="ms-1.5">({$t('albums_count', { values: { count: albums.length } })})</span>
|
||||
</button>
|
||||
<hr class="dark:border-immich-dark-gray" />
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
{#if onShowContextMenu}
|
||||
<div
|
||||
id="icon-{album.id}"
|
||||
class="absolute right-6 top-6 z-10 opacity-0 group-hover:opacity-100 focus-within:opacity-100"
|
||||
class="absolute end-6 top-6 z-10 opacity-0 group-hover:opacity-100 focus-within:opacity-100"
|
||||
data-testid="context-button-parent"
|
||||
>
|
||||
<CircleIconButton
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
<!-- ALBUM DESCRIPTION -->
|
||||
{#if album.description}
|
||||
<p
|
||||
class="whitespace-pre-line mb-12 mt-6 w-full pb-2 text-left font-medium text-base text-black dark:text-gray-300"
|
||||
class="whitespace-pre-line mb-12 mt-6 w-full pb-2 text-start font-medium text-base text-black dark:text-gray-300"
|
||||
>
|
||||
{album.description}
|
||||
</p>
|
||||
|
||||
@@ -35,13 +35,13 @@
|
||||
onclick={() => goto(`${AppRoute.ALBUMS}/${album.id}`)}
|
||||
{oncontextmenu}
|
||||
>
|
||||
<td class="text-md text-ellipsis text-left w-8/12 sm:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%] items-center">
|
||||
<td class="text-md text-ellipsis text-start w-8/12 sm:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%] items-center">
|
||||
{album.albumName}
|
||||
{#if album.shared}
|
||||
<Icon
|
||||
path={mdiShareVariantOutline}
|
||||
size="16"
|
||||
class="inline ml-1 opacity-70"
|
||||
class="inline ms-1 opacity-70"
|
||||
title={album.ownerId === $user.id
|
||||
? $t('shared_by_you')
|
||||
: $t('shared_by_user', { values: { user: album.owner.name } })}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
let { groupedAlbums, albumGroupOption = AlbumGroupBy.None, onShowContextMenu }: Props = $props();
|
||||
</script>
|
||||
|
||||
<table class="mt-2 w-full text-left">
|
||||
<table class="mt-2 w-full text-start">
|
||||
<thead
|
||||
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
|
||||
>
|
||||
@@ -48,18 +48,18 @@
|
||||
class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray dark:text-immich-dark-fg"
|
||||
>
|
||||
<tr
|
||||
class="flex w-full place-items-center p-2 md:pl-5 md:pr-5 md:pt-3 md:pb-3"
|
||||
class="flex w-full place-items-center p-2 md:ps-5 md:pe-5 md:pt-3 md:pb-3"
|
||||
onclick={() => toggleAlbumGroupCollapsing(albumGroup.id)}
|
||||
aria-expanded={!isCollapsed}
|
||||
>
|
||||
<td class="text-md text-left -mb-1">
|
||||
<td class="text-md text-start -mb-1">
|
||||
<Icon
|
||||
path={mdiChevronRight}
|
||||
size="20"
|
||||
class="inline-block -mt-2 transition-all duration-[250ms] {iconRotation}"
|
||||
/>
|
||||
<span class="font-bold text-2xl">{albumGroup.name}</span>
|
||||
<span class="ml-1.5">
|
||||
<span class="ms-1.5">
|
||||
({$t('albums_count', { values: { count: albumGroup.albums.length } })})
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
</div>
|
||||
|
||||
<!-- <UserAvatar {user} size="md" /> -->
|
||||
<div class="text-left flex-grow">
|
||||
<div class="text-start flex-grow">
|
||||
<p class="text-immich-fg dark:text-immich-dark-fg">
|
||||
{user.name}
|
||||
</p>
|
||||
@@ -136,7 +136,7 @@
|
||||
class="flex w-full place-items-center gap-4 p-4"
|
||||
>
|
||||
<UserAvatar {user} size="md" />
|
||||
<div class="text-left flex-grow">
|
||||
<div class="text-start flex-grow">
|
||||
<p class="text-immich-fg dark:text-immich-dark-fg">
|
||||
{user.name}
|
||||
</p>
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
>
|
||||
{#each reactions as reaction, index (reaction.id)}
|
||||
{#if reaction.type === ReactionType.Comment}
|
||||
<div class="flex dark:bg-gray-800 bg-gray-200 py-3 pl-3 mt-3 rounded-lg gap-4 justify-start">
|
||||
<div class="flex dark:bg-gray-800 bg-gray-200 py-3 ps-3 mt-3 rounded-lg gap-4 justify-start">
|
||||
<div class="flex items-center">
|
||||
<UserAvatar user={reaction.user} size="sm" />
|
||||
</div>
|
||||
@@ -202,7 +202,7 @@
|
||||
</a>
|
||||
{/if}
|
||||
{#if reaction.user.id === user.id || albumOwnerId === user.id}
|
||||
<div class="mr-4">
|
||||
<div class="me-4">
|
||||
<ButtonContextMenu
|
||||
icon={mdiDotsVertical}
|
||||
title={$t('comment_options')}
|
||||
@@ -231,7 +231,7 @@
|
||||
{/if}
|
||||
{:else if reaction.type === ReactionType.Like}
|
||||
<div class="relative">
|
||||
<div class="flex py-3 pl-3 mt-3 gap-4 items-center text-sm">
|
||||
<div class="flex py-3 ps-3 mt-3 gap-4 items-center text-sm">
|
||||
<div class="text-red-600"><Icon path={mdiHeart} size={20} /></div>
|
||||
|
||||
<div class="w-full" title={`${reaction.user.name} (${reaction.user.email})`}>
|
||||
@@ -255,7 +255,7 @@
|
||||
</a>
|
||||
{/if}
|
||||
{#if reaction.user.id === user.id || albumOwnerId === user.id}
|
||||
<div class="mr-4">
|
||||
<div class="me-4">
|
||||
<ButtonContextMenu
|
||||
icon={mdiDotsVertical}
|
||||
title={$t('reaction_options')}
|
||||
@@ -307,17 +307,17 @@
|
||||
}}
|
||||
class="h-[18px] {disabled
|
||||
? 'cursor-not-allowed'
|
||||
: ''} w-full max-h-56 pr-2 items-center overflow-y-auto leading-4 outline-none resize-none bg-gray-200"
|
||||
: ''} w-full max-h-56 pe-2 items-center overflow-y-auto leading-4 outline-none resize-none bg-gray-200"
|
||||
></textarea>
|
||||
</div>
|
||||
{#if isSendingMessage}
|
||||
<div class="flex items-end place-items-center pb-2 ml-0">
|
||||
<div class="flex items-end place-items-center pb-2 ms-0">
|
||||
<div class="flex w-full place-items-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
</div>
|
||||
{:else if message}
|
||||
<div class="flex items-end w-fit ml-0">
|
||||
<div class="flex items-end w-fit ms-0">
|
||||
<CircleIconButton
|
||||
title={$t('send_message')}
|
||||
size="15"
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
type="button"
|
||||
onclick={onAlbumClick}
|
||||
use:scrollIntoViewIfSelected
|
||||
class="flex w-full gap-4 px-6 py-2 text-left transition-colors hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"
|
||||
class="flex w-full gap-4 px-6 py-2 text-start transition-colors hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"
|
||||
class:bg-gray-200={selected}
|
||||
class:dark:bg-gray-700={selected}
|
||||
>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import NextAssetAction from '$lib/components/asset-viewer/actions/next-asset-action.svelte';
|
||||
import PreviousAssetAction from '$lib/components/asset-viewer/actions/previous-asset-action.svelte';
|
||||
import { AssetAction, ProjectionType } from '$lib/constants';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { updateNumberOfComments } from '$lib/stores/activity.store';
|
||||
import { closeEditorCofirm } from '$lib/stores/asset-editor.store';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
@@ -12,7 +13,7 @@
|
||||
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { websocketEvents } from '$lib/stores/websocket';
|
||||
import { getAssetJobMessage, getSharedLink, handlePromiseError, isSharedLink } from '$lib/utils';
|
||||
import { getAssetJobMessage, getSharedLink, handlePromiseError } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { SlideshowHistory } from '$lib/utils/slideshow-history';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
@@ -117,7 +118,7 @@
|
||||
let zoomToggle = $state(() => void 0);
|
||||
|
||||
const refreshStack = async () => {
|
||||
if (isSharedLink()) {
|
||||
if (authManager.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -244,7 +245,7 @@
|
||||
});
|
||||
|
||||
const handleGetAllAlbums = async () => {
|
||||
if (isSharedLink()) {
|
||||
if (authManager.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -412,7 +413,7 @@
|
||||
}
|
||||
});
|
||||
$effect(() => {
|
||||
if (asset.id && !sharedLink) {
|
||||
if (asset.id) {
|
||||
handlePromiseError(handleGetAllAlbums());
|
||||
}
|
||||
});
|
||||
@@ -422,7 +423,7 @@
|
||||
|
||||
<section
|
||||
id="immich-asset-viewer"
|
||||
class="fixed left-0 top-0 z-[1001] grid size-full grid-cols-4 grid-rows-[64px_1fr] overflow-hidden bg-black"
|
||||
class="fixed start-0 top-0 z-[1001] grid size-full grid-cols-4 grid-rows-[64px_1fr] overflow-hidden bg-black"
|
||||
use:focusTrap
|
||||
>
|
||||
<!-- Top navigation bar -->
|
||||
@@ -547,7 +548,7 @@
|
||||
/>
|
||||
{/if}
|
||||
{#if $slideshowState === SlideshowState.None && isShared && ((album && album.isActivityEnabled) || numberOfComments > 0)}
|
||||
<div class="z-[9999] absolute bottom-0 right-0 mb-20 mr-8">
|
||||
<div class="z-[9999] absolute bottom-0 end-0 mb-20 me-8">
|
||||
<ActivityStatus
|
||||
disabled={!album?.isActivityEnabled}
|
||||
{isLiked}
|
||||
@@ -571,7 +572,7 @@
|
||||
<div
|
||||
transition:fly={{ duration: 150 }}
|
||||
id="detail-panel"
|
||||
class="z-[1002] row-start-1 row-span-4 w-[360px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-l-immich-dark-gray dark:bg-immich-dark-bg"
|
||||
class="z-[1002] row-start-1 row-span-4 w-[360px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-s-immich-dark-gray dark:bg-immich-dark-bg"
|
||||
translate="yes"
|
||||
>
|
||||
<DetailPanel {asset} currentAlbum={album} albums={appearsInAlbums} onClose={() => ($isShowDetail = false)} />
|
||||
@@ -582,7 +583,7 @@
|
||||
<div
|
||||
transition:fly={{ duration: 150 }}
|
||||
id="editor-panel"
|
||||
class="z-[1002] row-start-1 row-span-4 w-[400px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-l-immich-dark-gray dark:bg-immich-dark-bg"
|
||||
class="z-[1002] row-start-1 row-span-4 w-[400px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-s-immich-dark-gray dark:bg-immich-dark-bg"
|
||||
translate="yes"
|
||||
>
|
||||
<EditorPanel {asset} onUpdateSelectedType={handleUpdateSelectedEditType} onClose={closeEditor} />
|
||||
@@ -631,7 +632,7 @@
|
||||
<div
|
||||
transition:fly={{ duration: 150 }}
|
||||
id="activity-panel"
|
||||
class="z-[1002] row-start-1 row-span-5 w-[360px] md:w-[460px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-l-immich-dark-gray dark:bg-immich-dark-bg"
|
||||
class="z-[1002] row-start-1 row-span-5 w-[360px] md:w-[460px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-s-immich-dark-gray dark:bg-immich-dark-bg"
|
||||
translate="yes"
|
||||
>
|
||||
<ActivityViewer
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
{#if asset.exifInfo?.country}
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full text-left justify-between place-items-start gap-4 py-4"
|
||||
class="flex w-full text-start justify-between place-items-start gap-4 py-4"
|
||||
onclick={() => (isOwner ? (isShowChangeLocation = true) : null)}
|
||||
title={isOwner ? $t('edit_location') : ''}
|
||||
class:hover:dark:text-immich-dark-primary={isOwner}
|
||||
@@ -68,7 +68,7 @@
|
||||
{:else if !asset.exifInfo?.city && isOwner}
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full text-left justify-between place-items-start gap-4 py-4 rounded-lg hover:dark:text-immich-dark-primary hover:text-immich-primary"
|
||||
class="flex w-full text-start justify-between place-items-start gap-4 py-4 rounded-lg hover:dark:text-immich-dark-primary hover:text-immich-primary"
|
||||
onclick={() => (isShowChangeLocation = true)}
|
||||
title={$t('add_location')}
|
||||
>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import StarRating from '$lib/components/shared-components/star-rating.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { preferences } from '$lib/stores/user.store';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { updateAsset, type AssetResponseDto } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
import StarRating from '$lib/components/shared-components/star-rating.svelte';
|
||||
import { handlePromiseError, isSharedLink } from '$lib/utils';
|
||||
import { preferences } from '$lib/stores/user.store';
|
||||
|
||||
interface Props {
|
||||
asset: AssetResponseDto;
|
||||
@@ -24,7 +25,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if !isSharedLink() && $preferences?.ratings.enabled}
|
||||
{#if !authManager.key && $preferences?.ratings.enabled}
|
||||
<section class="px-4 pt-2">
|
||||
<StarRating {rating} readOnly={!isOwner} onRating={(rating) => handlePromiseError(handleChangeRating(rating))} />
|
||||
</section>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import TagAssetForm from '$lib/components/forms/tag-asset-form.svelte';
|
||||
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { isSharedLink } from '$lib/utils';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { removeTag, tagAssets } from '$lib/utils/asset-utils';
|
||||
import { getAssetInfo, type AssetResponseDto } from '@immich/sdk';
|
||||
import { mdiClose, mdiPlus } from '@mdi/js';
|
||||
@@ -41,7 +41,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if isOwner && !isSharedLink()}
|
||||
{#if isOwner && !authManager.key}
|
||||
<section class="px-4 mt-4">
|
||||
<div class="flex h-10 w-full items-center justify-between text-sm">
|
||||
<h2>{$t('tags').toUpperCase()}</h2>
|
||||
@@ -50,7 +50,7 @@
|
||||
{#each tags as tag (tag.id)}
|
||||
<div class="flex group transition-all">
|
||||
<a
|
||||
class="inline-block h-min whitespace-nowrap pl-3 pr-1 group-hover:pl-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-tl-full rounded-bl-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 roudned-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">
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-100 dark:text-immich-dark-gray bg-immich-primary/95 dark:bg-immich-dark-primary/95 rounded-tr-full rounded-br-full place-items-center place-content-center pr-2 pl-1 py-1 hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
|
||||
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(tag.id)}
|
||||
>
|
||||
|
||||
@@ -6,15 +6,19 @@
|
||||
import DetailPanelTags from '$lib/components/asset-viewer/detail-panel-tags.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import ChangeDate from '$lib/components/shared-components/change-date.svelte';
|
||||
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
||||
import { AppRoute, QueryParameter, timeToLoadTheMap } from '$lib/constants';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||
import { boundingBoxesArray } from '$lib/stores/people.store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { featureFlags } from '$lib/stores/server-config.store';
|
||||
import { preferences, user } from '$lib/stores/user.store';
|
||||
import { getAssetThumbnailUrl, getPeopleThumbnailUrl, handlePromiseError, isSharedLink } from '$lib/utils';
|
||||
import { getAssetThumbnailUrl, getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
|
||||
import { delay, isFlipped } from '$lib/utils/asset-utils';
|
||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
|
||||
import { fromDateTimeOriginal, fromLocalDateTime } from '$lib/utils/timeline-util';
|
||||
import {
|
||||
AssetMediaSize,
|
||||
@@ -44,9 +48,6 @@
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||
import AlbumListItemDetails from './album-list-item-details.svelte';
|
||||
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
||||
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
|
||||
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||
|
||||
interface Props {
|
||||
asset: AssetResponseDto;
|
||||
@@ -84,7 +85,7 @@
|
||||
|
||||
const handleNewAsset = async (newAsset: AssetResponseDto) => {
|
||||
// TODO: check if reloading asset data is necessary
|
||||
if (newAsset.id && !isSharedLink()) {
|
||||
if (newAsset.id && !authManager.key) {
|
||||
const data = await getAssetInfo({ id: asset.id });
|
||||
people = data?.people || [];
|
||||
unassignedFaces = data?.unassignedFaces || [];
|
||||
@@ -187,7 +188,7 @@
|
||||
<DetailPanelDescription {asset} {isOwner} />
|
||||
<DetailPanelRating {asset} {isOwner} />
|
||||
|
||||
{#if !isSharedLink() && isOwner}
|
||||
{#if !authManager.key && isOwner}
|
||||
<section class="px-4 pt-4 text-sm">
|
||||
<div class="flex h-10 w-full items-center justify-between">
|
||||
<h2>{$t('people').toUpperCase()}</h2>
|
||||
@@ -296,7 +297,7 @@
|
||||
{#if dateTime}
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full text-left justify-between place-items-start gap-4 py-4"
|
||||
class="flex w-full text-start justify-between place-items-start gap-4 py-4"
|
||||
onclick={() => (isOwner ? (isShowChangeDate = true) : null)}
|
||||
title={isOwner ? $t('edit_date') : ''}
|
||||
class:hover:dark:text-immich-dark-primary={isOwner}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { type DownloadProgress, downloadManager, downloadStore } from '$lib/stores/download-store.svelte';
|
||||
import { type DownloadProgress, downloadManager } from '$lib/managers/download-manager.svelte';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { mdiClose } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fly, slide } from 'svelte/transition';
|
||||
import { getByteUnitString } from '../../utils/byte-units';
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
import { mdiClose } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
const abort = (downloadKey: string, download: DownloadProgress) => {
|
||||
download.abort?.abort();
|
||||
@@ -13,17 +13,17 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if downloadStore.isDownloading}
|
||||
{#if downloadManager.isDownloading}
|
||||
<div
|
||||
transition:fly={{ x: -100, duration: 350 }}
|
||||
class="fixed bottom-10 left-2 z-[10000] max-h-[270px] w-[315px] rounded-2xl border bg-immich-bg p-4 text-sm shadow-sm"
|
||||
class="fixed bottom-10 start-2 z-[10000] max-h-[270px] w-[315px] rounded-2xl border bg-immich-bg p-4 text-sm shadow-sm"
|
||||
>
|
||||
<p class="mb-2 text-xs text-gray-500">{$t('downloading').toUpperCase()}</p>
|
||||
<div class="my-2 mb-2 flex max-h-[200px] flex-col overflow-y-auto text-sm">
|
||||
{#each Object.keys(downloadStore.assets) as downloadKey (downloadKey)}
|
||||
{@const download = downloadStore.assets[downloadKey]}
|
||||
{#each Object.keys(downloadManager.assets) as downloadKey (downloadKey)}
|
||||
{@const download = downloadManager.assets[downloadKey]}
|
||||
<div class="mb-2 flex place-items-center" transition:slide>
|
||||
<div class="w-full pr-10">
|
||||
<div class="w-full pe-10">
|
||||
<div class="flex place-items-center justify-between gap-2 text-xs font-medium">
|
||||
<p class="truncate">■ {downloadKey}</p>
|
||||
{#if download.total}
|
||||
@@ -41,7 +41,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute right-2">
|
||||
<div class="absolute end-2">
|
||||
<CircleIconButton
|
||||
title={$t('close')}
|
||||
onclick={() => abort(downloadKey, download)}
|
||||
|
||||
@@ -308,13 +308,13 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="absolute left-0 top-0">
|
||||
<canvas bind:this={canvasEl} id="face-editor" class="absolute top-0 left-0"></canvas>
|
||||
<div class="absolute start-0 top-0">
|
||||
<canvas bind:this={canvasEl} id="face-editor" class="absolute top-0 start-0"></canvas>
|
||||
|
||||
<div
|
||||
id="face-selector"
|
||||
bind:this={faceSelectorEl}
|
||||
class="absolute top-[calc(50%-250px)] left-[calc(50%-125px)] max-w-[250px] w-[250px] bg-white dark:bg-immich-dark-gray dark:text-immich-dark-fg backdrop-blur-sm px-2 py-4 rounded-xl border border-gray-200 dark:border-gray-800"
|
||||
class="absolute top-[calc(50%-250px)] start-[calc(50%-125px)] max-w-[250px] w-[250px] bg-white dark:bg-immich-dark-gray dark:text-immich-dark-fg backdrop-blur-sm px-2 py-4 rounded-xl border border-gray-200 dark:border-gray-800"
|
||||
>
|
||||
<p class="text-center text-sm">Select a person to tag</p>
|
||||
|
||||
@@ -329,7 +329,7 @@
|
||||
<button
|
||||
onclick={() => tagFace(person)}
|
||||
type="button"
|
||||
class="w-full flex place-items-center gap-2 rounded-lg pl-1 pr-4 py-2 hover:bg-immich-primary/25"
|
||||
class="w-full flex place-items-center gap-2 rounded-lg ps-1 pe-4 py-2 hover:bg-immich-primary/25"
|
||||
>
|
||||
<ImageThumbnail
|
||||
curve
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { getAssetOriginalUrl, getKey } from '$lib/utils';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { getAssetOriginalUrl } from '$lib/utils';
|
||||
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
|
||||
import { AssetMediaSize, viewAsset, type AssetResponseDto } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
asset: AssetResponseDto;
|
||||
@@ -13,7 +14,7 @@
|
||||
const { asset }: Props = $props();
|
||||
|
||||
const loadAssetData = async (id: string) => {
|
||||
const data = await viewAsset({ id, size: AssetMediaSize.Preview, key: getKey() });
|
||||
const data = await viewAsset({ id, size: AssetMediaSize.Preview, key: authManager.key });
|
||||
return URL.createObjectURL(data);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import { canCopyImageToClipboard, copyImageToClipboard, isWebCompatibleImage } from '$lib/utils/asset-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getBoundingBox } from '$lib/utils/people-utils';
|
||||
import { cancelImageUrl } from '$lib/utils/sw-messaging';
|
||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import { AssetMediaSize, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
|
||||
@@ -71,8 +72,10 @@
|
||||
|
||||
const preload = (targetSize: AssetMediaSize | 'original', preloadAssets?: { id: string }[]) => {
|
||||
for (const preloadAsset of preloadAssets || []) {
|
||||
let img = new Image();
|
||||
img.src = getAssetUrl(preloadAsset.id, targetSize, null);
|
||||
if (preloadAsset.isImage()) {
|
||||
let img = new Image();
|
||||
img.src = getAssetUrl(preloadAsset.id, targetSize, preloadAsset.thumbhash);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -167,6 +170,7 @@
|
||||
return () => {
|
||||
loader?.removeEventListener('load', onload);
|
||||
loader?.removeEventListener('error', onerror);
|
||||
cancelImageUrl(imageLoaderUrl);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -212,7 +216,7 @@
|
||||
<img
|
||||
src={assetFileUrl}
|
||||
alt=""
|
||||
class="absolute top-0 left-0 -z-10 object-cover h-full w-full blur-lg"
|
||||
class="absolute top-0 start-0 -z-10 object-cover h-full w-full blur-lg"
|
||||
draggable="false"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -25,6 +25,7 @@ describe('Thumbnail component', () => {
|
||||
vi.stubGlobal('IntersectionObserver', getIntersectionObserverMock());
|
||||
vi.mock('$lib/utils/navigation', () => ({
|
||||
currentUrlReplaceAssetId: vi.fn(),
|
||||
isSharedLinkRoute: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
import { thumbhash } from '$lib/actions/thumbhash';
|
||||
import BrokenAsset from '$lib/components/assets/broken-asset.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { cancelImageUrl } from '$lib/utils/sw-messaging';
|
||||
import { TUNABLES } from '$lib/utils/tunables';
|
||||
import { mdiEyeOffOutline } from '@mdi/js';
|
||||
import type { ClassValue } from 'svelte/elements';
|
||||
import type { ActionReturn } from 'svelte/action';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
@@ -59,11 +61,14 @@
|
||||
onComplete?.(true);
|
||||
};
|
||||
|
||||
function mount(elem: HTMLImageElement) {
|
||||
function mount(elem: HTMLImageElement): ActionReturn {
|
||||
if (elem.complete) {
|
||||
loaded = true;
|
||||
onComplete?.(false);
|
||||
}
|
||||
return {
|
||||
destroy: () => cancelImageUrl(url),
|
||||
};
|
||||
}
|
||||
|
||||
let optionalClasses = $derived(
|
||||
@@ -101,7 +106,7 @@
|
||||
{/if}
|
||||
|
||||
{#if hidden}
|
||||
<div class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform">
|
||||
<div class="absolute start-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform">
|
||||
<Icon {title} path={mdiEyeOffOutline} size="2em" class={hiddenIconClass} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { ProjectionType } from '$lib/constants';
|
||||
import { locale, playVideoThumbnailOnHover } from '$lib/stores/preferences.store';
|
||||
import { getAssetPlaybackUrl, getAssetThumbnailUrl, isSharedLink } from '$lib/utils';
|
||||
import { getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
|
||||
import { timeToSeconds } from '$lib/utils/date-time';
|
||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||
import { AssetMediaSize } from '@immich/sdk';
|
||||
@@ -17,6 +17,7 @@
|
||||
} from '@mdi/js';
|
||||
|
||||
import { thumbhash } from '$lib/actions/thumbhash';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
import { getFocusable } from '$lib/utils/focus-util';
|
||||
@@ -332,21 +333,21 @@
|
||||
></div>
|
||||
|
||||
<!-- Favorite asset star -->
|
||||
{#if !isSharedLink() && asset.isFavorite}
|
||||
<div class="absolute bottom-2 left-2 z-10">
|
||||
{#if !authManager.key && asset.isFavorite}
|
||||
<div class="absolute bottom-2 start-2 z-10">
|
||||
<Icon path={mdiHeart} size="24" class="text-white" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !isSharedLink() && showArchiveIcon && asset.isArchived}
|
||||
<div class={['absolute left-2 z-10', asset.isFavorite ? 'bottom-10' : 'bottom-2']}>
|
||||
{#if !authManager.key && showArchiveIcon && asset.isArchived}
|
||||
<div class={['absolute start-2 z-10', asset.isFavorite ? 'bottom-10' : 'bottom-2']}>
|
||||
<Icon path={mdiArchiveArrowDownOutline} size="24" class="text-white" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if asset.isImage && asset.projectionType === ProjectionType.EQUIRECTANGULAR}
|
||||
<div class="absolute right-0 top-0 z-10 flex place-items-center gap-1 text-xs font-medium text-white">
|
||||
<span class="pr-2 pt-2">
|
||||
<div class="absolute end-0 top-0 z-10 flex place-items-center gap-1 text-xs font-medium text-white">
|
||||
<span class="pe-2 pt-2">
|
||||
<Icon path={mdiRotate360} size="24" />
|
||||
</span>
|
||||
</div>
|
||||
@@ -357,10 +358,10 @@
|
||||
<div
|
||||
class={[
|
||||
'absolute z-10 flex place-items-center gap-1 text-xs font-medium text-white',
|
||||
asset.isImage && !asset.livePhotoVideoId ? 'top-0 right-0' : 'top-7 right-1',
|
||||
asset.isImage && !asset.livePhotoVideoId ? 'top-0 end-0' : 'top-7 end-1',
|
||||
]}
|
||||
>
|
||||
<span class="pr-2 pt-2 flex place-items-center gap-1">
|
||||
<span class="pe-2 pt-2 flex place-items-center gap-1">
|
||||
<p>{asset.stack.assetCount.toLocaleString($locale)}</p>
|
||||
<Icon path={mdiCameraBurst} size="24" />
|
||||
</span>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="absolute right-0 top-0 z-20 flex place-items-center gap-1 text-xs font-medium text-white">
|
||||
<div class="absolute end-0 top-0 z-20 flex place-items-center gap-1 text-xs font-medium text-white">
|
||||
{#if showTime}
|
||||
<span class="pt-2">
|
||||
{#if remainingSeconds < 60}
|
||||
@@ -69,7 +69,7 @@
|
||||
{/if}
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<span class="pr-2 pt-2" onmouseenter={onMouseEnter} onmouseleave={onMouseLeave}>
|
||||
<span class="pe-2 pt-2" onmouseenter={onMouseEnter} onmouseleave={onMouseLeave}>
|
||||
{#if enablePlayback}
|
||||
{#if loading}
|
||||
<LoadingSpinner />
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
};
|
||||
|
||||
const sizeClasses: Record<Size, string> = {
|
||||
tiny: 'p-0 ml-2 mr-0 align-top',
|
||||
tiny: 'p-0 ms-2 me-0 align-top',
|
||||
icon: 'p-2.5',
|
||||
link: 'p-2 font-medium',
|
||||
sm: 'px-4 py-2 text-sm font-medium',
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="absolute z-50 top-2 left-2 transition-transform {isFocused ? 'translate-y-0' : '-translate-y-10 sr-only'}">
|
||||
<div class="absolute z-50 top-2 start-2 transition-transform {isFocused ? 'translate-y-0' : '-translate-y-10 sr-only'}">
|
||||
<Button
|
||||
size="sm"
|
||||
rounded="none"
|
||||
|
||||
@@ -82,10 +82,10 @@
|
||||
const getAlignClass = (position: 'bottom-left' | 'bottom-right') => {
|
||||
switch (position) {
|
||||
case 'bottom-left': {
|
||||
return 'left-0';
|
||||
return 'start-0';
|
||||
}
|
||||
case 'bottom-right': {
|
||||
return 'right-0';
|
||||
return 'end-0';
|
||||
}
|
||||
|
||||
default: {
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
: 'rounded-lg'} bg-gray-100 p-2 dark:bg-gray-700 border border-gray-200 dark:border-immich-dark-gray"
|
||||
>
|
||||
<ImageThumbnail circle shadow url={thumbnailData} altText={person.name} widthStyle="2rem" heightStyle="2rem" />
|
||||
<form class="ml-4 flex w-full justify-between gap-16" autocomplete="off" {onsubmit}>
|
||||
<form class="ms-4 flex w-full justify-between gap-16" autocomplete="off" {onsubmit}>
|
||||
<SearchPeople
|
||||
bind:searchName={name}
|
||||
bind:searchedPeopleLocal={suggestedPeople}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute left-0 top-0 h-full w-full bg-immich-primary/30 opacity-0"
|
||||
class="absolute start-0 top-0 h-full w-full bg-immich-primary/30 opacity-0"
|
||||
class:hover:opacity-100={selectable}
|
||||
class:rounded-full={circle}
|
||||
class:rounded-lg={!circle}
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
{#if selected}
|
||||
<div
|
||||
class="absolute left-0 top-0 h-full w-full bg-blue-500/80"
|
||||
class="absolute start-0 top-0 h-full w-full bg-blue-500/80"
|
||||
class:rounded-full={circle}
|
||||
class:rounded-lg={!circle}
|
||||
></div>
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
{#if person.name}
|
||||
<span
|
||||
class="w-100 text-white-shadow absolute bottom-2 left-0 w-full text-ellipsis px-1 text-center font-medium text-white hover:cursor-pointer"
|
||||
class="w-100 text-white-shadow absolute bottom-2 start-0 w-full text-ellipsis px-1 text-center font-medium text-white hover:cursor-pointer"
|
||||
>
|
||||
{person.name}
|
||||
</span>
|
||||
|
||||
@@ -117,12 +117,12 @@
|
||||
<div class="flex items-center">
|
||||
<CircleIconButton title={$t('close')} icon={mdiClose} onclick={onClose} />
|
||||
<div class="flex gap-2 items-center">
|
||||
<p id={titleId} class="ml-2">{$t('show_and_hide_people')}</p>
|
||||
<p id={titleId} class="ms-2">{$t('show_and_hide_people')}</p>
|
||||
<p class="text-sm text-gray-400 dark:text-gray-600">({totalPeopleCount.toLocaleString($locale)})</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end">
|
||||
<div class="flex items-center md:mr-4">
|
||||
<div class="flex items-center md:me-4">
|
||||
<CircleIconButton title={$t('reset_people_visibility')} icon={mdiRestart} onclick={handleResetVisibility} />
|
||||
<CircleIconButton title={toggleButton.label} icon={toggleButton.icon} onclick={handleToggleVisibility} />
|
||||
</div>
|
||||
@@ -154,7 +154,7 @@
|
||||
hiddenIconClass="text-white group-hover:text-black transition-colors"
|
||||
/>
|
||||
{#if person.name}
|
||||
<span class="absolute bottom-2 left-0 w-full select-text px-1 text-center font-medium text-white">
|
||||
<span class="absolute bottom-2 start-0 w-full select-text px-1 text-center font-medium text-white">
|
||||
{person.name}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
|
||||
<section
|
||||
transition:fly={{ y: 500, duration: 100, easing: quintOut }}
|
||||
class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
|
||||
class="absolute start-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
|
||||
>
|
||||
<ControlAppBar onClose={onBack}>
|
||||
{#snippet leading()}
|
||||
@@ -113,7 +113,7 @@
|
||||
{#snippet trailing()}
|
||||
<Button size="sm" disabled={!hasSelection} onclick={handleMerge}>
|
||||
<Icon path={mdiMerge} size={18} />
|
||||
<span class="ml-2">{$t('merge')}</span></Button
|
||||
<span class="ms-2">{$t('merge')}</span></Button
|
||||
>
|
||||
{/snippet}
|
||||
</ControlAppBar>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
circle
|
||||
/>
|
||||
{#if person.isFavorite}
|
||||
<div class="absolute top-4 left-4">
|
||||
<div class="absolute top-4 start-4">
|
||||
<Icon path={mdiHeart} size="24" class="text-white" />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -62,7 +62,7 @@
|
||||
</a>
|
||||
|
||||
{#if showVerticalDots}
|
||||
<div class="absolute top-2 right-2">
|
||||
<div class="absolute top-2 end-2">
|
||||
<ButtonContextMenu
|
||||
buttonClass="icon-white-drop-shadow focus:opacity-100 {showVerticalDots ? 'opacity-100' : 'opacity-0'}"
|
||||
color="opaque"
|
||||
|
||||
@@ -227,7 +227,7 @@
|
||||
<div
|
||||
role="button"
|
||||
tabindex={index}
|
||||
class="absolute left-0 top-0 h-[90px] w-[90px] cursor-default"
|
||||
class="absolute start-0 top-0 h-[90px] w-[90px] cursor-default"
|
||||
onfocus={() => ($boundingBoxesArray = [peopleWithFaces[index]])}
|
||||
onmouseover={() => ($boundingBoxesArray = [peopleWithFaces[index]])}
|
||||
onmouseleave={() => ($boundingBoxesArray = [])}
|
||||
@@ -303,7 +303,7 @@
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="absolute -right-[5px] -top-[5px] h-[20px] w-[20px] rounded-full">
|
||||
<div class="absolute -end-[5px] -top-[5px] h-[20px] w-[20px] rounded-full">
|
||||
{#if selectedPersonToCreate[face.id] || selectedPersonToReassign[face.id]}
|
||||
<CircleIconButton
|
||||
color="primary"
|
||||
@@ -311,7 +311,7 @@
|
||||
title={$t('reset')}
|
||||
size="18"
|
||||
padding="1"
|
||||
class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
|
||||
class="absolute start-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
|
||||
onclick={() => handleReset(face.id)}
|
||||
/>
|
||||
{:else}
|
||||
@@ -321,29 +321,29 @@
|
||||
title={$t('select_new_face')}
|
||||
size="18"
|
||||
padding="1"
|
||||
class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
|
||||
class="absolute start-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
|
||||
onclick={() => handleFacePicker(face)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="absolute right-[25px] -top-[5px] h-[20px] w-[20px] rounded-full">
|
||||
<div class="absolute end-[25px] -top-[5px] h-[20px] w-[20px] rounded-full">
|
||||
{#if !selectedPersonToCreate[face.id] && !selectedPersonToReassign[face.id] && !face.person}
|
||||
<div
|
||||
class="flex place-content-center place-items-center rounded-full bg-[#d3d3d3] p-1 transition-all absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
|
||||
class="flex place-content-center place-items-center rounded-full bg-[#d3d3d3] p-1 transition-all absolute start-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
|
||||
>
|
||||
<Icon color="primary" path={mdiAccountOff} ariaHidden size="18" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if face.person != null}
|
||||
<div class="absolute -right-[5px] top-[25px] h-[20px] w-[20px] rounded-full">
|
||||
<div class="absolute -end-[5px] top-[25px] h-[20px] w-[20px] rounded-full">
|
||||
<CircleIconButton
|
||||
color="red"
|
||||
icon={mdiTrashCan}
|
||||
title={$t('delete_face')}
|
||||
size="18"
|
||||
padding="1"
|
||||
class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
|
||||
class="absolute start-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
|
||||
onclick={() => deleteAssetFace(face)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
|
||||
<section
|
||||
transition:fly={{ y: 500, duration: 100, easing: quintOut }}
|
||||
class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
|
||||
class="absolute start-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
|
||||
>
|
||||
<ControlAppBar {onClose}>
|
||||
{#snippet leading()}
|
||||
@@ -140,7 +140,7 @@
|
||||
{:else}
|
||||
<LoadingSpinner />
|
||||
{/if}
|
||||
<span class="ml-2"> {$t('create_new_person')}</span></Button
|
||||
<span class="ms-2"> {$t('create_new_person')}</span></Button
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -155,7 +155,7 @@
|
||||
{:else}
|
||||
<LoadingSpinner />
|
||||
{/if}
|
||||
<span class="ml-2"> {$t('reassign')}</span></Button
|
||||
<span class="ms-2"> {$t('reassign')}</span></Button
|
||||
>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
{/if}
|
||||
|
||||
<form {onsubmit} autocomplete="off" class="m-4 flex flex-col gap-4">
|
||||
<table class="text-left">
|
||||
<table class="text-start">
|
||||
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
|
||||
{#each validatedPaths as validatedPath, listIndex (validatedPath.importPath)}
|
||||
<tr
|
||||
@@ -183,7 +183,7 @@
|
||||
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
|
||||
}`}
|
||||
>
|
||||
<td class="w-1/8 text-ellipsis pl-8 text-sm">
|
||||
<td class="w-1/8 text-ellipsis ps-8 text-sm">
|
||||
{#if validatedPath.isValid}
|
||||
<Icon
|
||||
path={mdiCheckCircleOutline}
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
{/if}
|
||||
|
||||
<form {onsubmit} autocomplete="off" class="m-4 flex flex-col gap-4">
|
||||
<table class="w-full text-left">
|
||||
<table class="w-full text-start">
|
||||
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
|
||||
{#each exclusionPatterns as exclusionPattern, listIndex (exclusionPattern)}
|
||||
<tr
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
{#if tag}
|
||||
<div class="flex group transition-all">
|
||||
<span
|
||||
class="inline-block h-min whitespace-nowrap pl-3 pr-1 group-hover:pl-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-tl-full rounded-bl-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 roudned-s-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
|
||||
>
|
||||
<p class="text-sm">
|
||||
{tag.value}
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-100 dark:text-immich-dark-gray bg-immich-primary/95 dark:bg-immich-dark-primary/95 rounded-tr-full rounded-br-full place-items-center place-content-center pr-2 pl-1 py-1 hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
|
||||
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)}
|
||||
>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
alt="Immich logo"
|
||||
/>
|
||||
<div
|
||||
class="w-full h-[99%] absolute left-0 top-0 backdrop-blur-[200px] bg-transparent dark:bg-immich-dark-bg/20"
|
||||
class="w-full h-[99%] absolute start-0 top-0 backdrop-blur-[200px] bg-transparent dark:bg-immich-dark-bg/20"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -367,8 +367,8 @@
|
||||
|
||||
{#each current.memory.assets as asset, index (asset.id)}
|
||||
<a class="relative w-full py-2" href={asHref(asset)} aria-label={$t('view')}>
|
||||
<span class="absolute left-0 h-[2px] w-full bg-gray-500"></span>
|
||||
<span class="absolute left-0 h-[2px] bg-white" style:width={`${toProgressPercentage(index)}%`}></span>
|
||||
<span class="absolute start-0 h-[2px] w-full bg-gray-500"></span>
|
||||
<span class="absolute start-0 h-[2px] bg-white" style:width={`${toProgressPercentage(index)}%`}></span>
|
||||
</a>
|
||||
{/each}
|
||||
|
||||
@@ -387,7 +387,7 @@
|
||||
|
||||
{#if galleryInView}
|
||||
<div
|
||||
class="fixed top-20 z-30 left-1/2 -translate-x-1/2 transition-opacity"
|
||||
class="fixed top-20 z-30 start-1/2 -translate-x-1/2 transition-opacity"
|
||||
class:opacity-0={!galleryInView}
|
||||
class:opacity-100={galleryInView}
|
||||
>
|
||||
@@ -403,7 +403,7 @@
|
||||
<!-- Viewer -->
|
||||
<section class="overflow-hidden pt-32 md:pt-20" bind:clientHeight={viewerHeight}>
|
||||
<div
|
||||
class="ml-[-100%] box-border flex h-[calc(100vh_-_224px)] md:h-[calc(100vh_-_180px)] w-[300%] items-center justify-center gap-10 overflow-hidden"
|
||||
class="ms-[-100%] box-border flex h-[calc(100vh_-_224px)] md:h-[calc(100vh_-_180px)] w-[300%] items-center justify-center gap-10 overflow-hidden"
|
||||
>
|
||||
<!-- PREVIOUS MEMORY -->
|
||||
<div class="h-1/2 w-[20vw] rounded-2xl {current.previousMemory ? 'opacity-25 hover:opacity-70' : 'opacity-0'}">
|
||||
@@ -431,7 +431,7 @@
|
||||
{/if}
|
||||
|
||||
{#if current.previousMemory}
|
||||
<div class="absolute bottom-4 right-4 text-left text-white">
|
||||
<div class="absolute bottom-4 end-4 text-start text-white">
|
||||
<p class="text-xs font-semibold text-gray-200">{$t('previous').toUpperCase()}</p>
|
||||
<p class="text-xl">{$memoryLaneTitle(current.previousMemory)}</p>
|
||||
</div>
|
||||
@@ -472,7 +472,7 @@
|
||||
{/key}
|
||||
|
||||
<div
|
||||
class="absolute bottom-0 right-0 p-2 transition-all flex h-full justify-between flex-col items-end gap-2"
|
||||
class="absolute bottom-0 end-0 p-2 transition-all flex h-full justify-between flex-col items-end gap-2"
|
||||
class:opacity-0={galleryInView}
|
||||
class:opacity-100={!galleryInView}
|
||||
>
|
||||
@@ -528,7 +528,7 @@
|
||||
</div>
|
||||
<!-- CONTROL BUTTONS -->
|
||||
{#if current.previous}
|
||||
<div class="absolute top-1/2 left-0 ml-4">
|
||||
<div class="absolute top-1/2 start-0 ms-4">
|
||||
<CircleIconButton
|
||||
title={$t('previous_memory')}
|
||||
icon={mdiChevronLeft}
|
||||
@@ -539,7 +539,7 @@
|
||||
{/if}
|
||||
|
||||
{#if current.next}
|
||||
<div class="absolute top-1/2 right-0 mr-4">
|
||||
<div class="absolute top-1/2 end-0 me-4">
|
||||
<CircleIconButton
|
||||
title={$t('next_memory')}
|
||||
icon={mdiChevronRight}
|
||||
@@ -549,9 +549,11 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="absolute left-8 top-4 text-sm font-medium text-white">
|
||||
<div class="absolute start-8 top-4 text-sm font-medium text-white">
|
||||
<p>
|
||||
{fromLocalDateTime(current.memory.assets[0].localDateTime).toLocaleString(DateTime.DATE_FULL)}
|
||||
{fromLocalDateTime(current.memory.assets[0].localDateTime).toLocaleString(DateTime.DATE_FULL, {
|
||||
locale: $locale,
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{#await currentMemoryAssetFull then asset}
|
||||
@@ -589,7 +591,7 @@
|
||||
{/if}
|
||||
|
||||
{#if current.nextMemory}
|
||||
<div class="absolute bottom-4 left-4 text-left text-white">
|
||||
<div class="absolute bottom-4 start-4 text-start text-white">
|
||||
<p class="text-xs font-semibold text-gray-200">{$t('up_next').toUpperCase()}</p>
|
||||
<p class="text-xl">{$memoryLaneTitle(current.nextMemory)}</p>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { getKey } from '$lib/utils';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { removeSharedLinkAssets, type SharedLinkResponseDto } from '@immich/sdk';
|
||||
import { mdiDeleteOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { NotificationType, notificationController } from '../../shared-components/notification/notification';
|
||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
sharedLink: SharedLinkResponseDto;
|
||||
@@ -34,7 +34,7 @@
|
||||
assetIdsDto: {
|
||||
assetIds: [...getAssets()].map((asset) => asset.id),
|
||||
},
|
||||
key: getKey(),
|
||||
key: authManager.key,
|
||||
});
|
||||
|
||||
for (const result of results) {
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<span class="w-full truncate first-letter:capitalize ml-2.5" title={getDateLocaleString(dateGroup.date)}>
|
||||
<span class="w-full truncate first-letter:capitalize ms-2.5" title={getDateLocaleString(dateGroup.date)}>
|
||||
{dateGroup.groupTitle}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -751,7 +751,7 @@
|
||||
<!-- Right margin MUST be equal to the width of immich-scrubbable-scrollbar -->
|
||||
<section
|
||||
id="asset-grid"
|
||||
class={['scrollbar-hidden h-full overflow-y-auto outline-none', { 'm-0': isEmpty }, { 'ml-0': !isEmpty }]}
|
||||
class={['scrollbar-hidden h-full overflow-y-auto outline-none', { 'm-0': isEmpty }, { 'ms-0': !isEmpty }]}
|
||||
style:margin-right={(usingMobileDevice ? 0 : scrubberWidth) + 'px'}
|
||||
tabindex="-1"
|
||||
bind:clientHeight={assetStore.viewportHeight}
|
||||
|
||||
@@ -45,9 +45,9 @@
|
||||
onscroll={onScroll}
|
||||
>
|
||||
{#if canScrollLeft || canScrollRight}
|
||||
<div class="sticky left-0 z-20">
|
||||
<div class="sticky start-0 z-20">
|
||||
{#if canScrollLeft}
|
||||
<div class="absolute left-4 top-[6rem] z-20" transition:fade={{ duration: 200 }}>
|
||||
<div class="absolute start-4 top-[6rem] z-20" transition:fade={{ duration: 200 }}>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full border border-gray-500 bg-gray-100 p-2 text-gray-500 opacity-50 hover:opacity-100"
|
||||
@@ -60,7 +60,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
{#if canScrollRight}
|
||||
<div class="absolute right-4 top-[6rem] z-20" transition:fade={{ duration: 200 }}>
|
||||
<div class="absolute end-4 top-[6rem] z-20" transition:fade={{ duration: 200 }}>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full border border-gray-500 bg-gray-100 p-2 text-gray-500 opacity-50 hover:opacity-100"
|
||||
@@ -77,7 +77,7 @@
|
||||
<div class="inline-block" use:resizeObserver={({ width }) => (innerWidth = width)}>
|
||||
{#each memoryStore.memories as memory (memory.id)}
|
||||
<a
|
||||
class="memory-card relative mr-2 md:mr-4 last:mr-0 inline-block aspect-[3/4] md:aspect-[4/3] max-md:h-[150px] xl:aspect-video h-[215px] rounded-xl"
|
||||
class="memory-card relative me-2 md:me-4 last:me-0 inline-block aspect-[3/4] md:aspect-[4/3] max-md:h-[150px] xl:aspect-video h-[215px] rounded-xl"
|
||||
href="{AppRoute.MEMORY}?{QueryParameter.ID}={memory.assets[0].id}"
|
||||
>
|
||||
<img
|
||||
@@ -86,11 +86,11 @@
|
||||
alt={$t('memory_lane_title', { values: { title: $getAltText(toTimelineAsset(memory.assets[0])) } })}
|
||||
draggable="false"
|
||||
/>
|
||||
<p class="absolute bottom-2 left-4 z-10 text-lg text-white max-md:text-sm">
|
||||
<p class="absolute bottom-2 start-4 z-10 text-lg text-white max-md:text-sm">
|
||||
{$memoryLaneTitle(memory)}
|
||||
</p>
|
||||
<div
|
||||
class="absolute left-0 top-0 z-0 h-full w-full rounded-xl bg-gradient-to-t from-black/40 via-transparent to-transparent transition-all hover:bg-black/20"
|
||||
class="absolute start-0 top-0 z-0 h-full w-full rounded-xl bg-gradient-to-t from-black/40 via-transparent to-transparent transition-all hover:bg-black/20"
|
||||
></div>
|
||||
</a>
|
||||
{/each}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{title}
|
||||
</div>
|
||||
<div
|
||||
class="animate-pulse absolute h-full ml-[10px] mr-[10px]"
|
||||
class="animate-pulse absolute h-full ms-[10px] me-[10px]"
|
||||
style:width="calc(100% - 20px)"
|
||||
data-skeleton="true"
|
||||
></div>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => togglePlacesGroupCollapsing(group.id)}
|
||||
class="w-fit mt-2 pt-2 pr-2 mb-2 dark:text-immich-dark-fg"
|
||||
class="w-fit mt-2 pt-2 pe-2 mb-2 dark:text-immich-dark-fg"
|
||||
aria-expanded={!isCollapsed}
|
||||
>
|
||||
<Icon
|
||||
@@ -34,7 +34,7 @@
|
||||
class="inline-block -mt-2.5 transition-all duration-[250ms] {iconRotation}"
|
||||
/>
|
||||
<span class="font-bold text-3xl text-black dark:text-white">{group.name}</span>
|
||||
<span class="ml-1.5">({$t('places_count', { values: { count: places.length } })})</span>
|
||||
<span class="ms-1.5">({$t('places_count', { values: { count: places.length } })})</span>
|
||||
</button>
|
||||
<hr class="dark:border-immich-dark-gray" />
|
||||
</div>
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
||||
import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte';
|
||||
import { AppRoute, AssetAction } from '$lib/constants';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import type { Viewport } from '$lib/stores/assets-store.svelte';
|
||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||
import { getKey, handlePromiseError } from '$lib/utils';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { cancelMultiselect, downloadArchive } from '$lib/utils/asset-utils';
|
||||
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
@@ -58,7 +59,7 @@
|
||||
assetIdsDto: {
|
||||
assetIds: results.filter((id) => !!id) as string[],
|
||||
},
|
||||
key: getKey(),
|
||||
key: authManager.key,
|
||||
});
|
||||
|
||||
const added = data.filter((item) => item.success).length;
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
<!-- @migration-task: migrate this slot by hand, `prompt` would shadow a prop on the parent component -->
|
||||
<!-- @migration-task: migrate this slot by hand, `prompt` would shadow a prop on the parent component -->
|
||||
{#snippet promptSnippet()}
|
||||
<div class="flex flex-col text-left gap-2">
|
||||
<div class="flex flex-col text-start gap-2">
|
||||
<div class="flex flex-col">
|
||||
<label for="datetime">{$t('date_and_time')}</label>
|
||||
<DateInput class="immich-form-input" id="datetime" type="datetime-local" bind:value={selectedDate} />
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
: ''}"
|
||||
onclick={() => handleUseSuggested(place.latitude, place.longitude)}
|
||||
>
|
||||
<p class="ml-4 text-sm text-gray-700 dark:text-gray-100 truncate">
|
||||
<p class="ms-4 text-sm text-gray-700 dark:text-gray-100 truncate">
|
||||
{getLocation(place.name, place.admin1name, place.admin2name)}
|
||||
</p>
|
||||
</button>
|
||||
@@ -189,7 +189,7 @@
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
<div class="grid sm:grid-cols-2 gap-4 text-sm text-left mt-4">
|
||||
<div class="grid sm:grid-cols-2 gap-4 text-sm text-start mt-4">
|
||||
<CoordinatesInput
|
||||
lat={point ? point.lat : assetLat}
|
||||
lng={point ? point.lng : assetLng}
|
||||
|
||||
@@ -258,7 +258,7 @@
|
||||
>
|
||||
<div>
|
||||
{#if isActive}
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3">
|
||||
<div class="dark:text-immich-dark-fg/75">
|
||||
<Icon path={mdiMagnify} ariaHidden={true} />
|
||||
</div>
|
||||
@@ -273,11 +273,11 @@
|
||||
aria-expanded={isOpen}
|
||||
autocomplete="off"
|
||||
bind:this={input}
|
||||
class:!pl-8={isActive}
|
||||
class:!ps-8={isActive}
|
||||
class:!rounded-b-none={isOpen && dropdownDirection === 'bottom'}
|
||||
class:!rounded-t-none={isOpen && dropdownDirection === 'top'}
|
||||
class:cursor-pointer={!isActive}
|
||||
class="immich-form-input text-sm text-left w-full !pr-12 transition-all"
|
||||
class="immich-form-input text-sm w-full !pe-12 transition-all"
|
||||
id={inputId}
|
||||
onfocus={activate}
|
||||
oninput={onInput}
|
||||
@@ -325,8 +325,8 @@
|
||||
/>
|
||||
|
||||
<div
|
||||
class="absolute right-0 top-0 h-full flex px-4 justify-center items-center content-between"
|
||||
class:pr-2={selectedOption}
|
||||
class="absolute end-0 top-0 h-full flex px-4 justify-center items-center content-between"
|
||||
class:pe-2={selectedOption}
|
||||
class:pointer-events-none={!selectedOption}
|
||||
>
|
||||
{#if selectedOption}
|
||||
@@ -341,7 +341,7 @@
|
||||
role="listbox"
|
||||
id={listboxId}
|
||||
transition:fly={{ duration: 250 }}
|
||||
class="fixed text-left text-sm w-full overflow-y-auto bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-900 z-[10000]"
|
||||
class="fixed text-start text-sm w-full overflow-y-auto bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-900 z-[10000]"
|
||||
class:rounded-b-xl={dropdownDirection === 'bottom'}
|
||||
class:rounded-t-xl={dropdownDirection === 'top'}
|
||||
class:shadow={dropdownDirection === 'bottom'}
|
||||
@@ -360,7 +360,7 @@
|
||||
role="option"
|
||||
aria-selected={selectedIndex === 0}
|
||||
aria-disabled={true}
|
||||
class="text-left w-full px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 cursor-default aria-selected:bg-gray-200 aria-selected:dark:bg-gray-700"
|
||||
class="text-start w-full px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 cursor-default aria-selected:bg-gray-200 aria-selected:dark:bg-gray-700"
|
||||
id={`${listboxId}-${0}`}
|
||||
onclick={closeDropdown}
|
||||
>
|
||||
@@ -372,7 +372,7 @@
|
||||
<li
|
||||
aria-selected={index === selectedIndex}
|
||||
bind:this={optionRefs[index]}
|
||||
class="text-left w-full px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 transition-all cursor-pointer aria-selected:bg-gray-200 aria-selected:dark:bg-gray-700 break-words"
|
||||
class="text-start w-full px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 transition-all cursor-pointer aria-selected:bg-gray-200 aria-selected:dark:bg-gray-700 break-words"
|
||||
id={`${listboxId}-${index}`}
|
||||
onclick={() => handleSelect(option)}
|
||||
role="option"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
type Padding,
|
||||
} from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
|
||||
import { languageManager } from '$lib/managers/language-manager.svelte';
|
||||
import { optionClickCallbackStore, selectedIdStore } from '$lib/stores/context-menu.store';
|
||||
import {
|
||||
getContextMenuPositionFromBoundingRect,
|
||||
@@ -26,6 +27,7 @@
|
||||
/**
|
||||
* The direction in which the context menu should open.
|
||||
*/
|
||||
// TODO change to start vs end
|
||||
direction?: 'left' | 'right';
|
||||
color?: Color;
|
||||
size?: string | undefined;
|
||||
@@ -62,7 +64,15 @@
|
||||
const menuId = `context-menu-${id}`;
|
||||
|
||||
const openDropdown = (event: KeyboardEvent | MouseEvent) => {
|
||||
contextMenuPosition = getContextMenuPositionFromEvent(event, align);
|
||||
let layoutAlign = align;
|
||||
if (languageManager.rtl) {
|
||||
if (align.includes('left')) {
|
||||
layoutAlign = align.replace('left', 'right') as Align;
|
||||
} else if (align.includes('right')) {
|
||||
layoutAlign = align.replace('right', 'left') as Align;
|
||||
}
|
||||
}
|
||||
contextMenuPosition = getContextMenuPositionFromEvent(event, layoutAlign);
|
||||
isOpen = true;
|
||||
menuContainer?.focus();
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { clickOutside } from '$lib/actions/click-outside';
|
||||
import { languageManager } from '$lib/managers/language-manager.svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { clickOutside } from '$lib/actions/click-outside';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
isVisible?: boolean;
|
||||
@@ -41,12 +42,17 @@
|
||||
|
||||
$effect(() => {
|
||||
if (menuElement) {
|
||||
let layoutDirection = direction;
|
||||
if (languageManager.rtl) {
|
||||
layoutDirection = direction === 'left' ? 'right' : 'left';
|
||||
}
|
||||
|
||||
const rect = menuElement.getBoundingClientRect();
|
||||
const directionWidth = direction === 'left' ? rect.width : 0;
|
||||
const directionWidth = layoutDirection === 'left' ? rect.width : 0;
|
||||
const menuHeight = Math.min(menuElement.clientHeight, height) || 0;
|
||||
|
||||
left = Math.min(window.innerWidth - rect.width, x - directionWidth);
|
||||
top = Math.min(window.innerHeight - menuHeight, y);
|
||||
left = Math.max(8, Math.min(window.innerWidth - rect.width, x - directionWidth));
|
||||
top = Math.max(8, Math.min(window.innerHeight - menuHeight, y));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -66,7 +72,7 @@
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
bind:this={menuElement}
|
||||
class="{isVisible
|
||||
? 'max-h-dvh max-h-svh'
|
||||
? 'max-h-dvh'
|
||||
: 'max-h-0'} flex flex-col transition-all duration-[250ms] ease-in-out outline-none overflow-auto"
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
onclick={handleClick}
|
||||
onmouseover={() => ($selectedIdStore = id)}
|
||||
onmouseleave={() => ($selectedIdStore = undefined)}
|
||||
class="w-full p-4 text-left text-sm font-medium {textColor} focus:outline-none focus:ring-2 focus:ring-inset cursor-pointer border-gray-200 flex gap-2 items-center {isActive
|
||||
class="w-full p-4 text-start text-sm font-medium {textColor} focus:outline-none focus:ring-2 focus:ring-inset cursor-pointer border-gray-200 flex gap-2 items-center {isActive
|
||||
? activeColor
|
||||
: 'bg-slate-100'}"
|
||||
role="menuitem"
|
||||
@@ -65,7 +65,7 @@
|
||||
<div class="flex justify-between">
|
||||
{text}
|
||||
{#if shortcutLabel}
|
||||
<span class="text-gray-500 pl-4">
|
||||
<span class="text-gray-500 ps-4">
|
||||
{shortcutLabel}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
const elements = document.elementsFromPoint(event.x, event.y);
|
||||
|
||||
if (menuContainer && elements.includes(menuContainer)) {
|
||||
// User right-clicked on the context menu itself, we keep the context
|
||||
// User end-clicked on the context menu itself, we keep the context
|
||||
// menu as is
|
||||
return;
|
||||
}
|
||||
@@ -91,7 +91,7 @@
|
||||
},
|
||||
]}
|
||||
>
|
||||
<section class="fixed left-0 top-0 z-10 flex h-dvh w-dvw" {oncontextmenu} role="presentation">
|
||||
<section class="fixed start-0 top-0 z-10 flex h-dvh w-dvw" {oncontextmenu} role="presentation">
|
||||
<ContextMenu
|
||||
{direction}
|
||||
{x}
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
<div class="mr-4 flex place-items-center gap-1 justify-self-end">
|
||||
<div class="me-4 flex place-items-center gap-1 justify-self-end">
|
||||
{@render trailing?.()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { shouldIgnoreEvent } from '$lib/actions/shortcut';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||
import { fileUploadHandler } from '$lib/utils/file-uploader';
|
||||
import { isAlbumsRoute, isSharedLinkRoute } from '$lib/utils/navigation';
|
||||
import { isAlbumsRoute } from '$lib/utils/navigation';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
import ImmichLogo from './immich-logo.svelte';
|
||||
|
||||
let albumId = $derived(isAlbumsRoute(page.route?.id) ? page.params.albumId : undefined);
|
||||
let isShare = $derived(isSharedLinkRoute(page.route?.id));
|
||||
|
||||
let dragStartTarget: EventTarget | null = $state(null);
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
}
|
||||
|
||||
const filesArray: File[] = Array.from<File>(files);
|
||||
if (isShare) {
|
||||
if (authManager.key) {
|
||||
dragAndDropFilesStore.set({ isDragging: true, files: filesArray });
|
||||
} else {
|
||||
await fileUploadHandler(filesArray, albumId);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<FullScreenModal title={$t('deduplication_info')} width="auto" {onClose}>
|
||||
<div class="text-sm dark:text-white">
|
||||
<p>{$t('deduplication_info_description')}</p>
|
||||
<ol class="ml-8 mt-2" style="list-style: decimal">
|
||||
<ol class="ms-8 mt-2" style="list-style: decimal">
|
||||
<li>{$t('deduplication_criteria_1')}</li>
|
||||
<li>{$t('deduplication_criteria_2')}</li>
|
||||
</ol>
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
role="presentation"
|
||||
in:fade={{ duration: 100 }}
|
||||
out:fade={{ duration: 100 }}
|
||||
class="fixed left-0 top-0 z-[9999] flex h-dvh w-dvw place-content-center place-items-center bg-black/40"
|
||||
class="fixed start-0 top-0 z-[9999] flex h-dvh w-dvw place-content-center place-items-center bg-black/40"
|
||||
onkeydown={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
</script>
|
||||
|
||||
<a data-sveltekit-preload-data="hover" class="ml-4" href="/">
|
||||
<a data-sveltekit-preload-data="hover" class="ms-4" href="/">
|
||||
<ImmichLogo class="h-[50px] max-w-none md:w-auto md:max-w-full" noText={mobileDevice.maxMd} />
|
||||
</a>
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { preferences, user } from '$lib/stores/user.store';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { deleteProfileImage, updateMyPreferences, type UserAvatarColor } from '@immich/sdk';
|
||||
import { deleteProfileImage, updateMyUser, type UserAvatarColor } from '@immich/sdk';
|
||||
import { mdiCog, mdiLogout, mdiPencil, mdiWrench } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
@@ -30,8 +30,7 @@
|
||||
await deleteProfileImage();
|
||||
}
|
||||
|
||||
$preferences = await updateMyPreferences({ userPreferencesUpdateDto: { avatar: { color } } });
|
||||
$user = { ...$user, profileImagePath: '', avatarColor: $preferences.avatar.color };
|
||||
$user = await updateMyUser({ userUpdateMeDto: { avatarColor: color } });
|
||||
isShowSelectAvatar = false;
|
||||
|
||||
notificationController.show({
|
||||
@@ -48,7 +47,7 @@
|
||||
in:fade={{ duration: 100 }}
|
||||
out:fade={{ duration: 100 }}
|
||||
id="account-info-panel"
|
||||
class="absolute right-[25px] top-[75px] z-[100] w-[min(360px,100vw-50px)] rounded-3xl bg-gray-200 shadow-lg dark:border dark:border-immich-dark-gray dark:bg-immich-dark-gray"
|
||||
class="absolute end-[25px] top-[75px] z-[100] w-[min(360px,100vw-50px)] rounded-3xl bg-gray-200 shadow-lg dark:border dark:border-immich-dark-gray dark:bg-immich-dark-gray"
|
||||
use:focusTrap
|
||||
>
|
||||
<div
|
||||
@@ -56,7 +55,7 @@
|
||||
>
|
||||
<div class="relative">
|
||||
<UserAvatar user={$user} size="xl" />
|
||||
<div class="absolute z-10 bottom-0 right-0 rounded-full w-6 h-6">
|
||||
<div class="absolute z-10 bottom-0 end-0 rounded-full w-6 h-6">
|
||||
<CircleIconButton
|
||||
color="primary"
|
||||
icon={mdiPencil}
|
||||
|
||||
@@ -8,23 +8,25 @@
|
||||
import SkipLink from '$lib/components/elements/buttons/skip-link.svelte';
|
||||
import HelpAndFeedbackModal from '$lib/components/shared-components/help-and-feedback-modal.svelte';
|
||||
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
|
||||
import NotificationPanel from '$lib/components/shared-components/navigation-bar/notification-panel.svelte';
|
||||
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
import { featureFlags } from '$lib/stores/server-config.store';
|
||||
import { sidebarStore } from '$lib/stores/sidebar.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { userInteraction } from '$lib/stores/user.svelte';
|
||||
import { handleLogout } from '$lib/utils/auth';
|
||||
import { getAboutInfo, logout, type ServerAboutResponseDto } from '@immich/sdk';
|
||||
import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk';
|
||||
import { Button, IconButton } from '@immich/ui';
|
||||
import { mdiHelpCircleOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js';
|
||||
import { mdiBellBadge, mdiBellOutline, mdiHelpCircleOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
import ThemeButton from '../theme-button.svelte';
|
||||
import UserAvatar from '../user-avatar.svelte';
|
||||
import AccountInfoPanel from './account-info-panel.svelte';
|
||||
import { sidebarStore } from '$lib/stores/sidebar.svelte';
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
import { notificationManager } from '$lib/stores/notification-manager.svelte';
|
||||
|
||||
interface Props {
|
||||
showUploadButton?: boolean;
|
||||
@@ -36,12 +38,9 @@
|
||||
let shouldShowAccountInfo = $state(false);
|
||||
let shouldShowAccountInfoPanel = $state(false);
|
||||
let shouldShowHelpPanel = $state(false);
|
||||
let shouldShowNotificationPanel = $state(false);
|
||||
let innerWidth: number = $state(0);
|
||||
|
||||
const onLogout = async () => {
|
||||
const { redirectUri } = await logout();
|
||||
await handleLogout(redirectUri);
|
||||
};
|
||||
const hasUnreadNotifications = $derived(notificationManager.notifications.length > 0);
|
||||
|
||||
let info: ServerAboutResponseDto | undefined = $state();
|
||||
|
||||
@@ -88,8 +87,8 @@
|
||||
<ImmichLogo class="max-md:h-[48px] h-[50px]" noText={!mobileDevice.isFullSidebar} />
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4 lg:gap-8 pr-6">
|
||||
<div class="hidden w-full max-w-5xl flex-1 tall:pl-0 sm:block">
|
||||
<div class="flex justify-between gap-4 lg:gap-8 pe-6">
|
||||
<div class="hidden w-full max-w-5xl flex-1 tall:ps-0 sm:block">
|
||||
{#if $featureFlags.search}
|
||||
<SearchBar grayTheme={true} />
|
||||
{/if}
|
||||
@@ -151,6 +150,27 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
use:clickOutside={{
|
||||
onOutclick: () => (shouldShowNotificationPanel = false),
|
||||
onEscape: () => (shouldShowNotificationPanel = false),
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
shape="round"
|
||||
color={hasUnreadNotifications ? 'primary' : 'secondary'}
|
||||
variant="ghost"
|
||||
size="medium"
|
||||
icon={hasUnreadNotifications ? mdiBellBadge : mdiBellOutline}
|
||||
onclick={() => (shouldShowNotificationPanel = !shouldShowNotificationPanel)}
|
||||
aria-label={$t('notifications')}
|
||||
/>
|
||||
|
||||
{#if shouldShowNotificationPanel}
|
||||
<NotificationPanel />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
use:clickOutside={{
|
||||
onOutclick: () => (shouldShowAccountInfoPanel = false),
|
||||
@@ -159,7 +179,7 @@
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex pl-2"
|
||||
class="flex ps-2"
|
||||
onmouseover={() => (shouldShowAccountInfo = true)}
|
||||
onfocus={() => (shouldShowAccountInfo = true)}
|
||||
onblur={() => (shouldShowAccountInfo = false)}
|
||||
@@ -175,7 +195,7 @@
|
||||
<div
|
||||
in:fade={{ delay: 500, duration: 150 }}
|
||||
out:fade={{ delay: 200, duration: 150 }}
|
||||
class="absolute -bottom-12 right-5 rounded-md border bg-gray-500 p-2 text-[12px] text-gray-100 shadow-md dark:border-immich-dark-gray dark:bg-immich-dark-gray"
|
||||
class="absolute -bottom-12 end-5 rounded-md border bg-gray-500 p-2 text-[12px] text-gray-100 shadow-md dark:border-immich-dark-gray dark:bg-immich-dark-gray"
|
||||
>
|
||||
<p>{$user.name}</p>
|
||||
<p>{$user.email}</p>
|
||||
@@ -183,7 +203,7 @@
|
||||
{/if}
|
||||
|
||||
{#if shouldShowAccountInfoPanel}
|
||||
<AccountInfoPanel {onLogout} />
|
||||
<AccountInfoPanel onLogout={() => authManager.logout()} />
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
<script lang="ts">
|
||||
import { NotificationLevel, NotificationType, type NotificationDto } from '@immich/sdk';
|
||||
import { IconButton, Stack, Text } from '@immich/ui';
|
||||
import { mdiBackupRestore, mdiInformationOutline, mdiMessageBadgeOutline, mdiSync } from '@mdi/js';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
interface Props {
|
||||
notification: NotificationDto;
|
||||
onclick: (id: string) => void;
|
||||
}
|
||||
|
||||
let { notification, onclick }: Props = $props();
|
||||
|
||||
const getAlertColor = (level: NotificationLevel) => {
|
||||
switch (level) {
|
||||
case NotificationLevel.Error: {
|
||||
return 'danger';
|
||||
}
|
||||
case NotificationLevel.Warning: {
|
||||
return 'warning';
|
||||
}
|
||||
case NotificationLevel.Info: {
|
||||
return 'primary';
|
||||
}
|
||||
case NotificationLevel.Success: {
|
||||
return 'success';
|
||||
}
|
||||
default: {
|
||||
return 'primary';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getIconBgColor = (level: NotificationLevel) => {
|
||||
switch (level) {
|
||||
case NotificationLevel.Error: {
|
||||
return 'bg-red-500 dark:bg-red-300 dark:hover:bg-red-200';
|
||||
}
|
||||
case NotificationLevel.Warning: {
|
||||
return 'bg-amber-500 dark:bg-amber-200 dark:hover:bg-amber-200';
|
||||
}
|
||||
case NotificationLevel.Info: {
|
||||
return 'bg-blue-500 dark:bg-blue-200 dark:hover:bg-blue-200';
|
||||
}
|
||||
case NotificationLevel.Success: {
|
||||
return 'bg-green-500 dark:bg-green-200 dark:hover:bg-green-200';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getIconType = (type: NotificationType) => {
|
||||
switch (type) {
|
||||
case NotificationType.BackupFailed: {
|
||||
return mdiBackupRestore;
|
||||
}
|
||||
case NotificationType.JobFailed: {
|
||||
return mdiSync;
|
||||
}
|
||||
case NotificationType.SystemMessage: {
|
||||
return mdiMessageBadgeOutline;
|
||||
}
|
||||
case NotificationType.Custom: {
|
||||
return mdiInformationOutline;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const formatRelativeTime = (dateString: string): string => {
|
||||
try {
|
||||
const date = DateTime.fromISO(dateString);
|
||||
if (!date.isValid) {
|
||||
return dateString; // Return original string if parsing fails
|
||||
}
|
||||
// Use Luxon's toRelative with the current locale
|
||||
return date.setLocale('en').toRelative() || dateString;
|
||||
} catch (error) {
|
||||
console.error('Error formatting relative time:', error);
|
||||
return dateString; // Fallback to original string on error
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="min-h-[80px] p-2 py-3 hover:bg-immich-primary/10 dark:hover:bg-immich-dark-primary/10 border-b border-gray-200 dark:border-immich-dark-gray w-full"
|
||||
type="button"
|
||||
onclick={() => onclick(notification.id)}
|
||||
title={notification.createdAt}
|
||||
>
|
||||
<div class="grid grid-cols-[56px_1fr_32px] items-center gap-2">
|
||||
<div class="flex place-items-center place-content-center">
|
||||
<IconButton
|
||||
icon={getIconType(notification.type)}
|
||||
color={getAlertColor(notification.level)}
|
||||
aria-label={notification.title}
|
||||
shape="round"
|
||||
class={getIconBgColor(notification.level)}
|
||||
size="small"
|
||||
></IconButton>
|
||||
</div>
|
||||
|
||||
<Stack class="text-left" gap={1}>
|
||||
<Text size="tiny" class="uppercase text-black dark:text-white font-semibold">{notification.title}</Text>
|
||||
{#if notification.description}
|
||||
<Text class="overflow-hidden text-gray-600 dark:text-gray-300">{notification.description}</Text>
|
||||
{/if}
|
||||
|
||||
<Text size="tiny" color="muted">{formatRelativeTime(notification.createdAt)}</Text>
|
||||
</Stack>
|
||||
|
||||
{#if !notification.readAt}
|
||||
<div class="w-2 h-2 rounded-full bg-primary text-right justify-self-center"></div>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import { focusTrap } from '$lib/actions/focus-trap';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import NotificationItem from '$lib/components/shared-components/navigation-bar/notification-item.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType as WebNotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
|
||||
import { notificationManager } from '$lib/stores/notification-manager.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { Button, Scrollable, Stack, Text } from '@immich/ui';
|
||||
import { mdiBellOutline, mdiCheckAll } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { flip } from 'svelte/animate';
|
||||
|
||||
const noUnreadNotifications = $derived(notificationManager.notifications.length === 0);
|
||||
|
||||
const markAsRead = async (id: string) => {
|
||||
try {
|
||||
await notificationManager.markAsRead(id);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.failed_to_update_notification_status'));
|
||||
}
|
||||
};
|
||||
|
||||
const markAllAsRead = async () => {
|
||||
try {
|
||||
await notificationManager.markAllAsRead();
|
||||
notificationController.show({ message: $t('marked_all_as_read'), type: WebNotificationType.Info });
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.failed_to_update_notification_status'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
in:fade={{ duration: 100 }}
|
||||
out:fade={{ duration: 100 }}
|
||||
id="notification-panel"
|
||||
class="absolute right-[25px] top-[70px] z-[100] 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"
|
||||
use:focusTrap
|
||||
>
|
||||
<Stack class="max-h-[500px]">
|
||||
<div class="flex justify-between items-center mt-4 mx-4">
|
||||
<Text size="medium" color="secondary" class="font-semibold">{$t('notifications')}</Text>
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={noUnreadNotifications}
|
||||
leadingIcon={mdiCheckAll}
|
||||
size="small"
|
||||
color="primary"
|
||||
onclick={() => markAllAsRead()}>{$t('mark_all_as_read')}</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
{#if noUnreadNotifications}
|
||||
<Stack
|
||||
class="py-12 flex flex-col place-items-center place-content-center text-gray-700 dark:text-gray-300"
|
||||
gap={1}
|
||||
>
|
||||
<Icon path={mdiBellOutline} size={20}></Icon>
|
||||
<Text>{$t('no_notifications')}</Text>
|
||||
</Stack>
|
||||
{:else}
|
||||
<Scrollable class="pb-6">
|
||||
<Stack gap={0}>
|
||||
{#each notificationManager.notifications as notification (notification.id)}
|
||||
<div animate:flip={{ duration: 400 }}>
|
||||
<NotificationItem {notification} onclick={(id) => markAsRead(id)} />
|
||||
</div>
|
||||
{/each}
|
||||
</Stack>
|
||||
</Scrollable>
|
||||
{/if}
|
||||
</Stack>
|
||||
</div>
|
||||
@@ -26,7 +26,7 @@
|
||||
</script>
|
||||
|
||||
{#if showing}
|
||||
<div class="absolute left-0 top-0 z-[999999999] h-[3px] w-dvw bg-white">
|
||||
<div class="absolute start-0 top-0 z-[999999999] h-[3px] w-dvw bg-white">
|
||||
<span class="absolute h-[3px] bg-immich-primary" style:width={`${$progress}%`}></span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="whitespace-pre-wrap pl-[28px] pr-[16px] text-sm" data-testid="message">
|
||||
<p class="whitespace-pre-wrap ps-[28px] pe-[16px] text-sm" data-testid="message">
|
||||
{#if isComponentNotification(notification)}
|
||||
<notification.component.type {...notification.component.props} />
|
||||
{:else}
|
||||
@@ -109,7 +109,7 @@
|
||||
</p>
|
||||
|
||||
{#if notification.button}
|
||||
<p class="pl-[28px] mt-2.5 text-sm">
|
||||
<p class="ps-[28px] mt-2.5 text-sm">
|
||||
<button
|
||||
type="button"
|
||||
class="{buttonStyle[notification.type]} rounded px-3 pt-1.5 pb-1 transition-all duration-200"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<div role="status" aria-relevant="additions" aria-label={$t('notifications')}>
|
||||
{#if $notificationList.length > 0}
|
||||
<section transition:fade={{ duration: 250 }} id="notification-list" class="fixed right-5 top-[80px] z-[99999999]">
|
||||
<section transition:fade={{ duration: 250 }} id="notification-list" class="fixed end-5 top-[80px] z-[99999999]">
|
||||
{#each $notificationList as notification (notification.id)}
|
||||
<div animate:flip={{ duration: 250, easing: quintOut }}>
|
||||
<NotificationCard {notification} />
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<div class="relative w-full">
|
||||
<input
|
||||
{...rest}
|
||||
class="immich-form-input w-full !pr-12"
|
||||
class="immich-form-input w-full !pe-12"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
{required}
|
||||
value={password}
|
||||
|
||||
@@ -88,5 +88,5 @@
|
||||
</script>
|
||||
|
||||
{#if !hidden}
|
||||
<span class="absolute left-0 h-[3px] bg-immich-primary shadow-2xl" style:width={`${$progress * 100}%`}></span>
|
||||
<span class="absolute start-0 h-[3px] bg-immich-primary shadow-2xl" style:width={`${$progress * 100}%`}></span>
|
||||
{/if}
|
||||
|
||||
@@ -446,7 +446,7 @@
|
||||
aria-valuemax={toScrollY(1)}
|
||||
aria-valuemin={toScrollY(0)}
|
||||
data-id="immich-scrubbable-scrollbar"
|
||||
class="absolute right-0 z-[1] select-none bg-immich-bg hover:cursor-row-resize"
|
||||
class="absolute end-0 z-[1] select-none bg-immich-bg hover:cursor-row-resize"
|
||||
style:padding-top={PADDING_TOP + 'px'}
|
||||
style:padding-bottom={PADDING_BOTTOM + 'px'}
|
||||
style:width
|
||||
@@ -464,7 +464,7 @@
|
||||
class={[
|
||||
{ 'border-b-2': isDragging },
|
||||
{ 'rounded-bl-md': !isDragging },
|
||||
'truncate opacity-85 pointer-events-none absolute right-0 z-[100] min-w-20 max-w-64 w-fit rounded-tl-md border-immich-primary bg-immich-bg py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:bg-immich-dark-gray dark:text-immich-dark-fg',
|
||||
'truncate opacity-85 pointer-events-none absolute end-0 z-[100] min-w-20 max-w-64 w-fit rounded-ss-md border-immich-primary bg-immich-bg py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:bg-immich-dark-gray dark:text-immich-dark-fg',
|
||||
]}
|
||||
style:top="{hoverY + 2}px"
|
||||
>
|
||||
@@ -474,7 +474,7 @@
|
||||
{#if usingMobileDevice && ((assetStore.scrolling && scrollHoverLabel) || isHover || isDragging)}
|
||||
<div
|
||||
id="time-label"
|
||||
class="rounded-l-full w-[32px] pl-2 text-white bg-immich-primary dark:bg-gray-600 hover:cursor-pointer select-none"
|
||||
class="rounded-s-full w-[32px] ps-2 text-white bg-immich-primary dark:bg-gray-600 hover:cursor-pointer select-none"
|
||||
style:top="{PADDING_TOP + (scrollY - 50 / 2)}px"
|
||||
style:height="50px"
|
||||
style:right="0"
|
||||
@@ -482,8 +482,8 @@
|
||||
in:fade={{ duration: 200 }}
|
||||
out:fade={{ duration: 200 }}
|
||||
>
|
||||
<Icon path={mdiPlay} size="20" class="-rotate-90 relative top-[9px] -right-[2px]" />
|
||||
<Icon path={mdiPlay} size="20" class="rotate-90 relative top-[1px] -right-[2px]" />
|
||||
<Icon path={mdiPlay} size="20" class="-rotate-90 relative top-[9px] -end-[2px]" />
|
||||
<Icon path={mdiPlay} size="20" class="rotate-90 relative top-[1px] -end-[2px]" />
|
||||
{#if (assetStore.scrolling && scrollHoverLabel) || isHover || isDragging}
|
||||
<p
|
||||
transition:fade={{ duration: 200 }}
|
||||
@@ -500,13 +500,13 @@
|
||||
<!-- Scroll Position Indicator Line -->
|
||||
{#if !usingMobileDevice && !isDragging}
|
||||
<div
|
||||
class="absolute right-0 h-[2px] w-10 bg-immich-primary dark:bg-immich-dark-primary"
|
||||
class="absolute end-0 h-[2px] w-10 bg-immich-primary dark:bg-immich-dark-primary"
|
||||
style:top="{scrollY + PADDING_TOP - 2}px"
|
||||
>
|
||||
{#if assetStore.scrolling && scrollHoverLabel && !isHover}
|
||||
<p
|
||||
transition:fade={{ duration: 200 }}
|
||||
class="truncate pointer-events-none absolute right-0 bottom-0 z-[100] min-w-20 max-w-64 w-fit rounded-tl-md border-b-2 border-immich-primary bg-immich-bg/80 py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:bg-immich-dark-gray/80 dark:text-immich-dark-fg"
|
||||
class="truncate pointer-events-none absolute end-0 bottom-0 z-[100] min-w-20 max-w-64 w-fit rounded-tl-md border-b-2 border-immich-primary bg-immich-bg/80 py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:bg-immich-dark-gray/80 dark:text-immich-dark-fg"
|
||||
>
|
||||
{scrollHoverLabel}
|
||||
</p>
|
||||
@@ -521,7 +521,7 @@
|
||||
data-label={segments.at(0)?.dateFormatted}
|
||||
>
|
||||
{#if relativeTopOffset > 6}
|
||||
<div class="absolute right-[0.75rem] h-[4px] w-[4px] rounded-full bg-gray-300"></div>
|
||||
<div class="absolute end-[0.75rem] h-[4px] w-[4px] rounded-full bg-gray-300"></div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Time Segment -->
|
||||
@@ -535,12 +535,12 @@
|
||||
>
|
||||
{#if !usingMobileDevice}
|
||||
{#if segment.hasLabel}
|
||||
<div class="absolute right-[1.25rem] top-[-16px] z-10 text-[12px] dark:text-immich-dark-fg font-immich-mono">
|
||||
<div class="absolute end-[1.25rem] top-[-16px] z-10 text-[12px] dark:text-immich-dark-fg font-immich-mono">
|
||||
{segment.date.year}
|
||||
</div>
|
||||
{/if}
|
||||
{#if segment.hasDot}
|
||||
<div class="absolute right-[0.75rem] bottom-0 h-[4px] w-[4px] rounded-full bg-gray-300"></div>
|
||||
<div class="absolute end-[0.75rem] bottom-0 h-[4px] w-[4px] rounded-full bg-gray-300"></div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -261,15 +261,15 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="absolute inset-y-0 {showClearIcon ? 'right-14' : 'right-2'} flex items-center pl-6 transition-all">
|
||||
<div class="absolute inset-y-0 {showClearIcon ? 'end-14' : 'end-2'} flex items-center ps-6 transition-all">
|
||||
<CircleIconButton title={$t('show_search_options')} icon={mdiTune} onclick={onFilterClick} size="20" />
|
||||
</div>
|
||||
|
||||
{#if isFocus}
|
||||
<div
|
||||
class="absolute inset-y-0 flex items-center"
|
||||
class:right-16={isFocus}
|
||||
class:right-28={isFocus && value.length > 0}
|
||||
class:end-16={isFocus}
|
||||
class:end-28={isFocus && value.length > 0}
|
||||
>
|
||||
<p
|
||||
class="bg-immich-primary text-white dark:bg-immich-dark-primary/90 dark:text-black/75 rounded-full px-3 py-1 text-xs z-10"
|
||||
@@ -280,11 +280,11 @@
|
||||
{/if}
|
||||
|
||||
{#if showClearIcon}
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<div class="absolute inset-y-0 end-0 flex items-center pe-2">
|
||||
<CircleIconButton onclick={onClear} icon={mdiClose} title={$t('clear')} size="20" />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-2">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-2">
|
||||
<CircleIconButton
|
||||
type="submit"
|
||||
disabled={showFilter}
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
id={getId(index)}
|
||||
class="relative flex w-full cursor-pointer gap-3 py-3 pl-5 hover:bg-gray-100 aria-selected:bg-gray-100 dark:aria-selected:bg-gray-500/30 dark:hover:bg-gray-500/30"
|
||||
class="relative flex w-full cursor-pointer gap-3 py-3 ps-5 hover:bg-gray-100 aria-selected:bg-gray-100 dark:aria-selected:bg-gray-500/30 dark:hover:bg-gray-500/30"
|
||||
onclick={() => handleSelect(savedSearchTerm)}
|
||||
role="option"
|
||||
tabindex="-1"
|
||||
@@ -132,7 +132,7 @@
|
||||
<Icon path={mdiMagnify} size="1.5em" ariaHidden={true} />
|
||||
{savedSearchTerm}
|
||||
</div>
|
||||
<div aria-hidden={true} class="absolute right-5 top-0 items-center justify-center py-3">
|
||||
<div aria-hidden={true} class="absolute end-5 top-0 items-center justify-center py-3">
|
||||
<CircleIconButton
|
||||
icon={mdiClose}
|
||||
title={$t('remove')}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
{#if tag}
|
||||
<div class="flex group transition-all">
|
||||
<span
|
||||
class="inline-block h-min whitespace-nowrap pl-3 pr-1 group-hover:pl-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-tl-full rounded-bl-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 roudned-s-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
|
||||
>
|
||||
<p class="text-sm">
|
||||
{tag.value}
|
||||
@@ -66,7 +66,7 @@
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-100 dark:text-immich-dark-gray bg-immich-primary/95 dark:bg-immich-dark-primary/95 rounded-tr-full rounded-br-full place-items-center place-content-center pr-2 pl-1 py-1 hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
|
||||
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)}
|
||||
>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { t } from 'svelte-i18n';
|
||||
import { mdiAlert } from '@mdi/js';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
@@ -177,16 +178,19 @@
|
||||
<span
|
||||
class="immich-form-label pb-2 text-xs"
|
||||
id="version-history"
|
||||
title={createdAt.toLocaleString(DateTime.DATETIME_SHORT_WITH_SECONDS)}
|
||||
title={createdAt.toLocaleString(DateTime.DATETIME_SHORT_WITH_SECONDS, { locale: $locale })}
|
||||
>
|
||||
{$t('version_history_item', {
|
||||
values: {
|
||||
version: item.version,
|
||||
date: createdAt.toLocaleString({
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
}),
|
||||
date: createdAt.toLocaleString(
|
||||
{
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
},
|
||||
{ locale: $locale },
|
||||
),
|
||||
},
|
||||
})}
|
||||
</span>
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
type="button"
|
||||
aria-expanded={isOpen}
|
||||
{onclick}
|
||||
class="flex w-full place-items-center justify-between text-left"
|
||||
class="flex w-full place-items-center justify-between text-start"
|
||||
>
|
||||
<div>
|
||||
<div class="flex gap-2 place-items-center">
|
||||
@@ -110,7 +110,7 @@
|
||||
</button>
|
||||
|
||||
{#if isOpen}
|
||||
<ul transition:slide={{ duration: 150 }} class="mb-2 ml-4">
|
||||
<ul transition:slide={{ duration: 150 }} class="mb-2 ms-4">
|
||||
{@render children?.()}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
{#if inputType === SettingInputFieldType.COLOR}
|
||||
<input
|
||||
bind:this={input}
|
||||
class="immich-form-input w-full pb-2 rounded-none mr-1"
|
||||
class="immich-form-input w-full pb-2 rounded-none me-1"
|
||||
aria-describedby={description ? `${label}-desc` : undefined}
|
||||
aria-labelledby="{label}-label"
|
||||
id={label}
|
||||
|
||||
@@ -65,12 +65,12 @@
|
||||
path={mdiChevronDown}
|
||||
size="1.2em"
|
||||
ariaHidden={true}
|
||||
class="pointer-events-none right-1 relative col-start-1 row-start-1 self-center justify-self-end {disabled
|
||||
class="pointer-events-none end-1 relative col-start-1 row-start-1 self-center justify-self-end {disabled
|
||||
? 'text-immich-bg'
|
||||
: 'text-immich-fg dark:text-immich-bg'}"
|
||||
/>
|
||||
<select
|
||||
class="immich-form-input w-full appearance-none row-start-1 col-start-1 !pr-6"
|
||||
class="immich-form-input w-full appearance-none row-start-1 col-start-1 !pe-6"
|
||||
{disabled}
|
||||
aria-describedby={desc ? `${name}-desc` : undefined}
|
||||
{name}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</script>
|
||||
|
||||
<div class="flex place-items-center justify-between">
|
||||
<div class="mr-2">
|
||||
<div class="me-2">
|
||||
<div class="flex h-[26px] place-items-center gap-1">
|
||||
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for={sliderId}>
|
||||
{title}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm">
|
||||
<div class="flex justify-self-end">
|
||||
{#each shortcut.key as key (key)}
|
||||
<p class="mr-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2">
|
||||
<p class="me-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2">
|
||||
{key}
|
||||
</p>
|
||||
{/each}
|
||||
@@ -74,7 +74,7 @@
|
||||
<div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm">
|
||||
<div class="flex justify-self-end">
|
||||
{#each shortcut.key as key (key)}
|
||||
<p class="mr-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2">
|
||||
<p class="me-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2">
|
||||
{key}
|
||||
</p>
|
||||
{/each}
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<LicenseModal onClose={() => (isOpen = false)} />
|
||||
{/if}
|
||||
|
||||
<div class="license-status pl-4 text-sm">
|
||||
<div class="license-status ps-4 text-sm">
|
||||
{#if $isPurchased && $preferences.purchase.showSupportBadge}
|
||||
<button
|
||||
onclick={() => goto(`${AppRoute.USER_SETTINGS}?isOpen=user-purchase-settings`)}
|
||||
@@ -123,7 +123,7 @@
|
||||
{#if showMessage}
|
||||
<dialog
|
||||
open
|
||||
class="hidden sidebar:block w-[500px] absolute bottom-[75px] left-[255px] bg-gray-50 dark:border-gray-800 border border-gray-200 dark:bg-immich-dark-gray dark:text-white text-black rounded-3xl z-10 shadow-2xl px-8 py-6"
|
||||
class="hidden sidebar:block w-[500px] absolute bottom-[75px] start-[255px] bg-gray-50 dark:border-gray-800 border border-gray-200 dark:bg-immich-dark-gray dark:text-white text-black rounded-3xl z-10 shadow-2xl px-8 py-6"
|
||||
transition:fade={{ duration: 150 }}
|
||||
onmouseover={() => (hoverMessage = true)}
|
||||
onmouseleave={() => (hoverMessage = false)}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<a
|
||||
href={'/albums/' + album.id}
|
||||
title={album.albumName}
|
||||
class="flex w-full place-items-center justify-between gap-4 rounded-r-full py-3 transition-[padding] delay-100 duration-100 hover:cursor-pointer hover:bg-immich-gray hover:text-immich-primary dark:text-immich-dark-fg dark:hover:bg-immich-dark-gray dark:hover:text-immich-dark-primary pl-10 group-hover:sm:px-10 md:px-10"
|
||||
class="flex w-full place-items-center justify-between gap-4 rounded-e-full py-3 transition-[padding] delay-100 duration-100 hover:cursor-pointer hover:bg-immich-gray hover:text-immich-primary dark:text-immich-dark-fg dark:hover:bg-immich-dark-gray dark:hover:text-immich-dark-primary ps-10 group-hover:sm:px-10 md:px-10"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user