chore(web): prettier (#2821)

Co-authored-by: Thomas Way <thomas@6f.io>
This commit is contained in:
Jason Rasmussen 2023-07-01 00:50:47 -04:00 committed by GitHub
parent 7c2f7d6c51
commit f55b3add80
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
242 changed files with 12794 additions and 13426 deletions

View file

@ -1,34 +1,30 @@
/** @type {import('eslint').Linter.Config} */ /** @type {import('eslint').Linter.Config} */
module.exports = { module.exports = {
root: true, root: true,
extends: [ extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:svelte/recommended'],
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended'
],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'], plugins: ['@typescript-eslint'],
parserOptions: { parserOptions: {
sourceType: 'module', sourceType: 'module',
ecmaVersion: 2020, ecmaVersion: 2020,
extraFileExtensions: ['.svelte'] extraFileExtensions: ['.svelte'],
}, },
env: { env: {
browser: true, browser: true,
es2017: true, es2017: true,
node: true node: true,
}, },
overrides: [ overrides: [
{ {
files: ['*.svelte'], files: ['*.svelte'],
parser: 'svelte-eslint-parser', parser: 'svelte-eslint-parser',
parserOptions: { parserOptions: {
parser: '@typescript-eslint/parser' parser: '@typescript-eslint/parser',
} },
} },
], ],
globals: { globals: {
NodeJS: true NodeJS: true,
}, },
rules: { rules: {
'@typescript-eslint/no-unused-vars': [ '@typescript-eslint/no-unused-vars': [
@ -36,8 +32,8 @@ module.exports = {
{ {
// Allow underscore (_) variables // Allow underscore (_) variables
argsIgnorePattern: '^_$', argsIgnorePattern: '^_$',
varsIgnorePattern: '^_$' varsIgnorePattern: '^_$',
} },
] ],
} },
}; };

View file

@ -1,6 +1,7 @@
{ {
"useTabs": true,
"singleQuote": true, "singleQuote": true,
"trailingComma": "none", "trailingComma": "all",
"printWidth": 100 "printWidth": 120,
"semi": true,
"organizeImportsSkipDestructiveCodeActions": true
} }

View file

@ -1,3 +1,3 @@
module.exports = { module.exports = {
browser: false browser: false,
}; };

View file

@ -1,3 +1,3 @@
module.exports = { module.exports = {
env: {} env: {},
}; };

View file

@ -1,3 +1,3 @@
module.exports = { module.exports = {
presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'] presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
}; };

View file

@ -27,8 +27,8 @@ export default {
coverageThreshold: { coverageThreshold: {
global: { global: {
lines: 4, lines: 4,
statements: 4 statements: 4,
} },
}, },
// An array of regexp pattern strings used to skip coverage collection // An array of regexp pattern strings used to skip coverage collection
@ -86,11 +86,10 @@ export default {
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
moduleNameMapper: { moduleNameMapper: {
'\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 'identity-obj-proxy',
'identity-obj-proxy',
'^\\$lib(.*)$': '<rootDir>/src/lib$1', '^\\$lib(.*)$': '<rootDir>/src/lib$1',
'^\\@api(.*)$': '<rootDir>/src/api$1', '^\\@api(.*)$': '<rootDir>/src/api$1',
'^\\@test-data(.*)$': '<rootDir>/src/test-data$1' '^\\@test-data(.*)$': '<rootDir>/src/test-data$1',
}, },
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
@ -181,13 +180,13 @@ export default {
'^.+\\.svelte$': [ '^.+\\.svelte$': [
'svelte-jester', 'svelte-jester',
{ {
preprocess: true preprocess: true,
} },
] ],
}, },
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
transformIgnorePatterns: ['/node_modules/(?!svelte-material-icons).*/', '\\.pnp\\.[^\\/]+$'] transformIgnorePatterns: ['/node_modules/(?!svelte-material-icons).*/', '\\.pnp\\.[^\\/]+$'],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined, // unmockedModulePathPatterns: undefined,

View file

@ -1,6 +1,6 @@
module.exports = { module.exports = {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {} autoprefixer: {},
} },
}; };

View file

@ -16,7 +16,7 @@ import {
SharedLinkApi, SharedLinkApi,
SystemConfigApi, SystemConfigApi,
UserApi, UserApi,
UserApiFp UserApiFp,
} from './open-api'; } from './open-api';
import { BASE_PATH } from './open-api/base'; import { BASE_PATH } from './open-api/base';
import { DUMMY_BASE_URL, toPathString } from './open-api/common'; import { DUMMY_BASE_URL, toPathString } from './open-api/common';
@ -84,16 +84,12 @@ export class ImmichApi {
this.config.basePath = baseUrl; this.config.basePath = baseUrl;
} }
public getAssetFileUrl( public getAssetFileUrl(...[assetId, isThumb, isWeb, key]: ApiParams<typeof AssetApiFp, 'serveFile'>) {
...[assetId, isThumb, isWeb, key]: ApiParams<typeof AssetApiFp, 'serveFile'>
) {
const path = `/asset/file/${assetId}`; const path = `/asset/file/${assetId}`;
return this.createUrl(path, { isThumb, isWeb, key }); return this.createUrl(path, { isThumb, isWeb, key });
} }
public getAssetThumbnailUrl( public getAssetThumbnailUrl(...[assetId, format, key]: ApiParams<typeof AssetApiFp, 'getAssetThumbnail'>) {
...[assetId, format, key]: ApiParams<typeof AssetApiFp, 'getAssetThumbnail'>
) {
const path = `/asset/thumbnail/${assetId}`; const path = `/asset/thumbnail/${assetId}`;
return this.createUrl(path, { format, key }); return this.createUrl(path, { format, key });
} }
@ -119,7 +115,7 @@ export class ImmichApi {
[JobName.VideoConversion]: 'Transcode Videos', [JobName.VideoConversion]: 'Transcode Videos',
[JobName.StorageTemplateMigration]: 'Storage Template Migration', [JobName.StorageTemplateMigration]: 'Storage Template Migration',
[JobName.BackgroundTask]: 'Background Tasks', [JobName.BackgroundTask]: 'Background Tasks',
[JobName.Search]: 'Search' [JobName.Search]: 'Search',
}; };
return names[jobName]; return names[jobName];

View file

@ -1,3 +1,3 @@
export * from './open-api';
export * from './api'; export * from './api';
export * from './open-api';
export * from './utils'; export * from './utils';

View file

@ -3,10 +3,6 @@ import type { Configuration } from './open-api';
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
export type ApiFp = (configuration: Configuration) => Record<any, (...args: any) => any>; export type ApiFp = (configuration: Configuration) => Record<any, (...args: any) => any>;
export type OmitLast<T extends readonly unknown[]> = T extends readonly [...infer U, any?] export type OmitLast<T extends readonly unknown[]> = T extends readonly [...infer U, any?] ? U : [...T];
? U
: [...T];
export type ApiParams<F extends ApiFp, K extends keyof ReturnType<F>> = OmitLast< export type ApiParams<F extends ApiFp, K extends keyof ReturnType<F>> = OmitLast<Parameters<ReturnType<F>[K]>>;
Parameters<ReturnType<F>[K]>
>;

View file

@ -31,5 +31,5 @@ export const oauth = {
}, },
unlink: () => { unlink: () => {
return api.oauthApi.unlink(); return api.oauthApi.unlink();
} },
}; };

View file

@ -52,6 +52,6 @@ export const handleError: HandleServerError = async ({ error }) => {
return { return {
message: response?.data?.message || httpError?.message || DEFAULT_MESSAGE, message: response?.data?.message || httpError?.message || DEFAULT_MESSAGE,
code, code,
stack: httpError?.stack stack: httpError?.stack,
}; };
}; };

View file

@ -2,7 +2,7 @@ const createObjectURLMock = jest.fn();
Object.defineProperty(URL, 'createObjectURL', { Object.defineProperty(URL, 'createObjectURL', {
writable: true, writable: true,
value: createObjectURLMock value: createObjectURLMock,
}); });
export { createObjectURLMock }; export { createObjectURLMock };

View file

@ -27,8 +27,7 @@
<svelte:fragment slot="prompt"> <svelte:fragment slot="prompt">
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<p> <p>
<b>{user.firstName} {user.lastName}</b>'s account and assets will be permanently deleted <b>{user.firstName} {user.lastName}</b>'s account and assets will be permanently deleted after 7 days.
after 7 days.
</p> </p>
<p>Are you sure you want to continue?</p> <p>Are you sure you want to continue?</p>
</div> </div>

View file

@ -7,7 +7,7 @@
const colorClasses: Record<Colors, string> = { const colorClasses: Record<Colors, string> = {
'light-gray': 'bg-gray-300/90 dark:bg-gray-600/90', 'light-gray': 'bg-gray-300/90 dark:bg-gray-600/90',
gray: 'bg-gray-300 dark:bg-gray-600' gray: 'bg-gray-300 dark:bg-gray-600',
}; };
</script> </script>

View file

@ -7,7 +7,7 @@
const colorClasses: Record<Color, string> = { const colorClasses: Record<Color, string> = {
success: 'bg-green-500/70 text-gray-900 dark:bg-green-700/90 dark:text-gray-100', success: 'bg-green-500/70 text-gray-900 dark:bg-green-700/90 dark:text-gray-100',
warning: 'bg-orange-400/70 text-gray-900 dark:bg-orange-900 dark:text-gray-100' warning: 'bg-orange-400/70 text-gray-900 dark:bg-orange-900 dark:text-gray-100',
}; };
</script> </script>

View file

@ -43,9 +43,7 @@
<JobTileStatus color="success">Active</JobTileStatus> <JobTileStatus color="success">Active</JobTileStatus>
{/if} {/if}
<div class="flex flex-col gap-2 p-5 sm:p-7 md:p-9"> <div class="flex flex-col gap-2 p-5 sm:p-7 md:p-9">
<div <div class="flex items-center gap-4 text-xl font-semibold text-immich-primary dark:text-immich-dark-primary">
class="flex items-center gap-4 text-xl font-semibold text-immich-primary dark:text-immich-dark-primary"
>
<span class="flex gap-2 items-center"> <span class="flex gap-2 items-center">
<svelte:component this={icon} size="1.25em" class="shrink-0 hidden sm:block" /> <svelte:component this={icon} size="1.25em" class="shrink-0 hidden sm:block" />
{title.toUpperCase()} {title.toUpperCase()}
@ -98,10 +96,7 @@
<div class="flex sm:flex-col flex-row sm:w-32 w-full overflow-hidden"> <div class="flex sm:flex-col flex-row sm:w-32 w-full overflow-hidden">
{#if !isIdle} {#if !isIdle}
{#if waitingCount > 0} {#if waitingCount > 0}
<JobTileButton <JobTileButton color="gray" on:click={() => dispatch('command', { command: JobCommand.Empty, force: false })}>
color="gray"
on:click={() => dispatch('command', { command: JobCommand.Empty, force: false })}
>
<Close size="24" /> CLEAR <Close size="24" /> CLEAR
</JobTileButton> </JobTileButton>
{/if} {/if}
@ -123,10 +118,7 @@
</JobTileButton> </JobTileButton>
{/if} {/if}
{:else if allowForceCommand} {:else if allowForceCommand}
<JobTileButton <JobTileButton color="gray" on:click={() => dispatch('command', { command: JobCommand.Start, force: true })}>
color="gray"
on:click={() => dispatch('command', { command: JobCommand.Start, force: true })}
>
<AllInclusive size="24" /> <AllInclusive size="24" />
{allText} {allText}
</JobTileButton> </JobTileButton>

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { import {
notificationController, notificationController,
NotificationType NotificationType,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
@ -55,48 +55,47 @@
[JobName.ThumbnailGeneration]: { [JobName.ThumbnailGeneration]: {
icon: FileJpgBox, icon: FileJpgBox,
title: api.getJobName(JobName.ThumbnailGeneration), title: api.getJobName(JobName.ThumbnailGeneration),
subtitle: 'Regenerate JPEG and WebP thumbnails' subtitle: 'Regenerate JPEG and WebP thumbnails',
}, },
[JobName.MetadataExtraction]: { [JobName.MetadataExtraction]: {
icon: Table, icon: Table,
title: api.getJobName(JobName.MetadataExtraction), title: api.getJobName(JobName.MetadataExtraction),
subtitle: 'Extract metadata information i.e. GPS, resolution...etc' subtitle: 'Extract metadata information i.e. GPS, resolution...etc',
}, },
[JobName.Sidecar]: { [JobName.Sidecar]: {
title: api.getJobName(JobName.Sidecar), title: api.getJobName(JobName.Sidecar),
icon: FileXmlBox, icon: FileXmlBox,
subtitle: 'Discover or synchronize sidecar metadata from the filesystem', subtitle: 'Discover or synchronize sidecar metadata from the filesystem',
allText: 'SYNC', allText: 'SYNC',
missingText: 'DISCOVER' missingText: 'DISCOVER',
}, },
[JobName.ObjectTagging]: { [JobName.ObjectTagging]: {
icon: TagMultiple, icon: TagMultiple,
title: api.getJobName(JobName.ObjectTagging), title: api.getJobName(JobName.ObjectTagging),
subtitle: subtitle: 'Run machine learning to tag objects\nNote that some assets may not have any objects detected',
'Run machine learning to tag objects\nNote that some assets may not have any objects detected'
}, },
[JobName.ClipEncoding]: { [JobName.ClipEncoding]: {
icon: VectorCircle, icon: VectorCircle,
title: api.getJobName(JobName.ClipEncoding), title: api.getJobName(JobName.ClipEncoding),
subtitle: 'Run machine learning to generate clip embeddings' subtitle: 'Run machine learning to generate clip embeddings',
}, },
[JobName.RecognizeFaces]: { [JobName.RecognizeFaces]: {
icon: FaceRecognition, icon: FaceRecognition,
title: api.getJobName(JobName.RecognizeFaces), title: api.getJobName(JobName.RecognizeFaces),
subtitle: 'Run machine learning to recognize faces', subtitle: 'Run machine learning to recognize faces',
handleCommand: handleFaceCommand handleCommand: handleFaceCommand,
}, },
[JobName.VideoConversion]: { [JobName.VideoConversion]: {
icon: Video, icon: Video,
title: api.getJobName(JobName.VideoConversion), title: api.getJobName(JobName.VideoConversion),
subtitle: 'Transcode videos not in the desired format' subtitle: 'Transcode videos not in the desired format',
}, },
[JobName.StorageTemplateMigration]: { [JobName.StorageTemplateMigration]: {
icon: FolderMove, icon: FolderMove,
title: api.getJobName(JobName.StorageTemplateMigration), title: api.getJobName(JobName.StorageTemplateMigration),
allowForceCommand: false, allowForceCommand: false,
component: StorageMigrationDescription component: StorageMigrationDescription,
} },
}; };
const jobDetailsArray = Object.entries(jobDetails) as [JobName, JobDetails][]; const jobDetailsArray = Object.entries(jobDetails) as [JobName, JobDetails][];
@ -112,7 +111,7 @@
case JobCommand.Empty: case JobCommand.Empty:
notificationController.show({ notificationController.show({
message: `Cleared jobs for: ${title}`, message: `Cleared jobs for: ${title}`,
type: NotificationType.Info type: NotificationType.Info,
}); });
break; break;
} }

View file

@ -3,8 +3,7 @@
</script> </script>
Apply the current Apply the current
<a <a href={`${AppRoute.ADMIN_SETTINGS}?open=storage-template`} class="text-immich-primary dark:text-immich-dark-primary"
href={`${AppRoute.ADMIN_SETTINGS}?open=storage-template`} >Storage template</a
class="text-immich-primary dark:text-immich-dark-primary">Storage template</a
> >
to previously uploaded assets to previously uploaded assets

View file

@ -14,13 +14,7 @@
}; };
</script> </script>
<ConfirmDialogue <ConfirmDialogue title="Restore User" confirmText="Continue" confirmColor="green" on:confirm={restoreUser} on:cancel>
title="Restore User"
confirmText="Continue"
confirmColor="green"
on:confirm={restoreUser}
on:cancel
>
<svelte:fragment slot="prompt"> <svelte:fragment slot="prompt">
<p><b>{user.firstName} {user.lastName}</b>'s account will be restored.</p> <p><b>{user.firstName} {user.lastName}</b>'s account will be restored.</p>
</svelte:fragment> </svelte:fragment>

View file

@ -11,7 +11,7 @@
photos: 0, photos: 0,
videos: 0, videos: 0,
usage: 0, usage: 0,
usageByUser: [] usageByUser: [],
}; };
$: zeros = (value: number) => { $: zeros = (value: number) => {
@ -35,13 +35,9 @@
<StatsCard logo={Memory} title="STORAGE" value={statsUsage} unit={statsUsageUnit} /> <StatsCard logo={Memory} title="STORAGE" value={statsUsage} unit={statsUsageUnit} />
</div> </div>
<div class="mt-5 lg:hidden flex"> <div class="mt-5 lg:hidden flex">
<div <div class="bg-immich-gray dark:bg-immich-dark-gray rounded-3xl p-5 flex flex-col justify-between">
class="bg-immich-gray dark:bg-immich-dark-gray rounded-3xl p-5 flex flex-col justify-between"
>
<div class="flex flex-wrap gap-x-12"> <div class="flex flex-wrap gap-x-12">
<div <div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary"
>
<CameraIris size="25" /> <CameraIris size="25" />
<p>PHOTOS</p> <p>PHOTOS</p>
</div> </div>
@ -53,9 +49,7 @@
</div> </div>
</div> </div>
<div class="flex flex-wrap gap-x-12"> <div class="flex flex-wrap gap-x-12">
<div <div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary"
>
<PlayCircle size="25" /> <PlayCircle size="25" />
<p>VIDEOS</p> <p>VIDEOS</p>
</div> </div>
@ -67,9 +61,7 @@
</div> </div>
</div> </div>
<div class="flex flex-wrap gap-x-7"> <div class="flex flex-wrap gap-x-7">
<div <div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary"
>
<Memory size="25" /> <Memory size="25" />
<p>STORAGE</p> <p>STORAGE</p>
</div> </div>
@ -78,9 +70,7 @@
<span class="text-[#DCDADA] dark:text-[#525252]">{zeros(statsUsage)}</span><span <span class="text-[#DCDADA] dark:text-[#525252]">{zeros(statsUsage)}</span><span
class="text-immich-primary dark:text-immich-dark-primary">{statsUsage}</span class="text-immich-primary dark:text-immich-dark-primary">{statsUsage}</span
> >
<span class="text-center my-auto ml-2 text-base font-light text-gray-400" <span class="text-center my-auto ml-2 text-base font-light text-gray-400">{statsUsageUnit}</span>
>{statsUsageUnit}</span
>
</div> </div>
</div> </div>
</div> </div>
@ -107,13 +97,10 @@
<tr <tr
class="text-center flex place-items-center w-full h-[50px] even:bg-immich-bg even:dark:bg-immich-dark-gray/50 odd:bg-immich-gray odd:dark:bg-immich-dark-gray/75" class="text-center flex place-items-center w-full h-[50px] even:bg-immich-bg even:dark:bg-immich-dark-gray/50 odd:bg-immich-gray odd:dark:bg-immich-dark-gray/75"
> >
<td class="text-sm px-2 w-1/4 text-ellipsis" <td class="text-sm px-2 w-1/4 text-ellipsis">{user.userFirstName} {user.userLastName}</td>
>{user.userFirstName} {user.userLastName}</td
>
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.photos.toLocaleString($locale)}</td> <td class="text-sm px-2 w-1/4 text-ellipsis">{user.photos.toLocaleString($locale)}</td>
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.videos.toLocaleString($locale)}</td> <td class="text-sm px-2 w-1/4 text-ellipsis">{user.videos.toLocaleString($locale)}</td>
<td class="text-sm px-2 w-1/4 text-ellipsis">{asByteUnitString(user.usage, $locale)}</td <td class="text-sm px-2 w-1/4 text-ellipsis">{asByteUnitString(user.usage, $locale)}</td>
>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>

View file

@ -15,9 +15,7 @@
}; };
</script> </script>
<div <div class="w-[250px] h-[140px] bg-immich-gray dark:bg-immich-dark-gray rounded-3xl p-5 flex flex-col justify-between">
class="w-[250px] h-[140px] bg-immich-gray dark:bg-immich-dark-gray rounded-3xl p-5 flex flex-col justify-between"
>
<div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary"> <div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
<svelte:component this={logo} size="40" /> <svelte:component this={logo} size="40" />
<p>{title}</p> <p>{title}</p>

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { import {
notificationController, notificationController,
NotificationType NotificationType,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { api, SystemConfigFFmpegDto, SystemConfigFFmpegDtoTranscodeEnum } from '@api'; import { api, SystemConfigFFmpegDto, SystemConfigFFmpegDtoTranscodeEnum } from '@api';
import SettingButtonsRow from '../setting-buttons-row.svelte'; import SettingButtonsRow from '../setting-buttons-row.svelte';
@ -19,7 +19,7 @@
async function getConfigs() { async function getConfigs() {
[savedConfig, defaultConfig] = await Promise.all([ [savedConfig, defaultConfig] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.ffmpeg), api.systemConfigApi.getConfig().then((res) => res.data.ffmpeg),
api.systemConfigApi.getDefaults().then((res) => res.data.ffmpeg) api.systemConfigApi.getDefaults().then((res) => res.data.ffmpeg),
]); ]);
} }
@ -30,8 +30,8 @@
const result = await api.systemConfigApi.updateConfig({ const result = await api.systemConfigApi.updateConfig({
systemConfigDto: { systemConfigDto: {
...configs, ...configs,
ffmpeg: ffmpegConfig ffmpeg: ffmpegConfig,
} },
}); });
ffmpegConfig = { ...result.data.ffmpeg }; ffmpegConfig = { ...result.data.ffmpeg };
@ -39,13 +39,13 @@
notificationController.show({ notificationController.show({
message: 'FFmpeg settings saved', message: 'FFmpeg settings saved',
type: NotificationType.Info type: NotificationType.Info,
}); });
} catch (e) { } catch (e) {
console.error('Error [ffmpeg-settings] [saveSetting]', e); console.error('Error [ffmpeg-settings] [saveSetting]', e);
notificationController.show({ notificationController.show({
message: 'Unable to save settings', message: 'Unable to save settings',
type: NotificationType.Error type: NotificationType.Error,
}); });
} }
} }
@ -58,7 +58,7 @@
notificationController.show({ notificationController.show({
message: 'Reset FFmpeg settings to the recent saved settings', message: 'Reset FFmpeg settings to the recent saved settings',
type: NotificationType.Info type: NotificationType.Info,
}); });
} }
@ -70,7 +70,7 @@
notificationController.show({ notificationController.show({
message: 'Reset FFmpeg settings to default', message: 'Reset FFmpeg settings to default',
type: NotificationType.Info type: NotificationType.Info,
}); });
} }
</script> </script>
@ -103,7 +103,7 @@
{ value: 'medium', text: 'medium' }, { value: 'medium', text: 'medium' },
{ value: 'slow', text: 'slow' }, { value: 'slow', text: 'slow' },
{ value: 'slower', text: 'slower' }, { value: 'slower', text: 'slower' },
{ value: 'veryslow', text: 'veryslow' } { value: 'veryslow', text: 'veryslow' },
]} ]}
isEdited={!(ffmpegConfig.preset == savedConfig.preset)} isEdited={!(ffmpegConfig.preset == savedConfig.preset)}
/> />
@ -115,7 +115,7 @@
options={[ options={[
{ value: 'aac', text: 'aac' }, { value: 'aac', text: 'aac' },
{ value: 'mp3', text: 'mp3' }, { value: 'mp3', text: 'mp3' },
{ value: 'opus', text: 'opus' } { value: 'opus', text: 'opus' },
]} ]}
name="acodec" name="acodec"
isEdited={!(ffmpegConfig.targetAudioCodec == savedConfig.targetAudioCodec)} isEdited={!(ffmpegConfig.targetAudioCodec == savedConfig.targetAudioCodec)}
@ -128,7 +128,7 @@
options={[ options={[
{ value: 'h264', text: 'h264' }, { value: 'h264', text: 'h264' },
{ value: 'hevc', text: 'hevc' }, { value: 'hevc', text: 'hevc' },
{ value: 'vp9', text: 'vp9' } { value: 'vp9', text: 'vp9' },
]} ]}
name="vcodec" name="vcodec"
isEdited={!(ffmpegConfig.targetVideoCodec == savedConfig.targetVideoCodec)} isEdited={!(ffmpegConfig.targetVideoCodec == savedConfig.targetVideoCodec)}
@ -144,7 +144,7 @@
{ value: '1080', text: '1080p' }, { value: '1080', text: '1080p' },
{ value: '720', text: '720p' }, { value: '720', text: '720p' },
{ value: '480', text: '480p' }, { value: '480', text: '480p' },
{ value: 'original', text: 'original' } { value: 'original', text: 'original' },
]} ]}
name="resolution" name="resolution"
isEdited={!(ffmpegConfig.targetResolution == savedConfig.targetResolution)} isEdited={!(ffmpegConfig.targetResolution == savedConfig.targetResolution)}
@ -175,16 +175,16 @@
{ value: SystemConfigFFmpegDtoTranscodeEnum.All, text: 'All videos' }, { value: SystemConfigFFmpegDtoTranscodeEnum.All, text: 'All videos' },
{ {
value: SystemConfigFFmpegDtoTranscodeEnum.Optimal, value: SystemConfigFFmpegDtoTranscodeEnum.Optimal,
text: 'Videos higher than target resolution or not in the desired format' text: 'Videos higher than target resolution or not in the desired format',
}, },
{ {
value: SystemConfigFFmpegDtoTranscodeEnum.Required, value: SystemConfigFFmpegDtoTranscodeEnum.Required,
text: 'Only videos not in the desired format' text: 'Only videos not in the desired format',
}, },
{ {
value: SystemConfigFFmpegDtoTranscodeEnum.Disabled, value: SystemConfigFFmpegDtoTranscodeEnum.Disabled,
text: "Don't transcode any videos, may break playback on some clients" text: "Don't transcode any videos, may break playback on some clients",
} },
]} ]}
isEdited={!(ffmpegConfig.transcode == savedConfig.transcode)} isEdited={!(ffmpegConfig.transcode == savedConfig.transcode)}
/> />

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { import {
notificationController, notificationController,
NotificationType NotificationType,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { api, JobName, SystemConfigJobDto } from '@api'; import { api, JobName, SystemConfigJobDto } from '@api';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
@ -16,14 +16,12 @@
let defaultConfig: SystemConfigJobDto; let defaultConfig: SystemConfigJobDto;
const ignoredJobs = [JobName.BackgroundTask, JobName.Search] as JobName[]; const ignoredJobs = [JobName.BackgroundTask, JobName.Search] as JobName[];
const jobNames = Object.values(JobName).filter( const jobNames = Object.values(JobName).filter((jobName) => !ignoredJobs.includes(jobName as JobName));
(jobName) => !ignoredJobs.includes(jobName as JobName)
);
async function getConfigs() { async function getConfigs() {
[savedConfig, defaultConfig] = await Promise.all([ [savedConfig, defaultConfig] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.job), api.systemConfigApi.getConfig().then((res) => res.data.job),
api.systemConfigApi.getDefaults().then((res) => res.data.job) api.systemConfigApi.getDefaults().then((res) => res.data.job),
]); ]);
} }
@ -34,8 +32,8 @@
const result = await api.systemConfigApi.updateConfig({ const result = await api.systemConfigApi.updateConfig({
systemConfigDto: { systemConfigDto: {
...configs, ...configs,
job: jobConfig job: jobConfig,
} },
}); });
jobConfig = { ...result.data.job }; jobConfig = { ...result.data.job };
@ -55,7 +53,7 @@
notificationController.show({ notificationController.show({
message: 'Reset Job settings to the recent saved settings', message: 'Reset Job settings to the recent saved settings',
type: NotificationType.Info type: NotificationType.Info,
}); });
} }
@ -67,7 +65,7 @@
notificationController.show({ notificationController.show({
message: 'Reset Job settings to default', message: 'Reset Job settings to default',
type: NotificationType.Info type: NotificationType.Info,
}); });
} }
</script> </script>

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { import {
notificationController, notificationController,
NotificationType NotificationType,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { api, SystemConfigOAuthDto } from '@api'; import { api, SystemConfigOAuthDto } from '@api';
@ -28,7 +28,7 @@
async function getConfigs() { async function getConfigs() {
[savedConfig, defaultConfig] = await Promise.all([ [savedConfig, defaultConfig] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.oauth), api.systemConfigApi.getConfig().then((res) => res.data.oauth),
api.systemConfigApi.getDefaults().then((res) => res.data.oauth) api.systemConfigApi.getDefaults().then((res) => res.data.oauth),
]); ]);
} }
@ -40,7 +40,7 @@
notificationController.show({ notificationController.show({
message: 'Reset OAuth settings to the last saved settings', message: 'Reset OAuth settings to the last saved settings',
type: NotificationType.Info type: NotificationType.Info,
}); });
} }
@ -75,8 +75,8 @@
const { data: updated } = await api.systemConfigApi.updateConfig({ const { data: updated } = await api.systemConfigApi.updateConfig({
systemConfigDto: { systemConfigDto: {
...current, ...current,
oauth: oauthConfig oauth: oauthConfig,
} },
}); });
oauthConfig = { ...updated.oauth }; oauthConfig = { ...updated.oauth };
@ -95,16 +95,13 @@
notificationController.show({ notificationController.show({
message: 'Reset OAuth settings to default', message: 'Reset OAuth settings to default',
type: NotificationType.Info type: NotificationType.Info,
}); });
} }
</script> </script>
{#if isConfirmOpen} {#if isConfirmOpen}
<ConfirmDisableLogin <ConfirmDisableLogin on:cancel={() => handleConfirm(false)} on:confirm={() => handleConfirm(true)} />
on:cancel={() => handleConfirm(false)}
on:confirm={() => handleConfirm(true)}
/>
{/if} {/if}
<div class="mt-2"> <div class="mt-2">

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { import {
notificationController, notificationController,
NotificationType NotificationType,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { api, SystemConfigPasswordLoginDto } from '@api'; import { api, SystemConfigPasswordLoginDto } from '@api';
@ -19,7 +19,7 @@
async function getConfigs() { async function getConfigs() {
[savedConfig, defaultConfig] = await Promise.all([ [savedConfig, defaultConfig] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.passwordLogin), api.systemConfigApi.getConfig().then((res) => res.data.passwordLogin),
api.systemConfigApi.getDefaults().then((res) => res.data.passwordLogin) api.systemConfigApi.getDefaults().then((res) => res.data.passwordLogin),
]); ]);
} }
@ -50,8 +50,8 @@
const { data: updated } = await api.systemConfigApi.updateConfig({ const { data: updated } = await api.systemConfigApi.updateConfig({
systemConfigDto: { systemConfigDto: {
...current, ...current,
passwordLogin: passwordLoginConfig passwordLogin: passwordLoginConfig,
} },
}); });
passwordLoginConfig = { ...updated.passwordLogin }; passwordLoginConfig = { ...updated.passwordLogin };
@ -71,7 +71,7 @@
notificationController.show({ notificationController.show({
message: 'Reset settings to the recent saved settings', message: 'Reset settings to the recent saved settings',
type: NotificationType.Info type: NotificationType.Info,
}); });
} }
@ -83,16 +83,13 @@
notificationController.show({ notificationController.show({
message: 'Reset password settings to default', message: 'Reset password settings to default',
type: NotificationType.Info type: NotificationType.Info,
}); });
} }
</script> </script>
{#if isConfirmOpen} {#if isConfirmOpen}
<ConfirmDisableLogin <ConfirmDisableLogin on:cancel={() => handleConfirm(false)} on:confirm={() => handleConfirm(true)} />
on:cancel={() => handleConfirm(false)}
on:confirm={() => handleConfirm(true)}
/>
{/if} {/if}
<div> <div>

View file

@ -3,7 +3,7 @@
EMAIL = 'email', EMAIL = 'email',
TEXT = 'text', TEXT = 'text',
NUMBER = 'number', NUMBER = 'number',
PASSWORD = 'password' PASSWORD = 'password',
} }
</script> </script>

View file

@ -29,13 +29,7 @@
</div> </div>
<label class="relative inline-block flex-none w-[36px] h-[10px]"> <label class="relative inline-block flex-none w-[36px] h-[10px]">
<input <input class="opacity-0 w-0 h-0 disabled::cursor-not-allowed" type="checkbox" bind:checked on:click {disabled} />
class="opacity-0 w-0 h-0 disabled::cursor-not-allowed"
type="checkbox"
bind:checked
on:click
{disabled}
/>
{#if disabled} {#if disabled}
<span class="slider-disable" /> <span class="slider-disable" />

View file

@ -1,10 +1,5 @@
<script lang="ts"> <script lang="ts">
import { import { api, SystemConfigStorageTemplateDto, SystemConfigTemplateStorageOptionDto, UserResponseDto } from '@api';
api,
SystemConfigStorageTemplateDto,
SystemConfigTemplateStorageOptionDto,
UserResponseDto
} from '@api';
import * as luxon from 'luxon'; import * as luxon from 'luxon';
import handlebar from 'handlebars'; import handlebar from 'handlebars';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
@ -15,7 +10,7 @@
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { import {
notificationController, notificationController,
NotificationType NotificationType,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte'; import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
@ -31,7 +26,7 @@
[savedConfig, defaultConfig, templateOptions] = await Promise.all([ [savedConfig, defaultConfig, templateOptions] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.storageTemplate), api.systemConfigApi.getConfig().then((res) => res.data.storageTemplate),
api.systemConfigApi.getDefaults().then((res) => res.data.storageTemplate), api.systemConfigApi.getDefaults().then((res) => res.data.storageTemplate),
api.systemConfigApi.getStorageTemplateOptions().then((res) => res.data) api.systemConfigApi.getStorageTemplateOptions().then((res) => res.data),
]); ]);
selectedPreset = savedConfig.template; selectedPreset = savedConfig.template;
@ -52,14 +47,14 @@
const renderTemplate = (templateString: string) => { const renderTemplate = (templateString: string) => {
const template = handlebar.compile(templateString, { const template = handlebar.compile(templateString, {
knownHelpers: undefined knownHelpers: undefined,
}); });
const substitutions: Record<string, string> = { const substitutions: Record<string, string> = {
filename: 'IMAGE_56437', filename: 'IMAGE_56437',
ext: 'jpg', ext: 'jpg',
filetype: 'IMG', filetype: 'IMG',
filetypefull: 'IMAGE' filetypefull: 'IMAGE',
}; };
const dt = luxon.DateTime.fromISO(new Date('2022-09-04T20:03:05.250').toISOString()); const dt = luxon.DateTime.fromISO(new Date('2022-09-04T20:03:05.250').toISOString());
@ -70,7 +65,7 @@
...templateOptions.dayOptions, ...templateOptions.dayOptions,
...templateOptions.hourOptions, ...templateOptions.hourOptions,
...templateOptions.minuteOptions, ...templateOptions.minuteOptions,
...templateOptions.secondOptions ...templateOptions.secondOptions,
]; ];
for (const token of dateTokens) { for (const token of dateTokens) {
@ -88,7 +83,7 @@
notificationController.show({ notificationController.show({
message: 'Reset storage template settings to the recent saved settings', message: 'Reset storage template settings to the recent saved settings',
type: NotificationType.Info type: NotificationType.Info,
}); });
} }
@ -99,8 +94,8 @@
const result = await api.systemConfigApi.updateConfig({ const result = await api.systemConfigApi.updateConfig({
systemConfigDto: { systemConfigDto: {
...currentConfig, ...currentConfig,
storageTemplate: storageConfig storageTemplate: storageConfig,
} },
}); });
storageConfig.template = result.data.storageTemplate.template; storageConfig.template = result.data.storageTemplate.template;
@ -108,13 +103,13 @@
notificationController.show({ notificationController.show({
message: 'Storage template saved', message: 'Storage template saved',
type: NotificationType.Info type: NotificationType.Info,
}); });
} catch (e) { } catch (e) {
console.error('Error [storage-template-settings] [saveSetting]', e); console.error('Error [storage-template-settings] [saveSetting]', e);
notificationController.show({ notificationController.show({
message: 'Unable to save settings', message: 'Unable to save settings',
type: NotificationType.Error type: NotificationType.Error,
}); });
} }
} }
@ -126,7 +121,7 @@
notificationController.show({ notificationController.show({
message: 'Reset storage template to default', message: 'Reset storage template to default',
type: NotificationType.Info type: NotificationType.Info,
}); });
} }
@ -138,9 +133,7 @@
<section class="dark:text-immich-dark-fg"> <section class="dark:text-immich-dark-fg">
{#await getConfigs() then} {#await getConfigs() then}
<div id="directory-path-builder" class="m-4"> <div id="directory-path-builder" class="m-4">
<h3 class="font-medium text-immich-primary dark:text-immich-dark-primary text-base"> <h3 class="font-medium text-immich-primary dark:text-immich-dark-primary text-base">Variables</h3>
Variables
</h3>
<section class="support-date"> <section class="support-date">
{#await getSupportDateTimeFormat()} {#await getSupportDateTimeFormat()}
@ -157,9 +150,7 @@
</section> </section>
<div class="mt-4 flex flex-col"> <div class="mt-4 flex flex-col">
<h3 class="font-medium text-immich-primary dark:text-immich-dark-primary text-base"> <h3 class="font-medium text-immich-primary dark:text-immich-dark-primary text-base">Template</h3>
Template
</h3>
<div class="text-xs my-2"> <div class="text-xs my-2">
<h4>PREVIEW</h4> <h4>PREVIEW</h4>
@ -176,9 +167,7 @@
<code>{user.storageLabel || user.id}</code> is the user's Storage Label <code>{user.storageLabel || user.id}</code> is the user's Storage Label
</p> </p>
<p <p class="text-xs p-4 bg-gray-200 dark:bg-gray-700 dark:text-immich-dark-fg py-2 rounded-lg mt-2">
class="text-xs p-4 bg-gray-200 dark:bg-gray-700 dark:text-immich-dark-fg py-2 rounded-lg mt-2"
>
<span class="text-immich-fg/25 dark:text-immich-dark-fg/50" <span class="text-immich-fg/25 dark:text-immich-dark-fg/50"
>UPLOAD_LOCATION/{user.storageLabel || user.id}</span >UPLOAD_LOCATION/{user.storageLabel || user.id}</span
>/{parsedTemplate()}.jpg >/{parsedTemplate()}.jpg
@ -209,21 +198,15 @@
/> />
<div class="flex-0"> <div class="flex-0">
<SettingInputField <SettingInputField label="EXTENSION" inputType={SettingInputFieldType.TEXT} value={'.jpg'} disabled />
label="EXTENSION"
inputType={SettingInputFieldType.TEXT}
value={'.jpg'}
disabled
/>
</div> </div>
</div> </div>
<div id="migration-info" class="text-sm mt-4"> <div id="migration-info" class="text-sm mt-4">
<p> <p>
Template changes will only apply to new assets. To retroactively apply the template to Template changes will only apply to new assets. To retroactively apply the template to previously uploaded
previously uploaded assets, run the <a assets, run the <a href="/admin/jobs-status" class="text-immich-primary dark:text-immich-dark-primary"
href="/admin/jobs-status" >Storage Migration Job</a
class="text-immich-primary dark:text-immich-dark-primary">Storage Migration Job</a
> >
</p> </p>
</div> </div>

View file

@ -5,9 +5,7 @@
export let options: SystemConfigTemplateStorageOptionDto; export let options: SystemConfigTemplateStorageOptionDto;
const getLuxonExample = (format: string) => { const getLuxonExample = (format: string) => {
return luxon.DateTime.fromISO(new Date('2022-09-04T20:03:05.250').toISOString()).toFormat( return luxon.DateTime.fromISO(new Date('2022-09-04T20:03:05.250').toISOString()).toFormat(format);
format
);
}; };
</script> </script>

View file

@ -1,10 +1,10 @@
import { jest, describe, it } from '@jest/globals';
import { render, RenderResult, waitFor, fireEvent } from '@testing-library/svelte';
import { createObjectURLMock } from '$lib/__mocks__/jsdom-url.mock'; import { createObjectURLMock } from '$lib/__mocks__/jsdom-url.mock';
import { api, ThumbnailFormat } from '@api'; import { api, ThumbnailFormat } from '@api';
import { describe, it, jest } from '@jest/globals';
import { albumFactory } from '@test-data'; import { albumFactory } from '@test-data';
import AlbumCard from '../album-card.svelte';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { fireEvent, render, RenderResult, waitFor } from '@testing-library/svelte';
import AlbumCard from '../album-card.svelte';
jest.mock('@api'); jest.mock('@api');
@ -17,26 +17,24 @@ describe('AlbumCard component', () => {
{ {
album: albumFactory.build({ albumThumbnailAssetId: null, shared: false, assetCount: 0 }), album: albumFactory.build({ albumThumbnailAssetId: null, shared: false, assetCount: 0 }),
count: 0, count: 0,
shared: false shared: false,
}, },
{ {
album: albumFactory.build({ albumThumbnailAssetId: null, shared: true, assetCount: 0 }), album: albumFactory.build({ albumThumbnailAssetId: null, shared: true, assetCount: 0 }),
count: 0, count: 0,
shared: true shared: true,
}, },
{ {
album: albumFactory.build({ albumThumbnailAssetId: null, shared: false, assetCount: 5 }), album: albumFactory.build({ albumThumbnailAssetId: null, shared: false, assetCount: 5 }),
count: 5, count: 5,
shared: false shared: false,
}, },
{ {
album: albumFactory.build({ albumThumbnailAssetId: null, shared: true, assetCount: 2 }), album: albumFactory.build({ albumThumbnailAssetId: null, shared: true, assetCount: 2 }),
count: 2, count: 2,
shared: true shared: true,
} },
])( ])('shows album data without thumbnail with count $count - shared: $shared', async ({ album, count, shared }) => {
'shows album data without thumbnail with count $count - shared: $shared',
async ({ album, count, shared }) => {
sut = render(AlbumCard, { album, user: album.owner }); sut = render(AlbumCard, { album, user: album.owner });
const albumImgElement = sut.getByTestId('album-image'); const albumImgElement = sut.getByTestId('album-image');
@ -54,8 +52,7 @@ describe('AlbumCard component', () => {
expect(albumNameElement).toHaveTextContent(album.albumName); expect(albumNameElement).toHaveTextContent(album.albumName);
expect(albumDetailsElement).toHaveTextContent(new RegExp(detailsText)); expect(albumDetailsElement).toHaveTextContent(new RegExp(detailsText));
} });
);
it('shows album data and and loads the thumbnail image when available', async () => { it('shows album data and and loads the thumbnail image when available', async () => {
const thumbnailFile = new File([new Blob()], 'fileThumbnail'); const thumbnailFile = new File([new Blob()], 'fileThumbnail');
@ -65,14 +62,14 @@ describe('AlbumCard component', () => {
config: {}, config: {},
headers: {}, headers: {},
status: 200, status: 200,
statusText: '' statusText: '',
}); });
createObjectURLMock.mockReturnValueOnce(thumbnailUrl); createObjectURLMock.mockReturnValueOnce(thumbnailUrl);
const album = albumFactory.build({ const album = albumFactory.build({
albumThumbnailAssetId: 'thumbnailIdOne', albumThumbnailAssetId: 'thumbnailIdOne',
shared: false, shared: false,
albumName: 'some album name' albumName: 'some album name',
}); });
sut = render(AlbumCard, { album, user: album.owner }); sut = render(AlbumCard, { album, user: album.owner });
@ -88,9 +85,9 @@ describe('AlbumCard component', () => {
expect(apiMock.assetApi.getAssetThumbnail).toHaveBeenCalledWith( expect(apiMock.assetApi.getAssetThumbnail).toHaveBeenCalledWith(
{ {
id: 'thumbnailIdOne', id: 'thumbnailIdOne',
format: ThumbnailFormat.Jpeg format: ThumbnailFormat.Jpeg,
}, },
{ responseType: 'blob' } { responseType: 'blob' },
); );
expect(createObjectURLMock).toHaveBeenCalledWith(thumbnailFile); expect(createObjectURLMock).toHaveBeenCalledWith(thumbnailFile);
@ -128,14 +125,12 @@ describe('AlbumCard component', () => {
contextMenuBtnParent, contextMenuBtnParent,
new MouseEvent('click', { new MouseEvent('click', {
clientX: 123, clientX: 123,
clientY: 456 clientY: 456,
}) }),
); );
expect(onClickHandler).toHaveBeenCalledTimes(1); expect(onClickHandler).toHaveBeenCalledTimes(1);
expect(onClickHandler).toHaveBeenCalledWith( expect(onClickHandler).toHaveBeenCalledWith(expect.objectContaining({ detail: { x: 123, y: 456 } }));
expect.objectContaining({ detail: { x: 123, y: 456 } })
);
}); });
}); });
}); });

View file

@ -28,11 +28,11 @@
const { data } = await api.assetApi.getAssetThumbnail( const { data } = await api.assetApi.getAssetThumbnail(
{ {
id: thubmnailId, id: thubmnailId,
format: ThumbnailFormat.Jpeg format: ThumbnailFormat.Jpeg,
}, },
{ {
responseType: 'blob' responseType: 'blob',
} },
); );
if (data instanceof Blob) { if (data instanceof Blob) {
@ -43,7 +43,7 @@
const showAlbumContextMenu = (e: MouseEvent) => { const showAlbumContextMenu = (e: MouseEvent) => {
dispatchShowContextMenu('showalbumcontextmenu', { dispatchShowContextMenu('showalbumcontextmenu', {
x: e.clientX, x: e.clientX,
y: e.clientY y: e.clientY,
}); });
}; };

View file

@ -11,7 +11,7 @@
SharedLinkResponseDto, SharedLinkResponseDto,
SharedLinkType, SharedLinkType,
UserResponseDto, UserResponseDto,
api api,
} from '@api'; } from '@api';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte'; import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
@ -34,10 +34,7 @@
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte'; import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
import ImmichLogo from '../shared-components/immich-logo.svelte'; import ImmichLogo from '../shared-components/immich-logo.svelte';
import SelectAll from 'svelte-material-icons/SelectAll.svelte'; import SelectAll from 'svelte-material-icons/SelectAll.svelte';
import { import { NotificationType, notificationController } from '../shared-components/notification/notification';
NotificationType,
notificationController
} from '../shared-components/notification/notification';
import ThemeButton from '../shared-components/theme-button.svelte'; import ThemeButton from '../shared-components/theme-button.svelte';
import AssetSelection from './asset-selection.svelte'; import AssetSelection from './asset-selection.svelte';
import ShareInfoModal from './share-info-modal.svelte'; import ShareInfoModal from './share-info-modal.svelte';
@ -108,7 +105,7 @@
const albumDateFormat: Intl.DateTimeFormatOptions = { const albumDateFormat: Intl.DateTimeFormatOptions = {
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
year: 'numeric' year: 'numeric',
}; };
const getDateRange = () => { const getDateRange = () => {
@ -119,9 +116,7 @@
const endDateString = endDate.toLocaleDateString($locale, albumDateFormat); const endDateString = endDate.toLocaleDateString($locale, albumDateFormat);
// If the start and end date are the same, only show one date // If the start and end date are the same, only show one date
return startDateString === endDateString return startDateString === endDateString ? startDateString : `${startDateString} - ${endDateString}`;
? startDateString
: `${startDateString} - ${endDateString}`;
}; };
onMount(async () => { onMount(async () => {
@ -142,8 +137,8 @@
.updateAlbumInfo({ .updateAlbumInfo({
id: album.id, id: album.id,
updateAlbumDto: { updateAlbumDto: {
albumName: album.albumName albumName: album.albumName,
} },
}) })
.then(() => { .then(() => {
currentAlbumName = album.albumName; currentAlbumName = album.albumName;
@ -152,7 +147,7 @@
console.error('Error [updateAlbumInfo] ', e); console.error('Error [updateAlbumInfo] ', e);
notificationController.show({ notificationController.show({
type: NotificationType.Error, type: NotificationType.Error,
message: "Error updating album's name, check console for more details" message: "Error updating album's name, check console for more details",
}); });
}); });
} }
@ -164,9 +159,9 @@
const { data } = await api.albumApi.addAssetsToAlbum({ const { data } = await api.albumApi.addAssetsToAlbum({
id: album.id, id: album.id,
addAssetsDto: { addAssetsDto: {
assetIds: assets.map((a) => a.id) assetIds: assets.map((a) => a.id),
}, },
key: sharedLink?.key key: sharedLink?.key,
}); });
if (data.album) { if (data.album) {
@ -177,7 +172,7 @@
console.error('Error [createAlbumHandler] ', e); console.error('Error [createAlbumHandler] ', e);
notificationController.show({ notificationController.show({
type: NotificationType.Error, type: NotificationType.Error,
message: 'Error creating album, check console for more details' message: 'Error creating album, check console for more details',
}); });
} }
}; };
@ -189,8 +184,8 @@
const { data } = await api.albumApi.addUsersToAlbum({ const { data } = await api.albumApi.addUsersToAlbum({
id: album.id, id: album.id,
addUsersDto: { addUsersDto: {
sharedUserIds: Array.from(selectedUsers).map((u) => u.id) sharedUserIds: Array.from(selectedUsers).map((u) => u.id),
} },
}); });
album = data; album = data;
@ -200,7 +195,7 @@
console.error('Error [addUserHandler] ', e); console.error('Error [addUserHandler] ', e);
notificationController.show({ notificationController.show({
type: NotificationType.Error, type: NotificationType.Error,
message: 'Error adding users to album, check console for more details' message: 'Error adding users to album, check console for more details',
}); });
} }
}; };
@ -232,7 +227,7 @@
console.error('Error [userDeleteMenu] ', e); console.error('Error [userDeleteMenu] ', e);
notificationController.show({ notificationController.show({
type: NotificationType.Error, type: NotificationType.Error,
message: 'Error deleting album, check console for more details' message: 'Error deleting album, check console for more details',
}); });
} finally { } finally {
isShowDeleteConfirmation = false; isShowDeleteConfirmation = false;
@ -240,12 +235,7 @@
}; };
const downloadAlbum = async () => { const downloadAlbum = async () => {
await downloadArchive( await downloadArchive(`${album.albumName}.zip`, { albumId: album.id }, undefined, sharedLink?.key);
`${album.albumName}.zip`,
{ albumId: album.id },
undefined,
sharedLink?.key
);
}; };
const showAlbumOptionsMenu = ({ x, y }: MouseEvent) => { const showAlbumOptionsMenu = ({ x, y }: MouseEvent) => {
@ -259,14 +249,14 @@
api.albumApi.updateAlbumInfo({ api.albumApi.updateAlbumInfo({
id: album.id, id: album.id,
updateAlbumDto: { updateAlbumDto: {
albumThumbnailAssetId: asset.id albumThumbnailAssetId: asset.id,
} },
}); });
} catch (e) { } catch (e) {
console.error('Error [setAlbumThumbnailHandler] ', e); console.error('Error [setAlbumThumbnailHandler] ', e);
notificationController.show({ notificationController.show({
type: NotificationType.Error, type: NotificationType.Error,
message: 'Error setting album thumbnail, check console for more details' message: 'Error setting album thumbnail, check console for more details',
}); });
} }
@ -286,10 +276,7 @@
<section class="bg-immich-bg dark:bg-immich-dark-bg" class:hidden={isShowThumbnailSelection}> <section class="bg-immich-bg dark:bg-immich-dark-bg" class:hidden={isShowThumbnailSelection}>
<!-- Multiselection mode app bar --> <!-- Multiselection mode app bar -->
{#if isMultiSelectionMode} {#if isMultiSelectionMode}
<AssetSelectControlBar <AssetSelectControlBar assets={multiSelectAsset} clearSelect={() => (multiSelectAsset = new Set())}>
assets={multiSelectAsset}
clearSelect={() => (multiSelectAsset = new Set())}
>
<CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} /> <CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
{#if sharedLink?.allowDownload || !isPublicShared} {#if sharedLink?.allowDownload || !isPublicShared}
<DownloadAction filename="{album.albumName}.zip" sharedLinkKey={sharedLink?.key} /> <DownloadAction filename="{album.albumName}.zip" sharedLinkKey={sharedLink?.key} />
@ -305,9 +292,7 @@
<ControlAppBar <ControlAppBar
on:close-button-click={() => goto(backUrl)} on:close-button-click={() => goto(backUrl)}
backIcon={ArrowLeft} backIcon={ArrowLeft}
showBackButton={(!isPublicShared && isOwned) || showBackButton={(!isPublicShared && isOwned) || (!isPublicShared && !isOwned) || (isPublicShared && isOwned)}
(!isPublicShared && !isOwned) ||
(isPublicShared && isOwned)}
> >
<svelte:fragment slot="leading"> <svelte:fragment slot="leading">
{#if isPublicShared && !isOwned} {#if isPublicShared && !isOwned}
@ -317,9 +302,7 @@
href="https://immich.app" href="https://immich.app"
> >
<ImmichLogo height={30} width={30} /> <ImmichLogo height={30} width={30} />
<h1 class="font-immich-title text-lg text-immich-primary dark:text-immich-dark-primary"> <h1 class="font-immich-title text-lg text-immich-primary dark:text-immich-dark-primary">IMMICH</h1>
IMMICH
</h1>
</a> </a>
{/if} {/if}
</svelte:fragment> </svelte:fragment>
@ -356,24 +339,13 @@
{#if album.assetCount > 0 && !isCreatingSharedAlbum} {#if album.assetCount > 0 && !isCreatingSharedAlbum}
{#if !isPublicShared || (isPublicShared && sharedLink?.allowDownload)} {#if !isPublicShared || (isPublicShared && sharedLink?.allowDownload)}
<CircleIconButton <CircleIconButton title="Download" on:click={() => downloadAlbum()} logo={FolderDownloadOutline} />
title="Download"
on:click={() => downloadAlbum()}
logo={FolderDownloadOutline}
/>
{/if} {/if}
{#if !isPublicShared && isOwned} {#if !isPublicShared && isOwned}
<CircleIconButton <CircleIconButton title="Album options" on:click={showAlbumOptionsMenu} logo={DotsVertical}>
title="Album options"
on:click={showAlbumOptionsMenu}
logo={DotsVertical}
>
{#if isShowAlbumOptions} {#if isShowAlbumOptions}
<ContextMenu <ContextMenu {...contextMenuPosition} on:outclick={() => (isShowAlbumOptions = false)}>
{...contextMenuPosition}
on:outclick={() => (isShowAlbumOptions = false)}
>
<MenuOption <MenuOption
on:click={() => { on:click={() => {
isShowThumbnailSelection = true; isShowThumbnailSelection = true;
@ -450,12 +422,7 @@
{/if} {/if}
{#if album.assetCount > 0} {#if album.assetCount > 0}
<GalleryViewer <GalleryViewer assets={album.assets} {sharedLink} bind:selectedAssets={multiSelectAsset} viewFrom="album-page" />
assets={album.assets}
{sharedLink}
bind:selectedAssets={multiSelectAsset}
viewFrom="album-page"
/>
{:else} {:else}
<!-- Album is empty - Show asset selectection buttons --> <!-- Album is empty - Show asset selectection buttons -->
<section id="empty-album" class=" mt-[200px] flex place-content-center place-items-center"> <section id="empty-album" class=" mt-[200px] flex place-content-center place-items-center">
@ -465,9 +432,7 @@
on:click={() => (isShowAssetSelection = true)} on:click={() => (isShowAssetSelection = true)}
class="w-full py-8 border bg-immich-bg dark:bg-immich-dark-gray text-immich-fg dark:text-immich-dark-fg dark:hover:text-immich-dark-primary rounded-md mt-5 flex place-items-center gap-6 px-8 transition-all hover:bg-gray-100 hover:text-immich-primary dark:border-none" class="w-full py-8 border bg-immich-bg dark:bg-immich-dark-gray text-immich-fg dark:text-immich-dark-fg dark:hover:text-immich-dark-primary rounded-md mt-5 flex place-items-center gap-6 px-8 transition-all hover:bg-gray-100 hover:text-immich-primary dark:border-none"
> >
<span class="text-text-immich-primary dark:text-immich-dark-primary" <span class="text-text-immich-primary dark:text-immich-dark-primary"><Plus size="24" /> </span>
><Plus size="24" />
</span>
<span class="text-lg">Select photos</span> <span class="text-lg">Select photos</span>
</button> </button>
</div> </div>
@ -496,18 +461,10 @@
{/if} {/if}
{#if isShowShareLinkModal} {#if isShowShareLinkModal}
<CreateSharedLinkModal <CreateSharedLinkModal on:close={() => (isShowShareLinkModal = false)} shareType={SharedLinkType.Album} {album} />
on:close={() => (isShowShareLinkModal = false)}
shareType={SharedLinkType.Album}
{album}
/>
{/if} {/if}
{#if isShowShareInfoModal} {#if isShowShareInfoModal}
<ShareInfoModal <ShareInfoModal on:close={() => (isShowShareInfoModal = false)} {album} on:user-deleted={sharedUserDeletedHandler} />
on:close={() => (isShowShareInfoModal = false)}
{album}
on:user-deleted={sharedUserDeletedHandler}
/>
{/if} {/if}
{#if isShowThumbnailSelection} {#if isShowThumbnailSelection}

View file

@ -1,9 +1,5 @@
<script lang="ts"> <script lang="ts">
import { import { assetInteractionStore, assetsInAlbumStoreState, selectedAssets } from '$lib/stores/asset-interaction.store';
assetInteractionStore,
assetsInAlbumStoreState,
selectedAssets
} from '$lib/stores/asset-interaction.store';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { openFileUploadDialog } from '$lib/utils/file-uploader';
import type { AssetResponseDto } from '@api'; import type { AssetResponseDto } from '@api';
@ -25,7 +21,7 @@
const addSelectedAssets = async () => { const addSelectedAssets = async () => {
dispatch('create-album', { dispatch('create-album', {
assets: Array.from($selectedAssets) assets: Array.from($selectedAssets),
}); });
assetInteractionStore.clearMultiselect(); assetInteractionStore.clearMultiselect();
@ -64,14 +60,7 @@
> >
Select from computer Select from computer
</button> </button>
<Button <Button size="sm" rounded="lg" disabled={$selectedAssets.size === 0} on:click={addSelectedAssets}>Done</Button>
size="sm"
rounded="lg"
disabled={$selectedAssets.size === 0}
on:click={addSelectedAssets}
>
Done
</Button>
</svelte:fragment> </svelte:fragment>
</ControlAppBar> </ControlAppBar>
<section class="pt-[100px] pl-[70px] grid h-screen bg-immich-bg dark:bg-immich-dark-bg"> <section class="pt-[100px] pl-[70px] grid h-screen bg-immich-bg dark:bg-immich-dark-bg">

View file

@ -7,10 +7,7 @@
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import ContextMenu from '../shared-components/context-menu/context-menu.svelte'; import ContextMenu from '../shared-components/context-menu/context-menu.svelte';
import MenuOption from '../shared-components/context-menu/menu-option.svelte'; import MenuOption from '../shared-components/context-menu/menu-option.svelte';
import { import { notificationController, NotificationType } from '../shared-components/notification/notification';
notificationController,
NotificationType
} from '../shared-components/notification/notification';
import { handleError } from '../../utils/handle-error'; import { handleError } from '../../utils/handle-error';
import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte'; import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
@ -40,7 +37,7 @@
if (iconButton) { if (iconButton) {
position = { position = {
x: iconButton.getBoundingClientRect().left, x: iconButton.getBoundingClientRect().left,
y: iconButton.getBoundingClientRect().bottom y: iconButton.getBoundingClientRect().bottom,
}; };
} }
@ -63,8 +60,7 @@
try { try {
await api.albumApi.removeUserFromAlbum({ id: album.id, userId }); await api.albumApi.removeUserFromAlbum({ id: album.id, userId });
dispatch('user-deleted', { userId }); dispatch('user-deleted', { userId });
const message = const message = userId === 'me' ? `Left ${album.albumName}` : `Removed ${selectedRemoveUser.firstName}`;
userId === 'me' ? `Left ${album.albumName}` : `Removed ${selectedRemoveUser.firstName}`;
notificationController.show({ type: NotificationType.Info, message }); notificationController.show({ type: NotificationType.Info, message });
} catch (e) { } catch (e) {
handleError(e, 'Unable to remove user'); handleError(e, 'Unable to remove user');

View file

@ -46,11 +46,7 @@
<!-- Image grid --> <!-- Image grid -->
<div class="flex flex-wrap gap-[2px]"> <div class="flex flex-wrap gap-[2px]">
{#each album.assets as asset} {#each album.assets as asset}
<Thumbnail <Thumbnail {asset} on:click={() => (selectedThumbnail = asset)} selected={isSelected(asset.id)} />
{asset}
on:click={() => (selectedThumbnail = asset)}
selected={isSelected(asset.id)}
/>
{/each} {/each}
</div> </div>
</section> </section>

View file

@ -112,16 +112,13 @@
</div> </div>
{:else} {:else}
<p class="text-sm p-5"> <p class="text-sm p-5">
Looks like you have shared this album with all users or you don't have any user to share Looks like you have shared this album with all users or you don't have any user to share with.
with.
</p> </p>
{/if} {/if}
{#if selectedUsers.length > 0} {#if selectedUsers.length > 0}
<div class="flex place-content-end p-5"> <div class="flex place-content-end p-5">
<Button size="sm" rounded="lg" on:click={() => dispatch('add-user', { selectedUsers })}> <Button size="sm" rounded="lg" on:click={() => dispatch('add-user', { selectedUsers })}>Add</Button>
Add
</Button>
</div> </div>
{/if} {/if}
</div> </div>

View file

@ -18,7 +18,7 @@
albumNameArray = [ albumNameArray = [
albumName.slice(0, findIndex), albumName.slice(0, findIndex),
albumName.slice(findIndex, findIndex + findLength), albumName.slice(findIndex, findIndex + findLength),
albumName.slice(findIndex + findLength) albumName.slice(findIndex + findLength),
]; ];
} }
</script> </script>

View file

@ -73,9 +73,7 @@
<CircleIconButton <CircleIconButton
isOpacity={true} isOpacity={true}
hideMobile={true} hideMobile={true}
logo={$photoZoomState && $photoZoomState.currentZoom > 1 logo={$photoZoomState && $photoZoomState.currentZoom > 1 ? MagnifyMinusOutline : MagnifyPlusOutline}
? MagnifyMinusOutline
: MagnifyPlusOutline}
title="Zoom Image" title="Zoom Image"
on:click={() => { on:click={() => {
const zoomImage = new CustomEvent('zoomImage'); const zoomImage = new CustomEvent('zoomImage');
@ -103,12 +101,7 @@
title="Download" title="Download"
/> />
{/if} {/if}
<CircleIconButton <CircleIconButton isOpacity={true} logo={InformationOutline} on:click={() => dispatch('showDetail')} title="Info" />
isOpacity={true}
logo={InformationOutline}
on:click={() => dispatch('showDetail')}
title="Info"
/>
{#if isOwner} {#if isOwner}
<CircleIconButton <CircleIconButton
isOpacity={true} isOpacity={true}
@ -119,26 +112,13 @@
{/if} {/if}
{#if isOwner} {#if isOwner}
<CircleIconButton <CircleIconButton isOpacity={true} logo={DeleteOutline} on:click={() => dispatch('delete')} title="Delete" />
isOpacity={true}
logo={DeleteOutline}
on:click={() => dispatch('delete')}
title="Delete"
/>
<div use:clickOutside on:outclick={() => (isShowAssetOptions = false)}> <div use:clickOutside on:outclick={() => (isShowAssetOptions = false)}>
<CircleIconButton <CircleIconButton isOpacity={true} logo={DotsVertical} on:click={showOptionsMenu} title="More">
isOpacity={true}
logo={DotsVertical}
on:click={showOptionsMenu}
title="More"
>
{#if isShowAssetOptions} {#if isShowAssetOptions}
<ContextMenu {...contextMenuPosition} direction="left"> <ContextMenu {...contextMenuPosition} direction="left">
<MenuOption on:click={() => onMenuClick('addToAlbum')} text="Add to Album" /> <MenuOption on:click={() => onMenuClick('addToAlbum')} text="Add to Album" />
<MenuOption <MenuOption on:click={() => onMenuClick('addToSharedAlbum')} text="Add to Shared Album" />
on:click={() => onMenuClick('addToSharedAlbum')}
text="Add to Shared Album"
/>
{#if isOwner} {#if isOwner}
<MenuOption <MenuOption

View file

@ -1,22 +1,13 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { import { AlbumResponseDto, api, AssetResponseDto, AssetTypeEnum, SharedLinkResponseDto } from '@api';
AlbumResponseDto,
api,
AssetResponseDto,
AssetTypeEnum,
SharedLinkResponseDto
} from '@api';
import { createEventDispatcher, onDestroy, onMount } from 'svelte'; import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte'; import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
import ChevronRight from 'svelte-material-icons/ChevronRight.svelte'; import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
import ImageBrokenVariant from 'svelte-material-icons/ImageBrokenVariant.svelte'; import ImageBrokenVariant from 'svelte-material-icons/ImageBrokenVariant.svelte';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import AlbumSelectionModal from '../shared-components/album-selection-modal.svelte'; import AlbumSelectionModal from '../shared-components/album-selection-modal.svelte';
import { import { notificationController, NotificationType } from '../shared-components/notification/notification';
notificationController,
NotificationType
} from '../shared-components/notification/notification';
import AssetViewerNavBar from './asset-viewer-nav-bar.svelte'; import AssetViewerNavBar from './asset-viewer-nav-bar.svelte';
import DetailPanel from './detail-panel.svelte'; import DetailPanel from './detail-panel.svelte';
import PhotoViewer from './photo-viewer.svelte'; import PhotoViewer from './photo-viewer.svelte';
@ -120,8 +111,8 @@
try { try {
const { data: deletedAssets } = await api.assetApi.deleteAsset({ const { data: deletedAssets } = await api.assetApi.deleteAsset({
deleteAssetDto: { deleteAssetDto: {
ids: [asset.id] ids: [asset.id],
} },
}); });
navigateAssetForward(); navigateAssetForward();
@ -134,7 +125,7 @@
} catch (e) { } catch (e) {
notificationController.show({ notificationController.show({
type: NotificationType.Error, type: NotificationType.Error,
message: 'Error deleting this asset, check console for more details' message: 'Error deleting this asset, check console for more details',
}); });
console.error('Error deleteAsset', e); console.error('Error deleteAsset', e);
} finally { } finally {
@ -146,8 +137,8 @@
const { data } = await api.assetApi.updateAsset({ const { data } = await api.assetApi.updateAsset({
id: asset.id, id: asset.id,
updateAssetDto: { updateAssetDto: {
isFavorite: !asset.isFavorite isFavorite: !asset.isFavorite,
} },
}); });
asset.isFavorite = data.isFavorite; asset.isFavorite = data.isFavorite;
@ -163,9 +154,7 @@
isShowAlbumPicker = false; isShowAlbumPicker = false;
const { albumName }: { albumName: string } = event.detail; const { albumName }: { albumName: string } = event.detail;
api.albumApi api.albumApi.createAlbum({ createAlbumDto: { albumName, assetIds: [asset.id] } }).then((response) => {
.createAlbum({ createAlbumDto: { albumName, assetIds: [asset.id] } })
.then((response) => {
const album = response.data; const album = response.data;
goto('/albums/' + album.id); goto('/albums/' + album.id);
}); });
@ -199,8 +188,8 @@
const { data } = await api.assetApi.updateAsset({ const { data } = await api.assetApi.updateAsset({
id: asset.id, id: asset.id,
updateAssetDto: { updateAssetDto: {
isArchived: !asset.isArchived isArchived: !asset.isArchived,
} },
}); });
asset.isArchived = data.isArchived; asset.isArchived = data.isArchived;
@ -213,15 +202,13 @@
notificationController.show({ notificationController.show({
type: NotificationType.Info, type: NotificationType.Info,
message: asset.isArchived ? `Added to archive` : `Removed from archive` message: asset.isArchived ? `Added to archive` : `Removed from archive`,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
notificationController.show({ notificationController.show({
type: NotificationType.Error, type: NotificationType.Error,
message: `Error ${ message: `Error ${asset.isArchived ? 'archiving' : 'unarchiving'} asset, check console for more details`,
asset.isArchived ? 'archiving' : 'unarchiving'
} asset, check console for more details`
}); });
} }
}; };
@ -377,8 +364,8 @@
> >
<svelte:fragment slot="prompt"> <svelte:fragment slot="prompt">
<p> <p>
Are you sure you want to delete this {getAssetType().toLowerCase()}? This will also remove Are you sure you want to delete this {getAssetType().toLowerCase()}? This will also remove it from its
it from its album(s). album(s).
</p> </p>
<p><b>You cannot undo this action!</b></p> <p><b>You cannot undo this action!</b></p>
</svelte:fragment> </svelte:fragment>

View file

@ -68,8 +68,8 @@
await api.assetApi.updateAsset({ await api.assetApi.updateAsset({
id: asset.id, id: asset.id,
updateAssetDto: { updateAssetDto: {
description: description description: description,
} },
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -95,9 +95,7 @@
class="max-h-[500px] class="max-h-[500px]
text-base text-black bg-transparent dark:text-white border-b focus:border-b-2 border-gray-500 w-full focus:border-immich-primary dark:focus:border-immich-dark-primary transition-all resize-none overflow-hidden outline-none disabled:border-none" text-base text-black bg-transparent dark:text-white border-b focus:border-b-2 border-gray-500 w-full focus:border-immich-primary dark:focus:border-immich-dark-primary transition-all resize-none overflow-hidden outline-none disabled:border-none"
placeholder={$page?.data?.user?.id !== asset.ownerId ? '' : 'Add a description'} placeholder={$page?.data?.user?.id !== asset.ownerId ? '' : 'Add a description'}
style:display={$page?.data?.user?.id !== asset.ownerId && textarea?.value == '' style:display={$page?.data?.user?.id !== asset.ownerId && textarea?.value == '' ? 'none' : 'block'}
? 'none'
: 'block'}
on:focusin={handleFocusIn} on:focusin={handleFocusIn}
on:focusout={handleFocusOut} on:focusout={handleFocusOut}
on:input={autoGrowHeight} on:input={autoGrowHeight}
@ -138,7 +136,7 @@
{#if asset.exifInfo?.dateTimeOriginal} {#if asset.exifInfo?.dateTimeOriginal}
{@const assetDateTimeOriginal = DateTime.fromISO(asset.exifInfo.dateTimeOriginal, { {@const assetDateTimeOriginal = DateTime.fromISO(asset.exifInfo.dateTimeOriginal, {
zone: asset.exifInfo.timeZone ?? undefined zone: asset.exifInfo.timeZone ?? undefined,
})} })}
<div class="flex gap-4 py-4"> <div class="flex gap-4 py-4">
<div> <div>
@ -151,9 +149,9 @@
{ {
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
year: 'numeric' year: 'numeric',
}, },
{ locale: $locale } { locale: $locale },
)} )}
</p> </p>
<div class="flex gap-2 text-sm"> <div class="flex gap-2 text-sm">
@ -163,9 +161,9 @@
weekday: 'short', weekday: 'short',
hour: 'numeric', hour: 'numeric',
minute: '2-digit', minute: '2-digit',
timeZoneName: 'longOffset' timeZoneName: 'longOffset',
}, },
{ locale: $locale } { locale: $locale },
)} )}
</p> </p>
</div> </div>
@ -248,8 +246,7 @@
<TileLayer <TileLayer
urlTemplate={'https://tile.openstreetmap.org/{z}/{x}/{y}.png'} urlTemplate={'https://tile.openstreetmap.org/{z}/{x}/{y}.png'}
options={{ options={{
attribution: attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}} }}
/> />
<Marker {latlng} popupContent="{latlng[0].toFixed(7)},{latlng[1].toFixed(7)}" /> <Marker {latlng} popupContent="{latlng[0].toFixed(7)},{latlng[1].toFixed(7)}" />

View file

@ -18,10 +18,7 @@
<span class="text-immich-primary font-medium">{$downloadAssets[fileName]}</span>/100 <span class="text-immich-primary font-medium">{$downloadAssets[fileName]}</span>/100
</p> </p>
<div class="w-full bg-gray-200 rounded-full h-[7px] dark:bg-gray-700"> <div class="w-full bg-gray-200 rounded-full h-[7px] dark:bg-gray-700">
<div <div class="bg-immich-primary h-[7px] rounded-full" style={`width: ${$downloadAssets[fileName]}%`} />
class="bg-immich-primary h-[7px] rounded-full"
style={`width: ${$downloadAssets[fileName]}%`}
/>
</div> </div>
</div> </div>
</div> </div>

View file

@ -38,14 +38,14 @@
dispatch('intersected', { dispatch('intersected', {
container, container,
position position,
}); });
} }
}, },
{ {
rootMargin, rootMargin,
root root,
} },
); );
observer.observe(container); observer.observe(container);

View file

@ -3,10 +3,7 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { api, AssetResponseDto } from '@api'; import { api, AssetResponseDto } from '@api';
import { import { notificationController, NotificationType } from '../shared-components/notification/notification';
notificationController,
NotificationType
} from '../shared-components/notification/notification';
import { useZoomImageWheel } from '@zoom-image/svelte'; import { useZoomImageWheel } from '@zoom-image/svelte';
import { photoZoomState } from '$lib/stores/zoom-image.store'; import { photoZoomState } from '$lib/stores/zoom-image.store';
@ -32,8 +29,8 @@
const { data } = await api.assetApi.serveFile( const { data } = await api.assetApi.serveFile(
{ id: asset.id, isThumb: false, isWeb: true, key: publicSharedKey }, { id: asset.id, isThumb: false, isWeb: true, key: publicSharedKey },
{ {
responseType: 'blob' responseType: 'blob',
} },
); );
if (!(data instanceof Blob)) { if (!(data instanceof Blob)) {
@ -63,27 +60,27 @@
notificationController.show({ notificationController.show({
type: NotificationType.Info, type: NotificationType.Info,
message: 'Copied image to clipboard.', message: 'Copied image to clipboard.',
timeout: 3000 timeout: 3000,
}); });
} catch (err) { } catch (err) {
console.error('Error [photo-viewer]:', err); console.error('Error [photo-viewer]:', err);
notificationController.show({ notificationController.show({
type: NotificationType.Error, type: NotificationType.Error,
message: 'Copying image to clipboard failed.' message: 'Copying image to clipboard failed.',
}); });
} }
}; };
const doZoomImage = async () => { const doZoomImage = async () => {
setZoomImageWheelState({ setZoomImageWheelState({
currentZoom: $zoomImageWheelState.currentZoom === 1 ? 2 : 1 currentZoom: $zoomImageWheelState.currentZoom === 1 ? 2 : 1,
}); });
}; };
const { const {
createZoomImage: createZoomImageWheel, createZoomImage: createZoomImageWheel,
zoomImageState: zoomImageWheelState, zoomImageState: zoomImageWheelState,
setZoomImageState: setZoomImageWheelState setZoomImageState: setZoomImageWheelState,
} = useZoomImageWheel(); } = useZoomImageWheel();
zoomImageWheelState.subscribe((state) => { zoomImageWheelState.subscribe((state) => {
@ -92,17 +89,14 @@
$: if (imgElement) { $: if (imgElement) {
createZoomImageWheel(imgElement, { createZoomImageWheel(imgElement, {
wheelZoomRatio: 0.06 wheelZoomRatio: 0.06,
}); });
} }
</script> </script>
<svelte:window on:keydown={handleKeypress} on:copyImage={doCopy} on:zoomImage={doZoomImage} /> <svelte:window on:keydown={handleKeypress} on:copyImage={doCopy} on:zoomImage={doZoomImage} />
<div <div transition:fade={{ duration: 150 }} class="flex place-items-center place-content-center h-full select-none">
transition:fade={{ duration: 150 }}
class="flex place-items-center place-content-center h-full select-none"
>
{#await loadAssetData()} {#await loadAssetData()}
<LoadingSpinner /> <LoadingSpinner />
{:then assetData} {:then assetData}

View file

@ -22,10 +22,7 @@
}; };
</script> </script>
<div <div transition:fade={{ duration: 150 }} class="flex place-items-center place-content-center h-full select-none">
transition:fade={{ duration: 150 }}
class="flex place-items-center place-content-center h-full select-none"
>
<video <video
controls controls
class="h-full object-contain" class="h-full object-contain"

View file

@ -29,9 +29,7 @@
} }
</script> </script>
<div <div class="absolute right-0 top-0 text-white text-xs font-medium flex gap-1 place-items-center z-20">
class="absolute right-0 top-0 text-white text-xs font-medium flex gap-1 place-items-center z-20"
>
{#if showTime} {#if showTime}
<span class="pt-2"> <span class="pt-2">
{Duration.fromObject({ seconds: remainingSeconds }).toFormat('m:ss')} {Duration.fromObject({ seconds: remainingSeconds }).toFormat('m:ss')}

View file

@ -8,10 +8,8 @@
export let rounded: Rounded = true; export let rounded: Rounded = true;
const colorClasses: Record<Color, string> = { const colorClasses: Record<Color, string> = {
primary: primary: 'text-gray-100 dark:text-immich-dark-gray bg-immich-primary dark:bg-immich-dark-primary',
'text-gray-100 dark:text-immich-dark-gray bg-immich-primary dark:bg-immich-dark-primary', secondary: 'text-immich-dark-bg dark:text-immich-gray dark:bg-gray-600 bg-gray-300 dark:text-immich-gray',
secondary:
'text-immich-dark-bg dark:text-immich-gray dark:bg-gray-600 bg-gray-300 dark:text-immich-gray'
}; };
</script> </script>

View file

@ -42,7 +42,7 @@
'dark:text-immich-dark-fg enabled:hover:bg-immich-primary/5 enabled:hover:text-gray-700 enabled:hover:dark:text-immich-dark-fg enabled:dark:hover:bg-immich-dark-primary/25', 'dark:text-immich-dark-fg enabled:hover:bg-immich-primary/5 enabled:hover:text-gray-700 enabled:hover:dark:text-immich-dark-fg enabled:dark:hover:bg-immich-dark-primary/25',
'dark-gray': 'dark-gray':
'dark:border-immich-dark-gray dark:bg-gray-500 enabled:dark:hover:bg-immich-dark-primary/50 enabled:hover:bg-immich-primary/10 dark:text-white', 'dark:border-immich-dark-gray dark:bg-gray-500 enabled:dark:hover:bg-immich-dark-primary/50 enabled:hover:bg-immich-primary/10 dark:text-white',
'overlay-primary': 'text-gray-500 enabled:hover:bg-gray-100' 'overlay-primary': 'text-gray-500 enabled:hover:bg-gray-100',
}; };
const sizeClasses: Record<Size, string> = { const sizeClasses: Record<Size, string> = {
@ -50,7 +50,7 @@
link: 'p-2 font-medium', link: 'p-2 font-medium',
sm: 'px-4 py-2 text-sm font-medium', sm: 'px-4 py-2 text-sm font-medium',
base: 'px-6 py-3 font-medium', base: 'px-6 py-3 font-medium',
lg: 'px-6 py-4 font-semibold' lg: 'px-6 py-4 font-semibold',
}; };
</script> </script>

View file

@ -44,9 +44,7 @@
<div class="text-immich-primary dark:text-immich-dark-primary font-medium"> <div class="text-immich-primary dark:text-immich-dark-primary font-medium">
<Check size="18" /> <Check size="18" />
</div> </div>
<p <p class="justify-self-start text-immich-primary dark:text-immich-dark-primary font-medium">
class="justify-self-start text-immich-primary dark:text-immich-dark-primary font-medium"
>
{option} {option}
</p> </p>
{:else} {:else}

View file

@ -35,8 +35,8 @@
email: String(email), email: String(email),
password: String(password), password: String(password),
firstName: String(firstName), firstName: String(firstName),
lastName: String(lastName) lastName: String(lastName),
} },
}); });
if (status === 201) { if (status === 201) {
@ -53,14 +53,7 @@
<form on:submit|preventDefault={registerAdmin} method="post" class="flex flex-col gap-5 mt-5"> <form on:submit|preventDefault={registerAdmin} method="post" class="flex flex-col gap-5 mt-5">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label class="immich-form-label" for="email">Admin Email</label> <label class="immich-form-label" for="email">Admin Email</label>
<input <input class="immich-form-input" id="email" name="email" type="email" autocomplete="email" required />
class="immich-form-input"
id="email"
name="email"
type="email"
autocomplete="email"
required
/>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@ -91,26 +84,12 @@
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label class="immich-form-label" for="firstName">First Name</label> <label class="immich-form-label" for="firstName">First Name</label>
<input <input class="immich-form-input" id="firstName" name="firstName" type="text" autocomplete="given-name" required />
class="immich-form-input"
id="firstName"
name="firstName"
type="text"
autocomplete="given-name"
required
/>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label class="immich-form-label" for="lastName">Last Name</label> <label class="immich-form-label" for="lastName">Last Name</label>
<input <input class="immich-form-input" id="lastName" name="lastName" type="text" autocomplete="family-name" required />
class="immich-form-input"
id="lastName"
name="lastName"
type="text"
autocomplete="family-name"
required
/>
</div> </div>
{#if error} {#if error}

View file

@ -31,13 +31,7 @@
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off"> <form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<div class="m-4 flex flex-col gap-2"> <div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="email">Name</label> <label class="immich-form-label" for="email">Name</label>
<input <input class="immich-form-input" id="name" name="name" type="text" bind:value={apiKey.name} />
class="immich-form-input"
id="name"
name="name"
type="text"
bind:value={apiKey.name}
/>
</div> </div>
<div class="flex w-full px-4 gap-4 mt-8"> <div class="flex w-full px-4 gap-4 mt-8">

View file

@ -3,10 +3,7 @@
import KeyVariant from 'svelte-material-icons/KeyVariant.svelte'; import KeyVariant from 'svelte-material-icons/KeyVariant.svelte';
import { handleError } from '../../utils/handle-error'; import { handleError } from '../../utils/handle-error';
import FullScreenModal from '../shared-components/full-screen-modal.svelte'; import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { import { notificationController, NotificationType } from '../shared-components/notification/notification';
notificationController,
NotificationType
} from '../shared-components/notification/notification';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
export let secret = ''; export let secret = '';
@ -24,7 +21,7 @@
await navigator.clipboard.writeText(secret); await navigator.clipboard.writeText(secret);
notificationController.show({ notificationController.show({
message: 'Copied to clipboard!', message: 'Copied to clipboard!',
type: NotificationType.Info type: NotificationType.Info,
}); });
} catch (error) { } catch (error) {
handleError(error, 'Unable to copy to clipboard'); handleError(error, 'Unable to copy to clipboard');
@ -40,9 +37,7 @@
class="flex flex-col place-items-center place-content-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary" class="flex flex-col place-items-center place-content-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
> >
<KeyVariant size="4em" /> <KeyVariant size="4em" />
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium"> <h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">API Key</h1>
API Key
</h1>
<p class="text-sm dark:text-immich-dark-fg"> <p class="text-sm dark:text-immich-dark-fg">
This value will only be shown once. Please be sure to copy it before closing the window. This value will only be shown once. Please be sure to copy it before closing the window.
@ -51,13 +46,7 @@
<div class="m-4 flex flex-col gap-2"> <div class="m-4 flex flex-col gap-2">
<!-- <label class="immich-form-label" for="email">API Key</label> --> <!-- <label class="immich-form-label" for="email">API Key</label> -->
<textarea <textarea class="immich-form-input" id="secret" name="secret" readonly={true} value={secret} />
class="immich-form-input"
id="secret"
name="secret"
readonly={true}
value={secret}
/>
</div> </div>
<div class="flex w-full px-4 gap-4 mt-8"> <div class="flex w-full px-4 gap-4 mt-8">

View file

@ -32,8 +32,8 @@
updateUserDto: { updateUserDto: {
id: user.id, id: user.id,
password: String(password), password: String(password),
shouldChangePassword: false shouldChangePassword: false,
} },
}); });
if (status === 200) { if (status === 200) {

View file

@ -2,10 +2,7 @@
import { api } from '@api'; import { api } from '@api';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import ImmichLogo from '../shared-components/immich-logo.svelte'; import ImmichLogo from '../shared-components/immich-logo.svelte';
import { import { notificationController, NotificationType } from '../shared-components/notification/notification';
notificationController,
NotificationType
} from '../shared-components/notification/notification';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
let error: string; let error: string;
@ -50,8 +47,8 @@
email: String(email), email: String(email),
password: String(password), password: String(password),
firstName: String(firstName), firstName: String(firstName),
lastName: String(lastName) lastName: String(lastName),
} },
}); });
if (status === 201) { if (status === 201) {
@ -73,7 +70,7 @@
notificationController.show({ notificationController.show({
message: `Error create new user, check console for more detail`, message: `Error create new user, check console for more detail`,
type: NotificationType.Error type: NotificationType.Error,
}); });
} }
} }
@ -85,14 +82,9 @@
> >
<div class="flex flex-col place-items-center place-content-center gap-4 px-4"> <div class="flex flex-col place-items-center place-content-center gap-4 px-4">
<ImmichLogo class="text-center" height="100" width="100" /> <ImmichLogo class="text-center" height="100" width="100" />
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium"> <h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">Create new user</h1>
Create new user <p class="text-sm border rounded-md p-4 font-mono text-gray-600 dark:border-immich-dark-bg dark:text-gray-300">
</h1> Please provide your user with the password, they will have to change it on their first sign in.
<p
class="text-sm border rounded-md p-4 font-mono text-gray-600 dark:border-immich-dark-bg dark:text-gray-300"
>
Please provide your user with the password, they will have to change it on their first sign
in.
</p> </p>
</div> </div>
@ -104,14 +96,7 @@
<div class="m-4 flex flex-col gap-2"> <div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="password">Password</label> <label class="immich-form-label" for="password">Password</label>
<input <input class="immich-form-input" id="password" name="password" type="password" required bind:value={password} />
class="immich-form-input"
id="password"
name="password"
type="password"
required
bind:value={password}
/>
</div> </div>
<div class="m-4 flex flex-col gap-2"> <div class="m-4 flex flex-col gap-2">

View file

@ -2,10 +2,7 @@
import { api, UserResponseDto } from '@api'; import { api, UserResponseDto } from '@api';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import AccountEditOutline from 'svelte-material-icons/AccountEditOutline.svelte'; import AccountEditOutline from 'svelte-material-icons/AccountEditOutline.svelte';
import { import { notificationController, NotificationType } from '../shared-components/notification/notification';
notificationController,
NotificationType
} from '../shared-components/notification/notification';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte'; import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
import { handleError } from '../../utils/handle-error'; import { handleError } from '../../utils/handle-error';
@ -30,8 +27,8 @@
firstName, firstName,
lastName, lastName,
storageLabel: storageLabel || '', storageLabel: storageLabel || '',
externalPath: externalPath || '' externalPath: externalPath || '',
} },
}); });
if (status === 200) { if (status === 200) {
@ -50,8 +47,8 @@
updateUserDto: { updateUserDto: {
id: user.id, id: user.id,
password: defaultPassword, password: defaultPassword,
shouldChangePassword: true shouldChangePassword: true,
} },
}); });
if (status == 200) { if (status == 200) {
@ -61,7 +58,7 @@
console.error('Error reseting user password', e); console.error('Error reseting user password', e);
notificationController.show({ notificationController.show({
message: 'Error reseting user password, check console for more details', message: 'Error reseting user password, check console for more details',
type: NotificationType.Error type: NotificationType.Error,
}); });
} finally { } finally {
isShowResetPasswordConfirmation = false; isShowResetPasswordConfirmation = false;
@ -76,21 +73,13 @@
class="flex flex-col place-items-center place-content-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary" class="flex flex-col place-items-center place-content-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
> >
<AccountEditOutline size="4em" /> <AccountEditOutline size="4em" />
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium"> <h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">Edit user</h1>
Edit user
</h1>
</div> </div>
<form on:submit|preventDefault={editUser} autocomplete="off"> <form on:submit|preventDefault={editUser} autocomplete="off">
<div class="m-4 flex flex-col gap-2"> <div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="email">Email</label> <label class="immich-form-label" for="email">Email</label>
<input <input class="immich-form-input" id="email" name="email" type="email" bind:value={user.email} />
class="immich-form-input"
id="email"
name="email"
type="email"
bind:value={user.email}
/>
</div> </div>
<div class="m-4 flex flex-col gap-2"> <div class="m-4 flex flex-col gap-2">
@ -107,14 +96,7 @@
<div class="m-4 flex flex-col gap-2"> <div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="lastName">Last Name</label> <label class="immich-form-label" for="lastName">Last Name</label>
<input <input class="immich-form-input" id="lastName" name="lastName" type="text" required bind:value={user.lastName} />
class="immich-form-input"
id="lastName"
name="lastName"
type="text"
required
bind:value={user.lastName}
/>
</div> </div>
<div class="m-4 flex flex-col gap-2"> <div class="m-4 flex flex-col gap-2">
@ -146,8 +128,8 @@
/> />
<p> <p>
Note: Absolute path of parent import directory. A user can only import files if they exist Note: Absolute path of parent import directory. A user can only import files if they exist at or under this
at or under this path. path.
</p> </p>
</div> </div>
@ -160,10 +142,8 @@
{/if} {/if}
<div class="flex w-full px-4 gap-4 mt-8"> <div class="flex w-full px-4 gap-4 mt-8">
{#if canResetPassword} {#if canResetPassword}
<Button <Button color="light-red" fullwidth on:click={() => (isShowResetPasswordConfirmation = true)}
color="light-red" >Reset password</Button
fullwidth
on:click={() => (isShowResetPasswordConfirmation = true)}>Reset password</Button
> >
{/if} {/if}
<Button type="submit" fullwidth>Confirm</Button> <Button type="submit" fullwidth>Confirm</Button>

View file

@ -59,8 +59,8 @@
const { data } = await api.authenticationApi.login({ const { data } = await api.authenticationApi.login({
loginCredentialDto: { loginCredentialDto: {
email, email,
password password,
} },
}); });
if (!data.isAdmin && data.shouldChangePassword) { if (!data.isAdmin && data.shouldChangePassword) {

View file

@ -33,9 +33,7 @@
<slot name="buttons" /> <slot name="buttons" />
</div> </div>
<div <div class="absolute overflow-y-auto top-16 h-[calc(100%-theme(spacing.16))] w-full immich-scrollbar p-4 pb-8">
class="absolute overflow-y-auto top-16 h-[calc(100%-theme(spacing.16))] w-full immich-scrollbar p-4 pb-8"
>
<slot /> <slot />
</div> </div>
</section> </section>

View file

@ -22,9 +22,7 @@
<div <div
class="flex flex-col gap-8 border bg-white dark:bg-immich-dark-gray dark:border-immich-dark-gray p-8 shadow-sm w-96 max-w-lg rounded-3xl" class="flex flex-col gap-8 border bg-white dark:bg-immich-dark-gray dark:border-immich-dark-gray p-8 shadow-sm w-96 max-w-lg rounded-3xl"
> >
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium self-center"> <h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium self-center">Map Settings</h1>
Map Settings
</h1>
<form <form
on:submit|preventDefault={() => dispatch('save', settings)} on:submit|preventDefault={() => dispatch('save', settings)}
@ -46,12 +44,7 @@
</div> </div>
<div class="flex justify-between items-center gap-8"> <div class="flex justify-between items-center gap-8">
<label class="immich-form-label text-sm shrink-0" for="date-before">Date before</label> <label class="immich-form-label text-sm shrink-0" for="date-before">Date before</label>
<input <input class="immich-form-input w-40" type="date" id="date-before" bind:value={settings.dateBefore} />
class="immich-form-input w-40"
type="date"
id="date-before"
bind:value={settings.dateBefore}
/>
</div> </div>
<div class="flex justify-center text-xs"> <div class="flex justify-center text-xs">
<LinkButton <LinkButton
@ -74,28 +67,28 @@
options={[ options={[
{ {
value: '', value: '',
text: 'All' text: 'All',
}, },
{ {
value: Duration.fromObject({ hours: 24 }).toISO() || '', value: Duration.fromObject({ hours: 24 }).toISO() || '',
text: 'Past 24 hours' text: 'Past 24 hours',
}, },
{ {
value: Duration.fromObject({ days: 7 }).toISO() || '', value: Duration.fromObject({ days: 7 }).toISO() || '',
text: 'Past 7 days' text: 'Past 7 days',
}, },
{ {
value: Duration.fromObject({ days: 30 }).toISO() || '', value: Duration.fromObject({ days: 30 }).toISO() || '',
text: 'Past 30 days' text: 'Past 30 days',
}, },
{ {
value: Duration.fromObject({ years: 1 }).toISO() || '', value: Duration.fromObject({ years: 1 }).toISO() || '',
text: 'Past year' text: 'Past year',
}, },
{ {
value: Duration.fromObject({ years: 3 }).toISO() || '', value: Duration.fromObject({ years: 3 }).toISO() || '',
text: 'Past 3 years' text: 'Past 3 years',
} },
]} ]}
/> />
<div class="text-xs"> <div class="text-xs">

View file

@ -20,8 +20,7 @@
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { tweened } from 'svelte/motion'; import { tweened } from 'svelte/motion';
const parseIndex = (s: string | null, max: number | null) => const parseIndex = (s: string | null, max: number | null) => Math.max(Math.min(parseInt(s ?? '') || 0, max ?? 0), 0);
Math.max(Math.min(parseInt(s ?? '') || 0, max ?? 0), 0);
$: memoryIndex = parseIndex($page.url.searchParams.get('memory'), $memoryStore?.length - 1); $: memoryIndex = parseIndex($page.url.searchParams.get('memory'), $memoryStore?.length - 1);
$: assetIndex = parseIndex($page.url.searchParams.get('asset'), currentMemory?.assets.length - 1); $: assetIndex = parseIndex($page.url.searchParams.get('asset'), currentMemory?.assets.length - 1);
@ -47,7 +46,7 @@
const toPrevious = () => (previousAsset ? toPreviousAsset() : toPreviousMemory()); const toPrevious = () => (previousAsset ? toPreviousAsset() : toPreviousMemory());
const progress = tweened<number>(0, { const progress = tweened<number>(0, {
duration: (from: number, to: number) => (to ? 5000 * (to - from) : 0) duration: (from: number, to: number) => (to ? 5000 * (to - from) : 0),
}); });
const play = () => progress.set(1); const play = () => progress.set(1);
@ -89,7 +88,7 @@
onMount(async () => { onMount(async () => {
if (!$memoryStore) { if (!$memoryStore) {
const { data } = await api.assetApi.getMemoryLane({ const { data } = await api.assetApi.getMemoryLane({
timestamp: DateTime.local().startOf('day').toISO() || '' timestamp: DateTime.local().startOf('day').toISO() || '',
}); });
$memoryStore = data; $memoryStore = data;
} }
@ -113,23 +112,13 @@
{#if !galleryInView} {#if !galleryInView}
<div class="flex place-items-center place-content-center overflow-hidden gap-2"> <div class="flex place-items-center place-content-center overflow-hidden gap-2">
<CircleIconButton <CircleIconButton logo={paused ? Play : Pause} forceDark on:click={() => (paused = !paused)} />
logo={paused ? Play : Pause}
forceDark
on:click={() => (paused = !paused)}
/>
{#each currentMemory.assets as _, i} {#each currentMemory.assets as _, i}
<button <button class="relative w-full py-2" on:click={() => goto(`?memory=${memoryIndex}&asset=${i}`)}>
class="relative w-full py-2"
on:click={() => goto(`?memory=${memoryIndex}&asset=${i}`)}
>
<span class="absolute left-0 w-full h-[2px] bg-gray-500" /> <span class="absolute left-0 w-full h-[2px] bg-gray-500" />
{#await resetPromise} {#await resetPromise}
<span <span class="absolute left-0 h-[2px] bg-white" style:width={`${i < assetIndex ? 100 : 0}%`} />
class="absolute left-0 h-[2px] bg-white"
style:width={`${i < assetIndex ? 100 : 0}%`}
/>
{:then} {:then}
<span <span
class="absolute left-0 h-[2px] bg-white" class="absolute left-0 h-[2px] bg-white"
@ -154,10 +143,7 @@
class:opacity-0={!galleryInView} class:opacity-0={!galleryInView}
class:opacity-100={galleryInView} class:opacity-100={galleryInView}
> >
<button <button on:click={() => memoryWrapper.scrollIntoView({ behavior: 'smooth' })} disabled={!galleryInView}>
on:click={() => memoryWrapper.scrollIntoView({ behavior: 'smooth' })}
disabled={!galleryInView}
>
<CircleIconButton logo={ChevronUp} backgroundColor="white" forceDark /> <CircleIconButton logo={ChevronUp} backgroundColor="white" forceDark />
</button> </button>
</div> </div>
@ -174,16 +160,10 @@
class:opacity-0={!previousMemory} class:opacity-0={!previousMemory}
class:hover:opacity-70={previousMemory} class:hover:opacity-70={previousMemory}
> >
<button <button class="rounded-2xl h-full w-full relative" disabled={!previousMemory} on:click={toPreviousMemory}>
class="rounded-2xl h-full w-full relative"
disabled={!previousMemory}
on:click={toPreviousMemory}
>
<img <img
class="rounded-2xl h-full w-full object-cover" class="rounded-2xl h-full w-full object-cover"
src={previousMemory src={previousMemory ? api.getAssetThumbnailUrl(previousMemory.assets[0].id, 'JPEG') : noThumbnailUrl}
? api.getAssetThumbnailUrl(previousMemory.assets[0].id, 'JPEG')
: noThumbnailUrl}
alt="" alt=""
draggable="false" draggable="false"
/> />
@ -207,22 +187,14 @@
<div class="flex h-full flex-col place-content-center place-items-center ml-4"> <div class="flex h-full flex-col place-content-center place-items-center ml-4">
<div class="inline-block"> <div class="inline-block">
{#if canGoBack} {#if canGoBack}
<CircleIconButton <CircleIconButton logo={ChevronLeft} backgroundColor="#202123" on:click={toPrevious} />
logo={ChevronLeft}
backgroundColor="#202123"
on:click={toPrevious}
/>
{/if} {/if}
</div> </div>
</div> </div>
<div class="flex h-full flex-col place-content-center place-items-center mr-4"> <div class="flex h-full flex-col place-content-center place-items-center mr-4">
<div class="inline-block"> <div class="inline-block">
{#if canGoForward} {#if canGoForward}
<CircleIconButton <CircleIconButton logo={ChevronRight} backgroundColor="#202123" on:click={toNext} />
logo={ChevronRight}
backgroundColor="#202123"
on:click={toNext}
/>
{/if} {/if}
</div> </div>
</div> </div>
@ -240,9 +212,7 @@
<div class="absolute top-4 left-8 text-white text-sm font-medium"> <div class="absolute top-4 left-8 text-white text-sm font-medium">
<p> <p>
{DateTime.fromISO(currentMemory.assets[0].fileCreatedAt).toLocaleString( {DateTime.fromISO(currentMemory.assets[0].fileCreatedAt).toLocaleString(DateTime.DATE_FULL)}
DateTime.DATE_FULL
)}
</p> </p>
<p> <p>
{currentAsset.exifInfo?.city || ''} {currentAsset.exifInfo?.city || ''}
@ -259,16 +229,10 @@
class:opacity-0={!nextMemory} class:opacity-0={!nextMemory}
class:hover:opacity-70={nextMemory} class:hover:opacity-70={nextMemory}
> >
<button <button class="rounded-2xl h-full w-full relative" on:click={toNextMemory} disabled={!nextMemory}>
class="rounded-2xl h-full w-full relative"
on:click={toNextMemory}
disabled={!nextMemory}
>
<img <img
class="rounded-2xl h-full w-full object-cover" class="rounded-2xl h-full w-full object-cover"
src={nextMemory src={nextMemory ? api.getAssetThumbnailUrl(nextMemory.assets[0].id, 'JPEG') : noThumbnailUrl}
? api.getAssetThumbnailUrl(nextMemory.assets[0].id, 'JPEG')
: noThumbnailUrl}
alt="" alt=""
draggable="false" draggable="false"
/> />

View file

@ -4,7 +4,7 @@
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import { import {
NotificationType, NotificationType,
notificationController notificationController,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { addAssetsToAlbum } from '$lib/utils/asset-utils'; import { addAssetsToAlbum } from '$lib/utils/asset-utils';
import { AlbumResponseDto, api } from '@api'; import { AlbumResponseDto, api } from '@api';
@ -32,7 +32,7 @@
notificationController.show({ notificationController.show({
message: `Added ${assetIds.length} to ${albumName}`, message: `Added ${assetIds.length} to ${albumName}`,
type: NotificationType.Info type: NotificationType.Info,
}); });
clearSelect(); clearSelect();
@ -51,10 +51,7 @@
}; };
</script> </script>
<MenuOption <MenuOption on:click={() => (showAlbumPicker = true)} text={shared ? 'Add to Shared Album' : 'Add to Album'} />
on:click={() => (showAlbumPicker = true)}
text={shared ? 'Add to Shared Album' : 'Add to Album'}
/>
{#if showAlbumPicker} {#if showAlbumPicker}
<AlbumSelectionModal <AlbumSelectionModal

View file

@ -2,7 +2,7 @@
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { import {
NotificationType, NotificationType,
notificationController notificationController,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { api } from '@api'; import { api } from '@api';
import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte'; import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
@ -37,7 +37,7 @@
notificationController.show({ notificationController.show({
message: `${isArchived ? 'Archived' : 'Unarchived'} ${cnt}`, message: `${isArchived ? 'Archived' : 'Unarchived'} ${cnt}`,
type: NotificationType.Info type: NotificationType.Info,
}); });
clearSelect(); clearSelect();

View file

@ -2,7 +2,7 @@
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { import {
NotificationType, NotificationType,
notificationController notificationController,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { api } from '@api'; import { api } from '@api';
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte'; import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
@ -21,8 +21,8 @@
const { data: deletedAssets } = await api.assetApi.deleteAsset({ const { data: deletedAssets } = await api.assetApi.deleteAsset({
deleteAssetDto: { deleteAssetDto: {
ids: Array.from(getAssets()).map((a) => a.id) ids: Array.from(getAssets()).map((a) => a.id),
} },
}); });
for (const asset of deletedAssets) { for (const asset of deletedAssets) {
@ -34,7 +34,7 @@
notificationController.show({ notificationController.show({
message: `Deleted ${count}`, message: `Deleted ${count}`,
type: NotificationType.Info type: NotificationType.Info,
}); });
clearSelect(); clearSelect();
@ -46,11 +46,7 @@
}; };
</script> </script>
<CircleIconButton <CircleIconButton title="Delete" logo={DeleteOutline} on:click={() => (isShowConfirmation = true)} />
title="Delete"
logo={DeleteOutline}
on:click={() => (isShowConfirmation = true)}
/>
{#if isShowConfirmation} {#if isShowConfirmation}
<ConfirmDialogue <ConfirmDialogue

View file

@ -19,12 +19,7 @@
return; return;
} }
await downloadArchive( await downloadArchive(filename, { assetIds: assets.map((asset) => asset.id) }, clearSelect, sharedLinkKey);
filename,
{ assetIds: assets.map((asset) => asset.id) },
clearSelect,
sharedLinkKey
);
}; };
</script> </script>

View file

@ -3,7 +3,7 @@
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import { import {
NotificationType, NotificationType,
notificationController notificationController,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { api } from '@api'; import { api } from '@api';
import HeartMinusOutline from 'svelte-material-icons/HeartMinusOutline.svelte'; import HeartMinusOutline from 'svelte-material-icons/HeartMinusOutline.svelte';
@ -36,7 +36,7 @@
notificationController.show({ notificationController.show({
message: isFavorite ? `Added ${cnt} to favorites` : `Removed ${cnt} from favorites`, message: isFavorite ? `Added ${cnt} to favorites` : `Removed ${cnt} from favorites`,
type: NotificationType.Info type: NotificationType.Info,
}); });
clearSelect(); clearSelect();

View file

@ -2,7 +2,7 @@
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { import {
NotificationType, NotificationType,
notificationController notificationController,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { AlbumResponseDto, api } from '@api'; import { AlbumResponseDto, api } from '@api';
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte'; import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
@ -20,8 +20,8 @@
const { data } = await api.albumApi.removeAssetFromAlbum({ const { data } = await api.albumApi.removeAssetFromAlbum({
id: album.id, id: album.id,
removeAssetsDto: { removeAssetsDto: {
assetIds: Array.from(getAssets()).map((a) => a.id) assetIds: Array.from(getAssets()).map((a) => a.id),
} },
}); });
album = data; album = data;
@ -30,7 +30,7 @@
console.error('Error [album-viewer] [removeAssetFromAlbum]', e); console.error('Error [album-viewer] [removeAssetFromAlbum]', e);
notificationController.show({ notificationController.show({
type: NotificationType.Error, type: NotificationType.Error,
message: 'Error removing assets from album, check console for more details' message: 'Error removing assets from album, check console for more details',
}); });
} finally { } finally {
isShowConfirmation = false; isShowConfirmation = false;
@ -38,11 +38,7 @@
}; };
</script> </script>
<CircleIconButton <CircleIconButton title="Remove from album" on:click={() => (isShowConfirmation = true)} logo={DeleteOutline} />
title="Remove from album"
on:click={() => (isShowConfirmation = true)}
logo={DeleteOutline}
/>
{#if isShowConfirmation} {#if isShowConfirmation}
<ConfirmDialogue <ConfirmDialogue

View file

@ -4,10 +4,7 @@
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte'; import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
import ConfirmDialogue from '../../shared-components/confirm-dialogue.svelte'; import ConfirmDialogue from '../../shared-components/confirm-dialogue.svelte';
import { getAssetControlContext } from '../asset-select-control-bar.svelte'; import { getAssetControlContext } from '../asset-select-control-bar.svelte';
import { import { NotificationType, notificationController } from '../../shared-components/notification/notification';
NotificationType,
notificationController
} from '../../shared-components/notification/notification';
import { handleError } from '../../../utils/handle-error'; import { handleError } from '../../../utils/handle-error';
export let sharedLink: SharedLinkResponseDto; export let sharedLink: SharedLinkResponseDto;
@ -21,9 +18,9 @@
const { data: results } = await api.sharedLinkApi.removeSharedLinkAssets({ const { data: results } = await api.sharedLinkApi.removeSharedLinkAssets({
id: sharedLink.id, id: sharedLink.id,
assetIdsDto: { assetIdsDto: {
assetIds: Array.from(getAssets()).map((asset) => asset.id) assetIds: Array.from(getAssets()).map((asset) => asset.id),
}, },
key: sharedLink.key key: sharedLink.key,
}); });
for (const result of results) { for (const result of results) {
@ -38,7 +35,7 @@
notificationController.show({ notificationController.show({
type: NotificationType.Info, type: NotificationType.Info,
message: `Removed ${count} assets` message: `Removed ${count} assets`,
}); });
clearSelect(); clearSelect();
@ -48,11 +45,7 @@
}; };
</script> </script>
<CircleIconButton <CircleIconButton title="Remove from shared link" on:click={() => (removing = true)} logo={DeleteOutline} />
title="Remove from shared link"
on:click={() => (removing = true)}
logo={DeleteOutline}
/>
{#if removing} {#if removing}
<ConfirmDialogue <ConfirmDialogue

View file

@ -18,10 +18,7 @@
}); });
for (let i = 0; i < _assetGridState.buckets.length; i++) { for (let i = 0; i < _assetGridState.buckets.length; i++) {
await assetStore.getAssetsByBucket( await assetStore.getAssetsByBucket(_assetGridState.buckets[i].bucketDate, BucketPosition.Unknown);
_assetGridState.buckets[i].bucketDate,
BucketPosition.Unknown
);
for (const asset of _assetGridState.buckets[i].assets) { for (const asset of _assetGridState.buckets[i].assets) {
assetInteractionStore.addAssetToMultiselectGroup(asset); assetInteractionStore.addAssetToMultiselectGroup(asset);
} }

View file

@ -4,7 +4,7 @@
assetsInAlbumStoreState, assetsInAlbumStoreState,
isMultiSelectStoreState, isMultiSelectStoreState,
selectedAssets, selectedAssets,
selectedGroup selectedGroup,
} from '$lib/stores/asset-interaction.store'; } from '$lib/stores/asset-interaction.store';
import { assetStore } from '$lib/stores/assets.store'; import { assetStore } from '$lib/stores/assets.store';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
@ -28,7 +28,7 @@
weekday: 'short', weekday: 'short',
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
year: 'numeric' year: 'numeric',
}; };
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@ -57,11 +57,11 @@
containerWidth: Math.floor(viewportWidth), containerWidth: Math.floor(viewportWidth),
containerPadding: 0, containerPadding: 0,
targetRowHeightTolerance: 0.15, targetRowHeightTolerance: 0.15,
targetRowHeight: 235 targetRowHeight: 235,
}); });
geometry.push({ geometry.push({
...justifiedLayoutResult, ...justifiedLayoutResult,
containerWidth: calculateWidth(justifiedLayoutResult.boxes) containerWidth: calculateWidth(justifiedLayoutResult.boxes),
}); });
} }
return geometry; return geometry;
@ -78,7 +78,7 @@
function scrollTimeline(heightDelta: number) { function scrollTimeline(heightDelta: number) {
dispatch('shift', { dispatch('shift', {
heightDelta heightDelta,
}); });
} }
@ -96,7 +96,7 @@
const assetClickHandler = ( const assetClickHandler = (
asset: AssetResponseDto, asset: AssetResponseDto,
assetsInDateGroup: AssetResponseDto[], assetsInDateGroup: AssetResponseDto[],
dateGroupTitle: string dateGroupTitle: string,
) => { ) => {
if (isAlbumSelectionMode) { if (isAlbumSelectionMode) {
assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle); assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle);
@ -110,10 +110,7 @@
} }
}; };
const selectAssetGroupHandler = ( const selectAssetGroupHandler = (selectAssetGroupHandler: AssetResponseDto[], dateGroupTitle: string) => {
selectAssetGroupHandler: AssetResponseDto[],
dateGroupTitle: string
) => {
if ($selectedGroup.has(dateGroupTitle)) { if ($selectedGroup.has(dateGroupTitle)) {
assetInteractionStore.removeGroupFromMultiselectGroup(dateGroupTitle); assetInteractionStore.removeGroupFromMultiselectGroup(dateGroupTitle);
selectAssetGroupHandler.forEach((asset) => { selectAssetGroupHandler.forEach((asset) => {
@ -130,7 +127,7 @@
const assetSelectHandler = ( const assetSelectHandler = (
asset: AssetResponseDto, asset: AssetResponseDto,
assetsInDateGroup: AssetResponseDto[], assetsInDateGroup: AssetResponseDto[],
dateGroupTitle: string dateGroupTitle: string,
) => { ) => {
if ($selectedAssets.has(asset)) { if ($selectedAssets.has(asset)) {
assetInteractionStore.removeAssetFromMultiselectGroup(asset); assetInteractionStore.removeAssetFromMultiselectGroup(asset);
@ -167,10 +164,7 @@
bind:clientWidth={viewportWidth} bind:clientWidth={viewportWidth}
> >
{#each assetsGroupByDate as assetsInDateGroup, groupIndex (assetsInDateGroup[0].id)} {#each assetsGroupByDate as assetsInDateGroup, groupIndex (assetsInDateGroup[0].id)}
{@const dateGroupTitle = new Date(assetsInDateGroup[0].fileCreatedAt).toLocaleDateString( {@const dateGroupTitle = new Date(assetsInDateGroup[0].fileCreatedAt).toLocaleDateString($locale, groupDateFormat)}
$locale,
groupDateFormat
)}
<!-- Asset Group By Date --> <!-- Asset Group By Date -->
<div <div
@ -209,8 +203,7 @@
<!-- Image grid --> <!-- Image grid -->
<div <div
class="relative" class="relative"
style="height: {geometry[groupIndex].containerHeight}px;width: {geometry[groupIndex] style="height: {geometry[groupIndex].containerHeight}px;width: {geometry[groupIndex].containerWidth}px"
.containerWidth}px"
> >
{#each assetsInDateGroup as asset, index (asset.id)} {#each assetsInDateGroup as asset, index (asset.id)}
{@const box = geometry[groupIndex].boxes[index]} {@const box = geometry[groupIndex].boxes[index]}
@ -224,8 +217,7 @@
on:click={() => assetClickHandler(asset, assetsInDateGroup, dateGroupTitle)} on:click={() => assetClickHandler(asset, assetsInDateGroup, dateGroupTitle)}
on:select={() => assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle)} on:select={() => assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle)}
on:mouse-event={() => assetMouseEventHandler(dateGroupTitle)} on:mouse-event={() => assetMouseEventHandler(dateGroupTitle)}
selected={$selectedAssets.has(asset) || selected={$selectedAssets.has(asset) || $assetsInAlbumStoreState.findIndex((a) => a.id == asset.id) != -1}
$assetsInAlbumStoreState.findIndex((a) => a.id == asset.id) != -1}
disabled={$assetsInAlbumStoreState.findIndex((a) => a.id == asset.id) != -1} disabled={$assetsInAlbumStoreState.findIndex((a) => a.id == asset.id) != -1}
thumbnailWidth={box.width} thumbnailWidth={box.width}
thumbnailHeight={box.height} thumbnailHeight={box.height}

View file

@ -2,7 +2,7 @@
import { import {
assetInteractionStore, assetInteractionStore,
isViewingAssetStoreState, isViewingAssetStoreState,
viewingAssetStoreState viewingAssetStoreState,
} from '$lib/stores/asset-interaction.store'; } from '$lib/stores/asset-interaction.store';
import { assetGridState, assetStore, loadingBucketState } from '$lib/stores/assets.store'; import { assetGridState, assetStore, loadingBucketState } from '$lib/stores/assets.store';
import type { UserResponseDto } from '@api'; import type { UserResponseDto } from '@api';
@ -13,7 +13,7 @@
import Portal from '../shared-components/portal/portal.svelte'; import Portal from '../shared-components/portal/portal.svelte';
import Scrollbar, { import Scrollbar, {
OnScrollbarClickDetail, OnScrollbarClickDetail,
OnScrollbarDragDetail OnScrollbarDragDetail,
} from '../shared-components/scrollbar/scrollbar.svelte'; } from '../shared-components/scrollbar/scrollbar.svelte';
import AssetDateGroup from './asset-date-group.svelte'; import AssetDateGroup from './asset-date-group.svelte';
import { BucketPosition } from '$lib/models/asset-grid-state'; import { BucketPosition } from '$lib/models/asset-grid-state';
@ -33,8 +33,8 @@
getAssetCountByTimeBucketDto: { getAssetCountByTimeBucketDto: {
timeGroup: TimeGroupEnum.Month, timeGroup: TimeGroupEnum.Month,
userId: user?.id, userId: user?.id,
withoutThumbs: true withoutThumbs: true,
} },
}); });
bucketInfo = assetCountByTimebucket; bucketInfo = assetCountByTimebucket;

View file

@ -27,11 +27,7 @@
setContext({ getAssets: () => assets, clearSelect }); setContext({ getAssets: () => assets, clearSelect });
</script> </script>
<ControlAppBar <ControlAppBar on:close-button-click={clearSelect} backIcon={Close} tailwindClasses="bg-white shadow-md">
on:close-button-click={clearSelect}
backIcon={Close}
tailwindClasses="bg-white shadow-md"
>
<p class="font-medium text-immich-primary dark:text-immich-dark-primary" slot="leading"> <p class="font-medium text-immich-primary dark:text-immich-dark-primary" slot="leading">
Selected {assets.size.toLocaleString($locale)} Selected {assets.size.toLocaleString($locale)}
</p> </p>

View file

@ -12,7 +12,7 @@
onMount(async () => { onMount(async () => {
const { data } = await api.assetApi.getMemoryLane({ const { data } = await api.assetApi.getMemoryLane({
timestamp: DateTime.local().startOf('day').toISO() || '' timestamp: DateTime.local().startOf('day').toISO() || '',
}); });
$memoryStore = data; $memoryStore = data;
}); });

View file

@ -16,10 +16,7 @@
import SelectAll from 'svelte-material-icons/SelectAll.svelte'; import SelectAll from 'svelte-material-icons/SelectAll.svelte';
import ImmichLogo from '../shared-components/immich-logo.svelte'; import ImmichLogo from '../shared-components/immich-logo.svelte';
import { import { notificationController, NotificationType } from '../shared-components/notification/notification';
notificationController,
NotificationType
} from '../shared-components/notification/notification';
import { handleError } from '../../utils/handle-error'; import { handleError } from '../../utils/handle-error';
export let sharedLink: SharedLinkResponseDto; export let sharedLink: SharedLinkResponseDto;
@ -42,7 +39,7 @@
`immich-shared.zip`, `immich-shared.zip`,
{ assetIds: assets.map((asset) => asset.id) }, { assetIds: assets.map((asset) => asset.id) },
undefined, undefined,
sharedLink.key sharedLink.key,
); );
}; };
@ -57,16 +54,16 @@
const { data } = await api.sharedLinkApi.addSharedLinkAssets({ const { data } = await api.sharedLinkApi.addSharedLinkAssets({
id: sharedLink.id, id: sharedLink.id,
assetIdsDto: { assetIdsDto: {
assetIds: results.filter((id) => !!id) as string[] assetIds: results.filter((id) => !!id) as string[],
}, },
key: sharedLink.key key: sharedLink.key,
}); });
const added = data.filter((item) => item.success).length; const added = data.filter((item) => item.success).length;
notificationController.show({ notificationController.show({
message: `Added ${added} assets`, message: `Added ${added} assets`,
type: NotificationType.Info type: NotificationType.Info,
}); });
} catch (e) { } catch (e) {
handleError(e, 'Unable to add assets to shared link'); handleError(e, 'Unable to add assets to shared link');
@ -90,11 +87,7 @@
{/if} {/if}
</AssetSelectControlBar> </AssetSelectControlBar>
{:else} {:else}
<ControlAppBar <ControlAppBar on:close-button-click={() => goto('/photos')} backIcon={ArrowLeft} showBackButton={false}>
on:close-button-click={() => goto('/photos')}
backIcon={ArrowLeft}
showBackButton={false}
>
<svelte:fragment slot="leading"> <svelte:fragment slot="leading">
<a <a
data-sveltekit-preload-data="hover" data-sveltekit-preload-data="hover"
@ -102,27 +95,17 @@
href="https://immich.app" href="https://immich.app"
> >
<ImmichLogo height="30" width="30" /> <ImmichLogo height="30" width="30" />
<h1 class="font-immich-title text-lg text-immich-primary dark:text-immich-dark-primary"> <h1 class="font-immich-title text-lg text-immich-primary dark:text-immich-dark-primary">IMMICH</h1>
IMMICH
</h1>
</a> </a>
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="trailing"> <svelte:fragment slot="trailing">
{#if sharedLink?.allowUpload} {#if sharedLink?.allowUpload}
<CircleIconButton <CircleIconButton title="Add Photos" on:click={() => handleUploadAssets()} logo={FileImagePlusOutline} />
title="Add Photos"
on:click={() => handleUploadAssets()}
logo={FileImagePlusOutline}
/>
{/if} {/if}
{#if sharedLink?.allowDownload} {#if sharedLink?.allowDownload}
<CircleIconButton <CircleIconButton title="Download" on:click={downloadAssets} logo={FolderDownloadOutline} />
title="Download"
on:click={downloadAssets}
logo={FolderDownloadOutline}
/>
{/if} {/if}
</svelte:fragment> </svelte:fragment>
</ControlAppBar> </ControlAppBar>

View file

@ -19,9 +19,7 @@
const { data } = await api.albumApi.getAllAlbums({ shared: shared || undefined }); const { data } = await api.albumApi.getAllAlbums({ shared: shared || undefined });
albums = data; albums = data;
recentAlbums = albums recentAlbums = albums.sort((a, b) => (new Date(a.createdAt) > new Date(b.createdAt) ? -1 : 1)).slice(0, 3);
.sort((a, b) => (new Date(a.createdAt) > new Date(b.createdAt) ? -1 : 1))
.slice(0, 3);
loading = false; loading = false;
}); });
@ -109,9 +107,7 @@
<AlbumListItem {album} searchQuery={search} on:album={() => handleSelect(album)} /> <AlbumListItem {album} searchQuery={search} on:album={() => handleSelect(album)} />
{/each} {/each}
{:else if albums.length > 0} {:else if albums.length > 0}
<p class="text-sm px-5 py-1"> <p class="text-sm px-5 py-1">It looks like you do not have any albums with this name yet.</p>
It looks like you do not have any albums with this name yet.
</p>
{:else} {:else}
<p class="text-sm px-5 py-1">It looks like you do not have any albums yet.</p> <p class="text-sm px-5 py-1">It looks like you do not have any albums yet.</p>
{/if} {/if}

View file

@ -48,12 +48,7 @@
{cancelText} {cancelText}
</Button> </Button>
{/if} {/if}
<Button <Button color={confirmColor} fullwidth on:click={handleConfirm} disabled={isConfirmButtonDisabled}>
color={confirmColor}
fullwidth
on:click={handleConfirm}
disabled={isConfirmButtonDisabled}
>
{confirmText} {confirmText}
</Button> </Button>
</div> </div>

View file

@ -1,17 +1,11 @@
<script lang="ts"> <script lang="ts">
import SettingInputField, { import SettingInputField, {
SettingInputFieldType SettingInputFieldType,
} from '$lib/components/admin-page/settings/setting-input-field.svelte'; } from '$lib/components/admin-page/settings/setting-input-field.svelte';
import SettingSwitch from '$lib/components/admin-page/settings/setting-switch.svelte'; import SettingSwitch from '$lib/components/admin-page/settings/setting-switch.svelte';
import Button from '$lib/components/elements/buttons/button.svelte'; import Button from '$lib/components/elements/buttons/button.svelte';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { import { AlbumResponseDto, api, AssetResponseDto, SharedLinkResponseDto, SharedLinkType } from '@api';
AlbumResponseDto,
api,
AssetResponseDto,
SharedLinkResponseDto,
SharedLinkType
} from '@api';
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import Link from 'svelte-material-icons/Link.svelte'; import Link from 'svelte-material-icons/Link.svelte';
import BaseModal from '../base-modal.svelte'; import BaseModal from '../base-modal.svelte';
@ -36,7 +30,7 @@
const expiredDateOption: ImmichDropDownOption = { const expiredDateOption: ImmichDropDownOption = {
default: 'Never', default: 'Never',
options: ['Never', '30 minutes', '1 hour', '6 hours', '1 day', '7 days', '30 days'] options: ['Never', '30 minutes', '1 hour', '6 hours', '1 day', '7 days', '30 days'],
}; };
onMount(async () => { onMount(async () => {
@ -56,9 +50,7 @@
const handleCreateSharedLink = async () => { const handleCreateSharedLink = async () => {
const expirationTime = getExpirationTimeInMillisecond(); const expirationTime = getExpirationTimeInMillisecond();
const currentTime = new Date().getTime(); const currentTime = new Date().getTime();
const expirationDate = expirationTime const expirationDate = expirationTime ? new Date(currentTime + expirationTime).toISOString() : undefined;
? new Date(currentTime + expirationTime).toISOString()
: undefined;
try { try {
const { data } = await api.sharedLinkApi.createSharedLink({ const { data } = await api.sharedLinkApi.createSharedLink({
@ -70,8 +62,8 @@
allowUpload, allowUpload,
description, description,
allowDownload, allowDownload,
showExif showExif,
} },
}); });
sharedLink = `${window.location.origin}/share/${data.key}`; sharedLink = `${window.location.origin}/share/${data.key}`;
} catch (e) { } catch (e) {
@ -88,10 +80,7 @@
await navigator.clipboard.writeText(sharedLink); await navigator.clipboard.writeText(sharedLink);
notificationController.show({ message: 'Copied to clipboard!', type: NotificationType.Info }); notificationController.show({ message: 'Copied to clipboard!', type: NotificationType.Info });
} catch (e) { } catch (e) {
handleError( handleError(e, 'Cannot copy to clipboard, make sure you are accessing the page through https');
e,
'Cannot copy to clipboard, make sure you are accessing the page through https'
);
} }
}; };
@ -133,13 +122,13 @@
expiresAt: shouldChangeExpirationTime ? expirationDate : undefined, expiresAt: shouldChangeExpirationTime ? expirationDate : undefined,
allowUpload: allowUpload, allowUpload: allowUpload,
allowDownload: allowDownload, allowDownload: allowDownload,
showExif: showExif showExif: showExif,
} },
}); });
notificationController.show({ notificationController.show({
type: NotificationType.Info, type: NotificationType.Info,
message: 'Edited' message: 'Edited',
}); });
dispatch('close'); dispatch('close');
@ -192,11 +181,7 @@
<div class="p-4 bg-gray-100 dark:bg-black/40 rounded-lg"> <div class="p-4 bg-gray-100 dark:bg-black/40 rounded-lg">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="mb-2"> <div class="mb-2">
<SettingInputField <SettingInputField inputType={SettingInputFieldType.TEXT} label="Description" bind:value={description} />
inputType={SettingInputFieldType.TEXT}
label="Description"
bind:value={description}
/>
</div> </div>
<div class="my-3"> <div class="my-3">
@ -214,10 +199,7 @@
<div class="text-sm"> <div class="text-sm">
{#if editingLink} {#if editingLink}
<p class="my-2 immich-form-label"> <p class="my-2 immich-form-label">
<SettingSwitch <SettingSwitch bind:checked={shouldChangeExpirationTime} title={'Change expiration time'} />
bind:checked={shouldChangeExpirationTime}
title={'Change expiration time'}
/>
</p> </p>
{:else} {:else}
<p class="my-2 immich-form-label">Expire after</p> <p class="my-2 immich-form-label">Expire after</p>

View file

@ -5,8 +5,7 @@
export let text = ''; export let text = '';
export let alt = ''; export let alt = '';
let hoverClasses = let hoverClasses = 'hover:bg-immich-primary/5 dark:hover:bg-immich-dark-primary/25 hover:cursor-pointer';
'hover:bg-immich-primary/5 dark:hover:bg-immich-dark-primary/25 hover:cursor-pointer';
</script> </script>
{#if actionHandler} {#if actionHandler}

View file

@ -1,10 +1,5 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
export type ViewFrom = export type ViewFrom = 'archive-page' | 'album-page' | 'favorites-page' | 'search-page' | 'shared-link-page';
| 'archive-page'
| 'album-page'
| 'favorites-page'
| 'search-page'
| 'shared-link-page';
</script> </script>
<script lang="ts"> <script lang="ts">

View file

@ -15,7 +15,7 @@
onMount(() => { onMount(() => {
const ControlClass = Control.extend({ const ControlClass = Control.extend({
position, position,
onAdd: () => target onAdd: () => target,
}); });
control = new ControlClass().addTo(map); control = new ControlClass().addTo(map);

View file

@ -1,5 +1,5 @@
export { default as AssetMarkerCluster } from './marker-cluster/asset-marker-cluster.svelte';
export { default as Control } from './control.svelte'; export { default as Control } from './control.svelte';
export { default as Map } from './map.svelte'; export { default as Map } from './map.svelte';
export { default as AssetMarkerCluster } from './marker-cluster/asset-marker-cluster.svelte';
export { default as Marker } from './marker.svelte'; export { default as Marker } from './marker.svelte';
export { default as TileLayer } from './tile-layer.svelte'; export { default as TileLayer } from './tile-layer.svelte';

View file

@ -4,9 +4,8 @@
@apply border; @apply border;
@apply border-immich-primary; @apply border-immich-primary;
@apply transition-all; @apply transition-all;
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, rgba(0, 0, 0, 0.07) 0px 2px 4px, box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, rgba(0, 0, 0, 0.07) 0px 2px 4px, rgba(0, 0, 0, 0.07) 0px 4px 8px,
rgba(0, 0, 0, 0.07) 0px 4px 8px, rgba(0, 0, 0, 0.07) 0px 8px 16px, rgba(0, 0, 0, 0.07) 0px 8px 16px, rgba(0, 0, 0, 0.07) 0px 16px 32px, rgba(0, 0, 0, 0.07) 0px 32px 64px;
rgba(0, 0, 0, 0.07) 0px 16px 32px, rgba(0, 0, 0, 0.07) 0px 32px 64px;
} }
.marker-cluster-icon { .marker-cluster-icon {

View file

@ -44,9 +44,9 @@
return new DivIcon({ return new DivIcon({
html: `<div class="marker-cluster-icon">${childCount}</div>`, html: `<div class="marker-cluster-icon">${childCount}</div>`,
className: '', className: '',
iconSize: new Point(iconSize, iconSize) iconSize: new Point(iconSize, iconSize),
}); });
} },
}); });
cluster.on('clusterclick', (event: LeafletEvent) => { cluster.on('clusterclick', (event: LeafletEvent) => {
@ -64,9 +64,7 @@
cluster.on('click', (event: LeafletMouseEvent) => { cluster.on('click', (event: LeafletMouseEvent) => {
const marker: AssetMarker = event.sourceTarget; const marker: AssetMarker = event.sourceTarget;
const markerCluster = getClusterByMarker(marker); const markerCluster = getClusterByMarker(marker);
const markers = markerCluster const markers = markerCluster ? (markerCluster.getAllChildMarkers() as AssetMarker[]) : [marker];
? (markerCluster.getAllChildMarkers() as AssetMarker[])
: [marker];
onView(markers, marker.id); onView(markers, marker.id);
}); });

View file

@ -1,5 +1,5 @@
import { MapMarkerResponseDto, api } from '@api'; import { api, MapMarkerResponseDto } from '@api';
import { Marker, Map, Icon } from 'leaflet'; import { Icon, Map, Marker } from 'leaflet';
export default class AssetMarker extends Marker { export default class AssetMarker extends Marker {
id: string; id: string;
@ -31,7 +31,7 @@ export default class AssetMarker extends Marker {
popupAnchor: [1, -34], popupAnchor: [1, -34],
tooltipAnchor: [16, -28], tooltipAnchor: [16, -28],
shadowSize: [41, 41], shadowSize: [41, 41],
className: 'asset-marker-icon' className: 'asset-marker-icon',
}); });
} }
} }

View file

@ -20,13 +20,13 @@
iconAnchor: [12, 41], iconAnchor: [12, 41],
popupAnchor: [1, -34], popupAnchor: [1, -34],
tooltipAnchor: [16, -28], tooltipAnchor: [16, -28],
shadowSize: [41, 41] shadowSize: [41, 41],
}); });
const map = getMapContext(); const map = getMapContext();
onMount(() => { onMount(() => {
marker = new Marker(latlng, { marker = new Marker(latlng, {
icon: defaultIcon icon: defaultIcon,
}).addTo(map); }).addTo(map);
}); });

View file

@ -37,15 +37,9 @@
<div <div
class="grid h-full md:grid-cols-[theme(spacing.64)_auto] grid-cols-[theme(spacing.18)_auto] border-b dark:border-b-immich-dark-gray items-center py-2 bg-immich-bg dark:bg-immich-dark-bg" class="grid h-full md:grid-cols-[theme(spacing.64)_auto] grid-cols-[theme(spacing.18)_auto] border-b dark:border-b-immich-dark-gray items-center py-2 bg-immich-bg dark:bg-immich-dark-bg"
> >
<a <a data-sveltekit-preload-data="hover" class="flex gap-2 md:mx-6 mx-4 place-items-center" href={AppRoute.PHOTOS}>
data-sveltekit-preload-data="hover"
class="flex gap-2 md:mx-6 mx-4 place-items-center"
href={AppRoute.PHOTOS}
>
<ImmichLogo height="35" width="35" /> <ImmichLogo height="35" width="35" />
<h1 <h1 class="font-immich-title text-2xl text-immich-primary dark:text-immich-dark-primary md:block hidden">
class="font-immich-title text-2xl text-immich-primary dark:text-immich-dark-primary md:block hidden"
>
IMMICH IMMICH
</h1> </h1>
</a> </a>

View file

@ -5,7 +5,7 @@
const progress = tweened(0, { const progress = tweened(0, {
duration: 1000, duration: 1000,
easing: cubicOut easing: cubicOut,
}); });
onMount(() => { onMount(() => {

View file

@ -1,8 +1,8 @@
import { jest, describe, it } from '@jest/globals'; import { describe, it, jest } from '@jest/globals';
import { render, cleanup, RenderResult } from '@testing-library/svelte'; import '@testing-library/jest-dom';
import { cleanup, render, RenderResult } from '@testing-library/svelte';
import { NotificationType } from '../notification'; import { NotificationType } from '../notification';
import NotificationCard from '../notification-card.svelte'; import NotificationCard from '../notification-card.svelte';
import '@testing-library/jest-dom';
describe('NotificationCard component', () => { describe('NotificationCard component', () => {
let sut: RenderResult<NotificationCard>; let sut: RenderResult<NotificationCard>;
@ -16,8 +16,8 @@ describe('NotificationCard component', () => {
message: 'Notification message', message: 'Notification message',
timeout: 1000, timeout: 1000,
type: NotificationType.Info, type: NotificationType.Info,
action: { type: 'discard' } action: { type: 'discard' },
} },
}); });
cleanup(); cleanup();
@ -31,8 +31,8 @@ describe('NotificationCard component', () => {
message: 'Notification message', message: 'Notification message',
timeout: 1000, timeout: 1000,
type: NotificationType.Info, type: NotificationType.Info,
action: { type: 'discard' } action: { type: 'discard' },
} },
}); });
expect(sut.getByTestId('title')).toHaveTextContent('Info'); expect(sut.getByTestId('title')).toHaveTextContent('Info');

View file

@ -1,13 +1,11 @@
import { jest, describe, it } from '@jest/globals'; import { describe, it, jest } from '@jest/globals';
import { render, RenderResult, waitFor } from '@testing-library/svelte';
import { notificationController, NotificationType } from '../notification';
import { get } from 'svelte/store';
import NotificationList from '../notification-list.svelte';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { render, RenderResult, waitFor } from '@testing-library/svelte';
import { get } from 'svelte/store';
import { notificationController, NotificationType } from '../notification';
import NotificationList from '../notification-list.svelte';
function _getNotificationListElement( function _getNotificationListElement(sut: RenderResult<NotificationList>): HTMLAnchorElement | null {
sut: RenderResult<NotificationList>
): HTMLAnchorElement | null {
return sut.container.querySelector('#notification-list'); return sut.container.querySelector('#notification-list');
} }
@ -28,7 +26,7 @@ describe('NotificationList component', () => {
notificationController.show({ notificationController.show({
message: 'Notification', message: 'Notification',
type: NotificationType.Info, type: NotificationType.Info,
timeout: 3000 timeout: 3000,
}); });
await waitFor(() => expect(_getNotificationListElement(sut)).toBeInTheDocument()); await waitFor(() => expect(_getNotificationListElement(sut)).toBeInTheDocument());

View file

@ -7,7 +7,7 @@
import { import {
ImmichNotification, ImmichNotification,
notificationController, notificationController,
NotificationType NotificationType,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
@ -16,8 +16,7 @@
let infoPrimaryColor = '#4250AF'; let infoPrimaryColor = '#4250AF';
let errorPrimaryColor = '#E64132'; let errorPrimaryColor = '#E64132';
$: icon = $: icon = notificationInfo.type === NotificationType.Error ? CloseCircleOutline : InformationOutline;
notificationInfo.type === NotificationType.Error ? CloseCircleOutline : InformationOutline;
$: backgroundColor = () => { $: backgroundColor = () => {
if (notificationInfo.type === NotificationType.Info) { if (notificationInfo.type === NotificationType.Info) {

View file

@ -10,11 +10,7 @@
</script> </script>
{#if $notificationList.length > 0} {#if $notificationList.length > 0}
<section <section transition:fade={{ duration: 250 }} id="notification-list" class="absolute right-5 top-[80px] z-[99999999]">
transition:fade={{ duration: 250 }}
id="notification-list"
class="absolute right-5 top-[80px] z-[99999999]"
>
{#each $notificationList as notificationInfo (notificationInfo.id)} {#each $notificationList as notificationInfo (notificationInfo.id)}
<div animate:flip={{ duration: 250, easing: quintOut }}> <div animate:flip={{ duration: 250, easing: quintOut }}>
<NotificationCard {notificationInfo} /> <NotificationCard {notificationInfo} />

View file

@ -2,7 +2,7 @@ import { writable } from 'svelte/store';
export enum NotificationType { export enum NotificationType {
Info = 'Info', Info = 'Info',
Error = 'Error' Error = 'Error',
} }
export class ImmichNotification { export class ImmichNotification {
@ -61,7 +61,7 @@ function createNotificationList() {
return { return {
show, show,
removeNotificationById, removeNotificationById,
notificationList notificationList,
}; };
} }

View file

@ -23,7 +23,7 @@
throw new TypeError( throw new TypeError(
`Unknown portal target type: ${ `Unknown portal target type: ${
target === null ? 'null' : typeof target target === null ? 'null' : typeof target
}. Allowed types: string (CSS selector) or HTMLElement.` }. Allowed types: string (CSS selector) or HTMLElement.`,
); );
} }
targetEl.appendChild(el); targetEl.appendChild(el);
@ -39,7 +39,7 @@
update(target); update(target);
return { return {
update, update,
destroy destroy,
}; };
} }
</script> </script>

View file

@ -26,7 +26,7 @@
const params = new URLSearchParams({ const params = new URLSearchParams({
q: searchValue, q: searchValue,
clip: clipSearch clip: clipSearch,
}); });
goto(`${AppRoute.SEARCH}?${params}`); goto(`${AppRoute.SEARCH}?${params}`);

View file

@ -26,11 +26,7 @@
" "
> >
<div class="flex gap-4 place-items-center w-full overflow-hidden truncate"> <div class="flex gap-4 place-items-center w-full overflow-hidden truncate">
<svelte:component <svelte:component this={logo} size="1.5em" class="shrink-0 {flippedLogo ? '-scale-x-100' : ''}" />
this={logo}
size="1.5em"
class="shrink-0 {flippedLogo ? '-scale-x-100' : ''}"
/>
<p class="font-medium text-sm">{title}</p> <p class="font-medium text-sm">{title}</p>
</div> </div>

View file

@ -24,7 +24,7 @@
return { return {
videos: allAssetCount.videos - archivedCount.videos, videos: allAssetCount.videos - archivedCount.videos,
photos: allAssetCount.photos - archivedCount.photos photos: allAssetCount.photos - archivedCount.photos,
}; };
}; };
@ -32,15 +32,15 @@
try { try {
const { data: assets } = await api.assetApi.getAllAssets({ const { data: assets } = await api.assetApi.getAllAssets({
isFavorite: true, isFavorite: true,
withoutThumbs: true withoutThumbs: true,
}); });
return { return {
favorites: assets.length favorites: assets.length,
}; };
} catch { } catch {
return { return {
favorites: 0 favorites: 0,
}; };
} }
}; };
@ -60,12 +60,12 @@
return { return {
videos: assetCount.videos, videos: assetCount.videos,
photos: assetCount.photos photos: assetCount.photos,
}; };
} catch { } catch {
return { return {
videos: 0, videos: 0,
photos: 0 photos: 0,
}; };
} }
}; };
@ -76,12 +76,7 @@
</script> </script>
<SideBarSection> <SideBarSection>
<a <a data-sveltekit-preload-data="hover" data-sveltekit-noscroll href={AppRoute.PHOTOS} draggable="false">
data-sveltekit-preload-data="hover"
data-sveltekit-noscroll
href={AppRoute.PHOTOS}
draggable="false"
>
<SideBarButton <SideBarButton
title="Photos" title="Photos"
logo={isPhotosSelected ? ImageMultiple : ImageMultipleOutline} logo={isPhotosSelected ? ImageMultiple : ImageMultipleOutline}
@ -99,17 +94,8 @@
</svelte:fragment> </svelte:fragment>
</SideBarButton> </SideBarButton>
</a> </a>
<a <a data-sveltekit-preload-data="hover" data-sveltekit-noscroll href={AppRoute.EXPLORE} draggable="false">
data-sveltekit-preload-data="hover" <SideBarButton title="Explore" logo={Magnify} isSelected={$page.route.id === '/(user)/explore'} />
data-sveltekit-noscroll
href={AppRoute.EXPLORE}
draggable="false"
>
<SideBarButton
title="Explore"
logo={Magnify}
isSelected={$page.route.id === '/(user)/explore'}
/>
</a> </a>
<a data-sveltekit-preload-data="hover" href={AppRoute.MAP} draggable="false"> <a data-sveltekit-preload-data="hover" href={AppRoute.MAP} draggable="false">
<SideBarButton title="Map" logo={Map} isSelected={$page.route.id === '/(user)/map'} /> <SideBarButton title="Map" logo={Map} isSelected={$page.route.id === '/(user)/map'} />
@ -154,12 +140,7 @@
</SideBarButton> </SideBarButton>
</a> </a>
<a data-sveltekit-preload-data="hover" href={AppRoute.ALBUMS} draggable="false"> <a data-sveltekit-preload-data="hover" href={AppRoute.ALBUMS} draggable="false">
<SideBarButton <SideBarButton title="Albums" logo={ImageAlbum} flippedLogo={true} isSelected={$page.route.id === '/(user)/albums'}>
title="Albums"
logo={ImageAlbum}
flippedLogo={true}
isSelected={$page.route.id === '/(user)/albums'}
>
<svelte:fragment slot="moreInformation"> <svelte:fragment slot="moreInformation">
{#await getAlbumCount()} {#await getAlbumCount()}
<LoadingSpinner /> <LoadingSpinner />
@ -172,11 +153,7 @@
</SideBarButton> </SideBarButton>
</a> </a>
<a data-sveltekit-preload-data="hover" href={AppRoute.ARCHIVE} draggable="false"> <a data-sveltekit-preload-data="hover" href={AppRoute.ARCHIVE} draggable="false">
<SideBarButton <SideBarButton title="Archive" logo={ArchiveArrowDownOutline} isSelected={$page.route.id === '/(user)/archive'}>
title="Archive"
logo={ArchiveArrowDownOutline}
isSelected={$page.route.id === '/(user)/archive'}
>
<svelte:fragment slot="moreInformation"> <svelte:fragment slot="moreInformation">
{#await getArchivedAssetsCount()} {#await getArchivedAssetsCount()}
<LoadingSpinner /> <LoadingSpinner />

View file

@ -51,9 +51,7 @@
<div class="dark:text-immich-dark-fg"> <div class="dark:text-immich-dark-fg">
<div class="storage-status grid grid-cols-[64px_auto]"> <div class="storage-status grid grid-cols-[64px_auto]">
<div <div class="pl-5 pr-6 text-immich-primary dark:text-immich-dark-primary pb-[2.15rem] group-hover:sm:pb-0 md:pb-0">
class="pl-5 pr-6 text-immich-primary dark:text-immich-dark-primary pb-[2.15rem] group-hover:sm:pb-0 md:pb-0"
>
<Cloud size={'24'} /> <Cloud size={'24'} />
</div> </div>
<div class="hidden md:block group-hover:sm:block"> <div class="hidden md:block group-hover:sm:block">
@ -81,9 +79,7 @@
<hr class="ml-5 my-4 dark:border-immich-dark-gray" /> <hr class="ml-5 my-4 dark:border-immich-dark-gray" />
</div> </div>
<div class="server-status grid grid-cols-[64px_auto]"> <div class="server-status grid grid-cols-[64px_auto]">
<div <div class="pl-5 pr-6 text-immich-primary dark:text-immich-dark-primary pb-11 md:pb-0 group-hover:sm:pb-0">
class="pl-5 pr-6 text-immich-primary dark:text-immich-dark-primary pb-11 md:pb-0 group-hover:sm:pb-0"
>
<Dns size={'24'} /> <Dns size={'24'} />
</div> </div>
<div class="text-xs hidden md:block group-hover:sm:block"> <div class="text-xs hidden md:block group-hover:sm:block">

View file

@ -55,10 +55,7 @@
/> />
<div class="w-full bg-gray-300 h-[15px] rounded-md mt-[5px] text-white relative"> <div class="w-full bg-gray-300 h-[15px] rounded-md mt-[5px] text-white relative">
<div <div class="bg-immich-primary h-[15px] rounded-md transition-all" style={`width: ${uploadAsset.progress}%`} />
class="bg-immich-primary h-[15px] rounded-md transition-all"
style={`width: ${uploadAsset.progress}%`}
/>
<p class="absolute h-full w-full text-center top-0 text-[10px]"> <p class="absolute h-full w-full text-center top-0 text-[10px]">
{uploadAsset.progress}/100 {uploadAsset.progress}/100
</p> </p>

View file

@ -30,7 +30,7 @@
on:outroend={() => { on:outroend={() => {
notificationController.show({ notificationController.show({
message: 'Upload success, refresh the page to see new upload assets', message: 'Upload success, refresh the page to see new upload assets',
type: NotificationType.Info type: NotificationType.Info,
}); });
}} }}
class="absolute right-6 bottom-6 z-[10000]" class="absolute right-6 bottom-6 z-[10000]"

View file

@ -17,20 +17,19 @@
let showFallback = true; let showFallback = true;
const colorClasses: Record<Color, string> = { const colorClasses: Record<Color, string> = {
primary: primary: 'bg-immich-primary dark:bg-immich-dark-primary text-immich-dark-fg dark:text-immich-fg',
'bg-immich-primary dark:bg-immich-dark-primary text-immich-dark-fg dark:text-immich-fg',
pink: 'bg-pink-400 text-immich-bg', pink: 'bg-pink-400 text-immich-bg',
red: 'bg-red-500 text-immich-bg', red: 'bg-red-500 text-immich-bg',
yellow: 'bg-yellow-500 text-immich-bg', yellow: 'bg-yellow-500 text-immich-bg',
blue: 'bg-blue-500 text-immich-bg', blue: 'bg-blue-500 text-immich-bg',
green: 'bg-green-600 text-immich-bg' green: 'bg-green-600 text-immich-bg',
}; };
const sizeClasses: Record<Size, string> = { const sizeClasses: Record<Size, string> = {
full: 'w-full h-full', full: 'w-full h-full',
sm: 'w-7 h-7', sm: 'w-7 h-7',
md: 'w-12 h-12', md: 'w-12 h-12',
lg: 'w-20 h-20' lg: 'w-20 h-20',
}; };
// Get color based on the user UUID. // Get color based on the user UUID.

View file

@ -49,19 +49,15 @@
<div> <div>
Hi friend, there is a new release of Hi friend, there is a new release of
<span class="font-immich-title text-immich-primary dark:text-immich-dark-primary font-bold" <span class="font-immich-title text-immich-primary dark:text-immich-dark-primary font-bold">IMMICH</span>,
>IMMICH</span please take your time to visit the
>, please take your time to visit the
<span class="underline font-medium" <span class="underline font-medium"
><a ><a href="https://github.com/immich-app/immich/releases/latest" target="_blank" rel="noopener noreferrer"
href="https://github.com/immich-app/immich/releases/latest" >release notes</a
target="_blank"
rel="noopener noreferrer">release notes</a
></span ></span
> >
and ensure your <code>docker-compose</code>, and <code>.env</code> setup is up-to-date to prevent and ensure your <code>docker-compose</code>, and <code>.env</code> setup is up-to-date to prevent any misconfigurations,
any misconfigurations, especially if you use WatchTower or any mechanism that handles updating especially if you use WatchTower or any mechanism that handles updating your application automatically.
your application automatically.
</div> </div>
<div class="font-medium mt-4">Your friend, Alex</div> <div class="font-medium mt-4">Your friend, Alex</div>

View file

@ -1,11 +1,5 @@
<script lang="ts"> <script lang="ts">
import { import { api, AssetResponseDto, SharedLinkResponseDto, SharedLinkType, ThumbnailFormat } from '@api';
api,
AssetResponseDto,
SharedLinkResponseDto,
SharedLinkType,
ThumbnailFormat
} from '@api';
import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import OpenInNew from 'svelte-material-icons/OpenInNew.svelte'; import OpenInNew from 'svelte-material-icons/OpenInNew.svelte';
import Delete from 'svelte-material-icons/TrashCanOutline.svelte'; import Delete from 'svelte-material-icons/TrashCanOutline.svelte';
@ -43,9 +37,7 @@
const expiresAtDate = luxon.DateTime.fromISO(new Date(link.expiresAt).toISOString()); const expiresAtDate = luxon.DateTime.fromISO(new Date(link.expiresAt).toISOString());
const now = luxon.DateTime.now(); const now = luxon.DateTime.now();
expirationCountdown = expiresAtDate expirationCountdown = expiresAtDate.diff(now, ['days', 'hours', 'minutes', 'seconds']).toObject();
.diff(now, ['days', 'hours', 'minutes', 'seconds'])
.toObject();
if (expirationCountdown.days && expirationCountdown.days > 0) { if (expirationCountdown.days && expirationCountdown.days > 0) {
return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'days' }); return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'days' });
@ -101,9 +93,7 @@
</div> </div>
<div class="text-sm"> <div class="text-sm">
<div <div class="flex gap-2 place-items-center text-immich-primary dark:text-immich-dark-primary">
class="flex gap-2 place-items-center text-immich-primary dark:text-immich-dark-primary"
>
{#if link.type === SharedLinkType.Album} {#if link.type === SharedLinkType.Album}
<p> <p>
{link.album?.albumName.toUpperCase()} {link.album?.albumName.toUpperCase()}

Some files were not shown because too many files have changed in this diff Show more