parent
7c2f7d6c51
commit
f55b3add80
242 changed files with 12794 additions and 13426 deletions
|
@ -1,34 +1,30 @@
|
||||||
/** @type {import('eslint').Linter.Config} */
|
/** @type {import('eslint').Linter.Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
extends: [
|
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:svelte/recommended'],
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:svelte/recommended'
|
|
||||||
],
|
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
plugins: ['@typescript-eslint'],
|
plugins: ['@typescript-eslint'],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
extraFileExtensions: ['.svelte']
|
extraFileExtensions: ['.svelte'],
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
es2017: true,
|
es2017: true,
|
||||||
node: true
|
node: true,
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ['*.svelte'],
|
files: ['*.svelte'],
|
||||||
parser: 'svelte-eslint-parser',
|
parser: 'svelte-eslint-parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
parser: '@typescript-eslint/parser'
|
parser: '@typescript-eslint/parser',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
globals: {
|
globals: {
|
||||||
NodeJS: true
|
NodeJS: true,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/no-unused-vars': [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
@ -36,8 +32,8 @@ module.exports = {
|
||||||
{
|
{
|
||||||
// Allow underscore (_) variables
|
// Allow underscore (_) variables
|
||||||
argsIgnorePattern: '^_$',
|
argsIgnorePattern: '^_$',
|
||||||
varsIgnorePattern: '^_$'
|
varsIgnorePattern: '^_$',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"useTabs": true,
|
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "none",
|
"trailingComma": "all",
|
||||||
"printWidth": 100
|
"printWidth": 120,
|
||||||
|
"semi": true,
|
||||||
|
"organizeImportsSkipDestructiveCodeActions": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
browser: false
|
browser: false,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
env: {}
|
env: {},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript']
|
presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,8 +27,8 @@ export default {
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
lines: 4,
|
lines: 4,
|
||||||
statements: 4
|
statements: 4,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// An array of regexp pattern strings used to skip coverage collection
|
// An array of regexp pattern strings used to skip coverage collection
|
||||||
|
@ -86,11 +86,10 @@ export default {
|
||||||
|
|
||||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
|
'\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 'identity-obj-proxy',
|
||||||
'identity-obj-proxy',
|
|
||||||
'^\\$lib(.*)$': '<rootDir>/src/lib$1',
|
'^\\$lib(.*)$': '<rootDir>/src/lib$1',
|
||||||
'^\\@api(.*)$': '<rootDir>/src/api$1',
|
'^\\@api(.*)$': '<rootDir>/src/api$1',
|
||||||
'^\\@test-data(.*)$': '<rootDir>/src/test-data$1'
|
'^\\@test-data(.*)$': '<rootDir>/src/test-data$1',
|
||||||
},
|
},
|
||||||
|
|
||||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||||
|
@ -181,13 +180,13 @@ export default {
|
||||||
'^.+\\.svelte$': [
|
'^.+\\.svelte$': [
|
||||||
'svelte-jester',
|
'svelte-jester',
|
||||||
{
|
{
|
||||||
preprocess: true
|
preprocess: true,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||||
transformIgnorePatterns: ['/node_modules/(?!svelte-material-icons).*/', '\\.pnp\\.[^\\/]+$']
|
transformIgnorePatterns: ['/node_modules/(?!svelte-material-icons).*/', '\\.pnp\\.[^\\/]+$'],
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||||
// unmockedModulePathPatterns: undefined,
|
// unmockedModulePathPatterns: undefined,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {}
|
autoprefixer: {},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
SharedLinkApi,
|
SharedLinkApi,
|
||||||
SystemConfigApi,
|
SystemConfigApi,
|
||||||
UserApi,
|
UserApi,
|
||||||
UserApiFp
|
UserApiFp,
|
||||||
} from './open-api';
|
} from './open-api';
|
||||||
import { BASE_PATH } from './open-api/base';
|
import { BASE_PATH } from './open-api/base';
|
||||||
import { DUMMY_BASE_URL, toPathString } from './open-api/common';
|
import { DUMMY_BASE_URL, toPathString } from './open-api/common';
|
||||||
|
@ -84,16 +84,12 @@ export class ImmichApi {
|
||||||
this.config.basePath = baseUrl;
|
this.config.basePath = baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAssetFileUrl(
|
public getAssetFileUrl(...[assetId, isThumb, isWeb, key]: ApiParams<typeof AssetApiFp, 'serveFile'>) {
|
||||||
...[assetId, isThumb, isWeb, key]: ApiParams<typeof AssetApiFp, 'serveFile'>
|
|
||||||
) {
|
|
||||||
const path = `/asset/file/${assetId}`;
|
const path = `/asset/file/${assetId}`;
|
||||||
return this.createUrl(path, { isThumb, isWeb, key });
|
return this.createUrl(path, { isThumb, isWeb, key });
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAssetThumbnailUrl(
|
public getAssetThumbnailUrl(...[assetId, format, key]: ApiParams<typeof AssetApiFp, 'getAssetThumbnail'>) {
|
||||||
...[assetId, format, key]: ApiParams<typeof AssetApiFp, 'getAssetThumbnail'>
|
|
||||||
) {
|
|
||||||
const path = `/asset/thumbnail/${assetId}`;
|
const path = `/asset/thumbnail/${assetId}`;
|
||||||
return this.createUrl(path, { format, key });
|
return this.createUrl(path, { format, key });
|
||||||
}
|
}
|
||||||
|
@ -119,7 +115,7 @@ export class ImmichApi {
|
||||||
[JobName.VideoConversion]: 'Transcode Videos',
|
[JobName.VideoConversion]: 'Transcode Videos',
|
||||||
[JobName.StorageTemplateMigration]: 'Storage Template Migration',
|
[JobName.StorageTemplateMigration]: 'Storage Template Migration',
|
||||||
[JobName.BackgroundTask]: 'Background Tasks',
|
[JobName.BackgroundTask]: 'Background Tasks',
|
||||||
[JobName.Search]: 'Search'
|
[JobName.Search]: 'Search',
|
||||||
};
|
};
|
||||||
|
|
||||||
return names[jobName];
|
return names[jobName];
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
export * from './open-api';
|
|
||||||
export * from './api';
|
export * from './api';
|
||||||
|
export * from './open-api';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
|
|
@ -3,10 +3,6 @@ import type { Configuration } from './open-api';
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
export type ApiFp = (configuration: Configuration) => Record<any, (...args: any) => any>;
|
export type ApiFp = (configuration: Configuration) => Record<any, (...args: any) => any>;
|
||||||
|
|
||||||
export type OmitLast<T extends readonly unknown[]> = T extends readonly [...infer U, any?]
|
export type OmitLast<T extends readonly unknown[]> = T extends readonly [...infer U, any?] ? U : [...T];
|
||||||
? U
|
|
||||||
: [...T];
|
|
||||||
|
|
||||||
export type ApiParams<F extends ApiFp, K extends keyof ReturnType<F>> = OmitLast<
|
export type ApiParams<F extends ApiFp, K extends keyof ReturnType<F>> = OmitLast<Parameters<ReturnType<F>[K]>>;
|
||||||
Parameters<ReturnType<F>[K]>
|
|
||||||
>;
|
|
||||||
|
|
|
@ -31,5 +31,5 @@ export const oauth = {
|
||||||
},
|
},
|
||||||
unlink: () => {
|
unlink: () => {
|
||||||
return api.oauthApi.unlink();
|
return api.oauthApi.unlink();
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -52,6 +52,6 @@ export const handleError: HandleServerError = async ({ error }) => {
|
||||||
return {
|
return {
|
||||||
message: response?.data?.message || httpError?.message || DEFAULT_MESSAGE,
|
message: response?.data?.message || httpError?.message || DEFAULT_MESSAGE,
|
||||||
code,
|
code,
|
||||||
stack: httpError?.stack
|
stack: httpError?.stack,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,7 +2,7 @@ const createObjectURLMock = jest.fn();
|
||||||
|
|
||||||
Object.defineProperty(URL, 'createObjectURL', {
|
Object.defineProperty(URL, 'createObjectURL', {
|
||||||
writable: true,
|
writable: true,
|
||||||
value: createObjectURLMock
|
value: createObjectURLMock,
|
||||||
});
|
});
|
||||||
|
|
||||||
export { createObjectURLMock };
|
export { createObjectURLMock };
|
||||||
|
|
|
@ -27,8 +27,7 @@
|
||||||
<svelte:fragment slot="prompt">
|
<svelte:fragment slot="prompt">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<p>
|
<p>
|
||||||
<b>{user.firstName} {user.lastName}</b>'s account and assets will be permanently deleted
|
<b>{user.firstName} {user.lastName}</b>'s account and assets will be permanently deleted after 7 days.
|
||||||
after 7 days.
|
|
||||||
</p>
|
</p>
|
||||||
<p>Are you sure you want to continue?</p>
|
<p>Are you sure you want to continue?</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
const colorClasses: Record<Colors, string> = {
|
const colorClasses: Record<Colors, string> = {
|
||||||
'light-gray': 'bg-gray-300/90 dark:bg-gray-600/90',
|
'light-gray': 'bg-gray-300/90 dark:bg-gray-600/90',
|
||||||
gray: 'bg-gray-300 dark:bg-gray-600'
|
gray: 'bg-gray-300 dark:bg-gray-600',
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
const colorClasses: Record<Color, string> = {
|
const colorClasses: Record<Color, string> = {
|
||||||
success: 'bg-green-500/70 text-gray-900 dark:bg-green-700/90 dark:text-gray-100',
|
success: 'bg-green-500/70 text-gray-900 dark:bg-green-700/90 dark:text-gray-100',
|
||||||
warning: 'bg-orange-400/70 text-gray-900 dark:bg-orange-900 dark:text-gray-100'
|
warning: 'bg-orange-400/70 text-gray-900 dark:bg-orange-900 dark:text-gray-100',
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -43,9 +43,7 @@
|
||||||
<JobTileStatus color="success">Active</JobTileStatus>
|
<JobTileStatus color="success">Active</JobTileStatus>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex flex-col gap-2 p-5 sm:p-7 md:p-9">
|
<div class="flex flex-col gap-2 p-5 sm:p-7 md:p-9">
|
||||||
<div
|
<div class="flex items-center gap-4 text-xl font-semibold text-immich-primary dark:text-immich-dark-primary">
|
||||||
class="flex items-center gap-4 text-xl font-semibold text-immich-primary dark:text-immich-dark-primary"
|
|
||||||
>
|
|
||||||
<span class="flex gap-2 items-center">
|
<span class="flex gap-2 items-center">
|
||||||
<svelte:component this={icon} size="1.25em" class="shrink-0 hidden sm:block" />
|
<svelte:component this={icon} size="1.25em" class="shrink-0 hidden sm:block" />
|
||||||
{title.toUpperCase()}
|
{title.toUpperCase()}
|
||||||
|
@ -98,10 +96,7 @@
|
||||||
<div class="flex sm:flex-col flex-row sm:w-32 w-full overflow-hidden">
|
<div class="flex sm:flex-col flex-row sm:w-32 w-full overflow-hidden">
|
||||||
{#if !isIdle}
|
{#if !isIdle}
|
||||||
{#if waitingCount > 0}
|
{#if waitingCount > 0}
|
||||||
<JobTileButton
|
<JobTileButton color="gray" on:click={() => dispatch('command', { command: JobCommand.Empty, force: false })}>
|
||||||
color="gray"
|
|
||||||
on:click={() => dispatch('command', { command: JobCommand.Empty, force: false })}
|
|
||||||
>
|
|
||||||
<Close size="24" /> CLEAR
|
<Close size="24" /> CLEAR
|
||||||
</JobTileButton>
|
</JobTileButton>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -123,10 +118,7 @@
|
||||||
</JobTileButton>
|
</JobTileButton>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if allowForceCommand}
|
{:else if allowForceCommand}
|
||||||
<JobTileButton
|
<JobTileButton color="gray" on:click={() => dispatch('command', { command: JobCommand.Start, force: true })}>
|
||||||
color="gray"
|
|
||||||
on:click={() => dispatch('command', { command: JobCommand.Start, force: true })}
|
|
||||||
>
|
|
||||||
<AllInclusive size="24" />
|
<AllInclusive size="24" />
|
||||||
{allText}
|
{allText}
|
||||||
</JobTileButton>
|
</JobTileButton>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
notificationController,
|
notificationController,
|
||||||
NotificationType
|
NotificationType,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
@ -55,48 +55,47 @@
|
||||||
[JobName.ThumbnailGeneration]: {
|
[JobName.ThumbnailGeneration]: {
|
||||||
icon: FileJpgBox,
|
icon: FileJpgBox,
|
||||||
title: api.getJobName(JobName.ThumbnailGeneration),
|
title: api.getJobName(JobName.ThumbnailGeneration),
|
||||||
subtitle: 'Regenerate JPEG and WebP thumbnails'
|
subtitle: 'Regenerate JPEG and WebP thumbnails',
|
||||||
},
|
},
|
||||||
[JobName.MetadataExtraction]: {
|
[JobName.MetadataExtraction]: {
|
||||||
icon: Table,
|
icon: Table,
|
||||||
title: api.getJobName(JobName.MetadataExtraction),
|
title: api.getJobName(JobName.MetadataExtraction),
|
||||||
subtitle: 'Extract metadata information i.e. GPS, resolution...etc'
|
subtitle: 'Extract metadata information i.e. GPS, resolution...etc',
|
||||||
},
|
},
|
||||||
[JobName.Sidecar]: {
|
[JobName.Sidecar]: {
|
||||||
title: api.getJobName(JobName.Sidecar),
|
title: api.getJobName(JobName.Sidecar),
|
||||||
icon: FileXmlBox,
|
icon: FileXmlBox,
|
||||||
subtitle: 'Discover or synchronize sidecar metadata from the filesystem',
|
subtitle: 'Discover or synchronize sidecar metadata from the filesystem',
|
||||||
allText: 'SYNC',
|
allText: 'SYNC',
|
||||||
missingText: 'DISCOVER'
|
missingText: 'DISCOVER',
|
||||||
},
|
},
|
||||||
[JobName.ObjectTagging]: {
|
[JobName.ObjectTagging]: {
|
||||||
icon: TagMultiple,
|
icon: TagMultiple,
|
||||||
title: api.getJobName(JobName.ObjectTagging),
|
title: api.getJobName(JobName.ObjectTagging),
|
||||||
subtitle:
|
subtitle: 'Run machine learning to tag objects\nNote that some assets may not have any objects detected',
|
||||||
'Run machine learning to tag objects\nNote that some assets may not have any objects detected'
|
|
||||||
},
|
},
|
||||||
[JobName.ClipEncoding]: {
|
[JobName.ClipEncoding]: {
|
||||||
icon: VectorCircle,
|
icon: VectorCircle,
|
||||||
title: api.getJobName(JobName.ClipEncoding),
|
title: api.getJobName(JobName.ClipEncoding),
|
||||||
subtitle: 'Run machine learning to generate clip embeddings'
|
subtitle: 'Run machine learning to generate clip embeddings',
|
||||||
},
|
},
|
||||||
[JobName.RecognizeFaces]: {
|
[JobName.RecognizeFaces]: {
|
||||||
icon: FaceRecognition,
|
icon: FaceRecognition,
|
||||||
title: api.getJobName(JobName.RecognizeFaces),
|
title: api.getJobName(JobName.RecognizeFaces),
|
||||||
subtitle: 'Run machine learning to recognize faces',
|
subtitle: 'Run machine learning to recognize faces',
|
||||||
handleCommand: handleFaceCommand
|
handleCommand: handleFaceCommand,
|
||||||
},
|
},
|
||||||
[JobName.VideoConversion]: {
|
[JobName.VideoConversion]: {
|
||||||
icon: Video,
|
icon: Video,
|
||||||
title: api.getJobName(JobName.VideoConversion),
|
title: api.getJobName(JobName.VideoConversion),
|
||||||
subtitle: 'Transcode videos not in the desired format'
|
subtitle: 'Transcode videos not in the desired format',
|
||||||
},
|
},
|
||||||
[JobName.StorageTemplateMigration]: {
|
[JobName.StorageTemplateMigration]: {
|
||||||
icon: FolderMove,
|
icon: FolderMove,
|
||||||
title: api.getJobName(JobName.StorageTemplateMigration),
|
title: api.getJobName(JobName.StorageTemplateMigration),
|
||||||
allowForceCommand: false,
|
allowForceCommand: false,
|
||||||
component: StorageMigrationDescription
|
component: StorageMigrationDescription,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const jobDetailsArray = Object.entries(jobDetails) as [JobName, JobDetails][];
|
const jobDetailsArray = Object.entries(jobDetails) as [JobName, JobDetails][];
|
||||||
|
@ -112,7 +111,7 @@
|
||||||
case JobCommand.Empty:
|
case JobCommand.Empty:
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: `Cleared jobs for: ${title}`,
|
message: `Cleared jobs for: ${title}`,
|
||||||
type: NotificationType.Info
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
Apply the current
|
Apply the current
|
||||||
<a
|
<a href={`${AppRoute.ADMIN_SETTINGS}?open=storage-template`} class="text-immich-primary dark:text-immich-dark-primary"
|
||||||
href={`${AppRoute.ADMIN_SETTINGS}?open=storage-template`}
|
>Storage template</a
|
||||||
class="text-immich-primary dark:text-immich-dark-primary">Storage template</a
|
|
||||||
>
|
>
|
||||||
to previously uploaded assets
|
to previously uploaded assets
|
||||||
|
|
|
@ -14,13 +14,7 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ConfirmDialogue
|
<ConfirmDialogue title="Restore User" confirmText="Continue" confirmColor="green" on:confirm={restoreUser} on:cancel>
|
||||||
title="Restore User"
|
|
||||||
confirmText="Continue"
|
|
||||||
confirmColor="green"
|
|
||||||
on:confirm={restoreUser}
|
|
||||||
on:cancel
|
|
||||||
>
|
|
||||||
<svelte:fragment slot="prompt">
|
<svelte:fragment slot="prompt">
|
||||||
<p><b>{user.firstName} {user.lastName}</b>'s account will be restored.</p>
|
<p><b>{user.firstName} {user.lastName}</b>'s account will be restored.</p>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
photos: 0,
|
photos: 0,
|
||||||
videos: 0,
|
videos: 0,
|
||||||
usage: 0,
|
usage: 0,
|
||||||
usageByUser: []
|
usageByUser: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
$: zeros = (value: number) => {
|
$: zeros = (value: number) => {
|
||||||
|
@ -35,13 +35,9 @@
|
||||||
<StatsCard logo={Memory} title="STORAGE" value={statsUsage} unit={statsUsageUnit} />
|
<StatsCard logo={Memory} title="STORAGE" value={statsUsage} unit={statsUsageUnit} />
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5 lg:hidden flex">
|
<div class="mt-5 lg:hidden flex">
|
||||||
<div
|
<div class="bg-immich-gray dark:bg-immich-dark-gray rounded-3xl p-5 flex flex-col justify-between">
|
||||||
class="bg-immich-gray dark:bg-immich-dark-gray rounded-3xl p-5 flex flex-col justify-between"
|
|
||||||
>
|
|
||||||
<div class="flex flex-wrap gap-x-12">
|
<div class="flex flex-wrap gap-x-12">
|
||||||
<div
|
<div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
|
||||||
class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary"
|
|
||||||
>
|
|
||||||
<CameraIris size="25" />
|
<CameraIris size="25" />
|
||||||
<p>PHOTOS</p>
|
<p>PHOTOS</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,9 +49,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-x-12">
|
<div class="flex flex-wrap gap-x-12">
|
||||||
<div
|
<div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
|
||||||
class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary"
|
|
||||||
>
|
|
||||||
<PlayCircle size="25" />
|
<PlayCircle size="25" />
|
||||||
<p>VIDEOS</p>
|
<p>VIDEOS</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -67,9 +61,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-x-7">
|
<div class="flex flex-wrap gap-x-7">
|
||||||
<div
|
<div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
|
||||||
class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary"
|
|
||||||
>
|
|
||||||
<Memory size="25" />
|
<Memory size="25" />
|
||||||
<p>STORAGE</p>
|
<p>STORAGE</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -78,9 +70,7 @@
|
||||||
<span class="text-[#DCDADA] dark:text-[#525252]">{zeros(statsUsage)}</span><span
|
<span class="text-[#DCDADA] dark:text-[#525252]">{zeros(statsUsage)}</span><span
|
||||||
class="text-immich-primary dark:text-immich-dark-primary">{statsUsage}</span
|
class="text-immich-primary dark:text-immich-dark-primary">{statsUsage}</span
|
||||||
>
|
>
|
||||||
<span class="text-center my-auto ml-2 text-base font-light text-gray-400"
|
<span class="text-center my-auto ml-2 text-base font-light text-gray-400">{statsUsageUnit}</span>
|
||||||
>{statsUsageUnit}</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -107,13 +97,10 @@
|
||||||
<tr
|
<tr
|
||||||
class="text-center flex place-items-center w-full h-[50px] even:bg-immich-bg even:dark:bg-immich-dark-gray/50 odd:bg-immich-gray odd:dark:bg-immich-dark-gray/75"
|
class="text-center flex place-items-center w-full h-[50px] even:bg-immich-bg even:dark:bg-immich-dark-gray/50 odd:bg-immich-gray odd:dark:bg-immich-dark-gray/75"
|
||||||
>
|
>
|
||||||
<td class="text-sm px-2 w-1/4 text-ellipsis"
|
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.userFirstName} {user.userLastName}</td>
|
||||||
>{user.userFirstName} {user.userLastName}</td
|
|
||||||
>
|
|
||||||
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.photos.toLocaleString($locale)}</td>
|
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.photos.toLocaleString($locale)}</td>
|
||||||
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.videos.toLocaleString($locale)}</td>
|
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.videos.toLocaleString($locale)}</td>
|
||||||
<td class="text-sm px-2 w-1/4 text-ellipsis">{asByteUnitString(user.usage, $locale)}</td
|
<td class="text-sm px-2 w-1/4 text-ellipsis">{asByteUnitString(user.usage, $locale)}</td>
|
||||||
>
|
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -15,9 +15,7 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div class="w-[250px] h-[140px] bg-immich-gray dark:bg-immich-dark-gray rounded-3xl p-5 flex flex-col justify-between">
|
||||||
class="w-[250px] h-[140px] bg-immich-gray dark:bg-immich-dark-gray rounded-3xl p-5 flex flex-col justify-between"
|
|
||||||
>
|
|
||||||
<div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
|
<div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
|
||||||
<svelte:component this={logo} size="40" />
|
<svelte:component this={logo} size="40" />
|
||||||
<p>{title}</p>
|
<p>{title}</p>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
notificationController,
|
notificationController,
|
||||||
NotificationType
|
NotificationType,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { api, SystemConfigFFmpegDto, SystemConfigFFmpegDtoTranscodeEnum } from '@api';
|
import { api, SystemConfigFFmpegDto, SystemConfigFFmpegDtoTranscodeEnum } from '@api';
|
||||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
async function getConfigs() {
|
async function getConfigs() {
|
||||||
[savedConfig, defaultConfig] = await Promise.all([
|
[savedConfig, defaultConfig] = await Promise.all([
|
||||||
api.systemConfigApi.getConfig().then((res) => res.data.ffmpeg),
|
api.systemConfigApi.getConfig().then((res) => res.data.ffmpeg),
|
||||||
api.systemConfigApi.getDefaults().then((res) => res.data.ffmpeg)
|
api.systemConfigApi.getDefaults().then((res) => res.data.ffmpeg),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,8 +30,8 @@
|
||||||
const result = await api.systemConfigApi.updateConfig({
|
const result = await api.systemConfigApi.updateConfig({
|
||||||
systemConfigDto: {
|
systemConfigDto: {
|
||||||
...configs,
|
...configs,
|
||||||
ffmpeg: ffmpegConfig
|
ffmpeg: ffmpegConfig,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
ffmpegConfig = { ...result.data.ffmpeg };
|
ffmpegConfig = { ...result.data.ffmpeg };
|
||||||
|
@ -39,13 +39,13 @@
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'FFmpeg settings saved',
|
message: 'FFmpeg settings saved',
|
||||||
type: NotificationType.Info
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error [ffmpeg-settings] [saveSetting]', e);
|
console.error('Error [ffmpeg-settings] [saveSetting]', e);
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Unable to save settings',
|
message: 'Unable to save settings',
|
||||||
type: NotificationType.Error
|
type: NotificationType.Error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset FFmpeg settings to the recent saved settings',
|
message: 'Reset FFmpeg settings to the recent saved settings',
|
||||||
type: NotificationType.Info
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset FFmpeg settings to default',
|
message: 'Reset FFmpeg settings to default',
|
||||||
type: NotificationType.Info
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -103,7 +103,7 @@
|
||||||
{ value: 'medium', text: 'medium' },
|
{ value: 'medium', text: 'medium' },
|
||||||
{ value: 'slow', text: 'slow' },
|
{ value: 'slow', text: 'slow' },
|
||||||
{ value: 'slower', text: 'slower' },
|
{ value: 'slower', text: 'slower' },
|
||||||
{ value: 'veryslow', text: 'veryslow' }
|
{ value: 'veryslow', text: 'veryslow' },
|
||||||
]}
|
]}
|
||||||
isEdited={!(ffmpegConfig.preset == savedConfig.preset)}
|
isEdited={!(ffmpegConfig.preset == savedConfig.preset)}
|
||||||
/>
|
/>
|
||||||
|
@ -115,7 +115,7 @@
|
||||||
options={[
|
options={[
|
||||||
{ value: 'aac', text: 'aac' },
|
{ value: 'aac', text: 'aac' },
|
||||||
{ value: 'mp3', text: 'mp3' },
|
{ value: 'mp3', text: 'mp3' },
|
||||||
{ value: 'opus', text: 'opus' }
|
{ value: 'opus', text: 'opus' },
|
||||||
]}
|
]}
|
||||||
name="acodec"
|
name="acodec"
|
||||||
isEdited={!(ffmpegConfig.targetAudioCodec == savedConfig.targetAudioCodec)}
|
isEdited={!(ffmpegConfig.targetAudioCodec == savedConfig.targetAudioCodec)}
|
||||||
|
@ -128,7 +128,7 @@
|
||||||
options={[
|
options={[
|
||||||
{ value: 'h264', text: 'h264' },
|
{ value: 'h264', text: 'h264' },
|
||||||
{ value: 'hevc', text: 'hevc' },
|
{ value: 'hevc', text: 'hevc' },
|
||||||
{ value: 'vp9', text: 'vp9' }
|
{ value: 'vp9', text: 'vp9' },
|
||||||
]}
|
]}
|
||||||
name="vcodec"
|
name="vcodec"
|
||||||
isEdited={!(ffmpegConfig.targetVideoCodec == savedConfig.targetVideoCodec)}
|
isEdited={!(ffmpegConfig.targetVideoCodec == savedConfig.targetVideoCodec)}
|
||||||
|
@ -144,7 +144,7 @@
|
||||||
{ value: '1080', text: '1080p' },
|
{ value: '1080', text: '1080p' },
|
||||||
{ value: '720', text: '720p' },
|
{ value: '720', text: '720p' },
|
||||||
{ value: '480', text: '480p' },
|
{ value: '480', text: '480p' },
|
||||||
{ value: 'original', text: 'original' }
|
{ value: 'original', text: 'original' },
|
||||||
]}
|
]}
|
||||||
name="resolution"
|
name="resolution"
|
||||||
isEdited={!(ffmpegConfig.targetResolution == savedConfig.targetResolution)}
|
isEdited={!(ffmpegConfig.targetResolution == savedConfig.targetResolution)}
|
||||||
|
@ -175,16 +175,16 @@
|
||||||
{ value: SystemConfigFFmpegDtoTranscodeEnum.All, text: 'All videos' },
|
{ value: SystemConfigFFmpegDtoTranscodeEnum.All, text: 'All videos' },
|
||||||
{
|
{
|
||||||
value: SystemConfigFFmpegDtoTranscodeEnum.Optimal,
|
value: SystemConfigFFmpegDtoTranscodeEnum.Optimal,
|
||||||
text: 'Videos higher than target resolution or not in the desired format'
|
text: 'Videos higher than target resolution or not in the desired format',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: SystemConfigFFmpegDtoTranscodeEnum.Required,
|
value: SystemConfigFFmpegDtoTranscodeEnum.Required,
|
||||||
text: 'Only videos not in the desired format'
|
text: 'Only videos not in the desired format',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: SystemConfigFFmpegDtoTranscodeEnum.Disabled,
|
value: SystemConfigFFmpegDtoTranscodeEnum.Disabled,
|
||||||
text: "Don't transcode any videos, may break playback on some clients"
|
text: "Don't transcode any videos, may break playback on some clients",
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
isEdited={!(ffmpegConfig.transcode == savedConfig.transcode)}
|
isEdited={!(ffmpegConfig.transcode == savedConfig.transcode)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
notificationController,
|
notificationController,
|
||||||
NotificationType
|
NotificationType,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { api, JobName, SystemConfigJobDto } from '@api';
|
import { api, JobName, SystemConfigJobDto } from '@api';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
|
@ -16,14 +16,12 @@
|
||||||
let defaultConfig: SystemConfigJobDto;
|
let defaultConfig: SystemConfigJobDto;
|
||||||
|
|
||||||
const ignoredJobs = [JobName.BackgroundTask, JobName.Search] as JobName[];
|
const ignoredJobs = [JobName.BackgroundTask, JobName.Search] as JobName[];
|
||||||
const jobNames = Object.values(JobName).filter(
|
const jobNames = Object.values(JobName).filter((jobName) => !ignoredJobs.includes(jobName as JobName));
|
||||||
(jobName) => !ignoredJobs.includes(jobName as JobName)
|
|
||||||
);
|
|
||||||
|
|
||||||
async function getConfigs() {
|
async function getConfigs() {
|
||||||
[savedConfig, defaultConfig] = await Promise.all([
|
[savedConfig, defaultConfig] = await Promise.all([
|
||||||
api.systemConfigApi.getConfig().then((res) => res.data.job),
|
api.systemConfigApi.getConfig().then((res) => res.data.job),
|
||||||
api.systemConfigApi.getDefaults().then((res) => res.data.job)
|
api.systemConfigApi.getDefaults().then((res) => res.data.job),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,8 +32,8 @@
|
||||||
const result = await api.systemConfigApi.updateConfig({
|
const result = await api.systemConfigApi.updateConfig({
|
||||||
systemConfigDto: {
|
systemConfigDto: {
|
||||||
...configs,
|
...configs,
|
||||||
job: jobConfig
|
job: jobConfig,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
jobConfig = { ...result.data.job };
|
jobConfig = { ...result.data.job };
|
||||||
|
@ -55,7 +53,7 @@
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset Job settings to the recent saved settings',
|
message: 'Reset Job settings to the recent saved settings',
|
||||||
type: NotificationType.Info
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +65,7 @@
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset Job settings to default',
|
message: 'Reset Job settings to default',
|
||||||
type: NotificationType.Info
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
notificationController,
|
notificationController,
|
||||||
NotificationType
|
NotificationType,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { api, SystemConfigOAuthDto } from '@api';
|
import { api, SystemConfigOAuthDto } from '@api';
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
async function getConfigs() {
|
async function getConfigs() {
|
||||||
[savedConfig, defaultConfig] = await Promise.all([
|
[savedConfig, defaultConfig] = await Promise.all([
|
||||||
api.systemConfigApi.getConfig().then((res) => res.data.oauth),
|
api.systemConfigApi.getConfig().then((res) => res.data.oauth),
|
||||||
api.systemConfigApi.getDefaults().then((res) => res.data.oauth)
|
api.systemConfigApi.getDefaults().then((res) => res.data.oauth),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset OAuth settings to the last saved settings',
|
message: 'Reset OAuth settings to the last saved settings',
|
||||||
type: NotificationType.Info
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,8 +75,8 @@
|
||||||
const { data: updated } = await api.systemConfigApi.updateConfig({
|
const { data: updated } = await api.systemConfigApi.updateConfig({
|
||||||
systemConfigDto: {
|
systemConfigDto: {
|
||||||
...current,
|
...current,
|
||||||
oauth: oauthConfig
|
oauth: oauthConfig,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
oauthConfig = { ...updated.oauth };
|
oauthConfig = { ...updated.oauth };
|
||||||
|
@ -95,16 +95,13 @@
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset OAuth settings to default',
|
message: 'Reset OAuth settings to default',
|
||||||
type: NotificationType.Info
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isConfirmOpen}
|
{#if isConfirmOpen}
|
||||||
<ConfirmDisableLogin
|
<ConfirmDisableLogin on:cancel={() => handleConfirm(false)} on:confirm={() => handleConfirm(true)} />
|
||||||
on:cancel={() => handleConfirm(false)}
|
|
||||||
on:confirm={() => handleConfirm(true)}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
notificationController,
|
notificationController,
|
||||||
NotificationType
|
NotificationType,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { api, SystemConfigPasswordLoginDto } from '@api';
|
import { api, SystemConfigPasswordLoginDto } from '@api';
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
async function getConfigs() {
|
async function getConfigs() {
|
||||||
[savedConfig, defaultConfig] = await Promise.all([
|
[savedConfig, defaultConfig] = await Promise.all([
|
||||||
api.systemConfigApi.getConfig().then((res) => res.data.passwordLogin),
|
api.systemConfigApi.getConfig().then((res) => res.data.passwordLogin),
|
||||||
api.systemConfigApi.getDefaults().then((res) => res.data.passwordLogin)
|
api.systemConfigApi.getDefaults().then((res) => res.data.passwordLogin),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,8 +50,8 @@
|
||||||
const { data: updated } = await api.systemConfigApi.updateConfig({
|
const { data: updated } = await api.systemConfigApi.updateConfig({
|
||||||
systemConfigDto: {
|
systemConfigDto: {
|
||||||
...current,
|
...current,
|
||||||
passwordLogin: passwordLoginConfig
|
passwordLogin: passwordLoginConfig,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
passwordLoginConfig = { ...updated.passwordLogin };
|
passwordLoginConfig = { ...updated.passwordLogin };
|
||||||
|
@ -71,7 +71,7 @@
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset settings to the recent saved settings',
|
message: 'Reset settings to the recent saved settings',
|
||||||
type: NotificationType.Info
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,16 +83,13 @@
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset password settings to default',
|
message: 'Reset password settings to default',
|
||||||
type: NotificationType.Info
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isConfirmOpen}
|
{#if isConfirmOpen}
|
||||||
<ConfirmDisableLogin
|
<ConfirmDisableLogin on:cancel={() => handleConfirm(false)} on:confirm={() => handleConfirm(true)} />
|
||||||
on:cancel={() => handleConfirm(false)}
|
|
||||||
on:confirm={() => handleConfirm(true)}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
EMAIL = 'email',
|
EMAIL = 'email',
|
||||||
TEXT = 'text',
|
TEXT = 'text',
|
||||||
NUMBER = 'number',
|
NUMBER = 'number',
|
||||||
PASSWORD = 'password'
|
PASSWORD = 'password',
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -29,13 +29,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="relative inline-block flex-none w-[36px] h-[10px]">
|
<label class="relative inline-block flex-none w-[36px] h-[10px]">
|
||||||
<input
|
<input class="opacity-0 w-0 h-0 disabled::cursor-not-allowed" type="checkbox" bind:checked on:click {disabled} />
|
||||||
class="opacity-0 w-0 h-0 disabled::cursor-not-allowed"
|
|
||||||
type="checkbox"
|
|
||||||
bind:checked
|
|
||||||
on:click
|
|
||||||
{disabled}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if disabled}
|
{#if disabled}
|
||||||
<span class="slider-disable" />
|
<span class="slider-disable" />
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import { api, SystemConfigStorageTemplateDto, SystemConfigTemplateStorageOptionDto, UserResponseDto } from '@api';
|
||||||
api,
|
|
||||||
SystemConfigStorageTemplateDto,
|
|
||||||
SystemConfigTemplateStorageOptionDto,
|
|
||||||
UserResponseDto
|
|
||||||
} from '@api';
|
|
||||||
import * as luxon from 'luxon';
|
import * as luxon from 'luxon';
|
||||||
import handlebar from 'handlebars';
|
import handlebar from 'handlebars';
|
||||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||||
|
@ -15,7 +10,7 @@
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
notificationController,
|
notificationController,
|
||||||
NotificationType
|
NotificationType,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||||
|
|
||||||
|
@ -31,7 +26,7 @@
|
||||||
[savedConfig, defaultConfig, templateOptions] = await Promise.all([
|
[savedConfig, defaultConfig, templateOptions] = await Promise.all([
|
||||||
api.systemConfigApi.getConfig().then((res) => res.data.storageTemplate),
|
api.systemConfigApi.getConfig().then((res) => res.data.storageTemplate),
|
||||||
api.systemConfigApi.getDefaults().then((res) => res.data.storageTemplate),
|
api.systemConfigApi.getDefaults().then((res) => res.data.storageTemplate),
|
||||||
api.systemConfigApi.getStorageTemplateOptions().then((res) => res.data)
|
api.systemConfigApi.getStorageTemplateOptions().then((res) => res.data),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
selectedPreset = savedConfig.template;
|
selectedPreset = savedConfig.template;
|
||||||
|
@ -52,14 +47,14 @@
|
||||||
|
|
||||||
const renderTemplate = (templateString: string) => {
|
const renderTemplate = (templateString: string) => {
|
||||||
const template = handlebar.compile(templateString, {
|
const template = handlebar.compile(templateString, {
|
||||||
knownHelpers: undefined
|
knownHelpers: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const substitutions: Record<string, string> = {
|
const substitutions: Record<string, string> = {
|
||||||
filename: 'IMAGE_56437',
|
filename: 'IMAGE_56437',
|
||||||
ext: 'jpg',
|
ext: 'jpg',
|
||||||
filetype: 'IMG',
|
filetype: 'IMG',
|
||||||
filetypefull: 'IMAGE'
|
filetypefull: 'IMAGE',
|
||||||
};
|
};
|
||||||
|
|
||||||
const dt = luxon.DateTime.fromISO(new Date('2022-09-04T20:03:05.250').toISOString());
|
const dt = luxon.DateTime.fromISO(new Date('2022-09-04T20:03:05.250').toISOString());
|
||||||
|
@ -70,7 +65,7 @@
|
||||||
...templateOptions.dayOptions,
|
...templateOptions.dayOptions,
|
||||||
...templateOptions.hourOptions,
|
...templateOptions.hourOptions,
|
||||||
...templateOptions.minuteOptions,
|
...templateOptions.minuteOptions,
|
||||||
...templateOptions.secondOptions
|
...templateOptions.secondOptions,
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const token of dateTokens) {
|
for (const token of dateTokens) {
|
||||||
|
@ -88,7 +83,7 @@
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset storage template settings to the recent saved settings',
|
message: 'Reset storage template settings to the recent saved settings',
|
||||||
type: NotificationType.Info
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,8 +94,8 @@
|
||||||
const result = await api.systemConfigApi.updateConfig({
|
const result = await api.systemConfigApi.updateConfig({
|
||||||
systemConfigDto: {
|
systemConfigDto: {
|
||||||
...currentConfig,
|
...currentConfig,
|
||||||
storageTemplate: storageConfig
|
storageTemplate: storageConfig,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
storageConfig.template = result.data.storageTemplate.template;
|
storageConfig.template = result.data.storageTemplate.template;
|
||||||
|
@ -108,13 +103,13 @@
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Storage template saved',
|
message: 'Storage template saved',
|
||||||
type: NotificationType.Info
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error [storage-template-settings] [saveSetting]', e);
|
console.error('Error [storage-template-settings] [saveSetting]', e);
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Unable to save settings',
|
message: 'Unable to save settings',
|
||||||
type: NotificationType.Error
|
type: NotificationType.Error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -126,7 +121,7 @@
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset storage template to default',
|
message: 'Reset storage template to default',
|
||||||
type: NotificationType.Info
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,9 +133,7 @@
|
||||||
<section class="dark:text-immich-dark-fg">
|
<section class="dark:text-immich-dark-fg">
|
||||||
{#await getConfigs() then}
|
{#await getConfigs() then}
|
||||||
<div id="directory-path-builder" class="m-4">
|
<div id="directory-path-builder" class="m-4">
|
||||||
<h3 class="font-medium text-immich-primary dark:text-immich-dark-primary text-base">
|
<h3 class="font-medium text-immich-primary dark:text-immich-dark-primary text-base">Variables</h3>
|
||||||
Variables
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<section class="support-date">
|
<section class="support-date">
|
||||||
{#await getSupportDateTimeFormat()}
|
{#await getSupportDateTimeFormat()}
|
||||||
|
@ -157,9 +150,7 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="mt-4 flex flex-col">
|
<div class="mt-4 flex flex-col">
|
||||||
<h3 class="font-medium text-immich-primary dark:text-immich-dark-primary text-base">
|
<h3 class="font-medium text-immich-primary dark:text-immich-dark-primary text-base">Template</h3>
|
||||||
Template
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="text-xs my-2">
|
<div class="text-xs my-2">
|
||||||
<h4>PREVIEW</h4>
|
<h4>PREVIEW</h4>
|
||||||
|
@ -176,9 +167,7 @@
|
||||||
<code>{user.storageLabel || user.id}</code> is the user's Storage Label
|
<code>{user.storageLabel || user.id}</code> is the user's Storage Label
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p
|
<p class="text-xs p-4 bg-gray-200 dark:bg-gray-700 dark:text-immich-dark-fg py-2 rounded-lg mt-2">
|
||||||
class="text-xs p-4 bg-gray-200 dark:bg-gray-700 dark:text-immich-dark-fg py-2 rounded-lg mt-2"
|
|
||||||
>
|
|
||||||
<span class="text-immich-fg/25 dark:text-immich-dark-fg/50"
|
<span class="text-immich-fg/25 dark:text-immich-dark-fg/50"
|
||||||
>UPLOAD_LOCATION/{user.storageLabel || user.id}</span
|
>UPLOAD_LOCATION/{user.storageLabel || user.id}</span
|
||||||
>/{parsedTemplate()}.jpg
|
>/{parsedTemplate()}.jpg
|
||||||
|
@ -209,21 +198,15 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex-0">
|
<div class="flex-0">
|
||||||
<SettingInputField
|
<SettingInputField label="EXTENSION" inputType={SettingInputFieldType.TEXT} value={'.jpg'} disabled />
|
||||||
label="EXTENSION"
|
|
||||||
inputType={SettingInputFieldType.TEXT}
|
|
||||||
value={'.jpg'}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="migration-info" class="text-sm mt-4">
|
<div id="migration-info" class="text-sm mt-4">
|
||||||
<p>
|
<p>
|
||||||
Template changes will only apply to new assets. To retroactively apply the template to
|
Template changes will only apply to new assets. To retroactively apply the template to previously uploaded
|
||||||
previously uploaded assets, run the <a
|
assets, run the <a href="/admin/jobs-status" class="text-immich-primary dark:text-immich-dark-primary"
|
||||||
href="/admin/jobs-status"
|
>Storage Migration Job</a
|
||||||
class="text-immich-primary dark:text-immich-dark-primary">Storage Migration Job</a
|
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,9 +5,7 @@
|
||||||
export let options: SystemConfigTemplateStorageOptionDto;
|
export let options: SystemConfigTemplateStorageOptionDto;
|
||||||
|
|
||||||
const getLuxonExample = (format: string) => {
|
const getLuxonExample = (format: string) => {
|
||||||
return luxon.DateTime.fromISO(new Date('2022-09-04T20:03:05.250').toISOString()).toFormat(
|
return luxon.DateTime.fromISO(new Date('2022-09-04T20:03:05.250').toISOString()).toFormat(format);
|
||||||
format
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { jest, describe, it } from '@jest/globals';
|
|
||||||
import { render, RenderResult, waitFor, fireEvent } from '@testing-library/svelte';
|
|
||||||
import { createObjectURLMock } from '$lib/__mocks__/jsdom-url.mock';
|
import { createObjectURLMock } from '$lib/__mocks__/jsdom-url.mock';
|
||||||
import { api, ThumbnailFormat } from '@api';
|
import { api, ThumbnailFormat } from '@api';
|
||||||
|
import { describe, it, jest } from '@jest/globals';
|
||||||
import { albumFactory } from '@test-data';
|
import { albumFactory } from '@test-data';
|
||||||
import AlbumCard from '../album-card.svelte';
|
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
|
import { fireEvent, render, RenderResult, waitFor } from '@testing-library/svelte';
|
||||||
|
import AlbumCard from '../album-card.svelte';
|
||||||
|
|
||||||
jest.mock('@api');
|
jest.mock('@api');
|
||||||
|
|
||||||
|
@ -17,26 +17,24 @@ describe('AlbumCard component', () => {
|
||||||
{
|
{
|
||||||
album: albumFactory.build({ albumThumbnailAssetId: null, shared: false, assetCount: 0 }),
|
album: albumFactory.build({ albumThumbnailAssetId: null, shared: false, assetCount: 0 }),
|
||||||
count: 0,
|
count: 0,
|
||||||
shared: false
|
shared: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
album: albumFactory.build({ albumThumbnailAssetId: null, shared: true, assetCount: 0 }),
|
album: albumFactory.build({ albumThumbnailAssetId: null, shared: true, assetCount: 0 }),
|
||||||
count: 0,
|
count: 0,
|
||||||
shared: true
|
shared: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
album: albumFactory.build({ albumThumbnailAssetId: null, shared: false, assetCount: 5 }),
|
album: albumFactory.build({ albumThumbnailAssetId: null, shared: false, assetCount: 5 }),
|
||||||
count: 5,
|
count: 5,
|
||||||
shared: false
|
shared: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
album: albumFactory.build({ albumThumbnailAssetId: null, shared: true, assetCount: 2 }),
|
album: albumFactory.build({ albumThumbnailAssetId: null, shared: true, assetCount: 2 }),
|
||||||
count: 2,
|
count: 2,
|
||||||
shared: true
|
shared: true,
|
||||||
}
|
},
|
||||||
])(
|
])('shows album data without thumbnail with count $count - shared: $shared', async ({ album, count, shared }) => {
|
||||||
'shows album data without thumbnail with count $count - shared: $shared',
|
|
||||||
async ({ album, count, shared }) => {
|
|
||||||
sut = render(AlbumCard, { album, user: album.owner });
|
sut = render(AlbumCard, { album, user: album.owner });
|
||||||
|
|
||||||
const albumImgElement = sut.getByTestId('album-image');
|
const albumImgElement = sut.getByTestId('album-image');
|
||||||
|
@ -54,8 +52,7 @@ describe('AlbumCard component', () => {
|
||||||
|
|
||||||
expect(albumNameElement).toHaveTextContent(album.albumName);
|
expect(albumNameElement).toHaveTextContent(album.albumName);
|
||||||
expect(albumDetailsElement).toHaveTextContent(new RegExp(detailsText));
|
expect(albumDetailsElement).toHaveTextContent(new RegExp(detailsText));
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
it('shows album data and and loads the thumbnail image when available', async () => {
|
it('shows album data and and loads the thumbnail image when available', async () => {
|
||||||
const thumbnailFile = new File([new Blob()], 'fileThumbnail');
|
const thumbnailFile = new File([new Blob()], 'fileThumbnail');
|
||||||
|
@ -65,14 +62,14 @@ describe('AlbumCard component', () => {
|
||||||
config: {},
|
config: {},
|
||||||
headers: {},
|
headers: {},
|
||||||
status: 200,
|
status: 200,
|
||||||
statusText: ''
|
statusText: '',
|
||||||
});
|
});
|
||||||
createObjectURLMock.mockReturnValueOnce(thumbnailUrl);
|
createObjectURLMock.mockReturnValueOnce(thumbnailUrl);
|
||||||
|
|
||||||
const album = albumFactory.build({
|
const album = albumFactory.build({
|
||||||
albumThumbnailAssetId: 'thumbnailIdOne',
|
albumThumbnailAssetId: 'thumbnailIdOne',
|
||||||
shared: false,
|
shared: false,
|
||||||
albumName: 'some album name'
|
albumName: 'some album name',
|
||||||
});
|
});
|
||||||
sut = render(AlbumCard, { album, user: album.owner });
|
sut = render(AlbumCard, { album, user: album.owner });
|
||||||
|
|
||||||
|
@ -88,9 +85,9 @@ describe('AlbumCard component', () => {
|
||||||
expect(apiMock.assetApi.getAssetThumbnail).toHaveBeenCalledWith(
|
expect(apiMock.assetApi.getAssetThumbnail).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
id: 'thumbnailIdOne',
|
id: 'thumbnailIdOne',
|
||||||
format: ThumbnailFormat.Jpeg
|
format: ThumbnailFormat.Jpeg,
|
||||||
},
|
},
|
||||||
{ responseType: 'blob' }
|
{ responseType: 'blob' },
|
||||||
);
|
);
|
||||||
expect(createObjectURLMock).toHaveBeenCalledWith(thumbnailFile);
|
expect(createObjectURLMock).toHaveBeenCalledWith(thumbnailFile);
|
||||||
|
|
||||||
|
@ -128,14 +125,12 @@ describe('AlbumCard component', () => {
|
||||||
contextMenuBtnParent,
|
contextMenuBtnParent,
|
||||||
new MouseEvent('click', {
|
new MouseEvent('click', {
|
||||||
clientX: 123,
|
clientX: 123,
|
||||||
clientY: 456
|
clientY: 456,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(onClickHandler).toHaveBeenCalledTimes(1);
|
expect(onClickHandler).toHaveBeenCalledTimes(1);
|
||||||
expect(onClickHandler).toHaveBeenCalledWith(
|
expect(onClickHandler).toHaveBeenCalledWith(expect.objectContaining({ detail: { x: 123, y: 456 } }));
|
||||||
expect.objectContaining({ detail: { x: 123, y: 456 } })
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,11 +28,11 @@
|
||||||
const { data } = await api.assetApi.getAssetThumbnail(
|
const { data } = await api.assetApi.getAssetThumbnail(
|
||||||
{
|
{
|
||||||
id: thubmnailId,
|
id: thubmnailId,
|
||||||
format: ThumbnailFormat.Jpeg
|
format: ThumbnailFormat.Jpeg,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
responseType: 'blob'
|
responseType: 'blob',
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (data instanceof Blob) {
|
if (data instanceof Blob) {
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
const showAlbumContextMenu = (e: MouseEvent) => {
|
const showAlbumContextMenu = (e: MouseEvent) => {
|
||||||
dispatchShowContextMenu('showalbumcontextmenu', {
|
dispatchShowContextMenu('showalbumcontextmenu', {
|
||||||
x: e.clientX,
|
x: e.clientX,
|
||||||
y: e.clientY
|
y: e.clientY,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
SharedLinkResponseDto,
|
SharedLinkResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
UserResponseDto,
|
UserResponseDto,
|
||||||
api
|
api,
|
||||||
} from '@api';
|
} from '@api';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
|
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
|
||||||
|
@ -34,10 +34,7 @@
|
||||||
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
|
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||||
import ImmichLogo from '../shared-components/immich-logo.svelte';
|
import ImmichLogo from '../shared-components/immich-logo.svelte';
|
||||||
import SelectAll from 'svelte-material-icons/SelectAll.svelte';
|
import SelectAll from 'svelte-material-icons/SelectAll.svelte';
|
||||||
import {
|
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||||
NotificationType,
|
|
||||||
notificationController
|
|
||||||
} from '../shared-components/notification/notification';
|
|
||||||
import ThemeButton from '../shared-components/theme-button.svelte';
|
import ThemeButton from '../shared-components/theme-button.svelte';
|
||||||
import AssetSelection from './asset-selection.svelte';
|
import AssetSelection from './asset-selection.svelte';
|
||||||
import ShareInfoModal from './share-info-modal.svelte';
|
import ShareInfoModal from './share-info-modal.svelte';
|
||||||
|
@ -108,7 +105,7 @@
|
||||||
const albumDateFormat: Intl.DateTimeFormatOptions = {
|
const albumDateFormat: Intl.DateTimeFormatOptions = {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
year: 'numeric'
|
year: 'numeric',
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDateRange = () => {
|
const getDateRange = () => {
|
||||||
|
@ -119,9 +116,7 @@
|
||||||
const endDateString = endDate.toLocaleDateString($locale, albumDateFormat);
|
const endDateString = endDate.toLocaleDateString($locale, albumDateFormat);
|
||||||
|
|
||||||
// If the start and end date are the same, only show one date
|
// If the start and end date are the same, only show one date
|
||||||
return startDateString === endDateString
|
return startDateString === endDateString ? startDateString : `${startDateString} - ${endDateString}`;
|
||||||
? startDateString
|
|
||||||
: `${startDateString} - ${endDateString}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
@ -142,8 +137,8 @@
|
||||||
.updateAlbumInfo({
|
.updateAlbumInfo({
|
||||||
id: album.id,
|
id: album.id,
|
||||||
updateAlbumDto: {
|
updateAlbumDto: {
|
||||||
albumName: album.albumName
|
albumName: album.albumName,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
currentAlbumName = album.albumName;
|
currentAlbumName = album.albumName;
|
||||||
|
@ -152,7 +147,7 @@
|
||||||
console.error('Error [updateAlbumInfo] ', e);
|
console.error('Error [updateAlbumInfo] ', e);
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
type: NotificationType.Error,
|
type: NotificationType.Error,
|
||||||
message: "Error updating album's name, check console for more details"
|
message: "Error updating album's name, check console for more details",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -164,9 +159,9 @@
|
||||||
const { data } = await api.albumApi.addAssetsToAlbum({
|
const { data } = await api.albumApi.addAssetsToAlbum({
|
||||||
id: album.id,
|
id: album.id,
|
||||||
addAssetsDto: {
|
addAssetsDto: {
|
||||||
assetIds: assets.map((a) => a.id)
|
assetIds: assets.map((a) => a.id),
|
||||||
},
|
},
|
||||||
key: sharedLink?.key
|
key: sharedLink?.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.album) {
|
if (data.album) {
|
||||||
|
@ -177,7 +172,7 @@
|
||||||
console.error('Error [createAlbumHandler] ', e);
|
console.error('Error [createAlbumHandler] ', e);
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
type: NotificationType.Error,
|
type: NotificationType.Error,
|
||||||
message: 'Error creating album, check console for more details'
|
message: 'Error creating album, check console for more details',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -189,8 +184,8 @@
|
||||||
const { data } = await api.albumApi.addUsersToAlbum({
|
const { data } = await api.albumApi.addUsersToAlbum({
|
||||||
id: album.id,
|
id: album.id,
|
||||||
addUsersDto: {
|
addUsersDto: {
|
||||||
sharedUserIds: Array.from(selectedUsers).map((u) => u.id)
|
sharedUserIds: Array.from(selectedUsers).map((u) => u.id),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
album = data;
|
album = data;
|
||||||
|
@ -200,7 +195,7 @@
|
||||||
console.error('Error [addUserHandler] ', e);
|
console.error('Error [addUserHandler] ', e);
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
type: NotificationType.Error,
|
type: NotificationType.Error,
|
||||||
message: 'Error adding users to album, check console for more details'
|
message: 'Error adding users to album, check console for more details',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -232,7 +227,7 @@
|
||||||
console.error('Error [userDeleteMenu] ', e);
|
console.error('Error [userDeleteMenu] ', e);
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
type: NotificationType.Error,
|
type: NotificationType.Error,
|
||||||
message: 'Error deleting album, check console for more details'
|
message: 'Error deleting album, check console for more details',
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
isShowDeleteConfirmation = false;
|
isShowDeleteConfirmation = false;
|
||||||
|
@ -240,12 +235,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadAlbum = async () => {
|
const downloadAlbum = async () => {
|
||||||
await downloadArchive(
|
await downloadArchive(`${album.albumName}.zip`, { albumId: album.id }, undefined, sharedLink?.key);
|
||||||
`${album.albumName}.zip`,
|
|
||||||
{ albumId: album.id },
|
|
||||||
undefined,
|
|
||||||
sharedLink?.key
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const showAlbumOptionsMenu = ({ x, y }: MouseEvent) => {
|
const showAlbumOptionsMenu = ({ x, y }: MouseEvent) => {
|
||||||
|
@ -259,14 +249,14 @@
|
||||||
api.albumApi.updateAlbumInfo({
|
api.albumApi.updateAlbumInfo({
|
||||||
id: album.id,
|
id: album.id,
|
||||||
updateAlbumDto: {
|
updateAlbumDto: {
|
||||||
albumThumbnailAssetId: asset.id
|
albumThumbnailAssetId: asset.id,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error [setAlbumThumbnailHandler] ', e);
|
console.error('Error [setAlbumThumbnailHandler] ', e);
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
type: NotificationType.Error,
|
type: NotificationType.Error,
|
||||||
message: 'Error setting album thumbnail, check console for more details'
|
message: 'Error setting album thumbnail, check console for more details',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,10 +276,7 @@
|
||||||
<section class="bg-immich-bg dark:bg-immich-dark-bg" class:hidden={isShowThumbnailSelection}>
|
<section class="bg-immich-bg dark:bg-immich-dark-bg" class:hidden={isShowThumbnailSelection}>
|
||||||
<!-- Multiselection mode app bar -->
|
<!-- Multiselection mode app bar -->
|
||||||
{#if isMultiSelectionMode}
|
{#if isMultiSelectionMode}
|
||||||
<AssetSelectControlBar
|
<AssetSelectControlBar assets={multiSelectAsset} clearSelect={() => (multiSelectAsset = new Set())}>
|
||||||
assets={multiSelectAsset}
|
|
||||||
clearSelect={() => (multiSelectAsset = new Set())}
|
|
||||||
>
|
|
||||||
<CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
|
<CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
|
||||||
{#if sharedLink?.allowDownload || !isPublicShared}
|
{#if sharedLink?.allowDownload || !isPublicShared}
|
||||||
<DownloadAction filename="{album.albumName}.zip" sharedLinkKey={sharedLink?.key} />
|
<DownloadAction filename="{album.albumName}.zip" sharedLinkKey={sharedLink?.key} />
|
||||||
|
@ -305,9 +292,7 @@
|
||||||
<ControlAppBar
|
<ControlAppBar
|
||||||
on:close-button-click={() => goto(backUrl)}
|
on:close-button-click={() => goto(backUrl)}
|
||||||
backIcon={ArrowLeft}
|
backIcon={ArrowLeft}
|
||||||
showBackButton={(!isPublicShared && isOwned) ||
|
showBackButton={(!isPublicShared && isOwned) || (!isPublicShared && !isOwned) || (isPublicShared && isOwned)}
|
||||||
(!isPublicShared && !isOwned) ||
|
|
||||||
(isPublicShared && isOwned)}
|
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="leading">
|
<svelte:fragment slot="leading">
|
||||||
{#if isPublicShared && !isOwned}
|
{#if isPublicShared && !isOwned}
|
||||||
|
@ -317,9 +302,7 @@
|
||||||
href="https://immich.app"
|
href="https://immich.app"
|
||||||
>
|
>
|
||||||
<ImmichLogo height={30} width={30} />
|
<ImmichLogo height={30} width={30} />
|
||||||
<h1 class="font-immich-title text-lg text-immich-primary dark:text-immich-dark-primary">
|
<h1 class="font-immich-title text-lg text-immich-primary dark:text-immich-dark-primary">IMMICH</h1>
|
||||||
IMMICH
|
|
||||||
</h1>
|
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
@ -356,24 +339,13 @@
|
||||||
|
|
||||||
{#if album.assetCount > 0 && !isCreatingSharedAlbum}
|
{#if album.assetCount > 0 && !isCreatingSharedAlbum}
|
||||||
{#if !isPublicShared || (isPublicShared && sharedLink?.allowDownload)}
|
{#if !isPublicShared || (isPublicShared && sharedLink?.allowDownload)}
|
||||||
<CircleIconButton
|
<CircleIconButton title="Download" on:click={() => downloadAlbum()} logo={FolderDownloadOutline} />
|
||||||
title="Download"
|
|
||||||
on:click={() => downloadAlbum()}
|
|
||||||
logo={FolderDownloadOutline}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !isPublicShared && isOwned}
|
{#if !isPublicShared && isOwned}
|
||||||
<CircleIconButton
|
<CircleIconButton title="Album options" on:click={showAlbumOptionsMenu} logo={DotsVertical}>
|
||||||
title="Album options"
|
|
||||||
on:click={showAlbumOptionsMenu}
|
|
||||||
logo={DotsVertical}
|
|
||||||
>
|
|
||||||
{#if isShowAlbumOptions}
|
{#if isShowAlbumOptions}
|
||||||
<ContextMenu
|
<ContextMenu {...contextMenuPosition} on:outclick={() => (isShowAlbumOptions = false)}>
|
||||||
{...contextMenuPosition}
|
|
||||||
on:outclick={() => (isShowAlbumOptions = false)}
|
|
||||||
>
|
|
||||||
<MenuOption
|
<MenuOption
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
isShowThumbnailSelection = true;
|
isShowThumbnailSelection = true;
|
||||||
|
@ -450,12 +422,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if album.assetCount > 0}
|
{#if album.assetCount > 0}
|
||||||
<GalleryViewer
|
<GalleryViewer assets={album.assets} {sharedLink} bind:selectedAssets={multiSelectAsset} viewFrom="album-page" />
|
||||||
assets={album.assets}
|
|
||||||
{sharedLink}
|
|
||||||
bind:selectedAssets={multiSelectAsset}
|
|
||||||
viewFrom="album-page"
|
|
||||||
/>
|
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Album is empty - Show asset selectection buttons -->
|
<!-- Album is empty - Show asset selectection buttons -->
|
||||||
<section id="empty-album" class=" mt-[200px] flex place-content-center place-items-center">
|
<section id="empty-album" class=" mt-[200px] flex place-content-center place-items-center">
|
||||||
|
@ -465,9 +432,7 @@
|
||||||
on:click={() => (isShowAssetSelection = true)}
|
on:click={() => (isShowAssetSelection = true)}
|
||||||
class="w-full py-8 border bg-immich-bg dark:bg-immich-dark-gray text-immich-fg dark:text-immich-dark-fg dark:hover:text-immich-dark-primary rounded-md mt-5 flex place-items-center gap-6 px-8 transition-all hover:bg-gray-100 hover:text-immich-primary dark:border-none"
|
class="w-full py-8 border bg-immich-bg dark:bg-immich-dark-gray text-immich-fg dark:text-immich-dark-fg dark:hover:text-immich-dark-primary rounded-md mt-5 flex place-items-center gap-6 px-8 transition-all hover:bg-gray-100 hover:text-immich-primary dark:border-none"
|
||||||
>
|
>
|
||||||
<span class="text-text-immich-primary dark:text-immich-dark-primary"
|
<span class="text-text-immich-primary dark:text-immich-dark-primary"><Plus size="24" /> </span>
|
||||||
><Plus size="24" />
|
|
||||||
</span>
|
|
||||||
<span class="text-lg">Select photos</span>
|
<span class="text-lg">Select photos</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -496,18 +461,10 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isShowShareLinkModal}
|
{#if isShowShareLinkModal}
|
||||||
<CreateSharedLinkModal
|
<CreateSharedLinkModal on:close={() => (isShowShareLinkModal = false)} shareType={SharedLinkType.Album} {album} />
|
||||||
on:close={() => (isShowShareLinkModal = false)}
|
|
||||||
shareType={SharedLinkType.Album}
|
|
||||||
{album}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if isShowShareInfoModal}
|
{#if isShowShareInfoModal}
|
||||||
<ShareInfoModal
|
<ShareInfoModal on:close={() => (isShowShareInfoModal = false)} {album} on:user-deleted={sharedUserDeletedHandler} />
|
||||||
on:close={() => (isShowShareInfoModal = false)}
|
|
||||||
{album}
|
|
||||||
on:user-deleted={sharedUserDeletedHandler}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isShowThumbnailSelection}
|
{#if isShowThumbnailSelection}
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import { assetInteractionStore, assetsInAlbumStoreState, selectedAssets } from '$lib/stores/asset-interaction.store';
|
||||||
assetInteractionStore,
|
|
||||||
assetsInAlbumStoreState,
|
|
||||||
selectedAssets
|
|
||||||
} from '$lib/stores/asset-interaction.store';
|
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||||
import type { AssetResponseDto } from '@api';
|
import type { AssetResponseDto } from '@api';
|
||||||
|
@ -25,7 +21,7 @@
|
||||||
|
|
||||||
const addSelectedAssets = async () => {
|
const addSelectedAssets = async () => {
|
||||||
dispatch('create-album', {
|
dispatch('create-album', {
|
||||||
assets: Array.from($selectedAssets)
|
assets: Array.from($selectedAssets),
|
||||||
});
|
});
|
||||||
|
|
||||||
assetInteractionStore.clearMultiselect();
|
assetInteractionStore.clearMultiselect();
|
||||||
|
@ -64,14 +60,7 @@
|
||||||
>
|
>
|
||||||
Select from computer
|
Select from computer
|
||||||
</button>
|
</button>
|
||||||
<Button
|
<Button size="sm" rounded="lg" disabled={$selectedAssets.size === 0} on:click={addSelectedAssets}>Done</Button>
|
||||||
size="sm"
|
|
||||||
rounded="lg"
|
|
||||||
disabled={$selectedAssets.size === 0}
|
|
||||||
on:click={addSelectedAssets}
|
|
||||||
>
|
|
||||||
Done
|
|
||||||
</Button>
|
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</ControlAppBar>
|
</ControlAppBar>
|
||||||
<section class="pt-[100px] pl-[70px] grid h-screen bg-immich-bg dark:bg-immich-dark-bg">
|
<section class="pt-[100px] pl-[70px] grid h-screen bg-immich-bg dark:bg-immich-dark-bg">
|
||||||
|
|
|
@ -7,10 +7,7 @@
|
||||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||||
import ContextMenu from '../shared-components/context-menu/context-menu.svelte';
|
import ContextMenu from '../shared-components/context-menu/context-menu.svelte';
|
||||||
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
|
||||||
import {
|
import { notificationController, NotificationType } from '../shared-components/notification/notification';
|
||||||
notificationController,
|
|
||||||
NotificationType
|
|
||||||
} from '../shared-components/notification/notification';
|
|
||||||
import { handleError } from '../../utils/handle-error';
|
import { handleError } from '../../utils/handle-error';
|
||||||
import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
|
import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
|
||||||
|
|
||||||
|
@ -40,7 +37,7 @@
|
||||||
if (iconButton) {
|
if (iconButton) {
|
||||||
position = {
|
position = {
|
||||||
x: iconButton.getBoundingClientRect().left,
|
x: iconButton.getBoundingClientRect().left,
|
||||||
y: iconButton.getBoundingClientRect().bottom
|
y: iconButton.getBoundingClientRect().bottom,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,8 +60,7 @@
|
||||||
try {
|
try {
|
||||||
await api.albumApi.removeUserFromAlbum({ id: album.id, userId });
|
await api.albumApi.removeUserFromAlbum({ id: album.id, userId });
|
||||||
dispatch('user-deleted', { userId });
|
dispatch('user-deleted', { userId });
|
||||||
const message =
|
const message = userId === 'me' ? `Left ${album.albumName}` : `Removed ${selectedRemoveUser.firstName}`;
|
||||||
userId === 'me' ? `Left ${album.albumName}` : `Removed ${selectedRemoveUser.firstName}`;
|
|
||||||
notificationController.show({ type: NotificationType.Info, message });
|
notificationController.show({ type: NotificationType.Info, message });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(e, 'Unable to remove user');
|
handleError(e, 'Unable to remove user');
|
||||||
|
|
|
@ -46,11 +46,7 @@
|
||||||
<!-- Image grid -->
|
<!-- Image grid -->
|
||||||
<div class="flex flex-wrap gap-[2px]">
|
<div class="flex flex-wrap gap-[2px]">
|
||||||
{#each album.assets as asset}
|
{#each album.assets as asset}
|
||||||
<Thumbnail
|
<Thumbnail {asset} on:click={() => (selectedThumbnail = asset)} selected={isSelected(asset.id)} />
|
||||||
{asset}
|
|
||||||
on:click={() => (selectedThumbnail = asset)}
|
|
||||||
selected={isSelected(asset.id)}
|
|
||||||
/>
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -112,16 +112,13 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="text-sm p-5">
|
<p class="text-sm p-5">
|
||||||
Looks like you have shared this album with all users or you don't have any user to share
|
Looks like you have shared this album with all users or you don't have any user to share with.
|
||||||
with.
|
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if selectedUsers.length > 0}
|
{#if selectedUsers.length > 0}
|
||||||
<div class="flex place-content-end p-5">
|
<div class="flex place-content-end p-5">
|
||||||
<Button size="sm" rounded="lg" on:click={() => dispatch('add-user', { selectedUsers })}>
|
<Button size="sm" rounded="lg" on:click={() => dispatch('add-user', { selectedUsers })}>Add</Button>
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
albumNameArray = [
|
albumNameArray = [
|
||||||
albumName.slice(0, findIndex),
|
albumName.slice(0, findIndex),
|
||||||
albumName.slice(findIndex, findIndex + findLength),
|
albumName.slice(findIndex, findIndex + findLength),
|
||||||
albumName.slice(findIndex + findLength)
|
albumName.slice(findIndex + findLength),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -73,9 +73,7 @@
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
isOpacity={true}
|
isOpacity={true}
|
||||||
hideMobile={true}
|
hideMobile={true}
|
||||||
logo={$photoZoomState && $photoZoomState.currentZoom > 1
|
logo={$photoZoomState && $photoZoomState.currentZoom > 1 ? MagnifyMinusOutline : MagnifyPlusOutline}
|
||||||
? MagnifyMinusOutline
|
|
||||||
: MagnifyPlusOutline}
|
|
||||||
title="Zoom Image"
|
title="Zoom Image"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
const zoomImage = new CustomEvent('zoomImage');
|
const zoomImage = new CustomEvent('zoomImage');
|
||||||
|
@ -103,12 +101,7 @@
|
||||||
title="Download"
|
title="Download"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<CircleIconButton
|
<CircleIconButton isOpacity={true} logo={InformationOutline} on:click={() => dispatch('showDetail')} title="Info" />
|
||||||
isOpacity={true}
|
|
||||||
logo={InformationOutline}
|
|
||||||
on:click={() => dispatch('showDetail')}
|
|
||||||
title="Info"
|
|
||||||
/>
|
|
||||||
{#if isOwner}
|
{#if isOwner}
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
isOpacity={true}
|
isOpacity={true}
|
||||||
|
@ -119,26 +112,13 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isOwner}
|
{#if isOwner}
|
||||||
<CircleIconButton
|
<CircleIconButton isOpacity={true} logo={DeleteOutline} on:click={() => dispatch('delete')} title="Delete" />
|
||||||
isOpacity={true}
|
|
||||||
logo={DeleteOutline}
|
|
||||||
on:click={() => dispatch('delete')}
|
|
||||||
title="Delete"
|
|
||||||
/>
|
|
||||||
<div use:clickOutside on:outclick={() => (isShowAssetOptions = false)}>
|
<div use:clickOutside on:outclick={() => (isShowAssetOptions = false)}>
|
||||||
<CircleIconButton
|
<CircleIconButton isOpacity={true} logo={DotsVertical} on:click={showOptionsMenu} title="More">
|
||||||
isOpacity={true}
|
|
||||||
logo={DotsVertical}
|
|
||||||
on:click={showOptionsMenu}
|
|
||||||
title="More"
|
|
||||||
>
|
|
||||||
{#if isShowAssetOptions}
|
{#if isShowAssetOptions}
|
||||||
<ContextMenu {...contextMenuPosition} direction="left">
|
<ContextMenu {...contextMenuPosition} direction="left">
|
||||||
<MenuOption on:click={() => onMenuClick('addToAlbum')} text="Add to Album" />
|
<MenuOption on:click={() => onMenuClick('addToAlbum')} text="Add to Album" />
|
||||||
<MenuOption
|
<MenuOption on:click={() => onMenuClick('addToSharedAlbum')} text="Add to Shared Album" />
|
||||||
on:click={() => onMenuClick('addToSharedAlbum')}
|
|
||||||
text="Add to Shared Album"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if isOwner}
|
{#if isOwner}
|
||||||
<MenuOption
|
<MenuOption
|
||||||
|
|
|
@ -1,22 +1,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import {
|
import { AlbumResponseDto, api, AssetResponseDto, AssetTypeEnum, SharedLinkResponseDto } from '@api';
|
||||||
AlbumResponseDto,
|
|
||||||
api,
|
|
||||||
AssetResponseDto,
|
|
||||||
AssetTypeEnum,
|
|
||||||
SharedLinkResponseDto
|
|
||||||
} from '@api';
|
|
||||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||||
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
|
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
|
||||||
import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
|
import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
|
||||||
import ImageBrokenVariant from 'svelte-material-icons/ImageBrokenVariant.svelte';
|
import ImageBrokenVariant from 'svelte-material-icons/ImageBrokenVariant.svelte';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import AlbumSelectionModal from '../shared-components/album-selection-modal.svelte';
|
import AlbumSelectionModal from '../shared-components/album-selection-modal.svelte';
|
||||||
import {
|
import { notificationController, NotificationType } from '../shared-components/notification/notification';
|
||||||
notificationController,
|
|
||||||
NotificationType
|
|
||||||
} from '../shared-components/notification/notification';
|
|
||||||
import AssetViewerNavBar from './asset-viewer-nav-bar.svelte';
|
import AssetViewerNavBar from './asset-viewer-nav-bar.svelte';
|
||||||
import DetailPanel from './detail-panel.svelte';
|
import DetailPanel from './detail-panel.svelte';
|
||||||
import PhotoViewer from './photo-viewer.svelte';
|
import PhotoViewer from './photo-viewer.svelte';
|
||||||
|
@ -120,8 +111,8 @@
|
||||||
try {
|
try {
|
||||||
const { data: deletedAssets } = await api.assetApi.deleteAsset({
|
const { data: deletedAssets } = await api.assetApi.deleteAsset({
|
||||||
deleteAssetDto: {
|
deleteAssetDto: {
|
||||||
ids: [asset.id]
|
ids: [asset.id],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
navigateAssetForward();
|
navigateAssetForward();
|
||||||
|
@ -134,7 +125,7 @@
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
type: NotificationType.Error,
|
type: NotificationType.Error,
|
||||||
message: 'Error deleting this asset, check console for more details'
|
message: 'Error deleting this asset, check console for more details',
|
||||||
});
|
});
|
||||||
console.error('Error deleteAsset', e);
|
console.error('Error deleteAsset', e);
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -146,8 +137,8 @@
|
||||||
const { data } = await api.assetApi.updateAsset({
|
const { data } = await api.assetApi.updateAsset({
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
updateAssetDto: {
|
updateAssetDto: {
|
||||||
isFavorite: !asset.isFavorite
|
isFavorite: !asset.isFavorite,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
asset.isFavorite = data.isFavorite;
|
asset.isFavorite = data.isFavorite;
|
||||||
|
@ -163,9 +154,7 @@
|
||||||
isShowAlbumPicker = false;
|
isShowAlbumPicker = false;
|
||||||
|
|
||||||
const { albumName }: { albumName: string } = event.detail;
|
const { albumName }: { albumName: string } = event.detail;
|
||||||
api.albumApi
|
api.albumApi.createAlbum({ createAlbumDto: { albumName, assetIds: [asset.id] } }).then((response) => {
|
||||||
.createAlbum({ createAlbumDto: { albumName, assetIds: [asset.id] } })
|
|
||||||
.then((response) => {
|
|
||||||
const album = response.data;
|
const album = response.data;
|
||||||
goto('/albums/' + album.id);
|
goto('/albums/' + album.id);
|
||||||
});
|
});
|
||||||
|
@ -199,8 +188,8 @@
|
||||||
const { data } = await api.assetApi.updateAsset({
|
const { data } = await api.assetApi.updateAsset({
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
updateAssetDto: {
|
updateAssetDto: {
|
||||||
isArchived: !asset.isArchived
|
isArchived: !asset.isArchived,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
asset.isArchived = data.isArchived;
|
asset.isArchived = data.isArchived;
|
||||||
|
@ -213,15 +202,13 @@
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
message: asset.isArchived ? `Added to archive` : `Removed from archive`
|
message: asset.isArchived ? `Added to archive` : `Removed from archive`,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
type: NotificationType.Error,
|
type: NotificationType.Error,
|
||||||
message: `Error ${
|
message: `Error ${asset.isArchived ? 'archiving' : 'unarchiving'} asset, check console for more details`,
|
||||||
asset.isArchived ? 'archiving' : 'unarchiving'
|
|
||||||
} asset, check console for more details`
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -377,8 +364,8 @@
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="prompt">
|
<svelte:fragment slot="prompt">
|
||||||
<p>
|
<p>
|
||||||
Are you sure you want to delete this {getAssetType().toLowerCase()}? This will also remove
|
Are you sure you want to delete this {getAssetType().toLowerCase()}? This will also remove it from its
|
||||||
it from its album(s).
|
album(s).
|
||||||
</p>
|
</p>
|
||||||
<p><b>You cannot undo this action!</b></p>
|
<p><b>You cannot undo this action!</b></p>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
|
@ -68,8 +68,8 @@
|
||||||
await api.assetApi.updateAsset({
|
await api.assetApi.updateAsset({
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
updateAssetDto: {
|
updateAssetDto: {
|
||||||
description: description
|
description: description,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -95,9 +95,7 @@
|
||||||
class="max-h-[500px]
|
class="max-h-[500px]
|
||||||
text-base text-black bg-transparent dark:text-white border-b focus:border-b-2 border-gray-500 w-full focus:border-immich-primary dark:focus:border-immich-dark-primary transition-all resize-none overflow-hidden outline-none disabled:border-none"
|
text-base text-black bg-transparent dark:text-white border-b focus:border-b-2 border-gray-500 w-full focus:border-immich-primary dark:focus:border-immich-dark-primary transition-all resize-none overflow-hidden outline-none disabled:border-none"
|
||||||
placeholder={$page?.data?.user?.id !== asset.ownerId ? '' : 'Add a description'}
|
placeholder={$page?.data?.user?.id !== asset.ownerId ? '' : 'Add a description'}
|
||||||
style:display={$page?.data?.user?.id !== asset.ownerId && textarea?.value == ''
|
style:display={$page?.data?.user?.id !== asset.ownerId && textarea?.value == '' ? 'none' : 'block'}
|
||||||
? 'none'
|
|
||||||
: 'block'}
|
|
||||||
on:focusin={handleFocusIn}
|
on:focusin={handleFocusIn}
|
||||||
on:focusout={handleFocusOut}
|
on:focusout={handleFocusOut}
|
||||||
on:input={autoGrowHeight}
|
on:input={autoGrowHeight}
|
||||||
|
@ -138,7 +136,7 @@
|
||||||
|
|
||||||
{#if asset.exifInfo?.dateTimeOriginal}
|
{#if asset.exifInfo?.dateTimeOriginal}
|
||||||
{@const assetDateTimeOriginal = DateTime.fromISO(asset.exifInfo.dateTimeOriginal, {
|
{@const assetDateTimeOriginal = DateTime.fromISO(asset.exifInfo.dateTimeOriginal, {
|
||||||
zone: asset.exifInfo.timeZone ?? undefined
|
zone: asset.exifInfo.timeZone ?? undefined,
|
||||||
})}
|
})}
|
||||||
<div class="flex gap-4 py-4">
|
<div class="flex gap-4 py-4">
|
||||||
<div>
|
<div>
|
||||||
|
@ -151,9 +149,9 @@
|
||||||
{
|
{
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
year: 'numeric'
|
year: 'numeric',
|
||||||
},
|
},
|
||||||
{ locale: $locale }
|
{ locale: $locale },
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex gap-2 text-sm">
|
<div class="flex gap-2 text-sm">
|
||||||
|
@ -163,9 +161,9 @@
|
||||||
weekday: 'short',
|
weekday: 'short',
|
||||||
hour: 'numeric',
|
hour: 'numeric',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
timeZoneName: 'longOffset'
|
timeZoneName: 'longOffset',
|
||||||
},
|
},
|
||||||
{ locale: $locale }
|
{ locale: $locale },
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -248,8 +246,7 @@
|
||||||
<TileLayer
|
<TileLayer
|
||||||
urlTemplate={'https://tile.openstreetmap.org/{z}/{x}/{y}.png'}
|
urlTemplate={'https://tile.openstreetmap.org/{z}/{x}/{y}.png'}
|
||||||
options={{
|
options={{
|
||||||
attribution:
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Marker {latlng} popupContent="{latlng[0].toFixed(7)},{latlng[1].toFixed(7)}" />
|
<Marker {latlng} popupContent="{latlng[0].toFixed(7)},{latlng[1].toFixed(7)}" />
|
||||||
|
|
|
@ -18,10 +18,7 @@
|
||||||
<span class="text-immich-primary font-medium">{$downloadAssets[fileName]}</span>/100
|
<span class="text-immich-primary font-medium">{$downloadAssets[fileName]}</span>/100
|
||||||
</p>
|
</p>
|
||||||
<div class="w-full bg-gray-200 rounded-full h-[7px] dark:bg-gray-700">
|
<div class="w-full bg-gray-200 rounded-full h-[7px] dark:bg-gray-700">
|
||||||
<div
|
<div class="bg-immich-primary h-[7px] rounded-full" style={`width: ${$downloadAssets[fileName]}%`} />
|
||||||
class="bg-immich-primary h-[7px] rounded-full"
|
|
||||||
style={`width: ${$downloadAssets[fileName]}%`}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -38,14 +38,14 @@
|
||||||
|
|
||||||
dispatch('intersected', {
|
dispatch('intersected', {
|
||||||
container,
|
container,
|
||||||
position
|
position,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rootMargin,
|
rootMargin,
|
||||||
root
|
root,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
observer.observe(container);
|
observer.observe(container);
|
||||||
|
|
|
@ -3,10 +3,7 @@
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||||
import { api, AssetResponseDto } from '@api';
|
import { api, AssetResponseDto } from '@api';
|
||||||
import {
|
import { notificationController, NotificationType } from '../shared-components/notification/notification';
|
||||||
notificationController,
|
|
||||||
NotificationType
|
|
||||||
} from '../shared-components/notification/notification';
|
|
||||||
import { useZoomImageWheel } from '@zoom-image/svelte';
|
import { useZoomImageWheel } from '@zoom-image/svelte';
|
||||||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
||||||
|
|
||||||
|
@ -32,8 +29,8 @@
|
||||||
const { data } = await api.assetApi.serveFile(
|
const { data } = await api.assetApi.serveFile(
|
||||||
{ id: asset.id, isThumb: false, isWeb: true, key: publicSharedKey },
|
{ id: asset.id, isThumb: false, isWeb: true, key: publicSharedKey },
|
||||||
{
|
{
|
||||||
responseType: 'blob'
|
responseType: 'blob',
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!(data instanceof Blob)) {
|
if (!(data instanceof Blob)) {
|
||||||
|
@ -63,27 +60,27 @@
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
message: 'Copied image to clipboard.',
|
message: 'Copied image to clipboard.',
|
||||||
timeout: 3000
|
timeout: 3000,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error [photo-viewer]:', err);
|
console.error('Error [photo-viewer]:', err);
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
type: NotificationType.Error,
|
type: NotificationType.Error,
|
||||||
message: 'Copying image to clipboard failed.'
|
message: 'Copying image to clipboard failed.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const doZoomImage = async () => {
|
const doZoomImage = async () => {
|
||||||
setZoomImageWheelState({
|
setZoomImageWheelState({
|
||||||
currentZoom: $zoomImageWheelState.currentZoom === 1 ? 2 : 1
|
currentZoom: $zoomImageWheelState.currentZoom === 1 ? 2 : 1,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
createZoomImage: createZoomImageWheel,
|
createZoomImage: createZoomImageWheel,
|
||||||
zoomImageState: zoomImageWheelState,
|
zoomImageState: zoomImageWheelState,
|
||||||
setZoomImageState: setZoomImageWheelState
|
setZoomImageState: setZoomImageWheelState,
|
||||||
} = useZoomImageWheel();
|
} = useZoomImageWheel();
|
||||||
|
|
||||||
zoomImageWheelState.subscribe((state) => {
|
zoomImageWheelState.subscribe((state) => {
|
||||||
|
@ -92,17 +89,14 @@
|
||||||
|
|
||||||
$: if (imgElement) {
|
$: if (imgElement) {
|
||||||
createZoomImageWheel(imgElement, {
|
createZoomImageWheel(imgElement, {
|
||||||
wheelZoomRatio: 0.06
|
wheelZoomRatio: 0.06,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKeypress} on:copyImage={doCopy} on:zoomImage={doZoomImage} />
|
<svelte:window on:keydown={handleKeypress} on:copyImage={doCopy} on:zoomImage={doZoomImage} />
|
||||||
|
|
||||||
<div
|
<div transition:fade={{ duration: 150 }} class="flex place-items-center place-content-center h-full select-none">
|
||||||
transition:fade={{ duration: 150 }}
|
|
||||||
class="flex place-items-center place-content-center h-full select-none"
|
|
||||||
>
|
|
||||||
{#await loadAssetData()}
|
{#await loadAssetData()}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{:then assetData}
|
{:then assetData}
|
||||||
|
|
|
@ -22,10 +22,7 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div transition:fade={{ duration: 150 }} class="flex place-items-center place-content-center h-full select-none">
|
||||||
transition:fade={{ duration: 150 }}
|
|
||||||
class="flex place-items-center place-content-center h-full select-none"
|
|
||||||
>
|
|
||||||
<video
|
<video
|
||||||
controls
|
controls
|
||||||
class="h-full object-contain"
|
class="h-full object-contain"
|
||||||
|
|
|
@ -29,9 +29,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div class="absolute right-0 top-0 text-white text-xs font-medium flex gap-1 place-items-center z-20">
|
||||||
class="absolute right-0 top-0 text-white text-xs font-medium flex gap-1 place-items-center z-20"
|
|
||||||
>
|
|
||||||
{#if showTime}
|
{#if showTime}
|
||||||
<span class="pt-2">
|
<span class="pt-2">
|
||||||
{Duration.fromObject({ seconds: remainingSeconds }).toFormat('m:ss')}
|
{Duration.fromObject({ seconds: remainingSeconds }).toFormat('m:ss')}
|
||||||
|
|
|
@ -8,10 +8,8 @@
|
||||||
export let rounded: Rounded = true;
|
export let rounded: Rounded = true;
|
||||||
|
|
||||||
const colorClasses: Record<Color, string> = {
|
const colorClasses: Record<Color, string> = {
|
||||||
primary:
|
primary: 'text-gray-100 dark:text-immich-dark-gray bg-immich-primary dark:bg-immich-dark-primary',
|
||||||
'text-gray-100 dark:text-immich-dark-gray bg-immich-primary dark:bg-immich-dark-primary',
|
secondary: 'text-immich-dark-bg dark:text-immich-gray dark:bg-gray-600 bg-gray-300 dark:text-immich-gray',
|
||||||
secondary:
|
|
||||||
'text-immich-dark-bg dark:text-immich-gray dark:bg-gray-600 bg-gray-300 dark:text-immich-gray'
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
'dark:text-immich-dark-fg enabled:hover:bg-immich-primary/5 enabled:hover:text-gray-700 enabled:hover:dark:text-immich-dark-fg enabled:dark:hover:bg-immich-dark-primary/25',
|
'dark:text-immich-dark-fg enabled:hover:bg-immich-primary/5 enabled:hover:text-gray-700 enabled:hover:dark:text-immich-dark-fg enabled:dark:hover:bg-immich-dark-primary/25',
|
||||||
'dark-gray':
|
'dark-gray':
|
||||||
'dark:border-immich-dark-gray dark:bg-gray-500 enabled:dark:hover:bg-immich-dark-primary/50 enabled:hover:bg-immich-primary/10 dark:text-white',
|
'dark:border-immich-dark-gray dark:bg-gray-500 enabled:dark:hover:bg-immich-dark-primary/50 enabled:hover:bg-immich-primary/10 dark:text-white',
|
||||||
'overlay-primary': 'text-gray-500 enabled:hover:bg-gray-100'
|
'overlay-primary': 'text-gray-500 enabled:hover:bg-gray-100',
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses: Record<Size, string> = {
|
const sizeClasses: Record<Size, string> = {
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
link: 'p-2 font-medium',
|
link: 'p-2 font-medium',
|
||||||
sm: 'px-4 py-2 text-sm font-medium',
|
sm: 'px-4 py-2 text-sm font-medium',
|
||||||
base: 'px-6 py-3 font-medium',
|
base: 'px-6 py-3 font-medium',
|
||||||
lg: 'px-6 py-4 font-semibold'
|
lg: 'px-6 py-4 font-semibold',
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -44,9 +44,7 @@
|
||||||
<div class="text-immich-primary dark:text-immich-dark-primary font-medium">
|
<div class="text-immich-primary dark:text-immich-dark-primary font-medium">
|
||||||
<Check size="18" />
|
<Check size="18" />
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p class="justify-self-start text-immich-primary dark:text-immich-dark-primary font-medium">
|
||||||
class="justify-self-start text-immich-primary dark:text-immich-dark-primary font-medium"
|
|
||||||
>
|
|
||||||
{option}
|
{option}
|
||||||
</p>
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
@ -35,8 +35,8 @@
|
||||||
email: String(email),
|
email: String(email),
|
||||||
password: String(password),
|
password: String(password),
|
||||||
firstName: String(firstName),
|
firstName: String(firstName),
|
||||||
lastName: String(lastName)
|
lastName: String(lastName),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (status === 201) {
|
if (status === 201) {
|
||||||
|
@ -53,14 +53,7 @@
|
||||||
<form on:submit|preventDefault={registerAdmin} method="post" class="flex flex-col gap-5 mt-5">
|
<form on:submit|preventDefault={registerAdmin} method="post" class="flex flex-col gap-5 mt-5">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="email">Admin Email</label>
|
<label class="immich-form-label" for="email">Admin Email</label>
|
||||||
<input
|
<input class="immich-form-input" id="email" name="email" type="email" autocomplete="email" required />
|
||||||
class="immich-form-input"
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
autocomplete="email"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
|
@ -91,26 +84,12 @@
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="firstName">First Name</label>
|
<label class="immich-form-label" for="firstName">First Name</label>
|
||||||
<input
|
<input class="immich-form-input" id="firstName" name="firstName" type="text" autocomplete="given-name" required />
|
||||||
class="immich-form-input"
|
|
||||||
id="firstName"
|
|
||||||
name="firstName"
|
|
||||||
type="text"
|
|
||||||
autocomplete="given-name"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="lastName">Last Name</label>
|
<label class="immich-form-label" for="lastName">Last Name</label>
|
||||||
<input
|
<input class="immich-form-input" id="lastName" name="lastName" type="text" autocomplete="family-name" required />
|
||||||
class="immich-form-input"
|
|
||||||
id="lastName"
|
|
||||||
name="lastName"
|
|
||||||
type="text"
|
|
||||||
autocomplete="family-name"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
|
|
|
@ -31,13 +31,7 @@
|
||||||
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
|
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="email">Name</label>
|
<label class="immich-form-label" for="email">Name</label>
|
||||||
<input
|
<input class="immich-form-input" id="name" name="name" type="text" bind:value={apiKey.name} />
|
||||||
class="immich-form-input"
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
bind:value={apiKey.name}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex w-full px-4 gap-4 mt-8">
|
<div class="flex w-full px-4 gap-4 mt-8">
|
||||||
|
|
|
@ -3,10 +3,7 @@
|
||||||
import KeyVariant from 'svelte-material-icons/KeyVariant.svelte';
|
import KeyVariant from 'svelte-material-icons/KeyVariant.svelte';
|
||||||
import { handleError } from '../../utils/handle-error';
|
import { handleError } from '../../utils/handle-error';
|
||||||
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
||||||
import {
|
import { notificationController, NotificationType } from '../shared-components/notification/notification';
|
||||||
notificationController,
|
|
||||||
NotificationType
|
|
||||||
} from '../shared-components/notification/notification';
|
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
|
|
||||||
export let secret = '';
|
export let secret = '';
|
||||||
|
@ -24,7 +21,7 @@
|
||||||
await navigator.clipboard.writeText(secret);
|
await navigator.clipboard.writeText(secret);
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Copied to clipboard!',
|
message: 'Copied to clipboard!',
|
||||||
type: NotificationType.Info
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to copy to clipboard');
|
handleError(error, 'Unable to copy to clipboard');
|
||||||
|
@ -40,9 +37,7 @@
|
||||||
class="flex flex-col place-items-center place-content-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
|
class="flex flex-col place-items-center place-content-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
|
||||||
>
|
>
|
||||||
<KeyVariant size="4em" />
|
<KeyVariant size="4em" />
|
||||||
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">
|
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">API Key</h1>
|
||||||
API Key
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p class="text-sm dark:text-immich-dark-fg">
|
<p class="text-sm dark:text-immich-dark-fg">
|
||||||
This value will only be shown once. Please be sure to copy it before closing the window.
|
This value will only be shown once. Please be sure to copy it before closing the window.
|
||||||
|
@ -51,13 +46,7 @@
|
||||||
|
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
<!-- <label class="immich-form-label" for="email">API Key</label> -->
|
<!-- <label class="immich-form-label" for="email">API Key</label> -->
|
||||||
<textarea
|
<textarea class="immich-form-input" id="secret" name="secret" readonly={true} value={secret} />
|
||||||
class="immich-form-input"
|
|
||||||
id="secret"
|
|
||||||
name="secret"
|
|
||||||
readonly={true}
|
|
||||||
value={secret}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex w-full px-4 gap-4 mt-8">
|
<div class="flex w-full px-4 gap-4 mt-8">
|
||||||
|
|
|
@ -32,8 +32,8 @@
|
||||||
updateUserDto: {
|
updateUserDto: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
password: String(password),
|
password: String(password),
|
||||||
shouldChangePassword: false
|
shouldChangePassword: false,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (status === 200) {
|
if (status === 200) {
|
||||||
|
|
|
@ -2,10 +2,7 @@
|
||||||
import { api } from '@api';
|
import { api } from '@api';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import ImmichLogo from '../shared-components/immich-logo.svelte';
|
import ImmichLogo from '../shared-components/immich-logo.svelte';
|
||||||
import {
|
import { notificationController, NotificationType } from '../shared-components/notification/notification';
|
||||||
notificationController,
|
|
||||||
NotificationType
|
|
||||||
} from '../shared-components/notification/notification';
|
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
|
|
||||||
let error: string;
|
let error: string;
|
||||||
|
@ -50,8 +47,8 @@
|
||||||
email: String(email),
|
email: String(email),
|
||||||
password: String(password),
|
password: String(password),
|
||||||
firstName: String(firstName),
|
firstName: String(firstName),
|
||||||
lastName: String(lastName)
|
lastName: String(lastName),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (status === 201) {
|
if (status === 201) {
|
||||||
|
@ -73,7 +70,7 @@
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: `Error create new user, check console for more detail`,
|
message: `Error create new user, check console for more detail`,
|
||||||
type: NotificationType.Error
|
type: NotificationType.Error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,14 +82,9 @@
|
||||||
>
|
>
|
||||||
<div class="flex flex-col place-items-center place-content-center gap-4 px-4">
|
<div class="flex flex-col place-items-center place-content-center gap-4 px-4">
|
||||||
<ImmichLogo class="text-center" height="100" width="100" />
|
<ImmichLogo class="text-center" height="100" width="100" />
|
||||||
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">
|
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">Create new user</h1>
|
||||||
Create new user
|
<p class="text-sm border rounded-md p-4 font-mono text-gray-600 dark:border-immich-dark-bg dark:text-gray-300">
|
||||||
</h1>
|
Please provide your user with the password, they will have to change it on their first sign in.
|
||||||
<p
|
|
||||||
class="text-sm border rounded-md p-4 font-mono text-gray-600 dark:border-immich-dark-bg dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Please provide your user with the password, they will have to change it on their first sign
|
|
||||||
in.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -104,14 +96,7 @@
|
||||||
|
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="password">Password</label>
|
<label class="immich-form-label" for="password">Password</label>
|
||||||
<input
|
<input class="immich-form-input" id="password" name="password" type="password" required bind:value={password} />
|
||||||
class="immich-form-input"
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
bind:value={password}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
|
|
|
@ -2,10 +2,7 @@
|
||||||
import { api, UserResponseDto } from '@api';
|
import { api, UserResponseDto } from '@api';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import AccountEditOutline from 'svelte-material-icons/AccountEditOutline.svelte';
|
import AccountEditOutline from 'svelte-material-icons/AccountEditOutline.svelte';
|
||||||
import {
|
import { notificationController, NotificationType } from '../shared-components/notification/notification';
|
||||||
notificationController,
|
|
||||||
NotificationType
|
|
||||||
} from '../shared-components/notification/notification';
|
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||||
import { handleError } from '../../utils/handle-error';
|
import { handleError } from '../../utils/handle-error';
|
||||||
|
@ -30,8 +27,8 @@
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
storageLabel: storageLabel || '',
|
storageLabel: storageLabel || '',
|
||||||
externalPath: externalPath || ''
|
externalPath: externalPath || '',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (status === 200) {
|
if (status === 200) {
|
||||||
|
@ -50,8 +47,8 @@
|
||||||
updateUserDto: {
|
updateUserDto: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
password: defaultPassword,
|
password: defaultPassword,
|
||||||
shouldChangePassword: true
|
shouldChangePassword: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (status == 200) {
|
if (status == 200) {
|
||||||
|
@ -61,7 +58,7 @@
|
||||||
console.error('Error reseting user password', e);
|
console.error('Error reseting user password', e);
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Error reseting user password, check console for more details',
|
message: 'Error reseting user password, check console for more details',
|
||||||
type: NotificationType.Error
|
type: NotificationType.Error,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
isShowResetPasswordConfirmation = false;
|
isShowResetPasswordConfirmation = false;
|
||||||
|
@ -76,21 +73,13 @@
|
||||||
class="flex flex-col place-items-center place-content-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
|
class="flex flex-col place-items-center place-content-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
|
||||||
>
|
>
|
||||||
<AccountEditOutline size="4em" />
|
<AccountEditOutline size="4em" />
|
||||||
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">
|
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">Edit user</h1>
|
||||||
Edit user
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form on:submit|preventDefault={editUser} autocomplete="off">
|
<form on:submit|preventDefault={editUser} autocomplete="off">
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="email">Email</label>
|
<label class="immich-form-label" for="email">Email</label>
|
||||||
<input
|
<input class="immich-form-input" id="email" name="email" type="email" bind:value={user.email} />
|
||||||
class="immich-form-input"
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
bind:value={user.email}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
|
@ -107,14 +96,7 @@
|
||||||
|
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="lastName">Last Name</label>
|
<label class="immich-form-label" for="lastName">Last Name</label>
|
||||||
<input
|
<input class="immich-form-input" id="lastName" name="lastName" type="text" required bind:value={user.lastName} />
|
||||||
class="immich-form-input"
|
|
||||||
id="lastName"
|
|
||||||
name="lastName"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
bind:value={user.lastName}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
|
@ -146,8 +128,8 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Note: Absolute path of parent import directory. A user can only import files if they exist
|
Note: Absolute path of parent import directory. A user can only import files if they exist at or under this
|
||||||
at or under this path.
|
path.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -160,10 +142,8 @@
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex w-full px-4 gap-4 mt-8">
|
<div class="flex w-full px-4 gap-4 mt-8">
|
||||||
{#if canResetPassword}
|
{#if canResetPassword}
|
||||||
<Button
|
<Button color="light-red" fullwidth on:click={() => (isShowResetPasswordConfirmation = true)}
|
||||||
color="light-red"
|
>Reset password</Button
|
||||||
fullwidth
|
|
||||||
on:click={() => (isShowResetPasswordConfirmation = true)}>Reset password</Button
|
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
<Button type="submit" fullwidth>Confirm</Button>
|
<Button type="submit" fullwidth>Confirm</Button>
|
||||||
|
|
|
@ -59,8 +59,8 @@
|
||||||
const { data } = await api.authenticationApi.login({
|
const { data } = await api.authenticationApi.login({
|
||||||
loginCredentialDto: {
|
loginCredentialDto: {
|
||||||
email,
|
email,
|
||||||
password
|
password,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!data.isAdmin && data.shouldChangePassword) {
|
if (!data.isAdmin && data.shouldChangePassword) {
|
||||||
|
|
|
@ -33,9 +33,7 @@
|
||||||
<slot name="buttons" />
|
<slot name="buttons" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="absolute overflow-y-auto top-16 h-[calc(100%-theme(spacing.16))] w-full immich-scrollbar p-4 pb-8">
|
||||||
class="absolute overflow-y-auto top-16 h-[calc(100%-theme(spacing.16))] w-full immich-scrollbar p-4 pb-8"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -22,9 +22,7 @@
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-8 border bg-white dark:bg-immich-dark-gray dark:border-immich-dark-gray p-8 shadow-sm w-96 max-w-lg rounded-3xl"
|
class="flex flex-col gap-8 border bg-white dark:bg-immich-dark-gray dark:border-immich-dark-gray p-8 shadow-sm w-96 max-w-lg rounded-3xl"
|
||||||
>
|
>
|
||||||
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium self-center">
|
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium self-center">Map Settings</h1>
|
||||||
Map Settings
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<form
|
<form
|
||||||
on:submit|preventDefault={() => dispatch('save', settings)}
|
on:submit|preventDefault={() => dispatch('save', settings)}
|
||||||
|
@ -46,12 +44,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center gap-8">
|
<div class="flex justify-between items-center gap-8">
|
||||||
<label class="immich-form-label text-sm shrink-0" for="date-before">Date before</label>
|
<label class="immich-form-label text-sm shrink-0" for="date-before">Date before</label>
|
||||||
<input
|
<input class="immich-form-input w-40" type="date" id="date-before" bind:value={settings.dateBefore} />
|
||||||
class="immich-form-input w-40"
|
|
||||||
type="date"
|
|
||||||
id="date-before"
|
|
||||||
bind:value={settings.dateBefore}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center text-xs">
|
<div class="flex justify-center text-xs">
|
||||||
<LinkButton
|
<LinkButton
|
||||||
|
@ -74,28 +67,28 @@
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
value: '',
|
value: '',
|
||||||
text: 'All'
|
text: 'All',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: Duration.fromObject({ hours: 24 }).toISO() || '',
|
value: Duration.fromObject({ hours: 24 }).toISO() || '',
|
||||||
text: 'Past 24 hours'
|
text: 'Past 24 hours',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: Duration.fromObject({ days: 7 }).toISO() || '',
|
value: Duration.fromObject({ days: 7 }).toISO() || '',
|
||||||
text: 'Past 7 days'
|
text: 'Past 7 days',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: Duration.fromObject({ days: 30 }).toISO() || '',
|
value: Duration.fromObject({ days: 30 }).toISO() || '',
|
||||||
text: 'Past 30 days'
|
text: 'Past 30 days',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: Duration.fromObject({ years: 1 }).toISO() || '',
|
value: Duration.fromObject({ years: 1 }).toISO() || '',
|
||||||
text: 'Past year'
|
text: 'Past year',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: Duration.fromObject({ years: 3 }).toISO() || '',
|
value: Duration.fromObject({ years: 3 }).toISO() || '',
|
||||||
text: 'Past 3 years'
|
text: 'Past 3 years',
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<div class="text-xs">
|
<div class="text-xs">
|
||||||
|
|
|
@ -20,8 +20,7 @@
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import { tweened } from 'svelte/motion';
|
import { tweened } from 'svelte/motion';
|
||||||
|
|
||||||
const parseIndex = (s: string | null, max: number | null) =>
|
const parseIndex = (s: string | null, max: number | null) => Math.max(Math.min(parseInt(s ?? '') || 0, max ?? 0), 0);
|
||||||
Math.max(Math.min(parseInt(s ?? '') || 0, max ?? 0), 0);
|
|
||||||
|
|
||||||
$: memoryIndex = parseIndex($page.url.searchParams.get('memory'), $memoryStore?.length - 1);
|
$: memoryIndex = parseIndex($page.url.searchParams.get('memory'), $memoryStore?.length - 1);
|
||||||
$: assetIndex = parseIndex($page.url.searchParams.get('asset'), currentMemory?.assets.length - 1);
|
$: assetIndex = parseIndex($page.url.searchParams.get('asset'), currentMemory?.assets.length - 1);
|
||||||
|
@ -47,7 +46,7 @@
|
||||||
const toPrevious = () => (previousAsset ? toPreviousAsset() : toPreviousMemory());
|
const toPrevious = () => (previousAsset ? toPreviousAsset() : toPreviousMemory());
|
||||||
|
|
||||||
const progress = tweened<number>(0, {
|
const progress = tweened<number>(0, {
|
||||||
duration: (from: number, to: number) => (to ? 5000 * (to - from) : 0)
|
duration: (from: number, to: number) => (to ? 5000 * (to - from) : 0),
|
||||||
});
|
});
|
||||||
|
|
||||||
const play = () => progress.set(1);
|
const play = () => progress.set(1);
|
||||||
|
@ -89,7 +88,7 @@
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (!$memoryStore) {
|
if (!$memoryStore) {
|
||||||
const { data } = await api.assetApi.getMemoryLane({
|
const { data } = await api.assetApi.getMemoryLane({
|
||||||
timestamp: DateTime.local().startOf('day').toISO() || ''
|
timestamp: DateTime.local().startOf('day').toISO() || '',
|
||||||
});
|
});
|
||||||
$memoryStore = data;
|
$memoryStore = data;
|
||||||
}
|
}
|
||||||
|
@ -113,23 +112,13 @@
|
||||||
|
|
||||||
{#if !galleryInView}
|
{#if !galleryInView}
|
||||||
<div class="flex place-items-center place-content-center overflow-hidden gap-2">
|
<div class="flex place-items-center place-content-center overflow-hidden gap-2">
|
||||||
<CircleIconButton
|
<CircleIconButton logo={paused ? Play : Pause} forceDark on:click={() => (paused = !paused)} />
|
||||||
logo={paused ? Play : Pause}
|
|
||||||
forceDark
|
|
||||||
on:click={() => (paused = !paused)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#each currentMemory.assets as _, i}
|
{#each currentMemory.assets as _, i}
|
||||||
<button
|
<button class="relative w-full py-2" on:click={() => goto(`?memory=${memoryIndex}&asset=${i}`)}>
|
||||||
class="relative w-full py-2"
|
|
||||||
on:click={() => goto(`?memory=${memoryIndex}&asset=${i}`)}
|
|
||||||
>
|
|
||||||
<span class="absolute left-0 w-full h-[2px] bg-gray-500" />
|
<span class="absolute left-0 w-full h-[2px] bg-gray-500" />
|
||||||
{#await resetPromise}
|
{#await resetPromise}
|
||||||
<span
|
<span class="absolute left-0 h-[2px] bg-white" style:width={`${i < assetIndex ? 100 : 0}%`} />
|
||||||
class="absolute left-0 h-[2px] bg-white"
|
|
||||||
style:width={`${i < assetIndex ? 100 : 0}%`}
|
|
||||||
/>
|
|
||||||
{:then}
|
{:then}
|
||||||
<span
|
<span
|
||||||
class="absolute left-0 h-[2px] bg-white"
|
class="absolute left-0 h-[2px] bg-white"
|
||||||
|
@ -154,10 +143,7 @@
|
||||||
class:opacity-0={!galleryInView}
|
class:opacity-0={!galleryInView}
|
||||||
class:opacity-100={galleryInView}
|
class:opacity-100={galleryInView}
|
||||||
>
|
>
|
||||||
<button
|
<button on:click={() => memoryWrapper.scrollIntoView({ behavior: 'smooth' })} disabled={!galleryInView}>
|
||||||
on:click={() => memoryWrapper.scrollIntoView({ behavior: 'smooth' })}
|
|
||||||
disabled={!galleryInView}
|
|
||||||
>
|
|
||||||
<CircleIconButton logo={ChevronUp} backgroundColor="white" forceDark />
|
<CircleIconButton logo={ChevronUp} backgroundColor="white" forceDark />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -174,16 +160,10 @@
|
||||||
class:opacity-0={!previousMemory}
|
class:opacity-0={!previousMemory}
|
||||||
class:hover:opacity-70={previousMemory}
|
class:hover:opacity-70={previousMemory}
|
||||||
>
|
>
|
||||||
<button
|
<button class="rounded-2xl h-full w-full relative" disabled={!previousMemory} on:click={toPreviousMemory}>
|
||||||
class="rounded-2xl h-full w-full relative"
|
|
||||||
disabled={!previousMemory}
|
|
||||||
on:click={toPreviousMemory}
|
|
||||||
>
|
|
||||||
<img
|
<img
|
||||||
class="rounded-2xl h-full w-full object-cover"
|
class="rounded-2xl h-full w-full object-cover"
|
||||||
src={previousMemory
|
src={previousMemory ? api.getAssetThumbnailUrl(previousMemory.assets[0].id, 'JPEG') : noThumbnailUrl}
|
||||||
? api.getAssetThumbnailUrl(previousMemory.assets[0].id, 'JPEG')
|
|
||||||
: noThumbnailUrl}
|
|
||||||
alt=""
|
alt=""
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
|
@ -207,22 +187,14 @@
|
||||||
<div class="flex h-full flex-col place-content-center place-items-center ml-4">
|
<div class="flex h-full flex-col place-content-center place-items-center ml-4">
|
||||||
<div class="inline-block">
|
<div class="inline-block">
|
||||||
{#if canGoBack}
|
{#if canGoBack}
|
||||||
<CircleIconButton
|
<CircleIconButton logo={ChevronLeft} backgroundColor="#202123" on:click={toPrevious} />
|
||||||
logo={ChevronLeft}
|
|
||||||
backgroundColor="#202123"
|
|
||||||
on:click={toPrevious}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex h-full flex-col place-content-center place-items-center mr-4">
|
<div class="flex h-full flex-col place-content-center place-items-center mr-4">
|
||||||
<div class="inline-block">
|
<div class="inline-block">
|
||||||
{#if canGoForward}
|
{#if canGoForward}
|
||||||
<CircleIconButton
|
<CircleIconButton logo={ChevronRight} backgroundColor="#202123" on:click={toNext} />
|
||||||
logo={ChevronRight}
|
|
||||||
backgroundColor="#202123"
|
|
||||||
on:click={toNext}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -240,9 +212,7 @@
|
||||||
|
|
||||||
<div class="absolute top-4 left-8 text-white text-sm font-medium">
|
<div class="absolute top-4 left-8 text-white text-sm font-medium">
|
||||||
<p>
|
<p>
|
||||||
{DateTime.fromISO(currentMemory.assets[0].fileCreatedAt).toLocaleString(
|
{DateTime.fromISO(currentMemory.assets[0].fileCreatedAt).toLocaleString(DateTime.DATE_FULL)}
|
||||||
DateTime.DATE_FULL
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{currentAsset.exifInfo?.city || ''}
|
{currentAsset.exifInfo?.city || ''}
|
||||||
|
@ -259,16 +229,10 @@
|
||||||
class:opacity-0={!nextMemory}
|
class:opacity-0={!nextMemory}
|
||||||
class:hover:opacity-70={nextMemory}
|
class:hover:opacity-70={nextMemory}
|
||||||
>
|
>
|
||||||
<button
|
<button class="rounded-2xl h-full w-full relative" on:click={toNextMemory} disabled={!nextMemory}>
|
||||||
class="rounded-2xl h-full w-full relative"
|
|
||||||
on:click={toNextMemory}
|
|
||||||
disabled={!nextMemory}
|
|
||||||
>
|
|
||||||
<img
|
<img
|
||||||
class="rounded-2xl h-full w-full object-cover"
|
class="rounded-2xl h-full w-full object-cover"
|
||||||
src={nextMemory
|
src={nextMemory ? api.getAssetThumbnailUrl(nextMemory.assets[0].id, 'JPEG') : noThumbnailUrl}
|
||||||
? api.getAssetThumbnailUrl(nextMemory.assets[0].id, 'JPEG')
|
|
||||||
: noThumbnailUrl}
|
|
||||||
alt=""
|
alt=""
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||||
import {
|
import {
|
||||||
NotificationType,
|
NotificationType,
|
||||||
notificationController
|
notificationController,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { addAssetsToAlbum } from '$lib/utils/asset-utils';
|
import { addAssetsToAlbum } from '$lib/utils/asset-utils';
|
||||||
import { AlbumResponseDto, api } from '@api';
|
import { AlbumResponseDto, api } from '@api';
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: `Added ${assetIds.length} to ${albumName}`,
|
message: `Added ${assetIds.length} to ${albumName}`,
|
||||||
type: NotificationType.Info
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
|
|
||||||
clearSelect();
|
clearSelect();
|
||||||
|
@ -51,10 +51,7 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MenuOption
|
<MenuOption on:click={() => (showAlbumPicker = true)} text={shared ? 'Add to Shared Album' : 'Add to Album'} />
|
||||||
on:click={() => (showAlbumPicker = true)}
|
|
||||||
text={shared ? 'Add to Shared Album' : 'Add to Album'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if showAlbumPicker}
|
{#if showAlbumPicker}
|
||||||
<AlbumSelectionModal
|
<AlbumSelectionModal
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
import {
|
import {
|
||||||
NotificationType,
|
NotificationType,
|
||||||
notificationController
|
notificationController,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { api } from '@api';
|
import { api } from '@api';
|
||||||
import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
|
import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: `${isArchived ? 'Archived' : 'Unarchived'} ${cnt}`,
|
message: `${isArchived ? 'Archived' : 'Unarchived'} ${cnt}`,
|
||||||
type: NotificationType.Info
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
|
|
||||||
clearSelect();
|
clearSelect();
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
import {
|
import {
|
||||||
NotificationType,
|
NotificationType,
|
||||||
notificationController
|
notificationController,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { api } from '@api';
|
import { api } from '@api';
|
||||||
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
||||||
|
@ -21,8 +21,8 @@
|
||||||
|
|
||||||
const { data: deletedAssets } = await api.assetApi.deleteAsset({
|
const { data: deletedAssets } = await api.assetApi.deleteAsset({
|
||||||
deleteAssetDto: {
|
deleteAssetDto: {
|
||||||
ids: Array.from(getAssets()).map((a) => a.id)
|
ids: Array.from(getAssets()).map((a) => a.id),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const asset of deletedAssets) {
|
for (const asset of deletedAssets) {
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: `Deleted ${count}`,
|
message: `Deleted ${count}`,
|
||||||
type: NotificationType.Info
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
|
|
||||||
clearSelect();
|
clearSelect();
|
||||||
|
@ -46,11 +46,7 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CircleIconButton
|
<CircleIconButton title="Delete" logo={DeleteOutline} on:click={() => (isShowConfirmation = true)} />
|
||||||
title="Delete"
|
|
||||||
logo={DeleteOutline}
|
|
||||||
on:click={() => (isShowConfirmation = true)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if isShowConfirmation}
|
{#if isShowConfirmation}
|
||||||
<ConfirmDialogue
|
<ConfirmDialogue
|
||||||
|
|
|
@ -19,12 +19,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await downloadArchive(
|
await downloadArchive(filename, { assetIds: assets.map((asset) => asset.id) }, clearSelect, sharedLinkKey);
|
||||||
filename,
|
|
||||||
{ assetIds: assets.map((asset) => asset.id) },
|
|
||||||
clearSelect,
|
|
||||||
sharedLinkKey
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||||
import {
|
import {
|
||||||
NotificationType,
|
NotificationType,
|
||||||
notificationController
|
notificationController,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { api } from '@api';
|
import { api } from '@api';
|
||||||
import HeartMinusOutline from 'svelte-material-icons/HeartMinusOutline.svelte';
|
import HeartMinusOutline from 'svelte-material-icons/HeartMinusOutline.svelte';
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: isFavorite ? `Added ${cnt} to favorites` : `Removed ${cnt} from favorites`,
|
message: isFavorite ? `Added ${cnt} to favorites` : `Removed ${cnt} from favorites`,
|
||||||
type: NotificationType.Info
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
|
|
||||||
clearSelect();
|
clearSelect();
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
import {
|
import {
|
||||||
NotificationType,
|
NotificationType,
|
||||||
notificationController
|
notificationController,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { AlbumResponseDto, api } from '@api';
|
import { AlbumResponseDto, api } from '@api';
|
||||||
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
||||||
|
@ -20,8 +20,8 @@
|
||||||
const { data } = await api.albumApi.removeAssetFromAlbum({
|
const { data } = await api.albumApi.removeAssetFromAlbum({
|
||||||
id: album.id,
|
id: album.id,
|
||||||
removeAssetsDto: {
|
removeAssetsDto: {
|
||||||
assetIds: Array.from(getAssets()).map((a) => a.id)
|
assetIds: Array.from(getAssets()).map((a) => a.id),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
album = data;
|
album = data;
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
console.error('Error [album-viewer] [removeAssetFromAlbum]', e);
|
console.error('Error [album-viewer] [removeAssetFromAlbum]', e);
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
type: NotificationType.Error,
|
type: NotificationType.Error,
|
||||||
message: 'Error removing assets from album, check console for more details'
|
message: 'Error removing assets from album, check console for more details',
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
isShowConfirmation = false;
|
isShowConfirmation = false;
|
||||||
|
@ -38,11 +38,7 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CircleIconButton
|
<CircleIconButton title="Remove from album" on:click={() => (isShowConfirmation = true)} logo={DeleteOutline} />
|
||||||
title="Remove from album"
|
|
||||||
on:click={() => (isShowConfirmation = true)}
|
|
||||||
logo={DeleteOutline}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if isShowConfirmation}
|
{#if isShowConfirmation}
|
||||||
<ConfirmDialogue
|
<ConfirmDialogue
|
||||||
|
|
|
@ -4,10 +4,7 @@
|
||||||
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
||||||
import ConfirmDialogue from '../../shared-components/confirm-dialogue.svelte';
|
import ConfirmDialogue from '../../shared-components/confirm-dialogue.svelte';
|
||||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
import {
|
import { NotificationType, notificationController } from '../../shared-components/notification/notification';
|
||||||
NotificationType,
|
|
||||||
notificationController
|
|
||||||
} from '../../shared-components/notification/notification';
|
|
||||||
import { handleError } from '../../../utils/handle-error';
|
import { handleError } from '../../../utils/handle-error';
|
||||||
|
|
||||||
export let sharedLink: SharedLinkResponseDto;
|
export let sharedLink: SharedLinkResponseDto;
|
||||||
|
@ -21,9 +18,9 @@
|
||||||
const { data: results } = await api.sharedLinkApi.removeSharedLinkAssets({
|
const { data: results } = await api.sharedLinkApi.removeSharedLinkAssets({
|
||||||
id: sharedLink.id,
|
id: sharedLink.id,
|
||||||
assetIdsDto: {
|
assetIdsDto: {
|
||||||
assetIds: Array.from(getAssets()).map((asset) => asset.id)
|
assetIds: Array.from(getAssets()).map((asset) => asset.id),
|
||||||
},
|
},
|
||||||
key: sharedLink.key
|
key: sharedLink.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
|
@ -38,7 +35,7 @@
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
message: `Removed ${count} assets`
|
message: `Removed ${count} assets`,
|
||||||
});
|
});
|
||||||
|
|
||||||
clearSelect();
|
clearSelect();
|
||||||
|
@ -48,11 +45,7 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CircleIconButton
|
<CircleIconButton title="Remove from shared link" on:click={() => (removing = true)} logo={DeleteOutline} />
|
||||||
title="Remove from shared link"
|
|
||||||
on:click={() => (removing = true)}
|
|
||||||
logo={DeleteOutline}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if removing}
|
{#if removing}
|
||||||
<ConfirmDialogue
|
<ConfirmDialogue
|
||||||
|
|
|
@ -18,10 +18,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 0; i < _assetGridState.buckets.length; i++) {
|
for (let i = 0; i < _assetGridState.buckets.length; i++) {
|
||||||
await assetStore.getAssetsByBucket(
|
await assetStore.getAssetsByBucket(_assetGridState.buckets[i].bucketDate, BucketPosition.Unknown);
|
||||||
_assetGridState.buckets[i].bucketDate,
|
|
||||||
BucketPosition.Unknown
|
|
||||||
);
|
|
||||||
for (const asset of _assetGridState.buckets[i].assets) {
|
for (const asset of _assetGridState.buckets[i].assets) {
|
||||||
assetInteractionStore.addAssetToMultiselectGroup(asset);
|
assetInteractionStore.addAssetToMultiselectGroup(asset);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
assetsInAlbumStoreState,
|
assetsInAlbumStoreState,
|
||||||
isMultiSelectStoreState,
|
isMultiSelectStoreState,
|
||||||
selectedAssets,
|
selectedAssets,
|
||||||
selectedGroup
|
selectedGroup,
|
||||||
} from '$lib/stores/asset-interaction.store';
|
} from '$lib/stores/asset-interaction.store';
|
||||||
import { assetStore } from '$lib/stores/assets.store';
|
import { assetStore } from '$lib/stores/assets.store';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
weekday: 'short',
|
weekday: 'short',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
year: 'numeric'
|
year: 'numeric',
|
||||||
};
|
};
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
@ -57,11 +57,11 @@
|
||||||
containerWidth: Math.floor(viewportWidth),
|
containerWidth: Math.floor(viewportWidth),
|
||||||
containerPadding: 0,
|
containerPadding: 0,
|
||||||
targetRowHeightTolerance: 0.15,
|
targetRowHeightTolerance: 0.15,
|
||||||
targetRowHeight: 235
|
targetRowHeight: 235,
|
||||||
});
|
});
|
||||||
geometry.push({
|
geometry.push({
|
||||||
...justifiedLayoutResult,
|
...justifiedLayoutResult,
|
||||||
containerWidth: calculateWidth(justifiedLayoutResult.boxes)
|
containerWidth: calculateWidth(justifiedLayoutResult.boxes),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return geometry;
|
return geometry;
|
||||||
|
@ -78,7 +78,7 @@
|
||||||
|
|
||||||
function scrollTimeline(heightDelta: number) {
|
function scrollTimeline(heightDelta: number) {
|
||||||
dispatch('shift', {
|
dispatch('shift', {
|
||||||
heightDelta
|
heightDelta,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@
|
||||||
const assetClickHandler = (
|
const assetClickHandler = (
|
||||||
asset: AssetResponseDto,
|
asset: AssetResponseDto,
|
||||||
assetsInDateGroup: AssetResponseDto[],
|
assetsInDateGroup: AssetResponseDto[],
|
||||||
dateGroupTitle: string
|
dateGroupTitle: string,
|
||||||
) => {
|
) => {
|
||||||
if (isAlbumSelectionMode) {
|
if (isAlbumSelectionMode) {
|
||||||
assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle);
|
assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle);
|
||||||
|
@ -110,10 +110,7 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectAssetGroupHandler = (
|
const selectAssetGroupHandler = (selectAssetGroupHandler: AssetResponseDto[], dateGroupTitle: string) => {
|
||||||
selectAssetGroupHandler: AssetResponseDto[],
|
|
||||||
dateGroupTitle: string
|
|
||||||
) => {
|
|
||||||
if ($selectedGroup.has(dateGroupTitle)) {
|
if ($selectedGroup.has(dateGroupTitle)) {
|
||||||
assetInteractionStore.removeGroupFromMultiselectGroup(dateGroupTitle);
|
assetInteractionStore.removeGroupFromMultiselectGroup(dateGroupTitle);
|
||||||
selectAssetGroupHandler.forEach((asset) => {
|
selectAssetGroupHandler.forEach((asset) => {
|
||||||
|
@ -130,7 +127,7 @@
|
||||||
const assetSelectHandler = (
|
const assetSelectHandler = (
|
||||||
asset: AssetResponseDto,
|
asset: AssetResponseDto,
|
||||||
assetsInDateGroup: AssetResponseDto[],
|
assetsInDateGroup: AssetResponseDto[],
|
||||||
dateGroupTitle: string
|
dateGroupTitle: string,
|
||||||
) => {
|
) => {
|
||||||
if ($selectedAssets.has(asset)) {
|
if ($selectedAssets.has(asset)) {
|
||||||
assetInteractionStore.removeAssetFromMultiselectGroup(asset);
|
assetInteractionStore.removeAssetFromMultiselectGroup(asset);
|
||||||
|
@ -167,10 +164,7 @@
|
||||||
bind:clientWidth={viewportWidth}
|
bind:clientWidth={viewportWidth}
|
||||||
>
|
>
|
||||||
{#each assetsGroupByDate as assetsInDateGroup, groupIndex (assetsInDateGroup[0].id)}
|
{#each assetsGroupByDate as assetsInDateGroup, groupIndex (assetsInDateGroup[0].id)}
|
||||||
{@const dateGroupTitle = new Date(assetsInDateGroup[0].fileCreatedAt).toLocaleDateString(
|
{@const dateGroupTitle = new Date(assetsInDateGroup[0].fileCreatedAt).toLocaleDateString($locale, groupDateFormat)}
|
||||||
$locale,
|
|
||||||
groupDateFormat
|
|
||||||
)}
|
|
||||||
<!-- Asset Group By Date -->
|
<!-- Asset Group By Date -->
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -209,8 +203,7 @@
|
||||||
<!-- Image grid -->
|
<!-- Image grid -->
|
||||||
<div
|
<div
|
||||||
class="relative"
|
class="relative"
|
||||||
style="height: {geometry[groupIndex].containerHeight}px;width: {geometry[groupIndex]
|
style="height: {geometry[groupIndex].containerHeight}px;width: {geometry[groupIndex].containerWidth}px"
|
||||||
.containerWidth}px"
|
|
||||||
>
|
>
|
||||||
{#each assetsInDateGroup as asset, index (asset.id)}
|
{#each assetsInDateGroup as asset, index (asset.id)}
|
||||||
{@const box = geometry[groupIndex].boxes[index]}
|
{@const box = geometry[groupIndex].boxes[index]}
|
||||||
|
@ -224,8 +217,7 @@
|
||||||
on:click={() => assetClickHandler(asset, assetsInDateGroup, dateGroupTitle)}
|
on:click={() => assetClickHandler(asset, assetsInDateGroup, dateGroupTitle)}
|
||||||
on:select={() => assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle)}
|
on:select={() => assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle)}
|
||||||
on:mouse-event={() => assetMouseEventHandler(dateGroupTitle)}
|
on:mouse-event={() => assetMouseEventHandler(dateGroupTitle)}
|
||||||
selected={$selectedAssets.has(asset) ||
|
selected={$selectedAssets.has(asset) || $assetsInAlbumStoreState.findIndex((a) => a.id == asset.id) != -1}
|
||||||
$assetsInAlbumStoreState.findIndex((a) => a.id == asset.id) != -1}
|
|
||||||
disabled={$assetsInAlbumStoreState.findIndex((a) => a.id == asset.id) != -1}
|
disabled={$assetsInAlbumStoreState.findIndex((a) => a.id == asset.id) != -1}
|
||||||
thumbnailWidth={box.width}
|
thumbnailWidth={box.width}
|
||||||
thumbnailHeight={box.height}
|
thumbnailHeight={box.height}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import {
|
import {
|
||||||
assetInteractionStore,
|
assetInteractionStore,
|
||||||
isViewingAssetStoreState,
|
isViewingAssetStoreState,
|
||||||
viewingAssetStoreState
|
viewingAssetStoreState,
|
||||||
} from '$lib/stores/asset-interaction.store';
|
} from '$lib/stores/asset-interaction.store';
|
||||||
import { assetGridState, assetStore, loadingBucketState } from '$lib/stores/assets.store';
|
import { assetGridState, assetStore, loadingBucketState } from '$lib/stores/assets.store';
|
||||||
import type { UserResponseDto } from '@api';
|
import type { UserResponseDto } from '@api';
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
import Portal from '../shared-components/portal/portal.svelte';
|
import Portal from '../shared-components/portal/portal.svelte';
|
||||||
import Scrollbar, {
|
import Scrollbar, {
|
||||||
OnScrollbarClickDetail,
|
OnScrollbarClickDetail,
|
||||||
OnScrollbarDragDetail
|
OnScrollbarDragDetail,
|
||||||
} from '../shared-components/scrollbar/scrollbar.svelte';
|
} from '../shared-components/scrollbar/scrollbar.svelte';
|
||||||
import AssetDateGroup from './asset-date-group.svelte';
|
import AssetDateGroup from './asset-date-group.svelte';
|
||||||
import { BucketPosition } from '$lib/models/asset-grid-state';
|
import { BucketPosition } from '$lib/models/asset-grid-state';
|
||||||
|
@ -33,8 +33,8 @@
|
||||||
getAssetCountByTimeBucketDto: {
|
getAssetCountByTimeBucketDto: {
|
||||||
timeGroup: TimeGroupEnum.Month,
|
timeGroup: TimeGroupEnum.Month,
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
withoutThumbs: true
|
withoutThumbs: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
bucketInfo = assetCountByTimebucket;
|
bucketInfo = assetCountByTimebucket;
|
||||||
|
|
|
@ -27,11 +27,7 @@
|
||||||
setContext({ getAssets: () => assets, clearSelect });
|
setContext({ getAssets: () => assets, clearSelect });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ControlAppBar
|
<ControlAppBar on:close-button-click={clearSelect} backIcon={Close} tailwindClasses="bg-white shadow-md">
|
||||||
on:close-button-click={clearSelect}
|
|
||||||
backIcon={Close}
|
|
||||||
tailwindClasses="bg-white shadow-md"
|
|
||||||
>
|
|
||||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary" slot="leading">
|
<p class="font-medium text-immich-primary dark:text-immich-dark-primary" slot="leading">
|
||||||
Selected {assets.size.toLocaleString($locale)}
|
Selected {assets.size.toLocaleString($locale)}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const { data } = await api.assetApi.getMemoryLane({
|
const { data } = await api.assetApi.getMemoryLane({
|
||||||
timestamp: DateTime.local().startOf('day').toISO() || ''
|
timestamp: DateTime.local().startOf('day').toISO() || '',
|
||||||
});
|
});
|
||||||
$memoryStore = data;
|
$memoryStore = data;
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,10 +16,7 @@
|
||||||
import SelectAll from 'svelte-material-icons/SelectAll.svelte';
|
import SelectAll from 'svelte-material-icons/SelectAll.svelte';
|
||||||
import ImmichLogo from '../shared-components/immich-logo.svelte';
|
import ImmichLogo from '../shared-components/immich-logo.svelte';
|
||||||
|
|
||||||
import {
|
import { notificationController, NotificationType } from '../shared-components/notification/notification';
|
||||||
notificationController,
|
|
||||||
NotificationType
|
|
||||||
} from '../shared-components/notification/notification';
|
|
||||||
import { handleError } from '../../utils/handle-error';
|
import { handleError } from '../../utils/handle-error';
|
||||||
|
|
||||||
export let sharedLink: SharedLinkResponseDto;
|
export let sharedLink: SharedLinkResponseDto;
|
||||||
|
@ -42,7 +39,7 @@
|
||||||
`immich-shared.zip`,
|
`immich-shared.zip`,
|
||||||
{ assetIds: assets.map((asset) => asset.id) },
|
{ assetIds: assets.map((asset) => asset.id) },
|
||||||
undefined,
|
undefined,
|
||||||
sharedLink.key
|
sharedLink.key,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -57,16 +54,16 @@
|
||||||
const { data } = await api.sharedLinkApi.addSharedLinkAssets({
|
const { data } = await api.sharedLinkApi.addSharedLinkAssets({
|
||||||
id: sharedLink.id,
|
id: sharedLink.id,
|
||||||
assetIdsDto: {
|
assetIdsDto: {
|
||||||
assetIds: results.filter((id) => !!id) as string[]
|
assetIds: results.filter((id) => !!id) as string[],
|
||||||
},
|
},
|
||||||
key: sharedLink.key
|
key: sharedLink.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
const added = data.filter((item) => item.success).length;
|
const added = data.filter((item) => item.success).length;
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: `Added ${added} assets`,
|
message: `Added ${added} assets`,
|
||||||
type: NotificationType.Info
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(e, 'Unable to add assets to shared link');
|
handleError(e, 'Unable to add assets to shared link');
|
||||||
|
@ -90,11 +87,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</AssetSelectControlBar>
|
</AssetSelectControlBar>
|
||||||
{:else}
|
{:else}
|
||||||
<ControlAppBar
|
<ControlAppBar on:close-button-click={() => goto('/photos')} backIcon={ArrowLeft} showBackButton={false}>
|
||||||
on:close-button-click={() => goto('/photos')}
|
|
||||||
backIcon={ArrowLeft}
|
|
||||||
showBackButton={false}
|
|
||||||
>
|
|
||||||
<svelte:fragment slot="leading">
|
<svelte:fragment slot="leading">
|
||||||
<a
|
<a
|
||||||
data-sveltekit-preload-data="hover"
|
data-sveltekit-preload-data="hover"
|
||||||
|
@ -102,27 +95,17 @@
|
||||||
href="https://immich.app"
|
href="https://immich.app"
|
||||||
>
|
>
|
||||||
<ImmichLogo height="30" width="30" />
|
<ImmichLogo height="30" width="30" />
|
||||||
<h1 class="font-immich-title text-lg text-immich-primary dark:text-immich-dark-primary">
|
<h1 class="font-immich-title text-lg text-immich-primary dark:text-immich-dark-primary">IMMICH</h1>
|
||||||
IMMICH
|
|
||||||
</h1>
|
|
||||||
</a>
|
</a>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
<svelte:fragment slot="trailing">
|
<svelte:fragment slot="trailing">
|
||||||
{#if sharedLink?.allowUpload}
|
{#if sharedLink?.allowUpload}
|
||||||
<CircleIconButton
|
<CircleIconButton title="Add Photos" on:click={() => handleUploadAssets()} logo={FileImagePlusOutline} />
|
||||||
title="Add Photos"
|
|
||||||
on:click={() => handleUploadAssets()}
|
|
||||||
logo={FileImagePlusOutline}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if sharedLink?.allowDownload}
|
{#if sharedLink?.allowDownload}
|
||||||
<CircleIconButton
|
<CircleIconButton title="Download" on:click={downloadAssets} logo={FolderDownloadOutline} />
|
||||||
title="Download"
|
|
||||||
on:click={downloadAssets}
|
|
||||||
logo={FolderDownloadOutline}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</ControlAppBar>
|
</ControlAppBar>
|
||||||
|
|
|
@ -19,9 +19,7 @@
|
||||||
const { data } = await api.albumApi.getAllAlbums({ shared: shared || undefined });
|
const { data } = await api.albumApi.getAllAlbums({ shared: shared || undefined });
|
||||||
albums = data;
|
albums = data;
|
||||||
|
|
||||||
recentAlbums = albums
|
recentAlbums = albums.sort((a, b) => (new Date(a.createdAt) > new Date(b.createdAt) ? -1 : 1)).slice(0, 3);
|
||||||
.sort((a, b) => (new Date(a.createdAt) > new Date(b.createdAt) ? -1 : 1))
|
|
||||||
.slice(0, 3);
|
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
});
|
});
|
||||||
|
@ -109,9 +107,7 @@
|
||||||
<AlbumListItem {album} searchQuery={search} on:album={() => handleSelect(album)} />
|
<AlbumListItem {album} searchQuery={search} on:album={() => handleSelect(album)} />
|
||||||
{/each}
|
{/each}
|
||||||
{:else if albums.length > 0}
|
{:else if albums.length > 0}
|
||||||
<p class="text-sm px-5 py-1">
|
<p class="text-sm px-5 py-1">It looks like you do not have any albums with this name yet.</p>
|
||||||
It looks like you do not have any albums with this name yet.
|
|
||||||
</p>
|
|
||||||
{:else}
|
{:else}
|
||||||
<p class="text-sm px-5 py-1">It looks like you do not have any albums yet.</p>
|
<p class="text-sm px-5 py-1">It looks like you do not have any albums yet.</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -48,12 +48,7 @@
|
||||||
{cancelText}
|
{cancelText}
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
<Button
|
<Button color={confirmColor} fullwidth on:click={handleConfirm} disabled={isConfirmButtonDisabled}>
|
||||||
color={confirmColor}
|
|
||||||
fullwidth
|
|
||||||
on:click={handleConfirm}
|
|
||||||
disabled={isConfirmButtonDisabled}
|
|
||||||
>
|
|
||||||
{confirmText}
|
{confirmText}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,17 +1,11 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SettingInputField, {
|
import SettingInputField, {
|
||||||
SettingInputFieldType
|
SettingInputFieldType,
|
||||||
} from '$lib/components/admin-page/settings/setting-input-field.svelte';
|
} from '$lib/components/admin-page/settings/setting-input-field.svelte';
|
||||||
import SettingSwitch from '$lib/components/admin-page/settings/setting-switch.svelte';
|
import SettingSwitch from '$lib/components/admin-page/settings/setting-switch.svelte';
|
||||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import {
|
import { AlbumResponseDto, api, AssetResponseDto, SharedLinkResponseDto, SharedLinkType } from '@api';
|
||||||
AlbumResponseDto,
|
|
||||||
api,
|
|
||||||
AssetResponseDto,
|
|
||||||
SharedLinkResponseDto,
|
|
||||||
SharedLinkType
|
|
||||||
} from '@api';
|
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
import Link from 'svelte-material-icons/Link.svelte';
|
import Link from 'svelte-material-icons/Link.svelte';
|
||||||
import BaseModal from '../base-modal.svelte';
|
import BaseModal from '../base-modal.svelte';
|
||||||
|
@ -36,7 +30,7 @@
|
||||||
|
|
||||||
const expiredDateOption: ImmichDropDownOption = {
|
const expiredDateOption: ImmichDropDownOption = {
|
||||||
default: 'Never',
|
default: 'Never',
|
||||||
options: ['Never', '30 minutes', '1 hour', '6 hours', '1 day', '7 days', '30 days']
|
options: ['Never', '30 minutes', '1 hour', '6 hours', '1 day', '7 days', '30 days'],
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
@ -56,9 +50,7 @@
|
||||||
const handleCreateSharedLink = async () => {
|
const handleCreateSharedLink = async () => {
|
||||||
const expirationTime = getExpirationTimeInMillisecond();
|
const expirationTime = getExpirationTimeInMillisecond();
|
||||||
const currentTime = new Date().getTime();
|
const currentTime = new Date().getTime();
|
||||||
const expirationDate = expirationTime
|
const expirationDate = expirationTime ? new Date(currentTime + expirationTime).toISOString() : undefined;
|
||||||
? new Date(currentTime + expirationTime).toISOString()
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await api.sharedLinkApi.createSharedLink({
|
const { data } = await api.sharedLinkApi.createSharedLink({
|
||||||
|
@ -70,8 +62,8 @@
|
||||||
allowUpload,
|
allowUpload,
|
||||||
description,
|
description,
|
||||||
allowDownload,
|
allowDownload,
|
||||||
showExif
|
showExif,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
sharedLink = `${window.location.origin}/share/${data.key}`;
|
sharedLink = `${window.location.origin}/share/${data.key}`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -88,10 +80,7 @@
|
||||||
await navigator.clipboard.writeText(sharedLink);
|
await navigator.clipboard.writeText(sharedLink);
|
||||||
notificationController.show({ message: 'Copied to clipboard!', type: NotificationType.Info });
|
notificationController.show({ message: 'Copied to clipboard!', type: NotificationType.Info });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(
|
handleError(e, 'Cannot copy to clipboard, make sure you are accessing the page through https');
|
||||||
e,
|
|
||||||
'Cannot copy to clipboard, make sure you are accessing the page through https'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -133,13 +122,13 @@
|
||||||
expiresAt: shouldChangeExpirationTime ? expirationDate : undefined,
|
expiresAt: shouldChangeExpirationTime ? expirationDate : undefined,
|
||||||
allowUpload: allowUpload,
|
allowUpload: allowUpload,
|
||||||
allowDownload: allowDownload,
|
allowDownload: allowDownload,
|
||||||
showExif: showExif
|
showExif: showExif,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
message: 'Edited'
|
message: 'Edited',
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch('close');
|
dispatch('close');
|
||||||
|
@ -192,11 +181,7 @@
|
||||||
<div class="p-4 bg-gray-100 dark:bg-black/40 rounded-lg">
|
<div class="p-4 bg-gray-100 dark:bg-black/40 rounded-lg">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<SettingInputField
|
<SettingInputField inputType={SettingInputFieldType.TEXT} label="Description" bind:value={description} />
|
||||||
inputType={SettingInputFieldType.TEXT}
|
|
||||||
label="Description"
|
|
||||||
bind:value={description}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
|
@ -214,10 +199,7 @@
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
{#if editingLink}
|
{#if editingLink}
|
||||||
<p class="my-2 immich-form-label">
|
<p class="my-2 immich-form-label">
|
||||||
<SettingSwitch
|
<SettingSwitch bind:checked={shouldChangeExpirationTime} title={'Change expiration time'} />
|
||||||
bind:checked={shouldChangeExpirationTime}
|
|
||||||
title={'Change expiration time'}
|
|
||||||
/>
|
|
||||||
</p>
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="my-2 immich-form-label">Expire after</p>
|
<p class="my-2 immich-form-label">Expire after</p>
|
||||||
|
|
|
@ -5,8 +5,7 @@
|
||||||
export let text = '';
|
export let text = '';
|
||||||
export let alt = '';
|
export let alt = '';
|
||||||
|
|
||||||
let hoverClasses =
|
let hoverClasses = 'hover:bg-immich-primary/5 dark:hover:bg-immich-dark-primary/25 hover:cursor-pointer';
|
||||||
'hover:bg-immich-primary/5 dark:hover:bg-immich-dark-primary/25 hover:cursor-pointer';
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if actionHandler}
|
{#if actionHandler}
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
export type ViewFrom =
|
export type ViewFrom = 'archive-page' | 'album-page' | 'favorites-page' | 'search-page' | 'shared-link-page';
|
||||||
| 'archive-page'
|
|
||||||
| 'album-page'
|
|
||||||
| 'favorites-page'
|
|
||||||
| 'search-page'
|
|
||||||
| 'shared-link-page';
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const ControlClass = Control.extend({
|
const ControlClass = Control.extend({
|
||||||
position,
|
position,
|
||||||
onAdd: () => target
|
onAdd: () => target,
|
||||||
});
|
});
|
||||||
|
|
||||||
control = new ControlClass().addTo(map);
|
control = new ControlClass().addTo(map);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export { default as AssetMarkerCluster } from './marker-cluster/asset-marker-cluster.svelte';
|
|
||||||
export { default as Control } from './control.svelte';
|
export { default as Control } from './control.svelte';
|
||||||
export { default as Map } from './map.svelte';
|
export { default as Map } from './map.svelte';
|
||||||
|
export { default as AssetMarkerCluster } from './marker-cluster/asset-marker-cluster.svelte';
|
||||||
export { default as Marker } from './marker.svelte';
|
export { default as Marker } from './marker.svelte';
|
||||||
export { default as TileLayer } from './tile-layer.svelte';
|
export { default as TileLayer } from './tile-layer.svelte';
|
||||||
|
|
|
@ -4,9 +4,8 @@
|
||||||
@apply border;
|
@apply border;
|
||||||
@apply border-immich-primary;
|
@apply border-immich-primary;
|
||||||
@apply transition-all;
|
@apply transition-all;
|
||||||
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, rgba(0, 0, 0, 0.07) 0px 2px 4px,
|
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, rgba(0, 0, 0, 0.07) 0px 2px 4px, rgba(0, 0, 0, 0.07) 0px 4px 8px,
|
||||||
rgba(0, 0, 0, 0.07) 0px 4px 8px, rgba(0, 0, 0, 0.07) 0px 8px 16px,
|
rgba(0, 0, 0, 0.07) 0px 8px 16px, rgba(0, 0, 0, 0.07) 0px 16px 32px, rgba(0, 0, 0, 0.07) 0px 32px 64px;
|
||||||
rgba(0, 0, 0, 0.07) 0px 16px 32px, rgba(0, 0, 0, 0.07) 0px 32px 64px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.marker-cluster-icon {
|
.marker-cluster-icon {
|
||||||
|
|
|
@ -44,9 +44,9 @@
|
||||||
return new DivIcon({
|
return new DivIcon({
|
||||||
html: `<div class="marker-cluster-icon">${childCount}</div>`,
|
html: `<div class="marker-cluster-icon">${childCount}</div>`,
|
||||||
className: '',
|
className: '',
|
||||||
iconSize: new Point(iconSize, iconSize)
|
iconSize: new Point(iconSize, iconSize),
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
cluster.on('clusterclick', (event: LeafletEvent) => {
|
cluster.on('clusterclick', (event: LeafletEvent) => {
|
||||||
|
@ -64,9 +64,7 @@
|
||||||
cluster.on('click', (event: LeafletMouseEvent) => {
|
cluster.on('click', (event: LeafletMouseEvent) => {
|
||||||
const marker: AssetMarker = event.sourceTarget;
|
const marker: AssetMarker = event.sourceTarget;
|
||||||
const markerCluster = getClusterByMarker(marker);
|
const markerCluster = getClusterByMarker(marker);
|
||||||
const markers = markerCluster
|
const markers = markerCluster ? (markerCluster.getAllChildMarkers() as AssetMarker[]) : [marker];
|
||||||
? (markerCluster.getAllChildMarkers() as AssetMarker[])
|
|
||||||
: [marker];
|
|
||||||
|
|
||||||
onView(markers, marker.id);
|
onView(markers, marker.id);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { MapMarkerResponseDto, api } from '@api';
|
import { api, MapMarkerResponseDto } from '@api';
|
||||||
import { Marker, Map, Icon } from 'leaflet';
|
import { Icon, Map, Marker } from 'leaflet';
|
||||||
|
|
||||||
export default class AssetMarker extends Marker {
|
export default class AssetMarker extends Marker {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -31,7 +31,7 @@ export default class AssetMarker extends Marker {
|
||||||
popupAnchor: [1, -34],
|
popupAnchor: [1, -34],
|
||||||
tooltipAnchor: [16, -28],
|
tooltipAnchor: [16, -28],
|
||||||
shadowSize: [41, 41],
|
shadowSize: [41, 41],
|
||||||
className: 'asset-marker-icon'
|
className: 'asset-marker-icon',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,13 +20,13 @@
|
||||||
iconAnchor: [12, 41],
|
iconAnchor: [12, 41],
|
||||||
popupAnchor: [1, -34],
|
popupAnchor: [1, -34],
|
||||||
tooltipAnchor: [16, -28],
|
tooltipAnchor: [16, -28],
|
||||||
shadowSize: [41, 41]
|
shadowSize: [41, 41],
|
||||||
});
|
});
|
||||||
const map = getMapContext();
|
const map = getMapContext();
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
marker = new Marker(latlng, {
|
marker = new Marker(latlng, {
|
||||||
icon: defaultIcon
|
icon: defaultIcon,
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -37,15 +37,9 @@
|
||||||
<div
|
<div
|
||||||
class="grid h-full md:grid-cols-[theme(spacing.64)_auto] grid-cols-[theme(spacing.18)_auto] border-b dark:border-b-immich-dark-gray items-center py-2 bg-immich-bg dark:bg-immich-dark-bg"
|
class="grid h-full md:grid-cols-[theme(spacing.64)_auto] grid-cols-[theme(spacing.18)_auto] border-b dark:border-b-immich-dark-gray items-center py-2 bg-immich-bg dark:bg-immich-dark-bg"
|
||||||
>
|
>
|
||||||
<a
|
<a data-sveltekit-preload-data="hover" class="flex gap-2 md:mx-6 mx-4 place-items-center" href={AppRoute.PHOTOS}>
|
||||||
data-sveltekit-preload-data="hover"
|
|
||||||
class="flex gap-2 md:mx-6 mx-4 place-items-center"
|
|
||||||
href={AppRoute.PHOTOS}
|
|
||||||
>
|
|
||||||
<ImmichLogo height="35" width="35" />
|
<ImmichLogo height="35" width="35" />
|
||||||
<h1
|
<h1 class="font-immich-title text-2xl text-immich-primary dark:text-immich-dark-primary md:block hidden">
|
||||||
class="font-immich-title text-2xl text-immich-primary dark:text-immich-dark-primary md:block hidden"
|
|
||||||
>
|
|
||||||
IMMICH
|
IMMICH
|
||||||
</h1>
|
</h1>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
const progress = tweened(0, {
|
const progress = tweened(0, {
|
||||||
duration: 1000,
|
duration: 1000,
|
||||||
easing: cubicOut
|
easing: cubicOut,
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { jest, describe, it } from '@jest/globals';
|
import { describe, it, jest } from '@jest/globals';
|
||||||
import { render, cleanup, RenderResult } from '@testing-library/svelte';
|
import '@testing-library/jest-dom';
|
||||||
|
import { cleanup, render, RenderResult } from '@testing-library/svelte';
|
||||||
import { NotificationType } from '../notification';
|
import { NotificationType } from '../notification';
|
||||||
import NotificationCard from '../notification-card.svelte';
|
import NotificationCard from '../notification-card.svelte';
|
||||||
import '@testing-library/jest-dom';
|
|
||||||
|
|
||||||
describe('NotificationCard component', () => {
|
describe('NotificationCard component', () => {
|
||||||
let sut: RenderResult<NotificationCard>;
|
let sut: RenderResult<NotificationCard>;
|
||||||
|
@ -16,8 +16,8 @@ describe('NotificationCard component', () => {
|
||||||
message: 'Notification message',
|
message: 'Notification message',
|
||||||
timeout: 1000,
|
timeout: 1000,
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
action: { type: 'discard' }
|
action: { type: 'discard' },
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
cleanup();
|
cleanup();
|
||||||
|
@ -31,8 +31,8 @@ describe('NotificationCard component', () => {
|
||||||
message: 'Notification message',
|
message: 'Notification message',
|
||||||
timeout: 1000,
|
timeout: 1000,
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
action: { type: 'discard' }
|
action: { type: 'discard' },
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(sut.getByTestId('title')).toHaveTextContent('Info');
|
expect(sut.getByTestId('title')).toHaveTextContent('Info');
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import { jest, describe, it } from '@jest/globals';
|
import { describe, it, jest } from '@jest/globals';
|
||||||
import { render, RenderResult, waitFor } from '@testing-library/svelte';
|
|
||||||
import { notificationController, NotificationType } from '../notification';
|
|
||||||
import { get } from 'svelte/store';
|
|
||||||
import NotificationList from '../notification-list.svelte';
|
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
|
import { render, RenderResult, waitFor } from '@testing-library/svelte';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import { notificationController, NotificationType } from '../notification';
|
||||||
|
import NotificationList from '../notification-list.svelte';
|
||||||
|
|
||||||
function _getNotificationListElement(
|
function _getNotificationListElement(sut: RenderResult<NotificationList>): HTMLAnchorElement | null {
|
||||||
sut: RenderResult<NotificationList>
|
|
||||||
): HTMLAnchorElement | null {
|
|
||||||
return sut.container.querySelector('#notification-list');
|
return sut.container.querySelector('#notification-list');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +26,7 @@ describe('NotificationList component', () => {
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Notification',
|
message: 'Notification',
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
timeout: 3000
|
timeout: 3000,
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => expect(_getNotificationListElement(sut)).toBeInTheDocument());
|
await waitFor(() => expect(_getNotificationListElement(sut)).toBeInTheDocument());
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
import {
|
import {
|
||||||
ImmichNotification,
|
ImmichNotification,
|
||||||
notificationController,
|
notificationController,
|
||||||
NotificationType
|
NotificationType,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
@ -16,8 +16,7 @@
|
||||||
let infoPrimaryColor = '#4250AF';
|
let infoPrimaryColor = '#4250AF';
|
||||||
let errorPrimaryColor = '#E64132';
|
let errorPrimaryColor = '#E64132';
|
||||||
|
|
||||||
$: icon =
|
$: icon = notificationInfo.type === NotificationType.Error ? CloseCircleOutline : InformationOutline;
|
||||||
notificationInfo.type === NotificationType.Error ? CloseCircleOutline : InformationOutline;
|
|
||||||
|
|
||||||
$: backgroundColor = () => {
|
$: backgroundColor = () => {
|
||||||
if (notificationInfo.type === NotificationType.Info) {
|
if (notificationInfo.type === NotificationType.Info) {
|
||||||
|
|
|
@ -10,11 +10,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $notificationList.length > 0}
|
{#if $notificationList.length > 0}
|
||||||
<section
|
<section transition:fade={{ duration: 250 }} id="notification-list" class="absolute right-5 top-[80px] z-[99999999]">
|
||||||
transition:fade={{ duration: 250 }}
|
|
||||||
id="notification-list"
|
|
||||||
class="absolute right-5 top-[80px] z-[99999999]"
|
|
||||||
>
|
|
||||||
{#each $notificationList as notificationInfo (notificationInfo.id)}
|
{#each $notificationList as notificationInfo (notificationInfo.id)}
|
||||||
<div animate:flip={{ duration: 250, easing: quintOut }}>
|
<div animate:flip={{ duration: 250, easing: quintOut }}>
|
||||||
<NotificationCard {notificationInfo} />
|
<NotificationCard {notificationInfo} />
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { writable } from 'svelte/store';
|
||||||
|
|
||||||
export enum NotificationType {
|
export enum NotificationType {
|
||||||
Info = 'Info',
|
Info = 'Info',
|
||||||
Error = 'Error'
|
Error = 'Error',
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ImmichNotification {
|
export class ImmichNotification {
|
||||||
|
@ -61,7 +61,7 @@ function createNotificationList() {
|
||||||
return {
|
return {
|
||||||
show,
|
show,
|
||||||
removeNotificationById,
|
removeNotificationById,
|
||||||
notificationList
|
notificationList,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
`Unknown portal target type: ${
|
`Unknown portal target type: ${
|
||||||
target === null ? 'null' : typeof target
|
target === null ? 'null' : typeof target
|
||||||
}. Allowed types: string (CSS selector) or HTMLElement.`
|
}. Allowed types: string (CSS selector) or HTMLElement.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
targetEl.appendChild(el);
|
targetEl.appendChild(el);
|
||||||
|
@ -39,7 +39,7 @@
|
||||||
update(target);
|
update(target);
|
||||||
return {
|
return {
|
||||||
update,
|
update,
|
||||||
destroy
|
destroy,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
q: searchValue,
|
q: searchValue,
|
||||||
clip: clipSearch
|
clip: clipSearch,
|
||||||
});
|
});
|
||||||
|
|
||||||
goto(`${AppRoute.SEARCH}?${params}`);
|
goto(`${AppRoute.SEARCH}?${params}`);
|
||||||
|
|
|
@ -26,11 +26,7 @@
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div class="flex gap-4 place-items-center w-full overflow-hidden truncate">
|
<div class="flex gap-4 place-items-center w-full overflow-hidden truncate">
|
||||||
<svelte:component
|
<svelte:component this={logo} size="1.5em" class="shrink-0 {flippedLogo ? '-scale-x-100' : ''}" />
|
||||||
this={logo}
|
|
||||||
size="1.5em"
|
|
||||||
class="shrink-0 {flippedLogo ? '-scale-x-100' : ''}"
|
|
||||||
/>
|
|
||||||
<p class="font-medium text-sm">{title}</p>
|
<p class="font-medium text-sm">{title}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
|
|
||||||
return {
|
return {
|
||||||
videos: allAssetCount.videos - archivedCount.videos,
|
videos: allAssetCount.videos - archivedCount.videos,
|
||||||
photos: allAssetCount.photos - archivedCount.photos
|
photos: allAssetCount.photos - archivedCount.photos,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -32,15 +32,15 @@
|
||||||
try {
|
try {
|
||||||
const { data: assets } = await api.assetApi.getAllAssets({
|
const { data: assets } = await api.assetApi.getAllAssets({
|
||||||
isFavorite: true,
|
isFavorite: true,
|
||||||
withoutThumbs: true
|
withoutThumbs: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
favorites: assets.length
|
favorites: assets.length,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return {
|
return {
|
||||||
favorites: 0
|
favorites: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -60,12 +60,12 @@
|
||||||
|
|
||||||
return {
|
return {
|
||||||
videos: assetCount.videos,
|
videos: assetCount.videos,
|
||||||
photos: assetCount.photos
|
photos: assetCount.photos,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return {
|
return {
|
||||||
videos: 0,
|
videos: 0,
|
||||||
photos: 0
|
photos: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -76,12 +76,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SideBarSection>
|
<SideBarSection>
|
||||||
<a
|
<a data-sveltekit-preload-data="hover" data-sveltekit-noscroll href={AppRoute.PHOTOS} draggable="false">
|
||||||
data-sveltekit-preload-data="hover"
|
|
||||||
data-sveltekit-noscroll
|
|
||||||
href={AppRoute.PHOTOS}
|
|
||||||
draggable="false"
|
|
||||||
>
|
|
||||||
<SideBarButton
|
<SideBarButton
|
||||||
title="Photos"
|
title="Photos"
|
||||||
logo={isPhotosSelected ? ImageMultiple : ImageMultipleOutline}
|
logo={isPhotosSelected ? ImageMultiple : ImageMultipleOutline}
|
||||||
|
@ -99,17 +94,8 @@
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</SideBarButton>
|
</SideBarButton>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a data-sveltekit-preload-data="hover" data-sveltekit-noscroll href={AppRoute.EXPLORE} draggable="false">
|
||||||
data-sveltekit-preload-data="hover"
|
<SideBarButton title="Explore" logo={Magnify} isSelected={$page.route.id === '/(user)/explore'} />
|
||||||
data-sveltekit-noscroll
|
|
||||||
href={AppRoute.EXPLORE}
|
|
||||||
draggable="false"
|
|
||||||
>
|
|
||||||
<SideBarButton
|
|
||||||
title="Explore"
|
|
||||||
logo={Magnify}
|
|
||||||
isSelected={$page.route.id === '/(user)/explore'}
|
|
||||||
/>
|
|
||||||
</a>
|
</a>
|
||||||
<a data-sveltekit-preload-data="hover" href={AppRoute.MAP} draggable="false">
|
<a data-sveltekit-preload-data="hover" href={AppRoute.MAP} draggable="false">
|
||||||
<SideBarButton title="Map" logo={Map} isSelected={$page.route.id === '/(user)/map'} />
|
<SideBarButton title="Map" logo={Map} isSelected={$page.route.id === '/(user)/map'} />
|
||||||
|
@ -154,12 +140,7 @@
|
||||||
</SideBarButton>
|
</SideBarButton>
|
||||||
</a>
|
</a>
|
||||||
<a data-sveltekit-preload-data="hover" href={AppRoute.ALBUMS} draggable="false">
|
<a data-sveltekit-preload-data="hover" href={AppRoute.ALBUMS} draggable="false">
|
||||||
<SideBarButton
|
<SideBarButton title="Albums" logo={ImageAlbum} flippedLogo={true} isSelected={$page.route.id === '/(user)/albums'}>
|
||||||
title="Albums"
|
|
||||||
logo={ImageAlbum}
|
|
||||||
flippedLogo={true}
|
|
||||||
isSelected={$page.route.id === '/(user)/albums'}
|
|
||||||
>
|
|
||||||
<svelte:fragment slot="moreInformation">
|
<svelte:fragment slot="moreInformation">
|
||||||
{#await getAlbumCount()}
|
{#await getAlbumCount()}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
|
@ -172,11 +153,7 @@
|
||||||
</SideBarButton>
|
</SideBarButton>
|
||||||
</a>
|
</a>
|
||||||
<a data-sveltekit-preload-data="hover" href={AppRoute.ARCHIVE} draggable="false">
|
<a data-sveltekit-preload-data="hover" href={AppRoute.ARCHIVE} draggable="false">
|
||||||
<SideBarButton
|
<SideBarButton title="Archive" logo={ArchiveArrowDownOutline} isSelected={$page.route.id === '/(user)/archive'}>
|
||||||
title="Archive"
|
|
||||||
logo={ArchiveArrowDownOutline}
|
|
||||||
isSelected={$page.route.id === '/(user)/archive'}
|
|
||||||
>
|
|
||||||
<svelte:fragment slot="moreInformation">
|
<svelte:fragment slot="moreInformation">
|
||||||
{#await getArchivedAssetsCount()}
|
{#await getArchivedAssetsCount()}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
|
|
|
@ -51,9 +51,7 @@
|
||||||
|
|
||||||
<div class="dark:text-immich-dark-fg">
|
<div class="dark:text-immich-dark-fg">
|
||||||
<div class="storage-status grid grid-cols-[64px_auto]">
|
<div class="storage-status grid grid-cols-[64px_auto]">
|
||||||
<div
|
<div class="pl-5 pr-6 text-immich-primary dark:text-immich-dark-primary pb-[2.15rem] group-hover:sm:pb-0 md:pb-0">
|
||||||
class="pl-5 pr-6 text-immich-primary dark:text-immich-dark-primary pb-[2.15rem] group-hover:sm:pb-0 md:pb-0"
|
|
||||||
>
|
|
||||||
<Cloud size={'24'} />
|
<Cloud size={'24'} />
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden md:block group-hover:sm:block">
|
<div class="hidden md:block group-hover:sm:block">
|
||||||
|
@ -81,9 +79,7 @@
|
||||||
<hr class="ml-5 my-4 dark:border-immich-dark-gray" />
|
<hr class="ml-5 my-4 dark:border-immich-dark-gray" />
|
||||||
</div>
|
</div>
|
||||||
<div class="server-status grid grid-cols-[64px_auto]">
|
<div class="server-status grid grid-cols-[64px_auto]">
|
||||||
<div
|
<div class="pl-5 pr-6 text-immich-primary dark:text-immich-dark-primary pb-11 md:pb-0 group-hover:sm:pb-0">
|
||||||
class="pl-5 pr-6 text-immich-primary dark:text-immich-dark-primary pb-11 md:pb-0 group-hover:sm:pb-0"
|
|
||||||
>
|
|
||||||
<Dns size={'24'} />
|
<Dns size={'24'} />
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs hidden md:block group-hover:sm:block">
|
<div class="text-xs hidden md:block group-hover:sm:block">
|
||||||
|
|
|
@ -55,10 +55,7 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="w-full bg-gray-300 h-[15px] rounded-md mt-[5px] text-white relative">
|
<div class="w-full bg-gray-300 h-[15px] rounded-md mt-[5px] text-white relative">
|
||||||
<div
|
<div class="bg-immich-primary h-[15px] rounded-md transition-all" style={`width: ${uploadAsset.progress}%`} />
|
||||||
class="bg-immich-primary h-[15px] rounded-md transition-all"
|
|
||||||
style={`width: ${uploadAsset.progress}%`}
|
|
||||||
/>
|
|
||||||
<p class="absolute h-full w-full text-center top-0 text-[10px]">
|
<p class="absolute h-full w-full text-center top-0 text-[10px]">
|
||||||
{uploadAsset.progress}/100
|
{uploadAsset.progress}/100
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
on:outroend={() => {
|
on:outroend={() => {
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Upload success, refresh the page to see new upload assets',
|
message: 'Upload success, refresh the page to see new upload assets',
|
||||||
type: NotificationType.Info
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
class="absolute right-6 bottom-6 z-[10000]"
|
class="absolute right-6 bottom-6 z-[10000]"
|
||||||
|
|
|
@ -17,20 +17,19 @@
|
||||||
let showFallback = true;
|
let showFallback = true;
|
||||||
|
|
||||||
const colorClasses: Record<Color, string> = {
|
const colorClasses: Record<Color, string> = {
|
||||||
primary:
|
primary: 'bg-immich-primary dark:bg-immich-dark-primary text-immich-dark-fg dark:text-immich-fg',
|
||||||
'bg-immich-primary dark:bg-immich-dark-primary text-immich-dark-fg dark:text-immich-fg',
|
|
||||||
pink: 'bg-pink-400 text-immich-bg',
|
pink: 'bg-pink-400 text-immich-bg',
|
||||||
red: 'bg-red-500 text-immich-bg',
|
red: 'bg-red-500 text-immich-bg',
|
||||||
yellow: 'bg-yellow-500 text-immich-bg',
|
yellow: 'bg-yellow-500 text-immich-bg',
|
||||||
blue: 'bg-blue-500 text-immich-bg',
|
blue: 'bg-blue-500 text-immich-bg',
|
||||||
green: 'bg-green-600 text-immich-bg'
|
green: 'bg-green-600 text-immich-bg',
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses: Record<Size, string> = {
|
const sizeClasses: Record<Size, string> = {
|
||||||
full: 'w-full h-full',
|
full: 'w-full h-full',
|
||||||
sm: 'w-7 h-7',
|
sm: 'w-7 h-7',
|
||||||
md: 'w-12 h-12',
|
md: 'w-12 h-12',
|
||||||
lg: 'w-20 h-20'
|
lg: 'w-20 h-20',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get color based on the user UUID.
|
// Get color based on the user UUID.
|
||||||
|
|
|
@ -49,19 +49,15 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
Hi friend, there is a new release of
|
Hi friend, there is a new release of
|
||||||
<span class="font-immich-title text-immich-primary dark:text-immich-dark-primary font-bold"
|
<span class="font-immich-title text-immich-primary dark:text-immich-dark-primary font-bold">IMMICH</span>,
|
||||||
>IMMICH</span
|
please take your time to visit the
|
||||||
>, please take your time to visit the
|
|
||||||
<span class="underline font-medium"
|
<span class="underline font-medium"
|
||||||
><a
|
><a href="https://github.com/immich-app/immich/releases/latest" target="_blank" rel="noopener noreferrer"
|
||||||
href="https://github.com/immich-app/immich/releases/latest"
|
>release notes</a
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer">release notes</a
|
|
||||||
></span
|
></span
|
||||||
>
|
>
|
||||||
and ensure your <code>docker-compose</code>, and <code>.env</code> setup is up-to-date to prevent
|
and ensure your <code>docker-compose</code>, and <code>.env</code> setup is up-to-date to prevent any misconfigurations,
|
||||||
any misconfigurations, especially if you use WatchTower or any mechanism that handles updating
|
especially if you use WatchTower or any mechanism that handles updating your application automatically.
|
||||||
your application automatically.
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="font-medium mt-4">Your friend, Alex</div>
|
<div class="font-medium mt-4">Your friend, Alex</div>
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import { api, AssetResponseDto, SharedLinkResponseDto, SharedLinkType, ThumbnailFormat } from '@api';
|
||||||
api,
|
|
||||||
AssetResponseDto,
|
|
||||||
SharedLinkResponseDto,
|
|
||||||
SharedLinkType,
|
|
||||||
ThumbnailFormat
|
|
||||||
} from '@api';
|
|
||||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||||
import OpenInNew from 'svelte-material-icons/OpenInNew.svelte';
|
import OpenInNew from 'svelte-material-icons/OpenInNew.svelte';
|
||||||
import Delete from 'svelte-material-icons/TrashCanOutline.svelte';
|
import Delete from 'svelte-material-icons/TrashCanOutline.svelte';
|
||||||
|
@ -43,9 +37,7 @@
|
||||||
const expiresAtDate = luxon.DateTime.fromISO(new Date(link.expiresAt).toISOString());
|
const expiresAtDate = luxon.DateTime.fromISO(new Date(link.expiresAt).toISOString());
|
||||||
const now = luxon.DateTime.now();
|
const now = luxon.DateTime.now();
|
||||||
|
|
||||||
expirationCountdown = expiresAtDate
|
expirationCountdown = expiresAtDate.diff(now, ['days', 'hours', 'minutes', 'seconds']).toObject();
|
||||||
.diff(now, ['days', 'hours', 'minutes', 'seconds'])
|
|
||||||
.toObject();
|
|
||||||
|
|
||||||
if (expirationCountdown.days && expirationCountdown.days > 0) {
|
if (expirationCountdown.days && expirationCountdown.days > 0) {
|
||||||
return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'days' });
|
return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'days' });
|
||||||
|
@ -101,9 +93,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<div
|
<div class="flex gap-2 place-items-center text-immich-primary dark:text-immich-dark-primary">
|
||||||
class="flex gap-2 place-items-center text-immich-primary dark:text-immich-dark-primary"
|
|
||||||
>
|
|
||||||
{#if link.type === SharedLinkType.Album}
|
{#if link.type === SharedLinkType.Album}
|
||||||
<p>
|
<p>
|
||||||
{link.album?.albumName.toUpperCase()}
|
{link.album?.albumName.toUpperCase()}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue