diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs index 08da2df8a..fdf66b700 100644 --- a/web/.eslintrc.cjs +++ b/web/.eslintrc.cjs @@ -1,43 +1,39 @@ /** @type {import('eslint').Linter.Config} */ module.exports = { - root: true, - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:svelte/recommended' - ], - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint'], - parserOptions: { - sourceType: 'module', - ecmaVersion: 2020, - extraFileExtensions: ['.svelte'] - }, - env: { - browser: true, - es2017: true, - node: true - }, - overrides: [ - { - files: ['*.svelte'], - parser: 'svelte-eslint-parser', - parserOptions: { - parser: '@typescript-eslint/parser' - } - } - ], - globals: { - NodeJS: true - }, - rules: { - '@typescript-eslint/no-unused-vars': [ - 'warn', - { - // Allow underscore (_) variables - argsIgnorePattern: '^_$', - varsIgnorePattern: '^_$' - } - ] - } + root: true, + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:svelte/recommended'], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + parserOptions: { + sourceType: 'module', + ecmaVersion: 2020, + extraFileExtensions: ['.svelte'], + }, + env: { + browser: true, + es2017: true, + node: true, + }, + overrides: [ + { + files: ['*.svelte'], + parser: 'svelte-eslint-parser', + parserOptions: { + parser: '@typescript-eslint/parser', + }, + }, + ], + globals: { + NodeJS: true, + }, + rules: { + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + // Allow underscore (_) variables + argsIgnorePattern: '^_$', + varsIgnorePattern: '^_$', + }, + ], + }, }; diff --git a/web/.prettierrc b/web/.prettierrc index ff2677efd..a095d3a51 100644 --- a/web/.prettierrc +++ b/web/.prettierrc @@ -1,6 +1,7 @@ { - "useTabs": true, - "singleQuote": true, - "trailingComma": "none", - "printWidth": 100 + "singleQuote": true, + "trailingComma": "all", + "printWidth": 120, + "semi": true, + "organizeImportsSkipDestructiveCodeActions": true } diff --git a/web/__mocks__/$app/environment.js b/web/__mocks__/$app/environment.js index 357e6533c..453d98b84 100644 --- a/web/__mocks__/$app/environment.js +++ b/web/__mocks__/$app/environment.js @@ -1,3 +1,3 @@ module.exports = { - browser: false + browser: false, }; diff --git a/web/__mocks__/$env/dynamic/public.js b/web/__mocks__/$env/dynamic/public.js index 643ba805f..910f4005a 100644 --- a/web/__mocks__/$env/dynamic/public.js +++ b/web/__mocks__/$env/dynamic/public.js @@ -1,3 +1,3 @@ module.exports = { - env: {} + env: {}, }; diff --git a/web/babel.config.cjs b/web/babel.config.cjs index 96a36d611..e6ffbd417 100644 --- a/web/babel.config.cjs +++ b/web/babel.config.cjs @@ -1,3 +1,3 @@ module.exports = { - presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'] + presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], }; diff --git a/web/jest.config.mjs b/web/jest.config.mjs index 9ba08b74b..5eeb60f95 100644 --- a/web/jest.config.mjs +++ b/web/jest.config.mjs @@ -4,200 +4,199 @@ */ export default { - // All imported modules in your tests should be mocked automatically - // automock: false, + // All imported modules in your tests should be mocked automatically + // automock: false, - // Stop running tests after `n` failures - // bail: 0, + // Stop running tests after `n` failures + // bail: 0, - // The directory where Jest should store its cached dependency information - // cacheDirectory: "/private/var/folders/6n/31wm28711gzbt3gzsxhzxx500000gn/T/jest_dx", + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/private/var/folders/6n/31wm28711gzbt3gzsxhzxx500000gn/T/jest_dx", - // Automatically clear mock calls, instances, contexts and results before every test - clearMocks: true, + // Automatically clear mock calls, instances, contexts and results before every test + clearMocks: true, - // Indicates whether the coverage information should be collected while executing the test - // collectCoverage: false, + // Indicates whether the coverage information should be collected while executing the test + // collectCoverage: false, - // An array of glob patterns indicating a set of files for which coverage information should be collected - collectCoverageFrom: ['src/**/*.*', '!src/api/open-api/**'], + // An array of glob patterns indicating a set of files for which coverage information should be collected + collectCoverageFrom: ['src/**/*.*', '!src/api/open-api/**'], - // The directory where Jest should output its coverage files - // coverageDirectory: undefined, - coverageThreshold: { - global: { - lines: 4, - statements: 4 - } - }, + // The directory where Jest should output its coverage files + // coverageDirectory: undefined, + coverageThreshold: { + global: { + lines: 4, + statements: 4, + }, + }, - // An array of regexp pattern strings used to skip coverage collection - // coveragePathIgnorePatterns: [ - // "/node_modules/" - // ], + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], - // Indicates which provider should be used to instrument code for coverage - coverageProvider: 'v8', + // Indicates which provider should be used to instrument code for coverage + coverageProvider: 'v8', - // A list of reporter names that Jest uses when writing coverage reports - // coverageReporters: [ - // "json", - // "text", - // "lcov", - // "clover" - // ], + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], - // An object that configures minimum threshold enforcement for coverage results - // coverageThreshold: undefined, + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: undefined, - // A path to a custom dependency extractor - // dependencyExtractor: undefined, + // A path to a custom dependency extractor + // dependencyExtractor: undefined, - // Make calling deprecated APIs throw helpful error messages - // errorOnDeprecated: false, + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, - // The default configuration for fake timers - // fakeTimers: { - // "enableGlobally": false - // }, + // The default configuration for fake timers + // fakeTimers: { + // "enableGlobally": false + // }, - // Force coverage collection from ignored files using an array of glob patterns - // forceCoverageMatch: [], + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], - // A path to a module which exports an async function that is triggered once before all test suites - // globalSetup: undefined, + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: undefined, - // A path to a module which exports an async function that is triggered once after all test suites - // globalTeardown: undefined, + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: undefined, - // A set of global variables that need to be available in all test environments - // globals: {}, + // A set of global variables that need to be available in all test environments + // globals: {}, - // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. - // maxWorkers: "50%", + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", - // An array of directory names to be searched recursively up from the requiring module's location - // moduleDirectories: [ - // "node_modules" - // ], + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], - // An array of file extensions your modules use - moduleFileExtensions: ['svelte', 'js', 'ts'], + // An array of file extensions your modules use + moduleFileExtensions: ['svelte', 'js', 'ts'], - // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module - moduleNameMapper: { - '\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': - 'identity-obj-proxy', - '^\\$lib(.*)$': '/src/lib$1', - '^\\@api(.*)$': '/src/api$1', - '^\\@test-data(.*)$': '/src/test-data$1' - }, + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + moduleNameMapper: { + '\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 'identity-obj-proxy', + '^\\$lib(.*)$': '/src/lib$1', + '^\\@api(.*)$': '/src/api$1', + '^\\@test-data(.*)$': '/src/test-data$1', + }, - // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader - // modulePathIgnorePatterns: [], + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], - // Activates notifications for test results - // notify: false, + // Activates notifications for test results + // notify: false, - // An enum that specifies notification mode. Requires { notify: true } - // notifyMode: "failure-change", + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", - // A preset that is used as a base for Jest's configuration - // preset: undefined, + // A preset that is used as a base for Jest's configuration + // preset: undefined, - // Run tests from one or more projects - // projects: undefined, + // Run tests from one or more projects + // projects: undefined, - // Use this configuration option to add custom reporters to Jest - // reporters: undefined, + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, - // Automatically reset mock state before every test - // resetMocks: false, + // Automatically reset mock state before every test + // resetMocks: false, - // Reset the module registry before running each individual test - // resetModules: false, + // Reset the module registry before running each individual test + // resetModules: false, - // A path to a custom resolver - // resolver: undefined, + // A path to a custom resolver + // resolver: undefined, - // Automatically restore mock state and implementation before every test - // restoreMocks: false, + // Automatically restore mock state and implementation before every test + // restoreMocks: false, - // The root directory that Jest should scan for tests and modules within - // rootDir: undefined, + // The root directory that Jest should scan for tests and modules within + // rootDir: undefined, - // A list of paths to directories that Jest should use to search for files in - // roots: [ - // "" - // ], + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], - // Allows you to use a custom runner instead of Jest's default test runner - // runner: "jest-runner", + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", - // The paths to modules that run some code to configure or set up the testing environment before each test - // setupFiles: [], + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], - // A list of paths to modules that run some code to configure or set up the testing framework before each test - // setupFilesAfterEnv: [], + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], - // The number of seconds after which a test is considered as slow and reported as such in the results. - // slowTestThreshold: 5, + // The number of seconds after which a test is considered as slow and reported as such in the results. + // slowTestThreshold: 5, - // A list of paths to snapshot serializer modules Jest should use for snapshot testing - // snapshotSerializers: [], + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], - // The test environment that will be used for testing - testEnvironment: 'jsdom', + // The test environment that will be used for testing + testEnvironment: 'jsdom', - // Options that will be passed to the testEnvironment - // testEnvironmentOptions: {}, + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, - // Adds a location field to test results - // testLocationInResults: false, + // Adds a location field to test results + // testLocationInResults: false, - // The glob patterns Jest uses to detect test files - // testMatch: [ - // "**/__tests__/**/*.[jt]s?(x)", - // "**/?(*.)+(spec|test).[tj]s?(x)" - // ], + // The glob patterns Jest uses to detect test files + // testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)" + // ], - // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped - // testPathIgnorePatterns: [ - // "/node_modules/" - // ], + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], - // The regexp pattern or array of patterns that Jest uses to detect test files - // testRegex: [], + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], - // This option allows the use of a custom results processor - // testResultsProcessor: undefined, + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, - // This option allows use of a custom test runner - // testRunner: "jest-circus/runner", + // This option allows use of a custom test runner + // testRunner: "jest-circus/runner", - // A map from regular expressions to paths to transformers - transform: { - '\\.[jt]sx?$': 'babel-jest', - '^.+\\.svelte$': [ - 'svelte-jester', - { - preprocess: true - } - ] - }, + // A map from regular expressions to paths to transformers + transform: { + '\\.[jt]sx?$': 'babel-jest', + '^.+\\.svelte$': [ + 'svelte-jester', + { + preprocess: true, + }, + ], + }, - // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation - transformIgnorePatterns: ['/node_modules/(?!svelte-material-icons).*/', '\\.pnp\\.[^\\/]+$'] + // 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\\.[^\\/]+$'], - // 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, + // 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, - // Indicates whether each individual test should be reported during the run - // verbose: undefined, + // Indicates whether each individual test should be reported during the run + // verbose: undefined, - // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode - // watchPathIgnorePatterns: [], + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], - // Whether to use watchman for file crawling - // watchman: true, + // Whether to use watchman for file crawling + // watchman: true, }; diff --git a/web/postcss.config.cjs b/web/postcss.config.cjs index 054c147cb..12a703d90 100644 --- a/web/postcss.config.cjs +++ b/web/postcss.config.cjs @@ -1,6 +1,6 @@ module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {} - } + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, }; diff --git a/web/src/api/api.ts b/web/src/api/api.ts index 2924d717d..22f998972 100644 --- a/web/src/api/api.ts +++ b/web/src/api/api.ts @@ -1,129 +1,125 @@ import { - AlbumApi, - APIKeyApi, - AssetApi, - AssetApiFp, - AuthenticationApi, - Configuration, - ConfigurationParameters, - JobApi, - JobName, - OAuthApi, - PartnerApi, - PersonApi, - SearchApi, - ServerInfoApi, - SharedLinkApi, - SystemConfigApi, - UserApi, - UserApiFp + AlbumApi, + APIKeyApi, + AssetApi, + AssetApiFp, + AuthenticationApi, + Configuration, + ConfigurationParameters, + JobApi, + JobName, + OAuthApi, + PartnerApi, + PersonApi, + SearchApi, + ServerInfoApi, + SharedLinkApi, + SystemConfigApi, + UserApi, + UserApiFp, } from './open-api'; import { BASE_PATH } from './open-api/base'; import { DUMMY_BASE_URL, toPathString } from './open-api/common'; import type { ApiParams } from './types'; export class ImmichApi { - public albumApi: AlbumApi; - public assetApi: AssetApi; - public authenticationApi: AuthenticationApi; - public jobApi: JobApi; - public keyApi: APIKeyApi; - public oauthApi: OAuthApi; - public partnerApi: PartnerApi; - public searchApi: SearchApi; - public serverInfoApi: ServerInfoApi; - public sharedLinkApi: SharedLinkApi; - public personApi: PersonApi; - public systemConfigApi: SystemConfigApi; - public userApi: UserApi; + public albumApi: AlbumApi; + public assetApi: AssetApi; + public authenticationApi: AuthenticationApi; + public jobApi: JobApi; + public keyApi: APIKeyApi; + public oauthApi: OAuthApi; + public partnerApi: PartnerApi; + public searchApi: SearchApi; + public serverInfoApi: ServerInfoApi; + public sharedLinkApi: SharedLinkApi; + public personApi: PersonApi; + public systemConfigApi: SystemConfigApi; + public userApi: UserApi; - private config: Configuration; + private config: Configuration; - constructor(params: ConfigurationParameters) { - this.config = new Configuration(params); + constructor(params: ConfigurationParameters) { + this.config = new Configuration(params); - this.albumApi = new AlbumApi(this.config); - this.assetApi = new AssetApi(this.config); - this.authenticationApi = new AuthenticationApi(this.config); - this.jobApi = new JobApi(this.config); - this.keyApi = new APIKeyApi(this.config); - this.oauthApi = new OAuthApi(this.config); - this.partnerApi = new PartnerApi(this.config); - this.searchApi = new SearchApi(this.config); - this.serverInfoApi = new ServerInfoApi(this.config); - this.sharedLinkApi = new SharedLinkApi(this.config); - this.personApi = new PersonApi(this.config); - this.systemConfigApi = new SystemConfigApi(this.config); - this.userApi = new UserApi(this.config); - } + this.albumApi = new AlbumApi(this.config); + this.assetApi = new AssetApi(this.config); + this.authenticationApi = new AuthenticationApi(this.config); + this.jobApi = new JobApi(this.config); + this.keyApi = new APIKeyApi(this.config); + this.oauthApi = new OAuthApi(this.config); + this.partnerApi = new PartnerApi(this.config); + this.searchApi = new SearchApi(this.config); + this.serverInfoApi = new ServerInfoApi(this.config); + this.sharedLinkApi = new SharedLinkApi(this.config); + this.personApi = new PersonApi(this.config); + this.systemConfigApi = new SystemConfigApi(this.config); + this.userApi = new UserApi(this.config); + } - private createUrl(path: string, params?: Record) { - const searchParams = new URLSearchParams(); - for (const key in params) { - const value = params[key]; - if (value !== undefined && value !== null) { - searchParams.set(key, value.toString()); - } - } + private createUrl(path: string, params?: Record) { + const searchParams = new URLSearchParams(); + for (const key in params) { + const value = params[key]; + if (value !== undefined && value !== null) { + searchParams.set(key, value.toString()); + } + } - const url = new URL(path, DUMMY_BASE_URL); - url.search = searchParams.toString(); + const url = new URL(path, DUMMY_BASE_URL); + url.search = searchParams.toString(); - return (this.config.basePath || BASE_PATH) + toPathString(url); - } + return (this.config.basePath || BASE_PATH) + toPathString(url); + } - public setAccessToken(accessToken: string) { - this.config.accessToken = accessToken; - } + public setAccessToken(accessToken: string) { + this.config.accessToken = accessToken; + } - public removeAccessToken() { - this.config.accessToken = undefined; - } + public removeAccessToken() { + this.config.accessToken = undefined; + } - public setBaseUrl(baseUrl: string) { - this.config.basePath = baseUrl; - } + public setBaseUrl(baseUrl: string) { + this.config.basePath = baseUrl; + } - public getAssetFileUrl( - ...[assetId, isThumb, isWeb, key]: ApiParams - ) { - const path = `/asset/file/${assetId}`; - return this.createUrl(path, { isThumb, isWeb, key }); - } + public getAssetFileUrl(...[assetId, isThumb, isWeb, key]: ApiParams) { + const path = `/asset/file/${assetId}`; + return this.createUrl(path, { isThumb, isWeb, key }); + } - public getAssetThumbnailUrl( - ...[assetId, format, key]: ApiParams - ) { - const path = `/asset/thumbnail/${assetId}`; - return this.createUrl(path, { format, key }); - } + public getAssetThumbnailUrl(...[assetId, format, key]: ApiParams) { + const path = `/asset/thumbnail/${assetId}`; + return this.createUrl(path, { format, key }); + } - public getProfileImageUrl(...[userId]: ApiParams) { - const path = `/user/profile-image/${userId}`; - return this.createUrl(path); - } + public getProfileImageUrl(...[userId]: ApiParams) { + const path = `/user/profile-image/${userId}`; + return this.createUrl(path); + } - public getPeopleThumbnailUrl(personId: string) { - const path = `/person/${personId}/thumbnail`; - return this.createUrl(path); - } + public getPeopleThumbnailUrl(personId: string) { + const path = `/person/${personId}/thumbnail`; + return this.createUrl(path); + } - public getJobName(jobName: JobName) { - const names: Record = { - [JobName.ThumbnailGeneration]: 'Generate Thumbnails', - [JobName.MetadataExtraction]: 'Extract Metadata', - [JobName.Sidecar]: 'Sidecar Metadata', - [JobName.ObjectTagging]: 'Tag Objects', - [JobName.ClipEncoding]: 'Encode Clip', - [JobName.RecognizeFaces]: 'Recognize Faces', - [JobName.VideoConversion]: 'Transcode Videos', - [JobName.StorageTemplateMigration]: 'Storage Template Migration', - [JobName.BackgroundTask]: 'Background Tasks', - [JobName.Search]: 'Search' - }; + public getJobName(jobName: JobName) { + const names: Record = { + [JobName.ThumbnailGeneration]: 'Generate Thumbnails', + [JobName.MetadataExtraction]: 'Extract Metadata', + [JobName.Sidecar]: 'Sidecar Metadata', + [JobName.ObjectTagging]: 'Tag Objects', + [JobName.ClipEncoding]: 'Encode Clip', + [JobName.RecognizeFaces]: 'Recognize Faces', + [JobName.VideoConversion]: 'Transcode Videos', + [JobName.StorageTemplateMigration]: 'Storage Template Migration', + [JobName.BackgroundTask]: 'Background Tasks', + [JobName.Search]: 'Search', + }; - return names[jobName]; - } + return names[jobName]; + } } export const api = new ImmichApi({ basePath: '/api' }); diff --git a/web/src/api/index.ts b/web/src/api/index.ts index cf4e108bd..1a16e38b5 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -1,3 +1,3 @@ -export * from './open-api'; export * from './api'; +export * from './open-api'; export * from './utils'; diff --git a/web/src/api/types.ts b/web/src/api/types.ts index 0bb28cca8..b26f26484 100644 --- a/web/src/api/types.ts +++ b/web/src/api/types.ts @@ -3,10 +3,6 @@ import type { Configuration } from './open-api'; /* eslint-disable @typescript-eslint/no-explicit-any */ export type ApiFp = (configuration: Configuration) => Record any>; -export type OmitLast = T extends readonly [...infer U, any?] - ? U - : [...T]; +export type OmitLast = T extends readonly [...infer U, any?] ? U : [...T]; -export type ApiParams> = OmitLast< - Parameters[K]> ->; +export type ApiParams> = OmitLast[K]>>; diff --git a/web/src/api/utils.ts b/web/src/api/utils.ts index 39c319901..75f3b4786 100644 --- a/web/src/api/utils.ts +++ b/web/src/api/utils.ts @@ -5,31 +5,31 @@ import type { UserResponseDto } from './open-api'; export type ApiError = AxiosError<{ message: string }>; export const oauth = { - isCallback: (location: Location) => { - const search = location.search; - return search.includes('code=') || search.includes('error='); - }, - isAutoLaunchDisabled: (location: Location) => { - const values = ['autoLaunch=0', 'password=1', 'password=true']; - for (const value of values) { - if (location.search.includes(value)) { - return true; - } - } - return false; - }, - getConfig: (location: Location) => { - const redirectUri = location.href.split('?')[0]; - console.log(`OAuth Redirect URI: ${redirectUri}`); - return api.oauthApi.generateConfig({ oAuthConfigDto: { redirectUri } }); - }, - login: (location: Location) => { - return api.oauthApi.callback({ oAuthCallbackDto: { url: location.href } }); - }, - link: (location: Location): AxiosPromise => { - return api.oauthApi.link({ oAuthCallbackDto: { url: location.href } }); - }, - unlink: () => { - return api.oauthApi.unlink(); - } + isCallback: (location: Location) => { + const search = location.search; + return search.includes('code=') || search.includes('error='); + }, + isAutoLaunchDisabled: (location: Location) => { + const values = ['autoLaunch=0', 'password=1', 'password=true']; + for (const value of values) { + if (location.search.includes(value)) { + return true; + } + } + return false; + }, + getConfig: (location: Location) => { + const redirectUri = location.href.split('?')[0]; + console.log(`OAuth Redirect URI: ${redirectUri}`); + return api.oauthApi.generateConfig({ oAuthConfigDto: { redirectUri } }); + }, + login: (location: Location) => { + return api.oauthApi.callback({ oAuthCallbackDto: { url: location.href } }); + }, + link: (location: Location): AxiosPromise => { + return api.oauthApi.link({ oAuthCallbackDto: { url: location.href } }); + }, + unlink: () => { + return api.oauthApi.unlink(); + }, }; diff --git a/web/src/app.css b/web/src/app.css index 3802a4b25..f959d2dc8 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -3,93 +3,93 @@ @tailwind utilities; @font-face { - font-family: 'Work Sans'; - src: url('$lib/assets/fonts/WorkSans-VariableFont_wght.ttf') format('truetype-variations'); - font-weight: 1 999; + font-family: 'Work Sans'; + src: url('$lib/assets/fonts/WorkSans-VariableFont_wght.ttf') format('truetype-variations'); + font-weight: 1 999; } @font-face { - font-family: 'Snowburst One'; - src: url('$lib/assets/fonts/SnowburstOne-Regular.ttf') format('truetype'); + font-family: 'Snowburst One'; + src: url('$lib/assets/fonts/SnowburstOne-Regular.ttf') format('truetype'); } :root { - font-family: 'Work Sans', sans-serif; + font-family: 'Work Sans', sans-serif; } html { - height: 100%; - width: 100%; + height: 100%; + width: 100%; } html::-webkit-scrollbar { - width: 8px; + width: 8px; } /* Track */ html::-webkit-scrollbar-track { - background: #f1f1f1; - border-radius: 16px; + background: #f1f1f1; + border-radius: 16px; } /* Handle */ html::-webkit-scrollbar-thumb { - background: rgba(85, 86, 87, 0.408); - border-radius: 16px; + background: rgba(85, 86, 87, 0.408); + border-radius: 16px; } /* Handle on hover */ html::-webkit-scrollbar-thumb:hover { - background: #4250afad; - border-radius: 16px; + background: #4250afad; + border-radius: 16px; } body { - margin: 0; - color: #5f6368; + margin: 0; + color: #5f6368; } input:focus-visible { - outline-offset: 0px !important; - outline: none !important; + outline-offset: 0px !important; + outline: none !important; } @layer utilities { - .immich-form-input { - @apply bg-slate-200 p-4 rounded-xl focus:border-immich-primary text-sm dark:bg-gray-600 dark:text-immich-dark-fg disabled:bg-gray-400 dark:disabled:bg-gray-800 disabled:cursor-not-allowed disabled:text-gray-200; - } + .immich-form-input { + @apply bg-slate-200 p-4 rounded-xl focus:border-immich-primary text-sm dark:bg-gray-600 dark:text-immich-dark-fg disabled:bg-gray-400 dark:disabled:bg-gray-800 disabled:cursor-not-allowed disabled:text-gray-200; + } - .immich-form-label { - @apply font-medium text-gray-500 dark:text-gray-300; - } + .immich-form-label { + @apply font-medium text-gray-500 dark:text-gray-300; + } - /* width */ - .immich-scrollbar::-webkit-scrollbar { - width: 8px; - } + /* width */ + .immich-scrollbar::-webkit-scrollbar { + width: 8px; + } - /* Track */ - .immich-scrollbar::-webkit-scrollbar-track { - background: #f1f1f1; - border-radius: 16px; - } + /* Track */ + .immich-scrollbar::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 16px; + } - /* Handle */ - .immich-scrollbar::-webkit-scrollbar-thumb { - background: rgba(85, 86, 87, 0.408); - border-radius: 16px; - } + /* Handle */ + .immich-scrollbar::-webkit-scrollbar-thumb { + background: rgba(85, 86, 87, 0.408); + border-radius: 16px; + } - /* Handle on hover */ - .immich-scrollbar::-webkit-scrollbar-thumb:hover { - background: #4250afad; - border-radius: 16px; - } + /* Handle on hover */ + .immich-scrollbar::-webkit-scrollbar-thumb:hover { + background: #4250afad; + border-radius: 16px; + } - /* Hidden scrollbar */ - /* width */ - .scrollbar-hidden::-webkit-scrollbar { - display: none; - scrollbar-width: none; - } + /* Hidden scrollbar */ + /* width */ + .scrollbar-hidden::-webkit-scrollbar { + display: none; + scrollbar-width: none; + } } diff --git a/web/src/app.d.ts b/web/src/app.d.ts index e6bff3ed8..c29b346be 100644 --- a/web/src/app.d.ts +++ b/web/src/app.d.ts @@ -3,32 +3,32 @@ // See https://kit.svelte.dev/docs/types#app // for information about these interfaces declare namespace App { - interface Locals { - user?: import('@api').UserResponseDto; - api: import('@api').ImmichApi; - } + interface Locals { + user?: import('@api').UserResponseDto; + api: import('@api').ImmichApi; + } - interface PageData { - meta: { - title: string; - description?: string; - imageUrl?: string; - }; - } + interface PageData { + meta: { + title: string; + description?: string; + imageUrl?: string; + }; + } - interface Error { - message: string; - stack?: string; - code?: string | number; - } + interface Error { + message: string; + stack?: string; + code?: string | number; + } } // Source: https://stackoverflow.com/questions/63814432/typescript-typing-of-non-standard-window-event-in-svelte // To fix the { - 'on:copyImage'?: () => void; - 'on:zoomImage'?: () => void; - } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface HTMLAttributes { + 'on:copyImage'?: () => void; + 'on:zoomImage'?: () => void; + } } diff --git a/web/src/app.html b/web/src/app.html index 8aed10653..9da6868f7 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -1,21 +1,21 @@ - - - - %sveltekit.head% - - + + + + %sveltekit.head% + + - -
%sveltekit.body%
- + +
%sveltekit.body%
+ diff --git a/web/src/hooks.server.ts b/web/src/hooks.server.ts index 8998257e7..6abd4a2f2 100644 --- a/web/src/hooks.server.ts +++ b/web/src/hooks.server.ts @@ -4,54 +4,54 @@ import type { AxiosError, AxiosResponse } from 'axios'; import { ImmichApi } from './api/api'; export const handle = (async ({ event, resolve }) => { - const basePath = env.PUBLIC_IMMICH_SERVER_URL || 'http://immich-server:3001'; - const accessToken = event.cookies.get('immich_access_token'); - const api = new ImmichApi({ basePath, accessToken }); + const basePath = env.PUBLIC_IMMICH_SERVER_URL || 'http://immich-server:3001'; + const accessToken = event.cookies.get('immich_access_token'); + const api = new ImmichApi({ basePath, accessToken }); - // API instance that should be used for all server-side requests. - event.locals.api = api; + // API instance that should be used for all server-side requests. + event.locals.api = api; - if (accessToken) { - try { - const { data: user } = await api.userApi.getMyUserInfo(); - event.locals.user = user; - } catch (err) { - const apiError = err as AxiosError; + if (accessToken) { + try { + const { data: user } = await api.userApi.getMyUserInfo(); + event.locals.user = user; + } catch (err) { + const apiError = err as AxiosError; - // Ignore 401 unauthorized errors and log all others. - if (apiError.response?.status !== 401) { - console.error('[ERROR] hooks.server.ts [handle]:', err); - } - } - } + // Ignore 401 unauthorized errors and log all others. + if (apiError.response?.status !== 401) { + console.error('[ERROR] hooks.server.ts [handle]:', err); + } + } + } - const res = await resolve(event); + const res = await resolve(event); - // The link header can grow quite big and has caused issues with our nginx - // proxy returning a 502 Bad Gateway error. Therefore the header gets deleted. - res.headers.delete('Link'); + // The link header can grow quite big and has caused issues with our nginx + // proxy returning a 502 Bad Gateway error. Therefore the header gets deleted. + res.headers.delete('Link'); - return res; + return res; }) satisfies Handle; const DEFAULT_MESSAGE = 'Hmm, not sure about that. Check the logs or open a ticket?'; export const handleError: HandleServerError = async ({ error }) => { - const httpError = error as AxiosError; - const response = httpError?.response as AxiosResponse<{ - message: string; - statusCode: number; - error: string; - }>; + const httpError = error as AxiosError; + const response = httpError?.response as AxiosResponse<{ + message: string; + statusCode: number; + error: string; + }>; - let code = response?.data?.statusCode || response?.status || httpError.code || '500'; - if (response) { - code += ` - ${response.data?.error || response.statusText}`; - } + let code = response?.data?.statusCode || response?.status || httpError.code || '500'; + if (response) { + code += ` - ${response.data?.error || response.statusText}`; + } - return { - message: response?.data?.message || httpError?.message || DEFAULT_MESSAGE, - code, - stack: httpError?.stack - }; + return { + message: response?.data?.message || httpError?.message || DEFAULT_MESSAGE, + code, + stack: httpError?.stack, + }; }; diff --git a/web/src/lib/__mocks__/jsdom-url.mock.ts b/web/src/lib/__mocks__/jsdom-url.mock.ts index b915c8438..abe00cd2e 100644 --- a/web/src/lib/__mocks__/jsdom-url.mock.ts +++ b/web/src/lib/__mocks__/jsdom-url.mock.ts @@ -1,8 +1,8 @@ const createObjectURLMock = jest.fn(); Object.defineProperty(URL, 'createObjectURL', { - writable: true, - value: createObjectURLMock + writable: true, + value: createObjectURLMock, }); export { createObjectURLMock }; diff --git a/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte b/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte index 8f687f3b6..2e703de8e 100644 --- a/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte +++ b/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte @@ -1,36 +1,35 @@ - -
-

- {user.firstName} {user.lastName}'s account and assets will be permanently deleted - after 7 days. -

-

Are you sure you want to continue?

-
-
+ +
+

+ {user.firstName} {user.lastName}'s account and assets will be permanently deleted after 7 days. +

+

Are you sure you want to continue?

+
+
diff --git a/web/src/lib/components/admin-page/jobs/job-tile-button.svelte b/web/src/lib/components/admin-page/jobs/job-tile-button.svelte index 42be6c323..cf12b7c43 100644 --- a/web/src/lib/components/admin-page/jobs/job-tile-button.svelte +++ b/web/src/lib/components/admin-page/jobs/job-tile-button.svelte @@ -1,21 +1,21 @@ diff --git a/web/src/lib/components/admin-page/jobs/job-tile-status.svelte b/web/src/lib/components/admin-page/jobs/job-tile-status.svelte index 4f31ad6ff..67edd30f9 100644 --- a/web/src/lib/components/admin-page/jobs/job-tile-status.svelte +++ b/web/src/lib/components/admin-page/jobs/job-tile-status.svelte @@ -1,16 +1,16 @@
- +
diff --git a/web/src/lib/components/admin-page/jobs/job-tile.svelte b/web/src/lib/components/admin-page/jobs/job-tile.svelte index d0043765c..554415d30 100644 --- a/web/src/lib/components/admin-page/jobs/job-tile.svelte +++ b/web/src/lib/components/admin-page/jobs/job-tile.svelte @@ -1,149 +1,141 @@
-
- {#if queueStatus.isPaused} - Paused - {:else if queueStatus.isActive} - Active - {/if} -
-
- - -
- {#if jobCounts.failed > 0} - - {jobCounts.failed.toLocaleString($locale)} failed - - {/if} - {#if jobCounts.delayed > 0} - - {jobCounts.delayed.toLocaleString($locale)} delayed - - {/if} -
-
+
+ {#if queueStatus.isPaused} + Paused + {:else if queueStatus.isActive} + Active + {/if} +
+
+ + +
+ {#if jobCounts.failed > 0} + + {jobCounts.failed.toLocaleString($locale)} failed + + {/if} + {#if jobCounts.delayed > 0} + + {jobCounts.delayed.toLocaleString($locale)} delayed + + {/if} +
+
- {#if subtitle} -
{subtitle}
- {/if} + {#if subtitle} +
{subtitle}
+ {/if} - {#if slots?.description} -
- -
- {/if} + {#if slots?.description} +
+ +
+ {/if} -
-
-

Active

-

- {jobCounts.active.toLocaleString($locale)} -

-
+
+
+

Active

+

+ {jobCounts.active.toLocaleString($locale)} +

+
-
-

- {waitingCount.toLocaleString($locale)} -

-

Waiting

-
-
-
-
-
- {#if !isIdle} - {#if waitingCount > 0} - dispatch('command', { command: JobCommand.Empty, force: false })} - > - CLEAR - - {/if} - {#if queueStatus.isPaused} - {@const size = waitingCount > 0 ? '24' : '48'} - dispatch('command', { command: JobCommand.Resume, force: false })} - > - - RESUME - - {:else} - dispatch('command', { command: JobCommand.Pause, force: false })} - > - PAUSE - - {/if} - {:else if allowForceCommand} - dispatch('command', { command: JobCommand.Start, force: true })} - > - - {allText} - - dispatch('command', { command: JobCommand.Start, force: false })} - > - - {missingText} - - {:else} - dispatch('command', { command: JobCommand.Start, force: false })} - > - START - - {/if} -
+
+

+ {waitingCount.toLocaleString($locale)} +

+

Waiting

+
+
+
+
+
+ {#if !isIdle} + {#if waitingCount > 0} + dispatch('command', { command: JobCommand.Empty, force: false })}> + CLEAR + + {/if} + {#if queueStatus.isPaused} + {@const size = waitingCount > 0 ? '24' : '48'} + dispatch('command', { command: JobCommand.Resume, force: false })} + > + + RESUME + + {:else} + dispatch('command', { command: JobCommand.Pause, force: false })} + > + PAUSE + + {/if} + {:else if allowForceCommand} + dispatch('command', { command: JobCommand.Start, force: true })}> + + {allText} + + dispatch('command', { command: JobCommand.Start, force: false })} + > + + {missingText} + + {:else} + dispatch('command', { command: JobCommand.Start, force: false })} + > + START + + {/if} +
diff --git a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte index fee23f173..55bc0cdf3 100644 --- a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte +++ b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte @@ -1,160 +1,159 @@ {#if faceConfirm} - (faceConfirm = false)} - /> + (faceConfirm = false)} + /> {/if}
- - {#each jobDetailsArray as [jobName, { title, subtitle, allText, missingText, allowForceCommand, icon, component, handleCommand: handleCommandOverride }]} - {@const { jobCounts, queueStatus } = jobs[jobName]} - (handleCommandOverride || handleCommand)(jobName, detail)} - > - {#if component} - - {/if} - - {/each} + + {#each jobDetailsArray as [jobName, { title, subtitle, allText, missingText, allowForceCommand, icon, component, handleCommand: handleCommandOverride }]} + {@const { jobCounts, queueStatus } = jobs[jobName]} + (handleCommandOverride || handleCommand)(jobName, detail)} + > + {#if component} + + {/if} + + {/each}
diff --git a/web/src/lib/components/admin-page/jobs/storage-migration-description.svelte b/web/src/lib/components/admin-page/jobs/storage-migration-description.svelte index d5cae7ccf..d84006595 100644 --- a/web/src/lib/components/admin-page/jobs/storage-migration-description.svelte +++ b/web/src/lib/components/admin-page/jobs/storage-migration-description.svelte @@ -1,10 +1,9 @@ Apply the current -Storage templateStorage template to previously uploaded assets diff --git a/web/src/lib/components/admin-page/restore-dialoge.svelte b/web/src/lib/components/admin-page/restore-dialoge.svelte index d78006413..7ee68daef 100644 --- a/web/src/lib/components/admin-page/restore-dialoge.svelte +++ b/web/src/lib/components/admin-page/restore-dialoge.svelte @@ -1,27 +1,21 @@ - - -

{user.firstName} {user.lastName}'s account will be restored.

-
+ + +

{user.firstName} {user.lastName}'s account will be restored.

+
diff --git a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte index c1d26f115..9305f98d7 100644 --- a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte +++ b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte @@ -1,122 +1,109 @@
-
-

TOTAL USAGE

+
+

TOTAL USAGE

- -
-
-
-
- -

PHOTOS

-
+ +
+
+
+
+ +

PHOTOS

+
-
- {zeros(stats.photos)}{stats.photos} -
-
-
-
- -

VIDEOS

-
+
+ {zeros(stats.photos)}{stats.photos} +
+
+
+
+ +

VIDEOS

+
-
- {zeros(stats.videos)}{stats.videos} -
-
-
-
- -

STORAGE

-
+
+ {zeros(stats.videos)}{stats.videos} +
+
+
+
+ +

STORAGE

+
-
- {zeros(statsUsage)}{statsUsage} - {statsUsageUnit} -
-
-
-
-
+
+ {zeros(statsUsage)}{statsUsage} + {statsUsageUnit} +
+
+
+
+
-
-

USER USAGE DETAIL

- - - - - - - - - - - {#each stats.usageByUser as user (user.userId)} - - - - - - - {/each} - -
UserPhotosVideosSize
{user.userFirstName} {user.userLastName}{user.photos.toLocaleString($locale)}{user.videos.toLocaleString($locale)}{asByteUnitString(user.usage, $locale)}
-
+
+

USER USAGE DETAIL

+ + + + + + + + + + + {#each stats.usageByUser as user (user.userId)} + + + + + + + {/each} + +
UserPhotosVideosSize
{user.userFirstName} {user.userLastName}{user.photos.toLocaleString($locale)}{user.videos.toLocaleString($locale)}{asByteUnitString(user.usage, $locale)}
+
diff --git a/web/src/lib/components/admin-page/server-stats/stats-card.svelte b/web/src/lib/components/admin-page/server-stats/stats-card.svelte index 4a704a787..3579638ba 100644 --- a/web/src/lib/components/admin-page/server-stats/stats-card.svelte +++ b/web/src/lib/components/admin-page/server-stats/stats-card.svelte @@ -1,34 +1,32 @@ -
-
- -

{title}

-
+
+
+ +

{title}

+
-
- {zeros()}{value} - {#if unit} - {unit} - {/if} -
+
+ {zeros()}{value} + {#if unit} + {unit} + {/if} +
diff --git a/web/src/lib/components/admin-page/settings/confirm-disable-login.svelte b/web/src/lib/components/admin-page/settings/confirm-disable-login.svelte index 37f740d43..b0c3e650d 100644 --- a/web/src/lib/components/admin-page/settings/confirm-disable-login.svelte +++ b/web/src/lib/components/admin-page/settings/confirm-disable-login.svelte @@ -1,22 +1,22 @@ - -
-

Are you sure you want to disable all login methods? Login will be completely disabled.

-

- To re-enable, use a - - Server Command. -

-
-
+ +
+

Are you sure you want to disable all login methods? Login will be completely disabled.

+

+ To re-enable, use a + + Server Command. +

+
+
diff --git a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte index c78e6f72d..0b3d3b981 100644 --- a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte +++ b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte @@ -1,211 +1,211 @@
- {#await getConfigs() then} -
-
-
- + {#await getConfigs() then} +
+ +
+ - + - + - + - + - + - + - + - -
+ +
-
- -
- -
- {/await} +
+ +
+ +
+ {/await}
diff --git a/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte b/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte index 2c98af296..37fc0a619 100644 --- a/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte +++ b/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte @@ -1,103 +1,101 @@
- {#await getConfigs() then} -
-
- {#each jobNames as jobName} -
- -
- {/each} + {#await getConfigs() then} +
+ + {#each jobNames as jobName} +
+ +
+ {/each} -
- -
- -
- {/await} +
+ +
+ +
+ {/await}
diff --git a/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte b/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte index 644135894..ffee598f8 100644 --- a/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte +++ b/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte @@ -1,212 +1,209 @@ {#if isConfirmOpen} - handleConfirm(false)} - on:confirm={() => handleConfirm(true)} - /> + handleConfirm(false)} on:confirm={() => handleConfirm(true)} /> {/if}
- {#await getConfigs() then} -
-
-

- For more details about this feature, refer to the docs. -

+ {#await getConfigs() then} +
+ +

+ For more details about this feature, refer to the docs. +

- -
- + +
+ - + - + - + - + - + - + - handleToggleOverride()} - bind:checked={oauthConfig.mobileOverrideEnabled} - /> + handleToggleOverride()} + bind:checked={oauthConfig.mobileOverrideEnabled} + /> - {#if oauthConfig.mobileOverrideEnabled} - - {/if} + {#if oauthConfig.mobileOverrideEnabled} + + {/if} - - -
- {/await} + + +
+ {/await}
diff --git a/web/src/lib/components/admin-page/settings/password-login/password-login-settings.svelte b/web/src/lib/components/admin-page/settings/password-login/password-login-settings.svelte index fb8f0ef7c..0cab82c55 100644 --- a/web/src/lib/components/admin-page/settings/password-login/password-login-settings.svelte +++ b/web/src/lib/components/admin-page/settings/password-login/password-login-settings.svelte @@ -1,121 +1,118 @@ {#if isConfirmOpen} - handleConfirm(false)} - on:confirm={() => handleConfirm(true)} - /> + handleConfirm(false)} on:confirm={() => handleConfirm(true)} /> {/if}
- {#await getConfigs() then} -
-
-
-
- + {#await getConfigs() then} +
+ +
+
+ - -
-
- -
- {/await} + +
+
+ +
+ {/await}
diff --git a/web/src/lib/components/admin-page/settings/setting-accordion.svelte b/web/src/lib/components/admin-page/settings/setting-accordion.svelte index 39b407143..941426ba2 100644 --- a/web/src/lib/components/admin-page/settings/setting-accordion.svelte +++ b/web/src/lib/components/admin-page/settings/setting-accordion.svelte @@ -1,56 +1,56 @@
-
-
-

- {title} -

+
+
+

+ {title} +

-

{subtitle}

-
+

{subtitle}

+
- -
+ +
- {#if isOpen} -
    - -
- {/if} + {#if isOpen} +
    + +
+ {/if}
diff --git a/web/src/lib/components/admin-page/settings/setting-buttons-row.svelte b/web/src/lib/components/admin-page/settings/setting-buttons-row.svelte index e82d9b86f..d1b6aa4d9 100644 --- a/web/src/lib/components/admin-page/settings/setting-buttons-row.svelte +++ b/web/src/lib/components/admin-page/settings/setting-buttons-row.svelte @@ -1,26 +1,26 @@
-
- {#if showResetToDefault} - - {/if} -
+
+ {#if showResetToDefault} + + {/if} +
-
- - -
+
+ + +
diff --git a/web/src/lib/components/admin-page/settings/setting-input-field.svelte b/web/src/lib/components/admin-page/settings/setting-input-field.svelte index 28ac4ca00..931fb896d 100644 --- a/web/src/lib/components/admin-page/settings/setting-input-field.svelte +++ b/web/src/lib/components/admin-page/settings/setting-input-field.svelte @@ -1,65 +1,65 @@
-
- - {#if required} -
*
- {/if} +
+ + {#if required} +
*
+ {/if} - {#if isEdited} -
- Unsaved change -
- {/if} -
+ {#if isEdited} +
+ Unsaved change +
+ {/if} +
- {#if desc} -

- {desc} -

- {/if} + {#if desc} +

+ {desc} +

+ {/if} - +
diff --git a/web/src/lib/components/admin-page/settings/setting-select.svelte b/web/src/lib/components/admin-page/settings/setting-select.svelte index ed9e297a4..2a40a910d 100644 --- a/web/src/lib/components/admin-page/settings/setting-select.svelte +++ b/web/src/lib/components/admin-page/settings/setting-select.svelte @@ -1,49 +1,49 @@
-
- +
+ - {#if isEdited} -
- Unsaved change -
- {/if} -
+ {#if isEdited} +
+ Unsaved change +
+ {/if} +
- {#if desc} -

- {desc} -

- {/if} + {#if desc} +

+ {desc} +

+ {/if} - +
diff --git a/web/src/lib/components/admin-page/settings/setting-switch.svelte b/web/src/lib/components/admin-page/settings/setting-switch.svelte index 22c149c9e..205ee0e24 100644 --- a/web/src/lib/components/admin-page/settings/setting-switch.svelte +++ b/web/src/lib/components/admin-page/settings/setting-switch.svelte @@ -1,96 +1,90 @@
-
-
- - {#if isEdited} -
- Unsaved change -
- {/if} -
+
+
+ + {#if isEdited} +
+ Unsaved change +
+ {/if} +
-

{subtitle}

-
+

{subtitle}

+
-
diff --git a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte index eae24ffe8..a405d42d9 100644 --- a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte +++ b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte @@ -1,241 +1,224 @@
- {#await getConfigs() then} -
-

- Variables -

+ {#await getConfigs() then} +
+

Variables

-
- {#await getSupportDateTimeFormat()} - - {:then options} -
- -
- {/await} -
+
+ {#await getSupportDateTimeFormat()} + + {:then options} +
+ +
+ {/await} +
-
- -
+
+ +
-
-

- Template -

+
+

Template

-
-

PREVIEW

-
+
+

PREVIEW

+
-

- Approximately path length limit : {parsedTemplate().length + user.id.length + 'UPLOAD_LOCATION'.length}/260 -

+

+ Approximately path length limit : {parsedTemplate().length + user.id.length + 'UPLOAD_LOCATION'.length}/260 +

-

- {user.storageLabel || user.id} is the user's Storage Label -

+

+ {user.storageLabel || user.id} is the user's Storage Label +

-

- UPLOAD_LOCATION/{user.storageLabel || user.id}/{parsedTemplate()}.jpg -

+

+ UPLOAD_LOCATION/{user.storageLabel || user.id}/{parsedTemplate()}.jpg +

-
-
- - -
-
- + +
+ + +
+
+ -
- -
-
+
+ +
+
-
-

- Template changes will only apply to new assets. To retroactively apply the template to - previously uploaded assets, run the Storage Migration Job -

-
+
+

+ Template changes will only apply to new assets. To retroactively apply the template to previously uploaded + assets, run the Storage Migration Job +

+
- - -
-
- {/await} + + +
+
+ {/await}
diff --git a/web/src/lib/components/admin-page/settings/storage-template/supported-datetime-panel.svelte b/web/src/lib/components/admin-page/settings/storage-template/supported-datetime-panel.svelte index d30798fb1..587ef3e0f 100644 --- a/web/src/lib/components/admin-page/settings/storage-template/supported-datetime-panel.svelte +++ b/web/src/lib/components/admin-page/settings/storage-template/supported-datetime-panel.svelte @@ -1,78 +1,76 @@
-

DATE & TIME

+

DATE & TIME

-
-

Asset's creation timestamp is used for the datetime information

-

Sample time 2022-09-04T20:03:05.250

-
-
-
-

YEAR

-
    - {#each options.yearOptions as yearFormat} -
  • {'{{'}{yearFormat}{'}}'} - {getLuxonExample(yearFormat)}
  • - {/each} -
-
+
+

Asset's creation timestamp is used for the datetime information

+

Sample time 2022-09-04T20:03:05.250

+
+
+
+

YEAR

+
    + {#each options.yearOptions as yearFormat} +
  • {'{{'}{yearFormat}{'}}'} - {getLuxonExample(yearFormat)}
  • + {/each} +
+
-
-

MONTH

-
    - {#each options.monthOptions as monthFormat} -
  • {'{{'}{monthFormat}{'}}'} - {getLuxonExample(monthFormat)}
  • - {/each} -
-
+
+

MONTH

+
    + {#each options.monthOptions as monthFormat} +
  • {'{{'}{monthFormat}{'}}'} - {getLuxonExample(monthFormat)}
  • + {/each} +
+
-
-

DAY

-
    - {#each options.dayOptions as dayFormat} -
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • - {/each} -
-
+
+

DAY

+
    + {#each options.dayOptions as dayFormat} +
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • + {/each} +
+
-
-

HOUR

-
    - {#each options.hourOptions as dayFormat} -
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • - {/each} -
-
+
+

HOUR

+
    + {#each options.hourOptions as dayFormat} +
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • + {/each} +
+
-
-

MINUTE

-
    - {#each options.minuteOptions as dayFormat} -
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • - {/each} -
-
+
+

MINUTE

+
    + {#each options.minuteOptions as dayFormat} +
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • + {/each} +
+
-
-

SECOND

-
    - {#each options.secondOptions as dayFormat} -
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • - {/each} -
-
-
+
+

SECOND

+
    + {#each options.secondOptions as dayFormat} +
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • + {/each} +
+
+
diff --git a/web/src/lib/components/admin-page/settings/storage-template/supported-variables-panel.svelte b/web/src/lib/components/admin-page/settings/storage-template/supported-variables-panel.svelte index a65c2bd7e..e9d815aa6 100644 --- a/web/src/lib/components/admin-page/settings/storage-template/supported-variables-panel.svelte +++ b/web/src/lib/components/admin-page/settings/storage-template/supported-variables-panel.svelte @@ -1,29 +1,29 @@
-

OTHER VARIABLES

+

OTHER VARIABLES

-
-
-

FILE NAME

-
    -
  • {`{{filename}}`}
  • -
-
+
+
+

FILE NAME

+
    +
  • {`{{filename}}`}
  • +
+
-
-

FILE EXTENSION

-
    -
  • {`{{ext}}`}
  • -
-
+
+

FILE EXTENSION

+
    +
  • {`{{ext}}`}
  • +
+
-
-

FILE TYPE

-
    -
  • {`{{filetype}}`} - VID or IMG
  • -
  • {`{{filetypefull}}`} - VIDEO or IMAGE
  • -
-
-
+
+

FILE TYPE

+
    +
  • {`{{filetype}}`} - VID or IMG
  • +
  • {`{{filetypefull}}`} - VIDEO or IMAGE
  • +
+
+
diff --git a/web/src/lib/components/album-page/__tests__/album-card.spec.ts b/web/src/lib/components/album-page/__tests__/album-card.spec.ts index ba7b194c5..33c8c48dd 100644 --- a/web/src/lib/components/album-page/__tests__/album-card.spec.ts +++ b/web/src/lib/components/album-page/__tests__/album-card.spec.ts @@ -1,141 +1,136 @@ -import { jest, describe, it } from '@jest/globals'; -import { render, RenderResult, waitFor, fireEvent } from '@testing-library/svelte'; import { createObjectURLMock } from '$lib/__mocks__/jsdom-url.mock'; import { api, ThumbnailFormat } from '@api'; +import { describe, it, jest } from '@jest/globals'; import { albumFactory } from '@test-data'; -import AlbumCard from '../album-card.svelte'; import '@testing-library/jest-dom'; +import { fireEvent, render, RenderResult, waitFor } from '@testing-library/svelte'; +import AlbumCard from '../album-card.svelte'; jest.mock('@api'); const apiMock: jest.MockedObject = api as jest.MockedObject; describe('AlbumCard component', () => { - let sut: RenderResult; + let sut: RenderResult; - it.each([ - { - album: albumFactory.build({ albumThumbnailAssetId: null, shared: false, assetCount: 0 }), - count: 0, - shared: false - }, - { - album: albumFactory.build({ albumThumbnailAssetId: null, shared: true, assetCount: 0 }), - count: 0, - shared: true - }, - { - album: albumFactory.build({ albumThumbnailAssetId: null, shared: false, assetCount: 5 }), - count: 5, - shared: false - }, - { - album: albumFactory.build({ albumThumbnailAssetId: null, shared: true, assetCount: 2 }), - count: 2, - shared: true - } - ])( - 'shows album data without thumbnail with count $count - shared: $shared', - async ({ album, count, shared }) => { - sut = render(AlbumCard, { album, user: album.owner }); + it.each([ + { + album: albumFactory.build({ albumThumbnailAssetId: null, shared: false, assetCount: 0 }), + count: 0, + shared: false, + }, + { + album: albumFactory.build({ albumThumbnailAssetId: null, shared: true, assetCount: 0 }), + count: 0, + shared: true, + }, + { + album: albumFactory.build({ albumThumbnailAssetId: null, shared: false, assetCount: 5 }), + count: 5, + shared: false, + }, + { + album: albumFactory.build({ albumThumbnailAssetId: null, shared: true, assetCount: 2 }), + count: 2, + shared: true, + }, + ])('shows album data without thumbnail with count $count - shared: $shared', async ({ album, count, shared }) => { + sut = render(AlbumCard, { album, user: album.owner }); - const albumImgElement = sut.getByTestId('album-image'); - const albumNameElement = sut.getByTestId('album-name'); - const albumDetailsElement = sut.getByTestId('album-details'); - const detailsText = `${count} items` + (shared ? ' . Shared' : ''); + const albumImgElement = sut.getByTestId('album-image'); + const albumNameElement = sut.getByTestId('album-name'); + const albumDetailsElement = sut.getByTestId('album-details'); + const detailsText = `${count} items` + (shared ? ' . Shared' : ''); - expect(albumImgElement).toHaveAttribute('src'); - expect(albumImgElement).toHaveAttribute('alt', album.id); + expect(albumImgElement).toHaveAttribute('src'); + expect(albumImgElement).toHaveAttribute('alt', album.id); - await waitFor(() => expect(albumImgElement).toHaveAttribute('src')); + await waitFor(() => expect(albumImgElement).toHaveAttribute('src')); - expect(albumImgElement).toHaveAttribute('alt', album.id); - expect(apiMock.assetApi.getAssetThumbnail).not.toHaveBeenCalled(); + expect(albumImgElement).toHaveAttribute('alt', album.id); + expect(apiMock.assetApi.getAssetThumbnail).not.toHaveBeenCalled(); - expect(albumNameElement).toHaveTextContent(album.albumName); - expect(albumDetailsElement).toHaveTextContent(new RegExp(detailsText)); - } - ); + expect(albumNameElement).toHaveTextContent(album.albumName); + expect(albumDetailsElement).toHaveTextContent(new RegExp(detailsText)); + }); - it('shows album data and and loads the thumbnail image when available', async () => { - const thumbnailFile = new File([new Blob()], 'fileThumbnail'); - const thumbnailUrl = 'blob:thumbnailUrlOne'; - apiMock.assetApi.getAssetThumbnail.mockResolvedValue({ - data: thumbnailFile, - config: {}, - headers: {}, - status: 200, - statusText: '' - }); - createObjectURLMock.mockReturnValueOnce(thumbnailUrl); + it('shows album data and and loads the thumbnail image when available', async () => { + const thumbnailFile = new File([new Blob()], 'fileThumbnail'); + const thumbnailUrl = 'blob:thumbnailUrlOne'; + apiMock.assetApi.getAssetThumbnail.mockResolvedValue({ + data: thumbnailFile, + config: {}, + headers: {}, + status: 200, + statusText: '', + }); + createObjectURLMock.mockReturnValueOnce(thumbnailUrl); - const album = albumFactory.build({ - albumThumbnailAssetId: 'thumbnailIdOne', - shared: false, - albumName: 'some album name' - }); - sut = render(AlbumCard, { album, user: album.owner }); + const album = albumFactory.build({ + albumThumbnailAssetId: 'thumbnailIdOne', + shared: false, + albumName: 'some album name', + }); + sut = render(AlbumCard, { album, user: album.owner }); - const albumImgElement = sut.getByTestId('album-image'); - const albumNameElement = sut.getByTestId('album-name'); - const albumDetailsElement = sut.getByTestId('album-details'); - expect(albumImgElement).toHaveAttribute('alt', album.id); + const albumImgElement = sut.getByTestId('album-image'); + const albumNameElement = sut.getByTestId('album-name'); + const albumDetailsElement = sut.getByTestId('album-details'); + expect(albumImgElement).toHaveAttribute('alt', album.id); - await waitFor(() => expect(albumImgElement).toHaveAttribute('src', thumbnailUrl)); + await waitFor(() => expect(albumImgElement).toHaveAttribute('src', thumbnailUrl)); - expect(albumImgElement).toHaveAttribute('alt', album.id); - expect(apiMock.assetApi.getAssetThumbnail).toHaveBeenCalledTimes(1); - expect(apiMock.assetApi.getAssetThumbnail).toHaveBeenCalledWith( - { - id: 'thumbnailIdOne', - format: ThumbnailFormat.Jpeg - }, - { responseType: 'blob' } - ); - expect(createObjectURLMock).toHaveBeenCalledWith(thumbnailFile); + expect(albumImgElement).toHaveAttribute('alt', album.id); + expect(apiMock.assetApi.getAssetThumbnail).toHaveBeenCalledTimes(1); + expect(apiMock.assetApi.getAssetThumbnail).toHaveBeenCalledWith( + { + id: 'thumbnailIdOne', + format: ThumbnailFormat.Jpeg, + }, + { responseType: 'blob' }, + ); + expect(createObjectURLMock).toHaveBeenCalledWith(thumbnailFile); - expect(albumNameElement).toHaveTextContent('some album name'); - expect(albumDetailsElement).toHaveTextContent('0 items'); - }); + expect(albumNameElement).toHaveTextContent('some album name'); + expect(albumDetailsElement).toHaveTextContent('0 items'); + }); - describe('with rendered component - no thumbnail', () => { - const album = Object.freeze(albumFactory.build({ albumThumbnailAssetId: null })); + describe('with rendered component - no thumbnail', () => { + const album = Object.freeze(albumFactory.build({ albumThumbnailAssetId: null })); - beforeEach(async () => { - sut = render(AlbumCard, { album, user: album.owner }); + beforeEach(async () => { + sut = render(AlbumCard, { album, user: album.owner }); - const albumImgElement = sut.getByTestId('album-image'); - await waitFor(() => expect(albumImgElement).toHaveAttribute('src')); - }); + const albumImgElement = sut.getByTestId('album-image'); + await waitFor(() => expect(albumImgElement).toHaveAttribute('src')); + }); - it('dispatches custom "click" event with the album in context', async () => { - const onClickHandler = jest.fn(); - sut.component.$on('click', onClickHandler); - const albumCardElement = sut.getByTestId('album-card'); + it('dispatches custom "click" event with the album in context', async () => { + const onClickHandler = jest.fn(); + sut.component.$on('click', onClickHandler); + const albumCardElement = sut.getByTestId('album-card'); - await fireEvent.click(albumCardElement); - expect(onClickHandler).toHaveBeenCalledTimes(1); - expect(onClickHandler).toHaveBeenCalledWith(expect.objectContaining({ detail: album })); - }); + await fireEvent.click(albumCardElement); + expect(onClickHandler).toHaveBeenCalledTimes(1); + expect(onClickHandler).toHaveBeenCalledWith(expect.objectContaining({ detail: album })); + }); - it('dispatches custom "click" event on context menu click with mouse coordinates', async () => { - const onClickHandler = jest.fn(); - sut.component.$on('showalbumcontextmenu', onClickHandler); + it('dispatches custom "click" event on context menu click with mouse coordinates', async () => { + const onClickHandler = jest.fn(); + sut.component.$on('showalbumcontextmenu', onClickHandler); - const contextMenuBtnParent = sut.getByTestId('context-button-parent'); + const contextMenuBtnParent = sut.getByTestId('context-button-parent'); - await fireEvent( - contextMenuBtnParent, - new MouseEvent('click', { - clientX: 123, - clientY: 456 - }) - ); + await fireEvent( + contextMenuBtnParent, + new MouseEvent('click', { + clientX: 123, + clientY: 456, + }), + ); - expect(onClickHandler).toHaveBeenCalledTimes(1); - expect(onClickHandler).toHaveBeenCalledWith( - expect.objectContaining({ detail: { x: 123, y: 456 } }) - ); - }); - }); + expect(onClickHandler).toHaveBeenCalledTimes(1); + expect(onClickHandler).toHaveBeenCalledWith(expect.objectContaining({ detail: { x: 123, y: 456 } })); + }); + }); }); diff --git a/web/src/lib/components/album-page/album-card.svelte b/web/src/lib/components/album-page/album-card.svelte index 17ec88676..62f350f74 100644 --- a/web/src/lib/components/album-page/album-card.svelte +++ b/web/src/lib/components/album-page/album-card.svelte @@ -1,133 +1,133 @@
dispatchClick('click', album)} - on:keydown={() => dispatchClick('click', album)} - data-testid="album-card" + class="group hover:cursor-pointer mt-4 border-[3px] border-transparent dark:hover:border-immich-dark-primary/75 hover:border-immich-primary/75 rounded-3xl p-5 relative" + on:click={() => dispatchClick('click', album)} + on:keydown={() => dispatchClick('click', album)} + data-testid="album-card" > - - {#if showContextMenu} -
- - - -
- {/if} + + {#if showContextMenu} +
+ + + +
+ {/if} -
- {album.id} -
-
+
+ {album.id} +
+
-
-

- {album.albumName} -

+
+

+ {album.albumName} +

- - {#if showItemCount} -

- {album.assetCount.toLocaleString($locale)} - {album.assetCount == 1 ? `item` : `items`} -

- {/if} + + {#if showItemCount} +

+ {album.assetCount.toLocaleString($locale)} + {album.assetCount == 1 ? `item` : `items`} +

+ {/if} - {#if isSharingView || album.shared} -

·

- {/if} + {#if isSharingView || album.shared} +

·

+ {/if} - {#if isSharingView} - {#await getAlbumOwnerInfo() then albumOwner} - {#if user.email == albumOwner.email} -

Owned

- {:else} -

- Shared by {albumOwner.firstName} - {albumOwner.lastName} -

- {/if} - {/await} - {:else if album.shared} -

Shared

- {/if} -
-
+ {#if isSharingView} + {#await getAlbumOwnerInfo() then albumOwner} + {#if user.email == albumOwner.email} +

Owned

+ {:else} +

+ Shared by {albumOwner.firstName} + {albumOwner.lastName} +

+ {/if} + {/await} + {:else if album.shared} +

Shared

+ {/if} + +
diff --git a/web/src/lib/components/album-page/album-card.ts b/web/src/lib/components/album-page/album-card.ts index c69b7e6c3..4d395d3c5 100644 --- a/web/src/lib/components/album-page/album-card.ts +++ b/web/src/lib/components/album-page/album-card.ts @@ -1,11 +1,11 @@ import type { AlbumResponseDto } from '@api'; export type OnShowContextMenu = { - showalbumcontextmenu: OnShowContextMenuDetail; + showalbumcontextmenu: OnShowContextMenuDetail; }; export type OnClick = { - click: OnClickDetail; + click: OnClickDetail; }; export type OnShowContextMenuDetail = { x: number; y: number }; diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index 4b0c28cc1..c56a579b9 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -1,533 +1,490 @@
- - {#if isMultiSelectionMode} - (multiSelectAsset = new Set())} - > - - {#if sharedLink?.allowDownload || !isPublicShared} - - {/if} - {#if isOwned} - - {/if} - - {/if} + + {#if isMultiSelectionMode} + (multiSelectAsset = new Set())}> + + {#if sharedLink?.allowDownload || !isPublicShared} + + {/if} + {#if isOwned} + + {/if} + + {/if} - - {#if !isMultiSelectionMode} - goto(backUrl)} - backIcon={ArrowLeft} - showBackButton={(!isPublicShared && isOwned) || - (!isPublicShared && !isOwned) || - (isPublicShared && isOwned)} - > - - {#if isPublicShared && !isOwned} - - -

- IMMICH -

-
- {/if} -
+ + {#if !isMultiSelectionMode} + goto(backUrl)} + backIcon={ArrowLeft} + showBackButton={(!isPublicShared && isOwned) || (!isPublicShared && !isOwned) || (isPublicShared && isOwned)} + > + + {#if isPublicShared && !isOwned} + + +

IMMICH

+
+ {/if} +
- - {#if !isCreatingSharedAlbum} - {#if !sharedLink} - (isShowAssetSelection = true)} - logo={FileImagePlusOutline} - /> - {:else if sharedLink?.allowUpload} - openFileUploadDialog(album.id, sharedLink?.key)} - logo={FileImagePlusOutline} - /> - {/if} + + {#if !isCreatingSharedAlbum} + {#if !sharedLink} + (isShowAssetSelection = true)} + logo={FileImagePlusOutline} + /> + {:else if sharedLink?.allowUpload} + openFileUploadDialog(album.id, sharedLink?.key)} + logo={FileImagePlusOutline} + /> + {/if} - {#if isOwned} - (isShowShareUserSelection = true)} - logo={ShareVariantOutline} - /> - (isShowDeleteConfirmation = true)} - logo={DeleteOutline} - /> - {/if} - {/if} + {#if isOwned} + (isShowShareUserSelection = true)} + logo={ShareVariantOutline} + /> + (isShowDeleteConfirmation = true)} + logo={DeleteOutline} + /> + {/if} + {/if} - {#if album.assetCount > 0 && !isCreatingSharedAlbum} - {#if !isPublicShared || (isPublicShared && sharedLink?.allowDownload)} - downloadAlbum()} - logo={FolderDownloadOutline} - /> - {/if} + {#if album.assetCount > 0 && !isCreatingSharedAlbum} + {#if !isPublicShared || (isPublicShared && sharedLink?.allowDownload)} + downloadAlbum()} logo={FolderDownloadOutline} /> + {/if} - {#if !isPublicShared && isOwned} - - {#if isShowAlbumOptions} - (isShowAlbumOptions = false)} - > - { - isShowThumbnailSelection = true; - isShowAlbumOptions = false; - }} - text="Set album cover" - /> - - {/if} - - {/if} - {/if} + {#if !isPublicShared && isOwned} + + {#if isShowAlbumOptions} + (isShowAlbumOptions = false)}> + { + isShowThumbnailSelection = true; + isShowAlbumOptions = false; + }} + text="Set album cover" + /> + + {/if} + + {/if} + {/if} - {#if isPublicShared} - - {/if} + {#if isPublicShared} + + {/if} - {#if isCreatingSharedAlbum && album.sharedUsers.length == 0} - - {/if} - -
- {/if} + {#if isCreatingSharedAlbum && album.sharedUsers.length == 0} + + {/if} + +
+ {/if} -
- { - if (e.key == 'Enter') { - isEditingTitle = false; - titleInput.blur(); - } - }} - on:focus={() => (isEditingTitle = true)} - on:blur={() => (isEditingTitle = false)} - class={`transition-all text-6xl text-immich-primary dark:text-immich-dark-primary w-[99%] border-b-2 border-transparent outline-none ${ - isOwned ? 'hover:border-gray-400' : 'hover:border-transparent' - } focus:outline-none focus:border-b-2 focus:border-immich-primary dark:focus:border-immich-dark-primary bg-immich-bg dark:bg-immich-dark-bg dark:focus:bg-immich-dark-gray`} - type="text" - bind:value={album.albumName} - disabled={!isOwned} - bind:this={titleInput} - /> +
+ { + if (e.key == 'Enter') { + isEditingTitle = false; + titleInput.blur(); + } + }} + on:focus={() => (isEditingTitle = true)} + on:blur={() => (isEditingTitle = false)} + class={`transition-all text-6xl text-immich-primary dark:text-immich-dark-primary w-[99%] border-b-2 border-transparent outline-none ${ + isOwned ? 'hover:border-gray-400' : 'hover:border-transparent' + } focus:outline-none focus:border-b-2 focus:border-immich-primary dark:focus:border-immich-dark-primary bg-immich-bg dark:bg-immich-dark-bg dark:focus:bg-immich-dark-gray`} + type="text" + bind:value={album.albumName} + disabled={!isOwned} + bind:this={titleInput} + /> - {#if album.assetCount > 0} - -

{getDateRange()}

-

·

-

{album.assetCount} items

-
- {/if} - {#if album.shared} -
- {#each album.sharedUsers as user (user.id)} - - {/each} + {#if album.assetCount > 0} + +

{getDateRange()}

+

·

+

{album.assetCount} items

+
+ {/if} + {#if album.shared} +
+ {#each album.sharedUsers as user (user.id)} + + {/each} - -
- {/if} + +
+ {/if} - {#if album.assetCount > 0} - - {:else} - -
-
-

ADD PHOTOS

- -
-
- {/if} -
+ {#if album.assetCount > 0} + + {:else} + +
+
+

ADD PHOTOS

+ +
+
+ {/if} +
{#if isShowAssetSelection} - (isShowAssetSelection = false)} - on:create-album={createAlbumHandler} - /> + (isShowAssetSelection = false)} + on:create-album={createAlbumHandler} + /> {/if} {#if isShowShareUserSelection} - (isShowShareUserSelection = false)} - on:add-user={addUserHandler} - on:sharedlinkclick={onSharedLinkClickHandler} - sharedUsersInAlbum={new Set(album.sharedUsers)} - /> + (isShowShareUserSelection = false)} + on:add-user={addUserHandler} + on:sharedlinkclick={onSharedLinkClickHandler} + sharedUsersInAlbum={new Set(album.sharedUsers)} + /> {/if} {#if isShowShareLinkModal} - (isShowShareLinkModal = false)} - shareType={SharedLinkType.Album} - {album} - /> + (isShowShareLinkModal = false)} shareType={SharedLinkType.Album} {album} /> {/if} {#if isShowShareInfoModal} - (isShowShareInfoModal = false)} - {album} - on:user-deleted={sharedUserDeletedHandler} - /> + (isShowShareInfoModal = false)} {album} on:user-deleted={sharedUserDeletedHandler} /> {/if} {#if isShowThumbnailSelection} - (isShowThumbnailSelection = false)} - on:thumbnail-selected={setAlbumThumbnailHandler} - /> + (isShowThumbnailSelection = false)} + on:thumbnail-selected={setAlbumThumbnailHandler} + /> {/if} {#if isShowDeleteConfirmation} - (isShowDeleteConfirmation = false)} - > - -

Are you sure you want to delete the album {album.albumName}?

-

If this album is shared, other users will not be able to access it anymore.

-
-
+ (isShowDeleteConfirmation = false)} + > + +

Are you sure you want to delete the album {album.albumName}?

+

If this album is shared, other users will not be able to access it anymore.

+
+
{/if} diff --git a/web/src/lib/components/album-page/asset-selection.svelte b/web/src/lib/components/album-page/asset-selection.svelte index 6282da2c3..054ed7c4c 100644 --- a/web/src/lib/components/album-page/asset-selection.svelte +++ b/web/src/lib/components/album-page/asset-selection.svelte @@ -1,80 +1,69 @@
- { - assetInteractionStore.clearMultiselect(); - dispatch('go-back'); - }} - > - - {#if $selectedAssets.size == 0} -

Add to album

- {:else} -

- {$selectedAssets.size.toLocaleString($locale)} selected -

- {/if} -
+ { + assetInteractionStore.clearMultiselect(); + dispatch('go-back'); + }} + > + + {#if $selectedAssets.size == 0} +

Add to album

+ {:else} +

+ {$selectedAssets.size.toLocaleString($locale)} selected +

+ {/if} +
- - - - -
-
- -
+ + + + +
+
+ +
diff --git a/web/src/lib/components/album-page/share-info-modal.svelte b/web/src/lib/components/album-page/share-info-modal.svelte index 593e93b34..f93bd3393 100644 --- a/web/src/lib/components/album-page/share-info-modal.svelte +++ b/web/src/lib/components/album-page/share-info-modal.svelte @@ -1,144 +1,140 @@ {#if !selectedRemoveUser} - dispatch('close')}> - - -

Options

-
-
+ dispatch('close')}> + + +

Options

+
+
-
- {#each album.sharedUsers as user} -
-
- -

{user.firstName} {user.lastName}

-
+
+ {#each album.sharedUsers as user} +
+
+ +

{user.firstName} {user.lastName}

+
-
- {#if isOwned} -
- showContextMenu(user)} - logo={DotsVertical} - backgroundColor="transparent" - hoverColor="#e2e7e9" - size="20" - /> +
+ {#if isOwned} +
+ showContextMenu(user)} + logo={DotsVertical} + backgroundColor="transparent" + hoverColor="#e2e7e9" + size="20" + /> - {#if selectedMenuUser === user} - (selectedMenuUser = null)}> - - - {/if} -
- {:else if user.id == currentUser?.id} - - {/if} -
-
- {/each} -
- + {#if selectedMenuUser === user} + (selectedMenuUser = null)}> + + + {/if} +
+ {:else if user.id == currentUser?.id} + + {/if} +
+
+ {/each} + + {/if} {#if selectedRemoveUser && selectedRemoveUser?.id === currentUser?.id} - (selectedRemoveUser = null)} - /> + (selectedRemoveUser = null)} + /> {/if} {#if selectedRemoveUser && selectedRemoveUser?.id !== currentUser?.id} - (selectedRemoveUser = null)} - /> + (selectedRemoveUser = null)} + /> {/if} diff --git a/web/src/lib/components/album-page/thumbnail-selection.svelte b/web/src/lib/components/album-page/thumbnail-selection.svelte index 6486ad60e..0d9d5deb3 100644 --- a/web/src/lib/components/album-page/thumbnail-selection.svelte +++ b/web/src/lib/components/album-page/thumbnail-selection.svelte @@ -1,57 +1,53 @@
- dispatch('close')}> - -

Select album cover

-
+ dispatch('close')}> + +

Select album cover

+
- - - -
+ + + +
-
- -
- {#each album.assets as asset} - (selectedThumbnail = asset)} - selected={isSelected(asset.id)} - /> - {/each} -
-
+
+ +
+ {#each album.assets as asset} + (selectedThumbnail = asset)} selected={isSelected(asset.id)} /> + {/each} +
+
diff --git a/web/src/lib/components/album-page/user-selection-modal.svelte b/web/src/lib/components/album-page/user-selection-modal.svelte index 207b9e440..3312dbd1d 100644 --- a/web/src/lib/components/album-page/user-selection-modal.svelte +++ b/web/src/lib/components/album-page/user-selection-modal.svelte @@ -1,149 +1,146 @@ dispatch('close')}> - - - -

Invite to album

-
-
+ + + +

Invite to album

+
+
-
- {#if selectedUsers.length > 0} -
-

To

+
+ {#if selectedUsers.length > 0} +
+

To

- {#each selectedUsers as user} - {#key user.id} - - {/key} - {/each} -
- {/if} + {#each selectedUsers as user} + {#key user.id} + + {/key} + {/each} +
+ {/if} - {#if users.length > 0} -

SUGGESTIONS

+ {#if users.length > 0} +

SUGGESTIONS

-
- {#each users as user} - - {/each} -
- {:else} -

- Looks like you have shared this album with all users or you don't have any user to share - with. -

- {/if} +
+

+ {user.firstName} + {user.lastName} +

+

+ {user.email} +

+
+ + {/each} +
+ {:else} +

+ Looks like you have shared this album with all users or you don't have any user to share with. +

+ {/if} - {#if selectedUsers.length > 0} -
- -
- {/if} -
+ {#if selectedUsers.length > 0} +
+ +
+ {/if} +
-
-
- +
+
+ - {#if sharedLinks.length} - - {/if} -
+ {#if sharedLinks.length} + + {/if} +
diff --git a/web/src/lib/components/asset-viewer/album-list-item.svelte b/web/src/lib/components/asset-viewer/album-list-item.svelte index dab58f24f..2e8781732 100644 --- a/web/src/lib/components/asset-viewer/album-list-item.svelte +++ b/web/src/lib/components/asset-viewer/album-list-item.svelte @@ -1,56 +1,56 @@ diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index 241ce2258..35bab9fa8 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -1,155 +1,135 @@
-
- dispatch('goBack')} /> -
-
- {#if showMotionPlayButton} - {#if isMotionPhotoPlaying} - dispatch('stopMotionPhoto')} - /> - {:else} - dispatch('playMotionPhoto')} - /> - {/if} - {/if} - {#if showZoomButton} - 1 - ? MagnifyMinusOutline - : MagnifyPlusOutline} - title="Zoom Image" - on:click={() => { - const zoomImage = new CustomEvent('zoomImage'); - window.dispatchEvent(zoomImage); - }} - /> - {/if} - {#if showCopyButton} - { - const copyEvent = new CustomEvent('copyImage'); - window.dispatchEvent(copyEvent); - }} - /> - {/if} +
+ dispatch('goBack')} /> +
+
+ {#if showMotionPlayButton} + {#if isMotionPhotoPlaying} + dispatch('stopMotionPhoto')} + /> + {:else} + dispatch('playMotionPhoto')} + /> + {/if} + {/if} + {#if showZoomButton} + 1 ? MagnifyMinusOutline : MagnifyPlusOutline} + title="Zoom Image" + on:click={() => { + const zoomImage = new CustomEvent('zoomImage'); + window.dispatchEvent(zoomImage); + }} + /> + {/if} + {#if showCopyButton} + { + const copyEvent = new CustomEvent('copyImage'); + window.dispatchEvent(copyEvent); + }} + /> + {/if} - {#if showDownloadButton} - dispatch('download')} - title="Download" - /> - {/if} - dispatch('showDetail')} - title="Info" - /> - {#if isOwner} - dispatch('favorite')} - title="Favorite" - /> - {/if} + {#if showDownloadButton} + dispatch('download')} + title="Download" + /> + {/if} + dispatch('showDetail')} title="Info" /> + {#if isOwner} + dispatch('favorite')} + title="Favorite" + /> + {/if} - {#if isOwner} - dispatch('delete')} - title="Delete" - /> -
(isShowAssetOptions = false)}> - - {#if isShowAssetOptions} - - onMenuClick('addToAlbum')} text="Add to Album" /> - onMenuClick('addToSharedAlbum')} - text="Add to Shared Album" - /> + {#if isOwner} + dispatch('delete')} title="Delete" /> +
(isShowAssetOptions = false)}> + + {#if isShowAssetOptions} + + onMenuClick('addToAlbum')} text="Add to Album" /> + onMenuClick('addToSharedAlbum')} text="Add to Shared Album" /> - {#if isOwner} - dispatch('toggleArchive')} - text={asset.isArchived ? 'Unarchive' : 'Archive'} - /> - {/if} - - {/if} - -
- {/if} -
+ {#if isOwner} + dispatch('toggleArchive')} + text={asset.isArchived ? 'Unarchive' : 'Archive'} + /> + {/if} + + {/if} +
+
+ {/if} +
diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 358572001..7de3b2f53 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -1,399 +1,386 @@
-
- downloadFile(asset, publicSharedKey)} - on:delete={() => (isShowDeleteConfirmation = true)} - on:favorite={toggleFavorite} - on:addToAlbum={() => openAlbumPicker(false)} - on:addToSharedAlbum={() => openAlbumPicker(true)} - on:playMotionPhoto={() => (shouldPlayMotionPhoto = true)} - on:stopMotionPhoto={() => (shouldPlayMotionPhoto = false)} - on:toggleArchive={toggleArchive} - /> -
+
+ downloadFile(asset, publicSharedKey)} + on:delete={() => (isShowDeleteConfirmation = true)} + on:favorite={toggleFavorite} + on:addToAlbum={() => openAlbumPicker(false)} + on:addToSharedAlbum={() => openAlbumPicker(true)} + on:playMotionPhoto={() => (shouldPlayMotionPhoto = true)} + on:stopMotionPhoto={() => (shouldPlayMotionPhoto = false)} + on:toggleArchive={toggleArchive} + /> +
- {#if showNavigation} -
{ - halfLeftHover = true; - halfRightHover = false; - }} - on:mouseleave={() => { - halfLeftHover = false; - }} - on:click={navigateAssetBackward} - on:keydown={navigateAssetBackward} - > - -
- {/if} + {#if showNavigation} +
{ + halfLeftHover = true; + halfRightHover = false; + }} + on:mouseleave={() => { + halfLeftHover = false; + }} + on:click={navigateAssetBackward} + on:keydown={navigateAssetBackward} + > + +
+ {/if} -
- {#key asset.id} - {#if !asset.resized} -
-
- -
-
- {:else if asset.type === AssetTypeEnum.Image} - {#if shouldPlayMotionPhoto && asset.livePhotoVideoId} - (shouldPlayMotionPhoto = false)} - /> - {:else} - - {/if} - {:else} - - {/if} - {/key} -
+
+ {#key asset.id} + {#if !asset.resized} +
+
+ +
+
+ {:else if asset.type === AssetTypeEnum.Image} + {#if shouldPlayMotionPhoto && asset.livePhotoVideoId} + (shouldPlayMotionPhoto = false)} + /> + {:else} + + {/if} + {:else} + + {/if} + {/key} +
- {#if showNavigation} -
{ - halfLeftHover = false; - halfRightHover = true; - }} - on:mouseleave={() => { - halfRightHover = false; - }} - > - -
- {/if} + {#if showNavigation} +
{ + halfLeftHover = false; + halfRightHover = true; + }} + on:mouseleave={() => { + halfRightHover = false; + }} + > + +
+ {/if} - {#if $isShowDetail} -
- ($isShowDetail = false)} - on:close-viewer={handleCloseViewer} - on:description-focus-in={disableKeyDownEvent} - on:description-focus-out={enableKeyDownEvent} - /> -
- {/if} + {#if $isShowDetail} +
+ ($isShowDetail = false)} + on:close-viewer={handleCloseViewer} + on:description-focus-in={disableKeyDownEvent} + on:description-focus-out={enableKeyDownEvent} + /> +
+ {/if} - {#if isShowAlbumPicker} - (isShowAlbumPicker = false)} - /> - {/if} + {#if isShowAlbumPicker} + (isShowAlbumPicker = false)} + /> + {/if} - {#if isShowDeleteConfirmation} - (isShowDeleteConfirmation = false)} - > - -

- Are you sure you want to delete this {getAssetType().toLowerCase()}? This will also remove - it from its album(s). -

-

You cannot undo this action!

-
-
- {/if} + {#if isShowDeleteConfirmation} + (isShowDeleteConfirmation = false)} + > + +

+ Are you sure you want to delete this {getAssetType().toLowerCase()}? This will also remove it from its + album(s). +

+

You cannot undo this action!

+
+
+ {/if}
diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 365c9bfa3..42bfb6de7 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -1,296 +1,293 @@
-
- +
+ -

Info

-
+

Info

+
-
-