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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,3 @@
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: {
global: {
lines: 4,
statements: 4
}
statements: 4,
},
},
// 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
moduleNameMapper: {
'\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'identity-obj-proxy',
'\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 'identity-obj-proxy',
'^\\$lib(.*)$': '<rootDir>/src/lib$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
@ -181,13 +180,13 @@ export default {
'^.+\\.svelte$': [
'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
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
// unmockedModulePathPatterns: undefined,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@
const colorClasses: Record<Colors, string> = {
'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>

View file

@ -7,7 +7,7 @@
const colorClasses: Record<Color, string> = {
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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
<script lang="ts">
import {
notificationController,
NotificationType
NotificationType,
} from '$lib/components/shared-components/notification/notification';
import { api, SystemConfigFFmpegDto, SystemConfigFFmpegDtoTranscodeEnum } from '@api';
import SettingButtonsRow from '../setting-buttons-row.svelte';
@ -19,7 +19,7 @@
async function getConfigs() {
[savedConfig, defaultConfig] = await Promise.all([
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({
systemConfigDto: {
...configs,
ffmpeg: ffmpegConfig
}
ffmpeg: ffmpegConfig,
},
});
ffmpegConfig = { ...result.data.ffmpeg };
@ -39,13 +39,13 @@
notificationController.show({
message: 'FFmpeg settings saved',
type: NotificationType.Info
type: NotificationType.Info,
});
} catch (e) {
console.error('Error [ffmpeg-settings] [saveSetting]', e);
notificationController.show({
message: 'Unable to save settings',
type: NotificationType.Error
type: NotificationType.Error,
});
}
}
@ -58,7 +58,7 @@
notificationController.show({
message: 'Reset FFmpeg settings to the recent saved settings',
type: NotificationType.Info
type: NotificationType.Info,
});
}
@ -70,7 +70,7 @@
notificationController.show({
message: 'Reset FFmpeg settings to default',
type: NotificationType.Info
type: NotificationType.Info,
});
}
</script>
@ -103,7 +103,7 @@
{ value: 'medium', text: 'medium' },
{ value: 'slow', text: 'slow' },
{ value: 'slower', text: 'slower' },
{ value: 'veryslow', text: 'veryslow' }
{ value: 'veryslow', text: 'veryslow' },
]}
isEdited={!(ffmpegConfig.preset == savedConfig.preset)}
/>
@ -115,7 +115,7 @@
options={[
{ value: 'aac', text: 'aac' },
{ value: 'mp3', text: 'mp3' },
{ value: 'opus', text: 'opus' }
{ value: 'opus', text: 'opus' },
]}
name="acodec"
isEdited={!(ffmpegConfig.targetAudioCodec == savedConfig.targetAudioCodec)}
@ -128,7 +128,7 @@
options={[
{ value: 'h264', text: 'h264' },
{ value: 'hevc', text: 'hevc' },
{ value: 'vp9', text: 'vp9' }
{ value: 'vp9', text: 'vp9' },
]}
name="vcodec"
isEdited={!(ffmpegConfig.targetVideoCodec == savedConfig.targetVideoCodec)}
@ -144,7 +144,7 @@
{ value: '1080', text: '1080p' },
{ value: '720', text: '720p' },
{ value: '480', text: '480p' },
{ value: 'original', text: 'original' }
{ value: 'original', text: 'original' },
]}
name="resolution"
isEdited={!(ffmpegConfig.targetResolution == savedConfig.targetResolution)}
@ -175,16 +175,16 @@
{ value: SystemConfigFFmpegDtoTranscodeEnum.All, text: 'All videos' },
{
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,
text: 'Only videos not in the desired format'
text: 'Only videos not in the desired format',
},
{
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)}
/>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,5 @@
<script lang="ts">
import {
assetInteractionStore,
assetsInAlbumStoreState,
selectedAssets
} from '$lib/stores/asset-interaction.store';
import { assetInteractionStore, assetsInAlbumStoreState, selectedAssets } from '$lib/stores/asset-interaction.store';
import { locale } from '$lib/stores/preferences.store';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
import type { AssetResponseDto } from '@api';
@ -25,7 +21,7 @@
const addSelectedAssets = async () => {
dispatch('create-album', {
assets: Array.from($selectedAssets)
assets: Array.from($selectedAssets),
});
assetInteractionStore.clearMultiselect();
@ -64,14 +60,7 @@
>
Select from computer
</button>
<Button
size="sm"
rounded="lg"
disabled={$selectedAssets.size === 0}
on:click={addSelectedAssets}
>
Done
</Button>
<Button size="sm" rounded="lg" disabled={$selectedAssets.size === 0} on:click={addSelectedAssets}>Done</Button>
</svelte:fragment>
</ControlAppBar>
<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 ContextMenu from '../shared-components/context-menu/context-menu.svelte';
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
import {
notificationController,
NotificationType
} from '../shared-components/notification/notification';
import { notificationController, NotificationType } from '../shared-components/notification/notification';
import { handleError } from '../../utils/handle-error';
import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
@ -40,7 +37,7 @@
if (iconButton) {
position = {
x: iconButton.getBoundingClientRect().left,
y: iconButton.getBoundingClientRect().bottom
y: iconButton.getBoundingClientRect().bottom,
};
}
@ -63,8 +60,7 @@
try {
await api.albumApi.removeUserFromAlbum({ id: album.id, userId });
dispatch('user-deleted', { userId });
const message =
userId === 'me' ? `Left ${album.albumName}` : `Removed ${selectedRemoveUser.firstName}`;
const message = userId === 'me' ? `Left ${album.albumName}` : `Removed ${selectedRemoveUser.firstName}`;
notificationController.show({ type: NotificationType.Info, message });
} catch (e) {
handleError(e, 'Unable to remove user');

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -68,8 +68,8 @@
await api.assetApi.updateAsset({
id: asset.id,
updateAssetDto: {
description: description
}
description: description,
},
});
} catch (error) {
console.error(error);
@ -95,9 +95,7 @@
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"
placeholder={$page?.data?.user?.id !== asset.ownerId ? '' : 'Add a description'}
style:display={$page?.data?.user?.id !== asset.ownerId && textarea?.value == ''
? 'none'
: 'block'}
style:display={$page?.data?.user?.id !== asset.ownerId && textarea?.value == '' ? 'none' : 'block'}
on:focusin={handleFocusIn}
on:focusout={handleFocusOut}
on:input={autoGrowHeight}
@ -138,7 +136,7 @@
{#if 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>
@ -151,9 +149,9 @@
{
month: 'short',
day: 'numeric',
year: 'numeric'
year: 'numeric',
},
{ locale: $locale }
{ locale: $locale },
)}
</p>
<div class="flex gap-2 text-sm">
@ -163,9 +161,9 @@
weekday: 'short',
hour: 'numeric',
minute: '2-digit',
timeZoneName: 'longOffset'
timeZoneName: 'longOffset',
},
{ locale: $locale }
{ locale: $locale },
)}
</p>
</div>
@ -248,8 +246,7 @@
<TileLayer
urlTemplate={'https://tile.openstreetmap.org/{z}/{x}/{y}.png'}
options={{
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}}
/>
<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
</p>
<div class="w-full bg-gray-200 rounded-full h-[7px] dark:bg-gray-700">
<div
class="bg-immich-primary h-[7px] rounded-full"
style={`width: ${$downloadAssets[fileName]}%`}
/>
<div class="bg-immich-primary h-[7px] rounded-full" style={`width: ${$downloadAssets[fileName]}%`} />
</div>
</div>
</div>

View file

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

View file

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

View file

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

View file

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

View file

@ -8,10 +8,8 @@
export let rounded: Rounded = true;
const colorClasses: Record<Color, string> = {
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'
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',
};
</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-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',
'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> = {
@ -50,7 +50,7 @@
link: 'p-2 font-medium',
sm: 'px-4 py-2 text-sm font-medium',
base: 'px-6 py-3 font-medium',
lg: 'px-6 py-4 font-semibold'
lg: 'px-6 py-4 font-semibold',
};
</script>

View file

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

View file

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

View file

@ -31,13 +31,7 @@
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="email">Name</label>
<input
class="immich-form-input"
id="name"
name="name"
type="text"
bind:value={apiKey.name}
/>
<input class="immich-form-input" id="name" name="name" type="text" bind:value={apiKey.name} />
</div>
<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 { handleError } from '../../utils/handle-error';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import {
notificationController,
NotificationType
} from '../shared-components/notification/notification';
import { notificationController, NotificationType } from '../shared-components/notification/notification';
import Button from '../elements/buttons/button.svelte';
export let secret = '';
@ -24,7 +21,7 @@
await navigator.clipboard.writeText(secret);
notificationController.show({
message: 'Copied to clipboard!',
type: NotificationType.Info
type: NotificationType.Info,
});
} catch (error) {
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"
>
<KeyVariant size="4em" />
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">
API Key
</h1>
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">API Key</h1>
<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.
@ -51,13 +46,7 @@
<div class="m-4 flex flex-col gap-2">
<!-- <label class="immich-form-label" for="email">API Key</label> -->
<textarea
class="immich-form-input"
id="secret"
name="secret"
readonly={true}
value={secret}
/>
<textarea class="immich-form-input" id="secret" name="secret" readonly={true} value={secret} />
</div>
<div class="flex w-full px-4 gap-4 mt-8">

View file

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

View file

@ -2,10 +2,7 @@
import { api } from '@api';
import { createEventDispatcher } from 'svelte';
import ImmichLogo from '../shared-components/immich-logo.svelte';
import {
notificationController,
NotificationType
} from '../shared-components/notification/notification';
import { notificationController, NotificationType } from '../shared-components/notification/notification';
import Button from '../elements/buttons/button.svelte';
let error: string;
@ -50,8 +47,8 @@
email: String(email),
password: String(password),
firstName: String(firstName),
lastName: String(lastName)
}
lastName: String(lastName),
},
});
if (status === 201) {
@ -73,7 +70,7 @@
notificationController.show({
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">
<ImmichLogo class="text-center" height="100" width="100" />
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">
Create new user
</h1>
<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.
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">Create new user</h1>
<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>
</div>
@ -104,14 +96,7 @@
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="password">Password</label>
<input
class="immich-form-input"
id="password"
name="password"
type="password"
required
bind:value={password}
/>
<input class="immich-form-input" id="password" name="password" type="password" required bind:value={password} />
</div>
<div class="m-4 flex flex-col gap-2">

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -27,11 +27,7 @@
setContext({ getAssets: () => assets, clearSelect });
</script>
<ControlAppBar
on:close-button-click={clearSelect}
backIcon={Close}
tailwindClasses="bg-white shadow-md"
>
<ControlAppBar 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">
Selected {assets.size.toLocaleString($locale)}
</p>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -15,7 +15,7 @@
onMount(() => {
const ControlClass = Control.extend({
position,
onAdd: () => target
onAdd: () => target,
});
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 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 TileLayer } from './tile-layer.svelte';

View file

@ -4,9 +4,8 @@
@apply border;
@apply border-immich-primary;
@apply transition-all;
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 8px 16px,
rgba(0, 0, 0, 0.07) 0px 16px 32px, rgba(0, 0, 0, 0.07) 0px 32px 64px;
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 8px 16px, rgba(0, 0, 0, 0.07) 0px 16px 32px, rgba(0, 0, 0, 0.07) 0px 32px 64px;
}
.marker-cluster-icon {

View file

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

View file

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

View file

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

View file

@ -37,15 +37,9 @@
<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"
>
<a
data-sveltekit-preload-data="hover"
class="flex gap-2 md:mx-6 mx-4 place-items-center"
href={AppRoute.PHOTOS}
>
<a 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" />
<h1
class="font-immich-title text-2xl text-immich-primary dark:text-immich-dark-primary md:block hidden"
>
<h1 class="font-immich-title text-2xl text-immich-primary dark:text-immich-dark-primary md:block hidden">
IMMICH
</h1>
</a>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -51,9 +51,7 @@
<div class="dark:text-immich-dark-fg">
<div class="storage-status grid grid-cols-[64px_auto]">
<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"
>
<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">
<Cloud size={'24'} />
</div>
<div class="hidden md:block group-hover:sm:block">
@ -81,9 +79,7 @@
<hr class="ml-5 my-4 dark:border-immich-dark-gray" />
</div>
<div class="server-status grid grid-cols-[64px_auto]">
<div
class="pl-5 pr-6 text-immich-primary dark:text-immich-dark-primary pb-11 md:pb-0 group-hover:sm:pb-0"
>
<div 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'} />
</div>
<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="bg-immich-primary h-[15px] rounded-md transition-all"
style={`width: ${uploadAsset.progress}%`}
/>
<div 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]">
{uploadAsset.progress}/100
</p>

View file

@ -30,7 +30,7 @@
on:outroend={() => {
notificationController.show({
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]"

View file

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

View file

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

View file

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

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