mirror of
https://github.com/immich-app/immich.git
synced 2025-06-02 13:14:54 -04:00
feat(web): translations (#9854)
* First test * Added translation using Weblate (French) * Translated using Weblate (German) Currently translated at 100.0% (4 of 4 strings) Translation: immich/web Translate-URL: http://familie-mach.net/projects/immich/web/de/ * Translated using Weblate (French) Currently translated at 100.0% (4 of 4 strings) Translation: immich/web Translate-URL: http://familie-mach.net/projects/immich/web/fr/ * Further testing * Further testing * Translated using Weblate (German) Currently translated at 100.0% (18 of 18 strings) Translation: immich/web Translate-URL: http://familie-mach.net/projects/immich/web/de/ * Further work * Update string file. * More strings * Automatically changed strings * Add automatically translated german file for testing purposes * Fix merge-face-selector component * Make server stats strings uppercase * Fix uppercase string * Fix some strings in jobs-panel * Fix lower and uppercase strings. Add a few additional string. Fix a few unnecessary replacements * Update german test translations * Fix typo in locales file * Change string keys * Extract more strings * Extract and replace some more strings * Update testtranslationfile * Change translation keys * Fix rebase errors * Fix one more rebase error * Remove german translation file * Co-authored-by: Daniel Dietzler <danieldietzler@users.noreply.github.com> * chore: clean up translations * chore: add new line * fix formatting * chore: fixes * fix: loading and tests --------- Co-authored-by: root <root@Blacki> Co-authored-by: admin <admin@example.com> Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
This commit is contained in:
parent
a2bccf23c9
commit
f446bc8caa
635
web/package-lock.json
generated
635
web/package-lock.json
generated
@ -23,6 +23,7 @@
|
|||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"socket.io-client": "^4.7.4",
|
"socket.io-client": "^4.7.4",
|
||||||
|
"svelte-i18n": "^4.0.0",
|
||||||
"svelte-local-storage-store": "^0.6.4",
|
"svelte-local-storage-store": "^0.6.4",
|
||||||
"svelte-maplibre": "^0.9.0",
|
"svelte-maplibre": "^0.9.0",
|
||||||
"thumbhash": "^0.1.1"
|
"thumbhash": "^0.1.1"
|
||||||
@ -916,6 +917,50 @@
|
|||||||
"npm": ">=6.14.13"
|
"npm": ">=6.14.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@formatjs/ecma402-abstract": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/intl-localematcher": "0.5.4",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/fast-memoize": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/icu-messageformat-parser": {
|
||||||
|
"version": "2.7.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.8.tgz",
|
||||||
|
"integrity": "sha512-nBZJYmhpcSX0WeJ5SDYUkZ42AgR3xiyhNCsQweFx3cz/ULJjym8bHAzWKvG5e2+1XO98dBYC0fWeeAECAVSwLA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/ecma402-abstract": "2.0.0",
|
||||||
|
"@formatjs/icu-skeleton-parser": "1.8.2",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/icu-skeleton-parser": {
|
||||||
|
"version": "1.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.2.tgz",
|
||||||
|
"integrity": "sha512-k4ERKgw7aKGWJZgTarIcNEmvyTVD9FYh0mTrrBMHZ1b8hUu6iOJ4SzsZlo3UNAvHYa+PnvntIwRPt1/vy4nA9Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/ecma402-abstract": "2.0.0",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/intl-localematcher": {
|
||||||
|
"version": "0.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz",
|
||||||
|
"integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.11.14",
|
"version": "0.11.14",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
||||||
@ -3305,6 +3350,21 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cli-color": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "^1.0.1",
|
||||||
|
"es5-ext": "^0.10.64",
|
||||||
|
"es6-iterator": "^2.0.3",
|
||||||
|
"memoizee": "^0.4.15",
|
||||||
|
"timers-ext": "^0.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cliui": {
|
"node_modules/cliui": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
@ -3523,6 +3583,18 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/d": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
|
||||||
|
"dependencies": {
|
||||||
|
"es5-ext": "^0.10.64",
|
||||||
|
"type": "^2.7.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/d3-array": {
|
"node_modules/d3-array": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||||
@ -3635,7 +3707,6 @@
|
|||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -3880,12 +3951,60 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es5-ext": {
|
||||||
|
"version": "0.10.64",
|
||||||
|
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
|
||||||
|
"integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"es6-iterator": "^2.0.3",
|
||||||
|
"es6-symbol": "^3.1.3",
|
||||||
|
"esniff": "^2.0.1",
|
||||||
|
"next-tick": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es6-iterator": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "1",
|
||||||
|
"es5-ext": "^0.10.35",
|
||||||
|
"es6-symbol": "^3.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es6-promise": {
|
"node_modules/es6-promise": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
|
||||||
"integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==",
|
"integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/es6-symbol": {
|
||||||
|
"version": "3.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
|
||||||
|
"integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "^1.0.2",
|
||||||
|
"ext": "^1.7.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es6-weak-map": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "1",
|
||||||
|
"es5-ext": "^0.10.46",
|
||||||
|
"es6-iterator": "^2.0.3",
|
||||||
|
"es6-symbol": "^3.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.20.2",
|
"version": "0.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
||||||
@ -4358,6 +4477,20 @@
|
|||||||
"integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==",
|
"integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/esniff": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "^1.0.1",
|
||||||
|
"es5-ext": "^0.10.62",
|
||||||
|
"event-emitter": "^0.3.5",
|
||||||
|
"type": "^2.7.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/espree": {
|
"node_modules/espree": {
|
||||||
"version": "9.6.1",
|
"version": "9.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
|
||||||
@ -4425,6 +4558,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/event-emitter": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
|
||||||
|
"integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "1",
|
||||||
|
"es5-ext": "~0.10.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/execa": {
|
"node_modules/execa": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
||||||
@ -4460,6 +4602,14 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ext": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
|
||||||
|
"dependencies": {
|
||||||
|
"type": "^2.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/extend-shallow": {
|
"node_modules/extend-shallow": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||||
@ -4820,8 +4970,7 @@
|
|||||||
"node_modules/globalyzer": {
|
"node_modules/globalyzer": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz",
|
||||||
"integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==",
|
"integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/globby": {
|
"node_modules/globby": {
|
||||||
"version": "11.1.0",
|
"version": "11.1.0",
|
||||||
@ -4847,8 +4996,7 @@
|
|||||||
"node_modules/globrex": {
|
"node_modules/globrex": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
|
||||||
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
|
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/gopd": {
|
"node_modules/gopd": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@ -5191,6 +5339,17 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/intl-messageformat": {
|
||||||
|
"version": "10.5.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.14.tgz",
|
||||||
|
"integrity": "sha512-IjC6sI0X7YRjjyVH9aUgdftcmZK7WXdHeil4KwbjDnRWjnVitKpAx3rr6t6di1joFp5188VqKcobOPA6mCLG/w==",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/ecma402-abstract": "2.0.0",
|
||||||
|
"@formatjs/fast-memoize": "2.2.0",
|
||||||
|
"@formatjs/icu-messageformat-parser": "2.7.8",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-arguments": {
|
"node_modules/is-arguments": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
|
||||||
@ -5435,6 +5594,11 @@
|
|||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/is-promise": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="
|
||||||
|
},
|
||||||
"node_modules/is-reference": {
|
"node_modules/is-reference": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
|
||||||
@ -5947,6 +6111,14 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lru-queue": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"es5-ext": "~0.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/luxon": {
|
"node_modules/luxon": {
|
||||||
"version": "3.4.4",
|
"version": "3.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
|
||||||
@ -6077,6 +6249,24 @@
|
|||||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
|
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/memoizee": {
|
||||||
|
"version": "0.4.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz",
|
||||||
|
"integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "^1.0.2",
|
||||||
|
"es5-ext": "^0.10.64",
|
||||||
|
"es6-weak-map": "^2.0.3",
|
||||||
|
"event-emitter": "^0.3.5",
|
||||||
|
"is-promise": "^2.2.2",
|
||||||
|
"lru-queue": "^0.1.0",
|
||||||
|
"next-tick": "^1.1.0",
|
||||||
|
"timers-ext": "^0.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/merge-stream": {
|
"node_modules/merge-stream": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||||
@ -6199,7 +6389,6 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||||
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
|
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
@ -6263,6 +6452,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
|
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/next-tick": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
|
||||||
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.14",
|
"version": "2.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
|
||||||
@ -7381,7 +7575,6 @@
|
|||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
||||||
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
|
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mri": "^1.1.0"
|
"mri": "^1.1.0"
|
||||||
},
|
},
|
||||||
@ -8088,6 +8281,416 @@
|
|||||||
"svelte": "^3.19.0 || ^4.0.0"
|
"svelte": "^3.19.0 || ^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svelte-i18n": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/svelte-i18n/-/svelte-i18n-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-4vivjKZADUMRIhTs38JuBNy3unbnh9AFRxWFLxq62P4NHic+/BaIZZlAsvqsCdnp7IdJf5EoSiH6TNdItcjA6g==",
|
||||||
|
"dependencies": {
|
||||||
|
"cli-color": "^2.0.3",
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
|
"esbuild": "^0.19.2",
|
||||||
|
"estree-walker": "^2",
|
||||||
|
"intl-messageformat": "^10.5.3",
|
||||||
|
"sade": "^1.8.1",
|
||||||
|
"tiny-glob": "^0.2.9"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"svelte-i18n": "dist/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^3 || ^4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/esbuild": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.19.12",
|
||||||
|
"@esbuild/android-arm": "0.19.12",
|
||||||
|
"@esbuild/android-arm64": "0.19.12",
|
||||||
|
"@esbuild/android-x64": "0.19.12",
|
||||||
|
"@esbuild/darwin-arm64": "0.19.12",
|
||||||
|
"@esbuild/darwin-x64": "0.19.12",
|
||||||
|
"@esbuild/freebsd-arm64": "0.19.12",
|
||||||
|
"@esbuild/freebsd-x64": "0.19.12",
|
||||||
|
"@esbuild/linux-arm": "0.19.12",
|
||||||
|
"@esbuild/linux-arm64": "0.19.12",
|
||||||
|
"@esbuild/linux-ia32": "0.19.12",
|
||||||
|
"@esbuild/linux-loong64": "0.19.12",
|
||||||
|
"@esbuild/linux-mips64el": "0.19.12",
|
||||||
|
"@esbuild/linux-ppc64": "0.19.12",
|
||||||
|
"@esbuild/linux-riscv64": "0.19.12",
|
||||||
|
"@esbuild/linux-s390x": "0.19.12",
|
||||||
|
"@esbuild/linux-x64": "0.19.12",
|
||||||
|
"@esbuild/netbsd-x64": "0.19.12",
|
||||||
|
"@esbuild/openbsd-x64": "0.19.12",
|
||||||
|
"@esbuild/sunos-x64": "0.19.12",
|
||||||
|
"@esbuild/win32-arm64": "0.19.12",
|
||||||
|
"@esbuild/win32-ia32": "0.19.12",
|
||||||
|
"@esbuild/win32-x64": "0.19.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-i18n/node_modules/estree-walker": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
|
||||||
|
},
|
||||||
"node_modules/svelte-local-storage-store": {
|
"node_modules/svelte-local-storage-store": {
|
||||||
"version": "0.6.4",
|
"version": "0.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-local-storage-store/-/svelte-local-storage-store-0.6.4.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-local-storage-store/-/svelte-local-storage-store-0.6.4.tgz",
|
||||||
@ -8359,11 +8962,19 @@
|
|||||||
"resolved": "https://registry.npmjs.org/thumbhash/-/thumbhash-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/thumbhash/-/thumbhash-0.1.1.tgz",
|
||||||
"integrity": "sha512-kH5pKeIIBPQXAOni2AiY/Cu/NKdkFREdpH+TLdM0g6WA7RriCv0kPLgP731ady67MhTAqrVG/4mnEeibVuCJcg=="
|
"integrity": "sha512-kH5pKeIIBPQXAOni2AiY/Cu/NKdkFREdpH+TLdM0g6WA7RriCv0kPLgP731ady67MhTAqrVG/4mnEeibVuCJcg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/timers-ext": {
|
||||||
|
"version": "0.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz",
|
||||||
|
"integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"es5-ext": "~0.10.46",
|
||||||
|
"next-tick": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tiny-glob": {
|
"node_modules/tiny-glob": {
|
||||||
"version": "0.2.9",
|
"version": "0.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
|
||||||
"integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==",
|
"integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"globalyzer": "0.1.0",
|
"globalyzer": "0.1.0",
|
||||||
"globrex": "^0.1.2"
|
"globrex": "^0.1.2"
|
||||||
@ -8480,8 +9091,12 @@
|
|||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.6.2",
|
"version": "2.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
|
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||||
"dev": true
|
},
|
||||||
|
"node_modules/type": {
|
||||||
|
"version": "2.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz",
|
||||||
|
"integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw=="
|
||||||
},
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
|
@ -75,6 +75,7 @@
|
|||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"socket.io-client": "^4.7.4",
|
"socket.io-client": "^4.7.4",
|
||||||
"svelte-local-storage-store": "^0.6.4",
|
"svelte-local-storage-store": "^0.6.4",
|
||||||
|
"svelte-i18n": "^4.0.0",
|
||||||
"svelte-maplibre": "^0.9.0",
|
"svelte-maplibre": "^0.9.0",
|
||||||
"thumbhash": "^0.1.1"
|
"thumbhash": "^0.1.1"
|
||||||
},
|
},
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import { serverConfig } from '$lib/stores/server-config.store';
|
import { serverConfig } from '$lib/stores/server-config.store';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import Checkbox from '$lib/components/elements/checkbox.svelte';
|
import Checkbox from '$lib/components/elements/checkbox.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let user: UserResponseDto;
|
export let user: UserResponseDto;
|
||||||
|
|
||||||
@ -31,7 +32,7 @@
|
|||||||
dispatch('success');
|
dispatch('success');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to delete user');
|
handleError(error, $t('errors.unable_to_delete_user'));
|
||||||
dispatch('fail');
|
dispatch('fail');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -43,8 +44,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
title="Delete user"
|
title={$t('delete_user')}
|
||||||
confirmText={forceDelete ? 'Permanently Delete' : 'Delete'}
|
confirmText={forceDelete ? $t('permanently_delete') : $t('delete')}
|
||||||
onConfirm={handleDeleteUser}
|
onConfirm={handleDeleteUser}
|
||||||
onCancel={() => dispatch('cancel')}
|
onCancel={() => dispatch('cancel')}
|
||||||
disabled={deleteButtonDisabled}
|
disabled={deleteButtonDisabled}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
import JobTileButton from './job-tile-button.svelte';
|
import JobTileButton from './job-tile-button.svelte';
|
||||||
import JobTileStatus from './job-tile-status.svelte';
|
import JobTileStatus from './job-tile-status.svelte';
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let title: string;
|
export let title: string;
|
||||||
export let subtitle: string | undefined;
|
export let subtitle: string | undefined;
|
||||||
@ -43,9 +44,9 @@
|
|||||||
>
|
>
|
||||||
<div class="flex w-full flex-col">
|
<div class="flex w-full flex-col">
|
||||||
{#if queueStatus.isPaused}
|
{#if queueStatus.isPaused}
|
||||||
<JobTileStatus color="warning">Paused</JobTileStatus>
|
<JobTileStatus color="warning">{$t('paused')}</JobTileStatus>
|
||||||
{:else if queueStatus.isActive}
|
{:else if queueStatus.isActive}
|
||||||
<JobTileStatus color="success">Active</JobTileStatus>
|
<JobTileStatus color="success">{$t('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 class="flex items-center gap-4 text-xl font-semibold text-immich-primary dark:text-immich-dark-primary">
|
<div class="flex items-center gap-4 text-xl font-semibold text-immich-primary dark:text-immich-dark-primary">
|
||||||
@ -63,7 +64,7 @@
|
|||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
icon={mdiClose}
|
icon={mdiClose}
|
||||||
title="Clear message"
|
title={$t('clear_message')}
|
||||||
size="12"
|
size="12"
|
||||||
padding="1"
|
padding="1"
|
||||||
on:click={() => dispatch('command', { command: JobCommand.ClearFailed, force: false })}
|
on:click={() => dispatch('command', { command: JobCommand.ClearFailed, force: false })}
|
||||||
@ -95,7 +96,7 @@
|
|||||||
<div
|
<div
|
||||||
class="{commonClasses} rounded-t-lg bg-immich-primary text-white dark:bg-immich-dark-primary dark:text-immich-dark-gray sm:rounded-l-lg sm:rounded-r-none"
|
class="{commonClasses} rounded-t-lg bg-immich-primary text-white dark:bg-immich-dark-primary dark:text-immich-dark-gray sm:rounded-l-lg sm:rounded-r-none"
|
||||||
>
|
>
|
||||||
<p>Active</p>
|
<p>{$t('active')}</p>
|
||||||
<p class="text-2xl">
|
<p class="text-2xl">
|
||||||
{jobCounts.active.toLocaleString($locale)}
|
{jobCounts.active.toLocaleString($locale)}
|
||||||
</p>
|
</p>
|
||||||
@ -107,7 +108,7 @@
|
|||||||
<p class="text-2xl">
|
<p class="text-2xl">
|
||||||
{waitingCount.toLocaleString($locale)}
|
{waitingCount.toLocaleString($locale)}
|
||||||
</p>
|
</p>
|
||||||
<p>Waiting</p>
|
<p>{$t('waiting')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
import JobTile from './job-tile.svelte';
|
import JobTile from './job-tile.svelte';
|
||||||
import StorageMigrationDescription from './storage-migration-description.svelte';
|
import StorageMigrationDescription from './storage-migration-description.svelte';
|
||||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let jobs: AllJobStatusResponseDto;
|
export let jobs: AllJobStatusResponseDto;
|
||||||
|
|
||||||
@ -60,38 +61,38 @@
|
|||||||
[JobName.ThumbnailGeneration]: {
|
[JobName.ThumbnailGeneration]: {
|
||||||
icon: mdiFileJpgBox,
|
icon: mdiFileJpgBox,
|
||||||
title: getJobName(JobName.ThumbnailGeneration),
|
title: getJobName(JobName.ThumbnailGeneration),
|
||||||
subtitle: 'Generate large, small and blurred thumbnails for each asset, as well as thumbnails for each person',
|
subtitle: $t('thumbnail_generation_job_description'),
|
||||||
},
|
},
|
||||||
[JobName.MetadataExtraction]: {
|
[JobName.MetadataExtraction]: {
|
||||||
icon: mdiTable,
|
icon: mdiTable,
|
||||||
title: getJobName(JobName.MetadataExtraction),
|
title: getJobName(JobName.MetadataExtraction),
|
||||||
subtitle: 'Extract metadata information from each asset, such as GPS and resolution',
|
subtitle: $t('metadata_extraction_job_description'),
|
||||||
},
|
},
|
||||||
[JobName.Library]: {
|
[JobName.Library]: {
|
||||||
icon: mdiLibraryShelves,
|
icon: mdiLibraryShelves,
|
||||||
title: getJobName(JobName.Library),
|
title: getJobName(JobName.Library),
|
||||||
subtitle: 'Perform library tasks',
|
subtitle: $t('perform_library_tasks'),
|
||||||
allText: 'ALL',
|
allText: $t('all').toUpperCase(),
|
||||||
missingText: 'REFRESH',
|
missingText: $t('refresh').toUpperCase(),
|
||||||
},
|
},
|
||||||
[JobName.Sidecar]: {
|
[JobName.Sidecar]: {
|
||||||
title: getJobName(JobName.Sidecar),
|
title: getJobName(JobName.Sidecar),
|
||||||
icon: mdiFileXmlBox,
|
icon: mdiFileXmlBox,
|
||||||
subtitle: 'Discover or synchronize sidecar metadata from the filesystem',
|
subtitle: $t('sidecar_job_description'),
|
||||||
allText: 'SYNC',
|
allText: $t('sync').toUpperCase(),
|
||||||
missingText: 'DISCOVER',
|
missingText: $t('discover').toUpperCase(),
|
||||||
disabled: !$featureFlags.sidecar,
|
disabled: !$featureFlags.sidecar,
|
||||||
},
|
},
|
||||||
[JobName.SmartSearch]: {
|
[JobName.SmartSearch]: {
|
||||||
icon: mdiImageSearch,
|
icon: mdiImageSearch,
|
||||||
title: getJobName(JobName.SmartSearch),
|
title: getJobName(JobName.SmartSearch),
|
||||||
subtitle: 'Run machine learning on assets to support smart search',
|
subtitle: $t('smart_search_job_description'),
|
||||||
disabled: !$featureFlags.smartSearch,
|
disabled: !$featureFlags.smartSearch,
|
||||||
},
|
},
|
||||||
[JobName.DuplicateDetection]: {
|
[JobName.DuplicateDetection]: {
|
||||||
icon: mdiContentDuplicate,
|
icon: mdiContentDuplicate,
|
||||||
title: getJobName(JobName.DuplicateDetection),
|
title: getJobName(JobName.DuplicateDetection),
|
||||||
subtitle: 'Run machine learning on assets to detect similar images. Relies on Smart Search',
|
subtitle: $t('duplicate_detection_job_description'),
|
||||||
disabled: !$featureFlags.duplicateDetection,
|
disabled: !$featureFlags.duplicateDetection,
|
||||||
},
|
},
|
||||||
[JobName.FaceDetection]: {
|
[JobName.FaceDetection]: {
|
||||||
@ -113,7 +114,7 @@
|
|||||||
[JobName.VideoConversion]: {
|
[JobName.VideoConversion]: {
|
||||||
icon: mdiVideo,
|
icon: mdiVideo,
|
||||||
title: getJobName(JobName.VideoConversion),
|
title: getJobName(JobName.VideoConversion),
|
||||||
subtitle: 'Transcode videos for wider compatibility with browsers and devices',
|
subtitle: $t('video_conversion_job_description'),
|
||||||
},
|
},
|
||||||
[JobName.StorageTemplateMigration]: {
|
[JobName.StorageTemplateMigration]: {
|
||||||
icon: mdiFolderMove,
|
icon: mdiFolderMove,
|
||||||
@ -124,7 +125,7 @@
|
|||||||
[JobName.Migration]: {
|
[JobName.Migration]: {
|
||||||
icon: mdiFolderMove,
|
icon: mdiFolderMove,
|
||||||
title: getJobName(JobName.Migration),
|
title: getJobName(JobName.Migration),
|
||||||
subtitle: 'Migrate thumbnails for assets and faces to the latest folder structure',
|
subtitle: $t('migration_job_description'),
|
||||||
allowForceCommand: false,
|
allowForceCommand: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -159,8 +160,8 @@
|
|||||||
{title}
|
{title}
|
||||||
{disabled}
|
{disabled}
|
||||||
{subtitle}
|
{subtitle}
|
||||||
allText={allText || 'ALL'}
|
allText={allText || $t('all').toUpperCase()}
|
||||||
missingText={missingText || 'MISSING'}
|
missingText={missingText || $t('missing').toUpperCase()}
|
||||||
{allowForceCommand}
|
{allowForceCommand}
|
||||||
{jobCounts}
|
{jobCounts}
|
||||||
{queueStatus}
|
{queueStatus}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
Apply the current
|
Apply the current
|
||||||
<a href="{AppRoute.ADMIN_SETTINGS}?open=storageTemplate" class="text-immich-primary dark:text-immich-dark-primary"
|
<a href="{AppRoute.ADMIN_SETTINGS}?open=storageTemplate" class="text-immich-primary dark:text-immich-dark-primary"
|
||||||
>Storage template</a
|
>{$t('storage_template_settings')}</a
|
||||||
>
|
>
|
||||||
to previously uploaded assets
|
to previously uploaded assets
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { restoreUserAdmin, type UserResponseDto } from '@immich/sdk';
|
import { restoreUserAdmin, type UserResponseDto } from '@immich/sdk';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let user: UserResponseDto;
|
export let user: UserResponseDto;
|
||||||
|
|
||||||
@ -21,15 +22,15 @@
|
|||||||
dispatch('fail');
|
dispatch('fail');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to restore user');
|
handleError(error, $t('errors.unable_to_restore_user'));
|
||||||
dispatch('fail');
|
dispatch('fail');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
title="Restore user"
|
title={$t('restore_user')}
|
||||||
confirmText="Continue"
|
confirmText={$t('continue')}
|
||||||
confirmColor="green"
|
confirmColor="green"
|
||||||
onConfirm={handleRestoreUser}
|
onConfirm={handleRestoreUser}
|
||||||
onCancel={() => dispatch('cancel')}
|
onCancel={() => dispatch('cancel')}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import type { ServerStatsResponseDto } from '@immich/sdk';
|
import type { ServerStatsResponseDto } from '@immich/sdk';
|
||||||
import { mdiCameraIris, mdiChartPie, mdiPlayCircle } from '@mdi/js';
|
import { mdiCameraIris, mdiChartPie, mdiPlayCircle } from '@mdi/js';
|
||||||
import StatsCard from './stats-card.svelte';
|
import StatsCard from './stats-card.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let stats: ServerStatsResponseDto = {
|
export let stats: ServerStatsResponseDto = {
|
||||||
photos: 0,
|
photos: 0,
|
||||||
@ -27,19 +28,19 @@
|
|||||||
|
|
||||||
<div class="flex flex-col gap-5">
|
<div class="flex flex-col gap-5">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm dark:text-immich-dark-fg">TOTAL USAGE</p>
|
<p class="text-sm dark:text-immich-dark-fg">{$t('total_usage').toUpperCase()}</p>
|
||||||
|
|
||||||
<div class="mt-5 hidden justify-between lg:flex">
|
<div class="mt-5 hidden justify-between lg:flex">
|
||||||
<StatsCard icon={mdiCameraIris} title="PHOTOS" value={stats.photos} />
|
<StatsCard icon={mdiCameraIris} title={$t('photos').toUpperCase()} value={stats.photos} />
|
||||||
<StatsCard icon={mdiPlayCircle} title="VIDEOS" value={stats.videos} />
|
<StatsCard icon={mdiPlayCircle} title={$t('videos').toUpperCase()} value={stats.videos} />
|
||||||
<StatsCard icon={mdiChartPie} title="STORAGE" value={statsUsage} unit={statsUsageUnit} />
|
<StatsCard icon={mdiChartPie} title={$t('storage').toUpperCase()} value={statsUsage} unit={statsUsageUnit} />
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5 flex lg:hidden">
|
<div class="mt-5 flex lg:hidden">
|
||||||
<div class="flex flex-col justify-between rounded-3xl bg-immich-gray p-5 dark:bg-immich-dark-gray">
|
<div class="flex flex-col justify-between rounded-3xl bg-immich-gray p-5 dark:bg-immich-dark-gray">
|
||||||
<div class="flex flex-wrap gap-x-12">
|
<div class="flex flex-wrap gap-x-12">
|
||||||
<div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
|
<div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
|
||||||
<Icon path={mdiCameraIris} size="25" />
|
<Icon path={mdiCameraIris} size="25" />
|
||||||
<p>PHOTOS</p>
|
<p>{$t('photos').toUpperCase()}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative text-center font-mono text-2xl font-semibold">
|
<div class="relative text-center font-mono text-2xl font-semibold">
|
||||||
@ -51,7 +52,7 @@
|
|||||||
<div class="flex flex-wrap gap-x-12">
|
<div class="flex flex-wrap gap-x-12">
|
||||||
<div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
|
<div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
|
||||||
<Icon path={mdiPlayCircle} size="25" />
|
<Icon path={mdiPlayCircle} size="25" />
|
||||||
<p>VIDEOS</p>
|
<p>{$t('videos').toUpperCase()}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative text-center font-mono text-2xl font-semibold">
|
<div class="relative text-center font-mono text-2xl font-semibold">
|
||||||
@ -63,7 +64,7 @@
|
|||||||
<div class="flex flex-wrap gap-x-7">
|
<div class="flex flex-wrap gap-x-7">
|
||||||
<div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
|
<div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
|
||||||
<Icon path={mdiChartPie} size="25" />
|
<Icon path={mdiChartPie} size="25" />
|
||||||
<p>STORAGE</p>
|
<p>{$t('storage').toUpperCase()}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative flex text-center font-mono text-2xl font-semibold">
|
<div class="relative flex text-center font-mono text-2xl font-semibold">
|
||||||
@ -78,16 +79,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm dark:text-immich-dark-fg">USER USAGE DETAIL</p>
|
<p class="text-sm dark:text-immich-dark-fg">{$t('user_usage_detail').toUpperCase()}</p>
|
||||||
<table class="mt-5 w-full text-left">
|
<table class="mt-5 w-full text-left">
|
||||||
<thead
|
<thead
|
||||||
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
|
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
|
||||||
>
|
>
|
||||||
<tr class="flex w-full place-items-center">
|
<tr class="flex w-full place-items-center">
|
||||||
<th class="w-1/4 text-center text-sm font-medium">User</th>
|
<th class="w-1/4 text-center text-sm font-medium">{$t('user')}</th>
|
||||||
<th class="w-1/4 text-center text-sm font-medium">Photos</th>
|
<th class="w-1/4 text-center text-sm font-medium">{$t('photos')}</th>
|
||||||
<th class="w-1/4 text-center text-sm font-medium">Videos</th>
|
<th class="w-1/4 text-center text-sm font-medium">{$t('videos')}</th>
|
||||||
<th class="w-1/4 text-center text-sm font-medium">Usage</th>
|
<th class="w-1/4 text-center text-sm font-medium">{$t('usage')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody
|
<tbody
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
import { cloneDeep } from 'lodash-es';
|
import { cloneDeep } from 'lodash-es';
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
import type { SettingsEventType } from './admin-settings';
|
import type { SettingsEventType } from './admin-settings';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let config: SystemConfigDto;
|
export let config: SystemConfigDto;
|
||||||
|
|
||||||
@ -34,13 +35,13 @@
|
|||||||
|
|
||||||
config = cloneDeep(newConfig);
|
config = cloneDeep(newConfig);
|
||||||
savedConfig = cloneDeep(newConfig);
|
savedConfig = cloneDeep(newConfig);
|
||||||
notificationController.show({ message: 'Settings saved', type: NotificationType.Info });
|
notificationController.show({ message: $t('settings_saved'), type: NotificationType.Info });
|
||||||
|
|
||||||
await loadConfig();
|
await loadConfig();
|
||||||
|
|
||||||
dispatch('save');
|
dispatch('save');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to save settings');
|
handleError(error, $t('errors.unable_to_save_settings'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -63,7 +64,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset settings to default',
|
message: $t('reset_settings_to_default'),
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import type { SettingsEventType } from '../admin-settings';
|
import type { SettingsEventType } from '../admin-settings';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let savedConfig: SystemConfigDto;
|
export let savedConfig: SystemConfigDto;
|
||||||
export let defaultConfig: SystemConfigDto;
|
export let defaultConfig: SystemConfigDto;
|
||||||
@ -42,7 +43,11 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isConfirmOpen}
|
{#if isConfirmOpen}
|
||||||
<ConfirmDialog title="Disable login" onCancel={() => (isConfirmOpen = false)} onConfirm={() => handleSave(true)}>
|
<ConfirmDialog
|
||||||
|
title={$t('admin.disable_login')}
|
||||||
|
onCancel={() => (isConfirmOpen = false)}
|
||||||
|
onConfirm={() => handleSave(true)}
|
||||||
|
>
|
||||||
<svelte:fragment slot="prompt">
|
<svelte:fragment slot="prompt">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<p>Are you sure you want to disable all login methods? Login will be completely disabled.</p>
|
<p>Are you sure you want to disable all login methods? Login will be completely disabled.</p>
|
||||||
@ -66,7 +71,11 @@
|
|||||||
<div in:fade={{ duration: 500 }}>
|
<div in:fade={{ duration: 500 }}>
|
||||||
<form autocomplete="off" on:submit|preventDefault>
|
<form autocomplete="off" on:submit|preventDefault>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingAccordion key="oauth" title="OAuth" subtitle="Manage OAuth login settings">
|
<SettingAccordion
|
||||||
|
key="oauth"
|
||||||
|
title={$t('admin.oauth_settings')}
|
||||||
|
subtitle={$t('admin.oauth_settings_description')}
|
||||||
|
>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<p class="text-sm dark:text-immich-dark-fg">
|
<p class="text-sm dark:text-immich-dark-fg">
|
||||||
For more details about this feature, refer to the <a
|
For more details about this feature, refer to the <a
|
||||||
@ -77,13 +86,18 @@
|
|||||||
>.
|
>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SettingSwitch {disabled} title="ENABLE" subtitle="Login with OAuth" bind:checked={config.oauth.enabled} />
|
<SettingSwitch
|
||||||
|
{disabled}
|
||||||
|
title={$t('enable').toUpperCase()}
|
||||||
|
subtitle={$t('admin.oauth_enable_description')}
|
||||||
|
bind:checked={config.oauth.enabled}
|
||||||
|
/>
|
||||||
|
|
||||||
{#if config.oauth.enabled}
|
{#if config.oauth.enabled}
|
||||||
<hr />
|
<hr />
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="ISSUER URL"
|
label={$t('admin.oauth_issuer_url').toUpperCase()}
|
||||||
bind:value={config.oauth.issuerUrl}
|
bind:value={config.oauth.issuerUrl}
|
||||||
required={true}
|
required={true}
|
||||||
disabled={disabled || !config.oauth.enabled}
|
disabled={disabled || !config.oauth.enabled}
|
||||||
@ -92,7 +106,7 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="CLIENT ID"
|
label={$t('admin.oauth_client_id').toUpperCase()}
|
||||||
bind:value={config.oauth.clientId}
|
bind:value={config.oauth.clientId}
|
||||||
required={true}
|
required={true}
|
||||||
disabled={disabled || !config.oauth.enabled}
|
disabled={disabled || !config.oauth.enabled}
|
||||||
@ -101,7 +115,7 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="CLIENT SECRET"
|
label={$t('admin.oauth_client_secret').toUpperCase()}
|
||||||
bind:value={config.oauth.clientSecret}
|
bind:value={config.oauth.clientSecret}
|
||||||
required={true}
|
required={true}
|
||||||
disabled={disabled || !config.oauth.enabled}
|
disabled={disabled || !config.oauth.enabled}
|
||||||
@ -110,7 +124,7 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="SCOPE"
|
label={$t('admin.oauth_scope').toUpperCase()}
|
||||||
bind:value={config.oauth.scope}
|
bind:value={config.oauth.scope}
|
||||||
required={true}
|
required={true}
|
||||||
disabled={disabled || !config.oauth.enabled}
|
disabled={disabled || !config.oauth.enabled}
|
||||||
@ -119,7 +133,7 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="SIGNING ALGORITHM"
|
label={$t('admin.oauth_signing_algorithm').toUpperCase()}
|
||||||
bind:value={config.oauth.signingAlgorithm}
|
bind:value={config.oauth.signingAlgorithm}
|
||||||
required={true}
|
required={true}
|
||||||
disabled={disabled || !config.oauth.enabled}
|
disabled={disabled || !config.oauth.enabled}
|
||||||
@ -128,8 +142,8 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="STORAGE LABEL CLAIM"
|
label={$t('admin.oauth_storage_label_claim').toUpperCase()}
|
||||||
desc="Automatically set the user's storage label to the value of this claim."
|
desc={$t('admin.oauth_storage_label_claim_description')}
|
||||||
bind:value={config.oauth.storageLabelClaim}
|
bind:value={config.oauth.storageLabelClaim}
|
||||||
required={true}
|
required={true}
|
||||||
disabled={disabled || !config.oauth.enabled}
|
disabled={disabled || !config.oauth.enabled}
|
||||||
@ -138,8 +152,8 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="STORAGE QUOTA CLAIM"
|
label={$t('admin.oauth_storage_quota_claim').toUpperCase()}
|
||||||
desc="Automatically set the user's storage quota to the value of this claim."
|
desc={$t('admin.oauth_storage_quota_claim_description')}
|
||||||
bind:value={config.oauth.storageQuotaClaim}
|
bind:value={config.oauth.storageQuotaClaim}
|
||||||
required={true}
|
required={true}
|
||||||
disabled={disabled || !config.oauth.enabled}
|
disabled={disabled || !config.oauth.enabled}
|
||||||
@ -148,8 +162,8 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
label="DEFAULT STORAGE QUOTA (GiB)"
|
label={$t('admin.oauth_storage_quota_default').toUpperCase()}
|
||||||
desc="Quota in GiB to be used when no claim is provided (Enter 0 for unlimited quota)."
|
desc={$t('admin.oauth_storage_quota_default_description')}
|
||||||
bind:value={config.oauth.defaultStorageQuota}
|
bind:value={config.oauth.defaultStorageQuota}
|
||||||
required={true}
|
required={true}
|
||||||
disabled={disabled || !config.oauth.enabled}
|
disabled={disabled || !config.oauth.enabled}
|
||||||
@ -158,7 +172,7 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="BUTTON TEXT"
|
label={$t('admin.oauth_button_text').toUpperCase()}
|
||||||
bind:value={config.oauth.buttonText}
|
bind:value={config.oauth.buttonText}
|
||||||
required={false}
|
required={false}
|
||||||
disabled={disabled || !config.oauth.enabled}
|
disabled={disabled || !config.oauth.enabled}
|
||||||
@ -166,22 +180,22 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="AUTO REGISTER"
|
title={$t('admin.oauth_auto_register').toUpperCase()}
|
||||||
subtitle="Automatically register new users after signing in with OAuth"
|
subtitle={$t('admin.oauth_auto_register_description')}
|
||||||
bind:checked={config.oauth.autoRegister}
|
bind:checked={config.oauth.autoRegister}
|
||||||
disabled={disabled || !config.oauth.enabled}
|
disabled={disabled || !config.oauth.enabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="AUTO LAUNCH"
|
title={$t('admin.oauth_auto_launch').toUpperCase()}
|
||||||
subtitle="Start the OAuth login flow automatically upon navigating to the login page"
|
subtitle={$t('admin.oauth_auto_launch_description')}
|
||||||
disabled={disabled || !config.oauth.enabled}
|
disabled={disabled || !config.oauth.enabled}
|
||||||
bind:checked={config.oauth.autoLaunch}
|
bind:checked={config.oauth.autoLaunch}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="MOBILE REDIRECT URI OVERRIDE"
|
title={$t('admin.oauth_mobile_redirect_uri_override').toUpperCase()}
|
||||||
subtitle="Enable when 'app.immich:/' is an invalid redirect URI."
|
subtitle={$t('admin.oauth_mobile_redirect_uri_override_description')}
|
||||||
disabled={disabled || !config.oauth.enabled}
|
disabled={disabled || !config.oauth.enabled}
|
||||||
on:click={() => handleToggleOverride()}
|
on:click={() => handleToggleOverride()}
|
||||||
bind:checked={config.oauth.mobileOverrideEnabled}
|
bind:checked={config.oauth.mobileOverrideEnabled}
|
||||||
@ -190,7 +204,7 @@
|
|||||||
{#if config.oauth.mobileOverrideEnabled}
|
{#if config.oauth.mobileOverrideEnabled}
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="MOBILE REDIRECT URI"
|
label={$t('admin.oauth_mobile_redirect_uri').toUpperCase()}
|
||||||
bind:value={config.oauth.mobileRedirectUri}
|
bind:value={config.oauth.mobileRedirectUri}
|
||||||
required={true}
|
required={true}
|
||||||
disabled={disabled || !config.oauth.enabled}
|
disabled={disabled || !config.oauth.enabled}
|
||||||
@ -201,13 +215,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</SettingAccordion>
|
</SettingAccordion>
|
||||||
|
|
||||||
<SettingAccordion key="password" title="Password" subtitle="Manage password login settings">
|
<SettingAccordion
|
||||||
|
key="password"
|
||||||
|
title={$t('admin.password_settings')}
|
||||||
|
subtitle={$t('admin.password_settings_description')}
|
||||||
|
>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<div class="ml-4 mt-4 flex flex-col">
|
<div class="ml-4 mt-4 flex flex-col">
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="ENABLED"
|
title={$t('enabled')}
|
||||||
{disabled}
|
{disabled}
|
||||||
subtitle="Login with email and password"
|
subtitle={$t('admin.password_enable_description')}
|
||||||
bind:checked={config.passwordLogin.enabled}
|
bind:checked={config.passwordLogin.enabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||||
import SettingCheckboxes from '$lib/components/shared-components/settings/setting-checkboxes.svelte';
|
import SettingCheckboxes from '$lib/components/shared-components/settings/setting-checkboxes.svelte';
|
||||||
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let savedConfig: SystemConfigDto;
|
export let savedConfig: SystemConfigDto;
|
||||||
export let defaultConfig: SystemConfigDto;
|
export let defaultConfig: SystemConfigDto;
|
||||||
@ -42,7 +43,7 @@
|
|||||||
>H.264 codec</a
|
>H.264 codec</a
|
||||||
>,
|
>,
|
||||||
<a href="https://trac.ffmpeg.org/wiki/Encode/H.265" class="underline" target="_blank" rel="noreferrer"
|
<a href="https://trac.ffmpeg.org/wiki/Encode/H.265" class="underline" target="_blank" rel="noreferrer"
|
||||||
>HEVC codec</a
|
>{$t('admin.transcoding_hevc_codec')}</a
|
||||||
>
|
>
|
||||||
and
|
and
|
||||||
<a href="https://trac.ffmpeg.org/wiki/Encode/VP9" class="underline" target="_blank" rel="noreferrer"
|
<a href="https://trac.ffmpeg.org/wiki/Encode/VP9" class="underline" target="_blank" rel="noreferrer"
|
||||||
@ -53,17 +54,17 @@
|
|||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
{disabled}
|
{disabled}
|
||||||
label="CONSTANT RATE FACTOR (-crf)"
|
label={$t('admin.transcoding_constant_rate_factor')}
|
||||||
desc="Video quality level. Typical values are 23 for H.264, 28 for HEVC, 31 for VP9, and 35 for AV1. Lower is better, but produces larger files."
|
desc={$t('admin.transcoding_constant_rate_factor_description')}
|
||||||
bind:value={config.ffmpeg.crf}
|
bind:value={config.ffmpeg.crf}
|
||||||
required={true}
|
required={true}
|
||||||
isEdited={config.ffmpeg.crf !== savedConfig.ffmpeg.crf}
|
isEdited={config.ffmpeg.crf !== savedConfig.ffmpeg.crf}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="PRESET (-preset)"
|
label={$t('admin.transcoding_preset_preset')}
|
||||||
{disabled}
|
{disabled}
|
||||||
desc="Compression speed. Slower presets produce smaller files, and increase quality when targeting a certain bitrate. VP9 ignores speeds above faster."
|
desc={$t('admin.transcoding_preset_preset_description')}
|
||||||
bind:value={config.ffmpeg.preset}
|
bind:value={config.ffmpeg.preset}
|
||||||
name="preset"
|
name="preset"
|
||||||
options={[
|
options={[
|
||||||
@ -81,9 +82,9 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="AUDIO CODEC"
|
label={$t('admin.transcoding_audio_codec').toUpperCase()}
|
||||||
{disabled}
|
{disabled}
|
||||||
desc="Opus is the highest quality option, but has lower compatibility with old devices or software."
|
desc={$t('admin.transcoding_audio_codec_description')}
|
||||||
bind:value={config.ffmpeg.targetAudioCodec}
|
bind:value={config.ffmpeg.targetAudioCodec}
|
||||||
options={[
|
options={[
|
||||||
{ value: AudioCodec.Aac, text: 'aac' },
|
{ value: AudioCodec.Aac, text: 'aac' },
|
||||||
@ -99,9 +100,9 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingCheckboxes
|
<SettingCheckboxes
|
||||||
label="ACCEPTED AUDIO CODECS"
|
label={$t('admin.transcoding_accepted_audio_codecs').toUpperCase()}
|
||||||
{disabled}
|
{disabled}
|
||||||
desc="Select which audio codecs do not need to be transcoded. Only used for certain transcode policies."
|
desc={$t('admin.transcoding_accepted_audio_codecs_description')}
|
||||||
bind:value={config.ffmpeg.acceptedAudioCodecs}
|
bind:value={config.ffmpeg.acceptedAudioCodecs}
|
||||||
name="audioCodecs"
|
name="audioCodecs"
|
||||||
options={[
|
options={[
|
||||||
@ -113,9 +114,9 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="VIDEO CODEC"
|
label={$t('admin.transcoding_video_codec').toUpperCase()}
|
||||||
{disabled}
|
{disabled}
|
||||||
desc="VP9 has high efficiency and web compatibility, but takes longer to transcode. HEVC performs similarly, but has lower web compatibility. H.264 is widely compatible and quick to transcode, but produces much larger files. AV1 is the most efficient codec but lacks support on older devices."
|
desc={$t('admin.transcoding_video_codec_description')}
|
||||||
bind:value={config.ffmpeg.targetVideoCodec}
|
bind:value={config.ffmpeg.targetVideoCodec}
|
||||||
options={[
|
options={[
|
||||||
{ value: VideoCodec.H264, text: 'h264' },
|
{ value: VideoCodec.H264, text: 'h264' },
|
||||||
@ -129,9 +130,9 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingCheckboxes
|
<SettingCheckboxes
|
||||||
label="ACCEPTED VIDEO CODECS"
|
label={$t('admin.transcoding_accepted_video_codecs').toUpperCase()}
|
||||||
{disabled}
|
{disabled}
|
||||||
desc="Select which video codecs do not need to be transcoded. Only used for certain transcode policies."
|
desc={$t('admin.transcoding_accepted_video_codecs_description')}
|
||||||
bind:value={config.ffmpeg.acceptedVideoCodecs}
|
bind:value={config.ffmpeg.acceptedVideoCodecs}
|
||||||
name="videoCodecs"
|
name="videoCodecs"
|
||||||
options={[
|
options={[
|
||||||
@ -144,9 +145,9 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="TARGET RESOLUTION"
|
label={$t('admin.transcoding_target_resolution').toUpperCase()}
|
||||||
{disabled}
|
{disabled}
|
||||||
desc="Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
|
desc={$t('admin.transcoding_target_resolution_description')}
|
||||||
bind:value={config.ffmpeg.targetResolution}
|
bind:value={config.ffmpeg.targetResolution}
|
||||||
options={[
|
options={[
|
||||||
{ value: '2160', text: '4k' },
|
{ value: '2160', text: '4k' },
|
||||||
@ -163,8 +164,8 @@
|
|||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
{disabled}
|
{disabled}
|
||||||
label="MAX BITRATE"
|
label={$t('admin.transcoding_max_bitrate').toUpperCase()}
|
||||||
desc="Setting a max bitrate can make file sizes more predictable at a minor cost to quality. At 720p, typical values are 2600k for VP9 or HEVC, or 4500k for H.264. Disabled if set to 0."
|
desc={$t('admin.transcoding_max_bitrate_description')}
|
||||||
bind:value={config.ffmpeg.maxBitrate}
|
bind:value={config.ffmpeg.maxBitrate}
|
||||||
isEdited={config.ffmpeg.maxBitrate !== savedConfig.ffmpeg.maxBitrate}
|
isEdited={config.ffmpeg.maxBitrate !== savedConfig.ffmpeg.maxBitrate}
|
||||||
/>
|
/>
|
||||||
@ -172,44 +173,44 @@
|
|||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
{disabled}
|
{disabled}
|
||||||
label="THREADS"
|
label={$t('admin.transcoding_threads').toUpperCase()}
|
||||||
desc="Higher values lead to faster encoding, but leave less room for the server to process other tasks while active. This value should not be more than the number of CPU cores. Maximizes utilization if set to 0."
|
desc={$t('admin.transcoding_threads_description')}
|
||||||
bind:value={config.ffmpeg.threads}
|
bind:value={config.ffmpeg.threads}
|
||||||
isEdited={config.ffmpeg.threads !== savedConfig.ffmpeg.threads}
|
isEdited={config.ffmpeg.threads !== savedConfig.ffmpeg.threads}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="TRANSCODE POLICY"
|
label={$t('admin.transcoding_transcode_policy').toUpperCase()}
|
||||||
{disabled}
|
{disabled}
|
||||||
desc="Policy for when a video should be transcoded. HDR videos will always be transcoded (except if transcoding is disabled)."
|
desc={$t('admin.transcoding_transcode_policy_description')}
|
||||||
bind:value={config.ffmpeg.transcode}
|
bind:value={config.ffmpeg.transcode}
|
||||||
name="transcode"
|
name="transcode"
|
||||||
options={[
|
options={[
|
||||||
{ value: TranscodePolicy.All, text: 'All videos' },
|
{ value: TranscodePolicy.All, text: 'All videos' },
|
||||||
{
|
{
|
||||||
value: TranscodePolicy.Optimal,
|
value: TranscodePolicy.Optimal,
|
||||||
text: 'Videos higher than target resolution or not in an accepted format',
|
text: $t('admin.transcoding_optimal_description'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: TranscodePolicy.Bitrate,
|
value: TranscodePolicy.Bitrate,
|
||||||
text: 'Videos higher than max bitrate or not in an accepted format',
|
text: $t('admin.transcoding_bitrate_description'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: TranscodePolicy.Required,
|
value: TranscodePolicy.Required,
|
||||||
text: 'Only videos not in an accepted format',
|
text: $t('admin.transcoding_required_description'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: TranscodePolicy.Disabled,
|
value: TranscodePolicy.Disabled,
|
||||||
text: "Don't transcode any videos, may break playback on some clients",
|
text: $t('admin.transcoding_disabled_description'),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
isEdited={config.ffmpeg.transcode !== savedConfig.ffmpeg.transcode}
|
isEdited={config.ffmpeg.transcode !== savedConfig.ffmpeg.transcode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="TONE-MAPPING"
|
label={$t('admin.transcoding_tone_mapping').toUpperCase()}
|
||||||
{disabled}
|
{disabled}
|
||||||
desc="Attempts to preserve the appearance of HDR videos when converted to SDR. Each algorithm makes different tradeoffs for color, detail and brightness. Hable preserves detail, Mobius preserves color, and Reinhard preserves brightness."
|
desc={$t('admin.transcoding_tone_mapping_description')}
|
||||||
bind:value={config.ffmpeg.tonemap}
|
bind:value={config.ffmpeg.tonemap}
|
||||||
name="tonemap"
|
name="tonemap"
|
||||||
options={[
|
options={[
|
||||||
@ -234,58 +235,58 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="TWO-PASS ENCODING"
|
title={$t('admin.transcoding_two_pass_encoding').toUpperCase()}
|
||||||
{disabled}
|
{disabled}
|
||||||
subtitle="Transcode in two passes to produce better encoded videos. When max bitrate is enabled (required for it to work with H.264 and HEVC), this mode uses a bitrate range based on the max bitrate and ignores CRF. For VP9, CRF can be used if max bitrate is disabled."
|
subtitle={$t('admin.transcoding_two_pass_encoding_setting_description')}
|
||||||
bind:checked={config.ffmpeg.twoPass}
|
bind:checked={config.ffmpeg.twoPass}
|
||||||
isEdited={config.ffmpeg.twoPass !== savedConfig.ffmpeg.twoPass}
|
isEdited={config.ffmpeg.twoPass !== savedConfig.ffmpeg.twoPass}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingAccordion
|
<SettingAccordion
|
||||||
key="hardware-acceleration"
|
key="hardware-acceleration"
|
||||||
title="Hardware Acceleration"
|
title={$t('admin.transcoding_hardware_acceleration')}
|
||||||
subtitle="Experimental; much faster, but will have lower quality at the same bitrate"
|
subtitle={$t('admin.transcoding_hardware_acceleration_description')}
|
||||||
>
|
>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="ACCELERATION API"
|
label={$t('admin.transcoding_acceleration_api').toUpperCase()}
|
||||||
{disabled}
|
{disabled}
|
||||||
desc="The API that will interact with your device to accelerate transcoding. This setting is 'best effort': it will fallback to software transcoding on failure. VP9 may or may not work depending on your hardware."
|
desc={$t('admin.transcoding_acceleration_api_description')}
|
||||||
bind:value={config.ffmpeg.accel}
|
bind:value={config.ffmpeg.accel}
|
||||||
name="accel"
|
name="accel"
|
||||||
options={[
|
options={[
|
||||||
{ value: TranscodeHWAccel.Nvenc, text: 'NVENC (requires NVIDIA GPU)' },
|
{ value: TranscodeHWAccel.Nvenc, text: $t('admin.transcoding_acceleration_nvenc') },
|
||||||
{
|
{
|
||||||
value: TranscodeHWAccel.Qsv,
|
value: TranscodeHWAccel.Qsv,
|
||||||
text: 'Quick Sync (requires 7th gen Intel CPU or later)',
|
text: $t('admin.transcoding_acceleration_qsv'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: TranscodeHWAccel.Vaapi,
|
value: TranscodeHWAccel.Vaapi,
|
||||||
text: 'VAAPI',
|
text: $t('admin.transcoding_acceleration_vaapi'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: TranscodeHWAccel.Rkmpp,
|
value: TranscodeHWAccel.Rkmpp,
|
||||||
text: 'RKMPP (only on Rockchip SOCs)',
|
text: $t('admin.transcoding_acceleration_rkmpp'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: TranscodeHWAccel.Disabled,
|
value: TranscodeHWAccel.Disabled,
|
||||||
text: 'Disabled',
|
text: $t('disabled'),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
isEdited={config.ffmpeg.accel !== savedConfig.ffmpeg.accel}
|
isEdited={config.ffmpeg.accel !== savedConfig.ffmpeg.accel}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="HARDWARE DECODING"
|
title={$t('admin.transcoding_hardware_decoding').toUpperCase()}
|
||||||
{disabled}
|
{disabled}
|
||||||
subtitle="Applies only to NVENC and RKMPP. Enables end-to-end acceleration instead of only accelerating encoding. May not work on all videos."
|
subtitle={$t('admin.transcoding_hardware_decoding_setting_description')}
|
||||||
bind:checked={config.ffmpeg.accelDecode}
|
bind:checked={config.ffmpeg.accelDecode}
|
||||||
isEdited={config.ffmpeg.accelDecode !== savedConfig.ffmpeg.accelDecode}
|
isEdited={config.ffmpeg.accelDecode !== savedConfig.ffmpeg.accelDecode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="CONSTANT QUALITY MODE"
|
label={$t('admin.transcoding_constant_quality_mode').toUpperCase()}
|
||||||
desc="ICQ is better than CQP, but some hardware acceleration devices do not support this mode. Setting this option will prefer the specified mode when using quality-based encoding. Ignored by NVENC as it does not support ICQ."
|
desc={$t('admin.transcoding_constant_quality_mode_description')}
|
||||||
bind:value={config.ffmpeg.cqMode}
|
bind:value={config.ffmpeg.cqMode}
|
||||||
options={[
|
options={[
|
||||||
{ value: CQMode.Auto, text: 'Auto' },
|
{ value: CQMode.Auto, text: 'Auto' },
|
||||||
@ -297,17 +298,17 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="TEMPORAL AQ"
|
title={$t('admin.transcoding_temporal_aq').toUpperCase()}
|
||||||
{disabled}
|
{disabled}
|
||||||
subtitle="Applies only to NVENC. Increases quality of high-detail, low-motion scenes. May not be compatible with older devices."
|
subtitle={$t('admin.transcoding_temporal_aq_description')}
|
||||||
bind:checked={config.ffmpeg.temporalAQ}
|
bind:checked={config.ffmpeg.temporalAQ}
|
||||||
isEdited={config.ffmpeg.temporalAQ !== savedConfig.ffmpeg.temporalAQ}
|
isEdited={config.ffmpeg.temporalAQ !== savedConfig.ffmpeg.temporalAQ}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="PREFERRED HARDWARE DEVICE"
|
label={$t('admin.transcoding_preferred_hardware_device').toUpperCase()}
|
||||||
desc="Applies only to VAAPI and QSV. Sets the dri node used for hardware transcoding."
|
desc={$t('admin.transcoding_preferred_hardware_device_description')}
|
||||||
bind:value={config.ffmpeg.preferredHwDevice}
|
bind:value={config.ffmpeg.preferredHwDevice}
|
||||||
isEdited={config.ffmpeg.preferredHwDevice !== savedConfig.ffmpeg.preferredHwDevice}
|
isEdited={config.ffmpeg.preferredHwDevice !== savedConfig.ffmpeg.preferredHwDevice}
|
||||||
{disabled}
|
{disabled}
|
||||||
@ -317,14 +318,14 @@
|
|||||||
|
|
||||||
<SettingAccordion
|
<SettingAccordion
|
||||||
key="advanced-options"
|
key="advanced-options"
|
||||||
title="Advanced"
|
title={$t('advanced')}
|
||||||
subtitle="Options most users should not need to change"
|
subtitle={$t('admin.transcoding_advanced_options_description')}
|
||||||
>
|
>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
label="TONE-MAPPING NPL"
|
label={$t('admin.transcoding_tone_mapping_npl').toUpperCase()}
|
||||||
desc="Colors will be adjusted to look normal for a display of this brightness. Counter-intuitively, lower values increase the brightness of the video and vice versa since it compensates for the brightness of the display. 0 sets this value automatically."
|
desc={$t('admin.transcoding_tone_mapping_npl_description')}
|
||||||
bind:value={config.ffmpeg.npl}
|
bind:value={config.ffmpeg.npl}
|
||||||
isEdited={config.ffmpeg.npl !== savedConfig.ffmpeg.npl}
|
isEdited={config.ffmpeg.npl !== savedConfig.ffmpeg.npl}
|
||||||
{disabled}
|
{disabled}
|
||||||
@ -332,8 +333,8 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
label="MAX B-FRAMES"
|
label={$t('admin.transcoding_max_b_frames').toUpperCase()}
|
||||||
desc="Higher values improve compression efficiency, but slow down encoding. May not be compatible with hardware acceleration on older devices. 0 disables B-frames, while -1 sets this value automatically."
|
desc={$t('admin.transcoding_max_b_frames_description')}
|
||||||
bind:value={config.ffmpeg.bframes}
|
bind:value={config.ffmpeg.bframes}
|
||||||
isEdited={config.ffmpeg.bframes !== savedConfig.ffmpeg.bframes}
|
isEdited={config.ffmpeg.bframes !== savedConfig.ffmpeg.bframes}
|
||||||
{disabled}
|
{disabled}
|
||||||
@ -341,8 +342,8 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
label="REFERENCE FRAMES"
|
label={$t('admin.transcoding_reference_frames').toUpperCase()}
|
||||||
desc="The number of frames to reference when compressing a given frame. Higher values improve compression efficiency, but slow down encoding. 0 sets this value automatically."
|
desc={$t('admin.transcoding_reference_frames_description')}
|
||||||
bind:value={config.ffmpeg.refs}
|
bind:value={config.ffmpeg.refs}
|
||||||
isEdited={config.ffmpeg.refs !== savedConfig.ffmpeg.refs}
|
isEdited={config.ffmpeg.refs !== savedConfig.ffmpeg.refs}
|
||||||
{disabled}
|
{disabled}
|
||||||
@ -350,8 +351,8 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
label="MAX KEYFRAME INTERVAL"
|
label={$t('admin.transcoding_max_keyframe_interval').toUpperCase()}
|
||||||
desc="Sets the maximum frame distance between keyframes. Lower values worsen compression efficiency, but improve seek times and may improve quality in scenes with fast movement. 0 sets this value automatically."
|
desc={$t('admin.transcoding_max_keyframe_interval_description')}
|
||||||
bind:value={config.ffmpeg.gopSize}
|
bind:value={config.ffmpeg.gopSize}
|
||||||
isEdited={config.ffmpeg.gopSize !== savedConfig.ffmpeg.gopSize}
|
isEdited={config.ffmpeg.gopSize !== savedConfig.ffmpeg.gopSize}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
import SettingInputField, {
|
import SettingInputField, {
|
||||||
SettingInputFieldType,
|
SettingInputFieldType,
|
||||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let savedConfig: SystemConfigDto;
|
export let savedConfig: SystemConfigDto;
|
||||||
export let defaultConfig: SystemConfigDto;
|
export let defaultConfig: SystemConfigDto;
|
||||||
@ -25,8 +26,8 @@
|
|||||||
<form autocomplete="off" on:submit|preventDefault>
|
<form autocomplete="off" on:submit|preventDefault>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="THUMBNAIL FORMAT"
|
label={$t('admin.image_thumbnail_format').toUpperCase()}
|
||||||
desc="WebP produces smaller files than JPEG, but is slower to encode."
|
desc={$t('admin.image_format_description')}
|
||||||
bind:value={config.image.thumbnailFormat}
|
bind:value={config.image.thumbnailFormat}
|
||||||
options={[
|
options={[
|
||||||
{ value: ImageFormat.Jpeg, text: 'JPEG' },
|
{ value: ImageFormat.Jpeg, text: 'JPEG' },
|
||||||
@ -38,8 +39,8 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="THUMBNAIL RESOLUTION"
|
label={$t('admin.image_thumbnail_resolution').toUpperCase()}
|
||||||
desc="Used when viewing groups of photos (main timeline, album view, etc.). Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
|
desc={$t('admin.image_thumbnail_resolution_description')}
|
||||||
number
|
number
|
||||||
bind:value={config.image.thumbnailSize}
|
bind:value={config.image.thumbnailSize}
|
||||||
options={[
|
options={[
|
||||||
@ -55,8 +56,8 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="PREVIEW FORMAT"
|
label={$t('admin.image_preview_format').toUpperCase()}
|
||||||
desc="WebP produces smaller files than JPEG, but is slower to encode."
|
desc={$t('admin.image_format_description')}
|
||||||
bind:value={config.image.previewFormat}
|
bind:value={config.image.previewFormat}
|
||||||
options={[
|
options={[
|
||||||
{ value: ImageFormat.Jpeg, text: 'JPEG' },
|
{ value: ImageFormat.Jpeg, text: 'JPEG' },
|
||||||
@ -68,8 +69,8 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="PREVIEW RESOLUTION"
|
label={$t('admin.image_preview_resolution').toUpperCase()}
|
||||||
desc="Used when viewing a single photo and for machine learning. Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
|
desc={$t('admin.image_preview_resolution_description')}
|
||||||
number
|
number
|
||||||
bind:value={config.image.previewSize}
|
bind:value={config.image.previewSize}
|
||||||
options={[
|
options={[
|
||||||
@ -85,16 +86,16 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
label="QUALITY"
|
label={$t('admin.image_quality').toUpperCase()}
|
||||||
desc="Image quality from 1-100. Higher is better for quality but produces larger files."
|
desc={$t('admin.image_quality_description')}
|
||||||
bind:value={config.image.quality}
|
bind:value={config.image.quality}
|
||||||
isEdited={config.image.quality !== savedConfig.image.quality}
|
isEdited={config.image.quality !== savedConfig.image.quality}
|
||||||
{disabled}
|
{disabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="PREFER WIDE GAMUT"
|
title={$t('admin.image_prefer_wide_gamut').toUpperCase()}
|
||||||
subtitle="Use Display P3 for thumbnails. This better preserves the vibrance of images with wide colorspaces, but images may appear differently on old devices with an old browser version. sRGB images are kept as sRGB to avoid color shifts."
|
subtitle={$t('admin.image_prefer_wide_gamut_setting_description')}
|
||||||
checked={config.image.colorspace === Colorspace.P3}
|
checked={config.image.colorspace === Colorspace.P3}
|
||||||
on:toggle={(e) => (config.image.colorspace = e.detail ? Colorspace.P3 : Colorspace.Srgb)}
|
on:toggle={(e) => (config.image.colorspace = e.detail ? Colorspace.P3 : Colorspace.Srgb)}
|
||||||
isEdited={config.image.colorspace !== savedConfig.image.colorspace}
|
isEdited={config.image.colorspace !== savedConfig.image.colorspace}
|
||||||
@ -102,8 +103,8 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="PREFER EMBEDDED PREVIEW"
|
title={$t('admin.image_prefer_embedded_preview').toUpperCase()}
|
||||||
subtitle="Use embedded previews in RAW photos as the input to image processing when available. This can produce more accurate colors for some images, but the quality of the preview is camera-dependent and the image may have more compression artifacts."
|
subtitle={$t('admin.image_prefer_embedded_preview_setting_description')}
|
||||||
checked={config.image.extractEmbedded}
|
checked={config.image.extractEmbedded}
|
||||||
on:toggle={() => (config.image.extractEmbedded = !config.image.extractEmbedded)}
|
on:toggle={() => (config.image.extractEmbedded = !config.image.extractEmbedded)}
|
||||||
isEdited={config.image.extractEmbedded !== savedConfig.image.extractEmbedded}
|
isEdited={config.image.extractEmbedded !== savedConfig.image.extractEmbedded}
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||||
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let savedConfig: SystemConfigDto;
|
export let savedConfig: SystemConfigDto;
|
||||||
export let defaultConfig: SystemConfigDto;
|
export let defaultConfig: SystemConfigDto;
|
||||||
@ -17,10 +18,10 @@
|
|||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
|
|
||||||
const cronExpressionOptions = [
|
const cronExpressionOptions = [
|
||||||
{ title: 'Every night at midnight', expression: '0 0 * * *' },
|
{ title: $t('interval.night_at_midnight'), expression: '0 0 * * *' },
|
||||||
{ title: 'Every night at 2am', expression: '0 2 * * *' },
|
{ title: $t('interval.night_at_twoam'), expression: '0 2 * * *' },
|
||||||
{ title: 'Every day at 1pm', expression: '0 13 * * *' },
|
{ title: $t('interval.day_at_onepm'), expression: '0 13 * * *' },
|
||||||
{ title: 'Every 6 hours', expression: '0 */6 * * *' },
|
{ title: $t('interval.hours', { values: { hours: 6 } }), expression: '0 */6 * * *' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<SettingsEventType>();
|
const dispatch = createEventDispatcher<SettingsEventType>();
|
||||||
@ -30,16 +31,16 @@
|
|||||||
<div in:fade={{ duration: 500 }}>
|
<div in:fade={{ duration: 500 }}>
|
||||||
<SettingAccordion
|
<SettingAccordion
|
||||||
key="library-watching"
|
key="library-watching"
|
||||||
title="Library watching (EXPERIMENTAL)"
|
title={$t('admin.library_watching_settings')}
|
||||||
subtitle="Automatically watch for changed files"
|
subtitle={$t('admin.library_watching_settings_description')}
|
||||||
isOpen
|
isOpen
|
||||||
>
|
>
|
||||||
<form autocomplete="off" on:submit|preventDefault>
|
<form autocomplete="off" on:submit|preventDefault>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="Watch filesystem"
|
title={$t('enable')}
|
||||||
{disabled}
|
{disabled}
|
||||||
subtitle="Watch external libraries for file changes"
|
subtitle={$t('admin.library_watching_enable_description')}
|
||||||
bind:checked={config.library.watch.enabled}
|
bind:checked={config.library.watch.enabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -57,21 +58,21 @@
|
|||||||
|
|
||||||
<SettingAccordion
|
<SettingAccordion
|
||||||
key="library-scanning"
|
key="library-scanning"
|
||||||
title="Periodic Scanning"
|
title={$t('admin.library_scanning')}
|
||||||
subtitle="Configure periodic library scanning"
|
subtitle={$t('admin.library_scanning_description')}
|
||||||
isOpen
|
isOpen
|
||||||
>
|
>
|
||||||
<form autocomplete="off" on:submit|preventDefault>
|
<form autocomplete="off" on:submit|preventDefault>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="ENABLED"
|
title={$t('enabled').toUpperCase()}
|
||||||
{disabled}
|
{disabled}
|
||||||
subtitle="Enable periodic library scanning"
|
subtitle={$t('admin.library_scanning_enable_description')}
|
||||||
bind:checked={config.library.scan.enabled}
|
bind:checked={config.library.scan.enabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-col my-2 dark:text-immich-dark-fg">
|
<div class="flex flex-col my-2 dark:text-immich-dark-fg">
|
||||||
<label class="text-sm" for="expression-select">Cron Expression Presets</label>
|
<label class="text-sm" for="expression-select">{$t('admin.library_cron_expression_presets')}</label>
|
||||||
<select
|
<select
|
||||||
class="p-2 mt-2 text-sm rounded-lg bg-slate-200 hover:cursor-pointer dark:bg-gray-600"
|
class="p-2 mt-2 text-sm rounded-lg bg-slate-200 hover:cursor-pointer dark:bg-gray-600"
|
||||||
disabled={disabled || !config.library.scan.enabled}
|
disabled={disabled || !config.library.scan.enabled}
|
||||||
@ -89,7 +90,7 @@
|
|||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
required={true}
|
required={true}
|
||||||
disabled={disabled || !config.library.scan.enabled}
|
disabled={disabled || !config.library.scan.enabled}
|
||||||
label="Cron Expression"
|
label={$t('admin.library_cron_expression')}
|
||||||
bind:value={config.library.scan.cronExpression}
|
bind:value={config.library.scan.cronExpression}
|
||||||
isEdited={config.library.scan.cronExpression !== savedConfig.library.scan.cronExpression}
|
isEdited={config.library.scan.cronExpression !== savedConfig.library.scan.cronExpression}
|
||||||
>
|
>
|
||||||
@ -99,7 +100,7 @@
|
|||||||
href="https://crontab.guru"
|
href="https://crontab.guru"
|
||||||
class="underline"
|
class="underline"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer">Crontab Guru</a
|
rel="noreferrer">{$t('crontab_guru')}</a
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
||||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let savedConfig: SystemConfigDto;
|
export let savedConfig: SystemConfigDto;
|
||||||
export let defaultConfig: SystemConfigDto;
|
export let defaultConfig: SystemConfigDto;
|
||||||
@ -20,10 +21,15 @@
|
|||||||
<div in:fade={{ duration: 500 }}>
|
<div in:fade={{ duration: 500 }}>
|
||||||
<form autocomplete="off" on:submit|preventDefault>
|
<form autocomplete="off" on:submit|preventDefault>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingSwitch title="ENABLED" {disabled} subtitle="Logging" bind:checked={config.logging.enabled} />
|
<SettingSwitch
|
||||||
|
title={$t('enabled').toUpperCase()}
|
||||||
|
{disabled}
|
||||||
|
subtitle={$t('admin.logging_enable_description')}
|
||||||
|
bind:checked={config.logging.enabled}
|
||||||
|
/>
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="LEVEL"
|
label={$t('level').toUpperCase()}
|
||||||
desc="When enabled, what log level to use."
|
desc={$t('admin.logging_level_description')}
|
||||||
bind:value={config.logging.level}
|
bind:value={config.logging.level}
|
||||||
options={[
|
options={[
|
||||||
{ value: LogLevel.Fatal, text: 'Fatal' },
|
{ value: LogLevel.Fatal, text: 'Fatal' },
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let savedConfig: SystemConfigDto;
|
export let savedConfig: SystemConfigDto;
|
||||||
export let defaultConfig: SystemConfigDto;
|
export let defaultConfig: SystemConfigDto;
|
||||||
@ -26,8 +27,8 @@
|
|||||||
<form autocomplete="off" on:submit|preventDefault class="mx-4 mt-4">
|
<form autocomplete="off" on:submit|preventDefault class="mx-4 mt-4">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="ENABLED"
|
title={$t('enabled').toUpperCase()}
|
||||||
subtitle="If disabled, all ML features will be disabled regardless of the below settings."
|
subtitle={$t('admin.machine_learning_enabled_description')}
|
||||||
{disabled}
|
{disabled}
|
||||||
bind:checked={config.machineLearning.enabled}
|
bind:checked={config.machineLearning.enabled}
|
||||||
/>
|
/>
|
||||||
@ -36,8 +37,8 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="URL"
|
label={$t('url').toUpperCase()}
|
||||||
desc="URL of the machine learning server"
|
desc={$t('admin.machine_learning_url_description')}
|
||||||
bind:value={config.machineLearning.url}
|
bind:value={config.machineLearning.url}
|
||||||
required={true}
|
required={true}
|
||||||
disabled={disabled || !config.machineLearning.enabled}
|
disabled={disabled || !config.machineLearning.enabled}
|
||||||
@ -47,13 +48,13 @@
|
|||||||
|
|
||||||
<SettingAccordion
|
<SettingAccordion
|
||||||
key="smart-search"
|
key="smart-search"
|
||||||
title="Smart Search"
|
title={$t('admin.machine_learning_smart_search')}
|
||||||
subtitle="Search for images semantically using CLIP embeddings"
|
subtitle={$t('admin.machine_learning_smart_search_description')}
|
||||||
>
|
>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="ENABLED"
|
title={$t('enabled').toUpperCase()}
|
||||||
subtitle="If disabled, images will not be encoded for smart search."
|
subtitle={$t('admin.machine_learning_smart_search_enabled_description')}
|
||||||
bind:checked={config.machineLearning.clip.enabled}
|
bind:checked={config.machineLearning.clip.enabled}
|
||||||
disabled={disabled || !config.machineLearning.enabled}
|
disabled={disabled || !config.machineLearning.enabled}
|
||||||
/>
|
/>
|
||||||
@ -62,7 +63,7 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="CLIP MODEL"
|
label={$t('admin.machine_learning_clip_model').toUpperCase()}
|
||||||
bind:value={config.machineLearning.clip.modelName}
|
bind:value={config.machineLearning.clip.modelName}
|
||||||
required={true}
|
required={true}
|
||||||
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.clip.enabled}
|
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.clip.enabled}
|
||||||
@ -78,13 +79,13 @@
|
|||||||
|
|
||||||
<SettingAccordion
|
<SettingAccordion
|
||||||
key="duplicate-detection"
|
key="duplicate-detection"
|
||||||
title="Duplicate Detection"
|
title={$t('admin.machine_learning_duplicate_detection')}
|
||||||
subtitle="Use CLIP embeddings to find likely duplicates"
|
subtitle={$t('admin.machine_learning_duplicate_detection_setting_description')}
|
||||||
>
|
>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="ENABLED"
|
title={$t('enabled').toUpperCase()}
|
||||||
subtitle="If disabled, exactly identical assets will still be de-duplicated."
|
subtitle={$t('admin.machine_learning_duplicate_detection_enabled_description')}
|
||||||
bind:checked={config.machineLearning.duplicateDetection.enabled}
|
bind:checked={config.machineLearning.duplicateDetection.enabled}
|
||||||
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.clip.enabled}
|
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.clip.enabled}
|
||||||
/>
|
/>
|
||||||
@ -93,12 +94,12 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
label="MAX DETECTION DISTANCE"
|
label={$t('admin.machine_learning_max_detection_distance').toUpperCase()}
|
||||||
bind:value={config.machineLearning.duplicateDetection.maxDistance}
|
bind:value={config.machineLearning.duplicateDetection.maxDistance}
|
||||||
step="0.0005"
|
step="0.0005"
|
||||||
min={0.001}
|
min={0.001}
|
||||||
max={0.1}
|
max={0.1}
|
||||||
desc="Maximum distance between two images to consider them duplicates, ranging from 0.001-0.1. Higher values will detect more duplicates, but may result in false positives."
|
desc={$t('admin.machine_learning_max_detection_distance_description')}
|
||||||
disabled={disabled || !$featureFlags.duplicateDetection}
|
disabled={disabled || !$featureFlags.duplicateDetection}
|
||||||
isEdited={config.machineLearning.duplicateDetection.maxDistance !==
|
isEdited={config.machineLearning.duplicateDetection.maxDistance !==
|
||||||
savedConfig.machineLearning.duplicateDetection.maxDistance}
|
savedConfig.machineLearning.duplicateDetection.maxDistance}
|
||||||
@ -108,13 +109,13 @@
|
|||||||
|
|
||||||
<SettingAccordion
|
<SettingAccordion
|
||||||
key="facial-recognition"
|
key="facial-recognition"
|
||||||
title="Facial Recognition"
|
title={$t('admin.machine_learning_facial_recognition')}
|
||||||
subtitle="Detect, recognize and group faces in images"
|
subtitle={$t('admin.machine_learning_facial_recognition_description')}
|
||||||
>
|
>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="ENABLED"
|
title={$t('enabled').toUpperCase()}
|
||||||
subtitle="If disabled, images will not be encoded for facial recognition and will not populate the People section in the Explore page."
|
subtitle={$t('admin.machine_learning_facial_recognition_setting_description')}
|
||||||
bind:checked={config.machineLearning.facialRecognition.enabled}
|
bind:checked={config.machineLearning.facialRecognition.enabled}
|
||||||
disabled={disabled || !config.machineLearning.enabled}
|
disabled={disabled || !config.machineLearning.enabled}
|
||||||
/>
|
/>
|
||||||
@ -122,8 +123,8 @@
|
|||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="FACIAL RECOGNITION MODEL"
|
label={$t('admin.machine_learning_facial_recognition_model').toUpperCase()}
|
||||||
desc="Models are listed in descending order of size. Larger models are slower and use more memory, but produce better results. Note that you must re-run the Face Detection job for all images upon changing a model."
|
desc={$t('admin.machine_learning_facial_recognition_model_description')}
|
||||||
name="facial-recognition-model"
|
name="facial-recognition-model"
|
||||||
bind:value={config.machineLearning.facialRecognition.modelName}
|
bind:value={config.machineLearning.facialRecognition.modelName}
|
||||||
options={[
|
options={[
|
||||||
@ -139,8 +140,8 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
label="MIN DETECTION SCORE"
|
label={$t('admin.machine_learning_min_detection_score').toUpperCase()}
|
||||||
desc="Minimum confidence score for a face to be detected from 0-1. Lower values will detect more faces but may result in false positives."
|
desc={$t('admin.machine_learning_min_detection_score_description')}
|
||||||
bind:value={config.machineLearning.facialRecognition.minScore}
|
bind:value={config.machineLearning.facialRecognition.minScore}
|
||||||
step="0.1"
|
step="0.1"
|
||||||
min={0}
|
min={0}
|
||||||
@ -152,8 +153,8 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
label="MAX RECOGNITION DISTANCE"
|
label={$t('admin.machine_learning_max_recognition_distance').toUpperCase()}
|
||||||
desc="Maximum distance between two faces to be considered the same person, ranging from 0-2. Lowering this can prevent labeling two people as the same person, while raising it can prevent labeling the same person as two different people. Note that it is easier to merge two people than to split one person in two, so err on the side of a lower threshold when possible."
|
desc={$t('admin.machine_learning_max_recognition_distance_description')}
|
||||||
bind:value={config.machineLearning.facialRecognition.maxDistance}
|
bind:value={config.machineLearning.facialRecognition.maxDistance}
|
||||||
step="0.1"
|
step="0.1"
|
||||||
min={0}
|
min={0}
|
||||||
@ -165,8 +166,8 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
label="MIN RECOGNIZED FACES"
|
label={$t('admin.machine_learning_min_recognized_faces').toUpperCase()}
|
||||||
desc="The minimum number of recognized faces for a person to be created. Increasing this makes Facial Recognition more precise at the cost of increasing the chance that a face is not assigned to a person."
|
desc={$t('admin.machine_learning_min_recognized_faces_description')}
|
||||||
bind:value={config.machineLearning.facialRecognition.minFaces}
|
bind:value={config.machineLearning.facialRecognition.minFaces}
|
||||||
step="1"
|
step="1"
|
||||||
min={1}
|
min={1}
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
import SettingInputField, {
|
import SettingInputField, {
|
||||||
SettingInputFieldType,
|
SettingInputFieldType,
|
||||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let savedConfig: SystemConfigDto;
|
export let savedConfig: SystemConfigDto;
|
||||||
export let defaultConfig: SystemConfigDto;
|
export let defaultConfig: SystemConfigDto;
|
||||||
@ -23,12 +24,12 @@
|
|||||||
<div in:fade={{ duration: 500 }}>
|
<div in:fade={{ duration: 500 }}>
|
||||||
<form autocomplete="off" on:submit|preventDefault>
|
<form autocomplete="off" on:submit|preventDefault>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<SettingAccordion key="map" title="Map Settings" subtitle="Manage map settings">
|
<SettingAccordion key="map" title={$t('admin.map_settings')} subtitle={$t('admin.map_settings_description')}>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="ENABLED"
|
title={$t('enabled').toUpperCase()}
|
||||||
{disabled}
|
{disabled}
|
||||||
subtitle="Enable map features"
|
subtitle={$t('admin.map_enable_description')}
|
||||||
bind:checked={config.map.enabled}
|
bind:checked={config.map.enabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -36,16 +37,16 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="Light Style"
|
label={$t('admin.map_light_style')}
|
||||||
desc="URL to a style.json map theme"
|
desc={$t('admin.map_style_description')}
|
||||||
bind:value={config.map.lightStyle}
|
bind:value={config.map.lightStyle}
|
||||||
disabled={disabled || !config.map.enabled}
|
disabled={disabled || !config.map.enabled}
|
||||||
isEdited={config.map.lightStyle !== savedConfig.map.lightStyle}
|
isEdited={config.map.lightStyle !== savedConfig.map.lightStyle}
|
||||||
/>
|
/>
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="Dark Style"
|
label={$t('admin.map_dark_style')}
|
||||||
desc="URL to a style.json map theme"
|
desc={$t('admin.map_style_description')}
|
||||||
bind:value={config.map.darkStyle}
|
bind:value={config.map.darkStyle}
|
||||||
disabled={disabled || !config.map.enabled}
|
disabled={disabled || !config.map.enabled}
|
||||||
isEdited={config.map.darkStyle !== savedConfig.map.darkStyle}
|
isEdited={config.map.darkStyle !== savedConfig.map.darkStyle}
|
||||||
@ -53,22 +54,22 @@
|
|||||||
</div></SettingAccordion
|
</div></SettingAccordion
|
||||||
>
|
>
|
||||||
|
|
||||||
<SettingAccordion key="reverse-geocoding" title="Reverse Geocoding Settings">
|
<SettingAccordion key="reverse-geocoding" title={$t('admin.map_reverse_geocoding_settings')}>
|
||||||
<svelte:fragment slot="subtitle">
|
<svelte:fragment slot="subtitle">
|
||||||
<p class="text-sm dark:text-immich-dark-fg">
|
<p class="text-sm dark:text-immich-dark-fg">
|
||||||
Manage <a
|
Manage <a
|
||||||
href="https://immich.app/docs/features/reverse-geocoding"
|
href="https://immich.app/docs/features/reverse-geocoding"
|
||||||
class="underline"
|
class="underline"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer">Reverse Geocoding</a
|
rel="noreferrer">{$t('admin.map_reverse_geocoding')}</a
|
||||||
> settings
|
> settings
|
||||||
</p>
|
</p>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="ENABLED"
|
title={$t('enabled').toUpperCase()}
|
||||||
{disabled}
|
{disabled}
|
||||||
subtitle="Enable reverse geocoding"
|
subtitle={$t('admin.map_reverse_geocoding_enable_description')}
|
||||||
bind:checked={config.reverseGeocoding.enabled}
|
bind:checked={config.reverseGeocoding.enabled}
|
||||||
/>
|
/>
|
||||||
</div></SettingAccordion
|
</div></SettingAccordion
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
import type { SettingsEventType } from '../admin-settings';
|
import type { SettingsEventType } from '../admin-settings';
|
||||||
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
||||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let savedConfig: SystemConfigDto;
|
export let savedConfig: SystemConfigDto;
|
||||||
export let defaultConfig: SystemConfigDto;
|
export let defaultConfig: SystemConfigDto;
|
||||||
@ -20,8 +21,8 @@
|
|||||||
<form autocomplete="off" on:submit|preventDefault>
|
<form autocomplete="off" on:submit|preventDefault>
|
||||||
<div class="ml-4 mt-4">
|
<div class="ml-4 mt-4">
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="ENABLED"
|
title={$t('enabled').toUpperCase()}
|
||||||
subtitle="Enable periodic requests to GitHub to check for new releases"
|
subtitle={$t('admin.version_check_enabled_description')}
|
||||||
bind:checked={config.newVersionCheck.enabled}
|
bind:checked={config.newVersionCheck.enabled}
|
||||||
{disabled}
|
{disabled}
|
||||||
/>
|
/>
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
||||||
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
|
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
|
||||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let savedConfig: SystemConfigDto;
|
export let savedConfig: SystemConfigDto;
|
||||||
export let defaultConfig: SystemConfigDto;
|
export let defaultConfig: SystemConfigDto;
|
||||||
@ -23,11 +24,11 @@
|
|||||||
<div in:fade={{ duration: 500 }}>
|
<div in:fade={{ duration: 500 }}>
|
||||||
<form autocomplete="off" on:submit|preventDefault class="mt-4">
|
<form autocomplete="off" on:submit|preventDefault class="mt-4">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<SettingAccordion key="email" title="Email" subtitle="Settings for sending email notifications">
|
<SettingAccordion key="email" title={$t('email')} subtitle={$t('admin.notification_email_setting_description')}>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="Enabled"
|
title={$t('enabled')}
|
||||||
subtitle="Enable email notifications"
|
subtitle={$t('admin.notification_enable_email_notifications')}
|
||||||
{disabled}
|
{disabled}
|
||||||
bind:checked={config.notifications.smtp.enabled}
|
bind:checked={config.notifications.smtp.enabled}
|
||||||
/>
|
/>
|
||||||
@ -37,8 +38,8 @@
|
|||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
required
|
required
|
||||||
label="Host"
|
label={$t('host')}
|
||||||
desc="Host of the email server (e.g. smtp.immich.app)"
|
desc={$t('admin.notification_email_host_description')}
|
||||||
disabled={disabled || !config.notifications.smtp.enabled}
|
disabled={disabled || !config.notifications.smtp.enabled}
|
||||||
bind:value={config.notifications.smtp.transport.host}
|
bind:value={config.notifications.smtp.transport.host}
|
||||||
isEdited={config.notifications.smtp.transport.host !== savedConfig.notifications.smtp.transport.host}
|
isEdited={config.notifications.smtp.transport.host !== savedConfig.notifications.smtp.transport.host}
|
||||||
@ -47,8 +48,8 @@
|
|||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
required
|
required
|
||||||
label="Port"
|
label={$t('port')}
|
||||||
desc="Port of the email server (e.g 25, 465, or 587)"
|
desc={$t('admin.notification_email_port_description')}
|
||||||
disabled={disabled || !config.notifications.smtp.enabled}
|
disabled={disabled || !config.notifications.smtp.enabled}
|
||||||
bind:value={config.notifications.smtp.transport.port}
|
bind:value={config.notifications.smtp.transport.port}
|
||||||
isEdited={config.notifications.smtp.transport.port !== savedConfig.notifications.smtp.transport.port}
|
isEdited={config.notifications.smtp.transport.port !== savedConfig.notifications.smtp.transport.port}
|
||||||
@ -56,8 +57,8 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="Username"
|
label={$t('username')}
|
||||||
desc="Username to use when authenticating with the email server"
|
desc={$t('admin.notification_email_username_description')}
|
||||||
disabled={disabled || !config.notifications.smtp.enabled}
|
disabled={disabled || !config.notifications.smtp.enabled}
|
||||||
bind:value={config.notifications.smtp.transport.username}
|
bind:value={config.notifications.smtp.transport.username}
|
||||||
isEdited={config.notifications.smtp.transport.username !==
|
isEdited={config.notifications.smtp.transport.username !==
|
||||||
@ -66,8 +67,8 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.PASSWORD}
|
inputType={SettingInputFieldType.PASSWORD}
|
||||||
label="Password"
|
label={$t('password')}
|
||||||
desc="Password to use when authenticating with the email server"
|
desc={$t('admin.notification_email_password_description')}
|
||||||
disabled={disabled || !config.notifications.smtp.enabled}
|
disabled={disabled || !config.notifications.smtp.enabled}
|
||||||
bind:value={config.notifications.smtp.transport.password}
|
bind:value={config.notifications.smtp.transport.password}
|
||||||
isEdited={config.notifications.smtp.transport.password !==
|
isEdited={config.notifications.smtp.transport.password !==
|
||||||
@ -75,8 +76,8 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="Ignore certificate errors"
|
title={$t('admin.notification_email_ignore_certificate_errors')}
|
||||||
subtitle="Ignore TLS certificate validation errors (not recommended)"
|
subtitle={$t('admin.notification_email_ignore_certificate_errors_description')}
|
||||||
disabled={disabled || !config.notifications.smtp.enabled}
|
disabled={disabled || !config.notifications.smtp.enabled}
|
||||||
bind:checked={config.notifications.smtp.transport.ignoreCert}
|
bind:checked={config.notifications.smtp.transport.ignoreCert}
|
||||||
/>
|
/>
|
||||||
@ -86,8 +87,8 @@
|
|||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
required
|
required
|
||||||
label="From address"
|
label={$t('admin.notification_email_from_address')}
|
||||||
desc="Sender email address, for example: "Immich Photo Server <noreply@immich.app>""
|
desc={$t('admin.notification_email_from_address_description')}
|
||||||
disabled={disabled || !config.notifications.smtp.enabled}
|
disabled={disabled || !config.notifications.smtp.enabled}
|
||||||
bind:value={config.notifications.smtp.from}
|
bind:value={config.notifications.smtp.from}
|
||||||
isEdited={config.notifications.smtp.from !== savedConfig.notifications.smtp.from}
|
isEdited={config.notifications.smtp.from !== savedConfig.notifications.smtp.from}
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
SettingInputFieldType,
|
SettingInputFieldType,
|
||||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||||
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let savedConfig: SystemConfigDto;
|
export let savedConfig: SystemConfigDto;
|
||||||
export let defaultConfig: SystemConfigDto;
|
export let defaultConfig: SystemConfigDto;
|
||||||
@ -23,16 +24,16 @@
|
|||||||
<div class="mt-4 ml-4">
|
<div class="mt-4 ml-4">
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="EXTERNAL DOMAIN"
|
label={$t('admin.server_external_domain_settings').toUpperCase()}
|
||||||
desc="Domain for public shared links, including http(s)://"
|
desc={$t('admin.server_external_domain_settings_description')}
|
||||||
bind:value={config.server.externalDomain}
|
bind:value={config.server.externalDomain}
|
||||||
isEdited={config.server.externalDomain !== savedConfig.server.externalDomain}
|
isEdited={config.server.externalDomain !== savedConfig.server.externalDomain}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="WELCOME MESSAGE"
|
label={$t('admin.server_welcome_message').toUpperCase()}
|
||||||
desc="A message that is displayed on the login page."
|
desc={$t('admin.server_welcome_message_description')}
|
||||||
bind:value={config.server.loginPageMessage}
|
bind:value={config.server.loginPageMessage}
|
||||||
isEdited={config.server.loginPageMessage !== savedConfig.server.loginPageMessage}
|
isEdited={config.server.loginPageMessage !== savedConfig.server.loginPageMessage}
|
||||||
/>
|
/>
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
SettingInputFieldType,
|
SettingInputFieldType,
|
||||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let savedConfig: SystemConfigDto;
|
export let savedConfig: SystemConfigDto;
|
||||||
export let defaultConfig: SystemConfigDto;
|
export let defaultConfig: SystemConfigDto;
|
||||||
@ -54,10 +55,10 @@
|
|||||||
const substitutions: Record<string, string> = {
|
const substitutions: Record<string, string> = {
|
||||||
filename: 'IMAGE_56437',
|
filename: 'IMAGE_56437',
|
||||||
ext: 'jpg',
|
ext: 'jpg',
|
||||||
filetype: 'IMG',
|
filetype: $t('img').toUpperCase(),
|
||||||
filetypefull: 'IMAGE',
|
filetypefull: 'IMAGE',
|
||||||
assetId: 'a8312960-e277-447d-b4ea-56717ccba856',
|
assetId: 'a8312960-e277-447d-b4ea-56717ccba856',
|
||||||
album: 'Album Name',
|
album: $t('album_name'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const dt = luxon.DateTime.fromISO(new Date('2022-02-03T04:56:05.250').toISOString());
|
const dt = luxon.DateTime.fromISO(new Date('2022-02-03T04:56:05.250').toISOString());
|
||||||
@ -107,18 +108,18 @@
|
|||||||
{#await getTemplateOptions() then}
|
{#await getTemplateOptions() then}
|
||||||
<div id="directory-path-builder" class="flex flex-col gap-4 {minified ? '' : 'ml-4 mt-4'}">
|
<div id="directory-path-builder" class="flex flex-col gap-4 {minified ? '' : 'ml-4 mt-4'}">
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="ENABLED"
|
title={$t('enabled').toUpperCase()}
|
||||||
{disabled}
|
{disabled}
|
||||||
subtitle="Enable storage template engine"
|
subtitle={$t('admin.storage_template_enable_description')}
|
||||||
bind:checked={config.storageTemplate.enabled}
|
bind:checked={config.storageTemplate.enabled}
|
||||||
isEdited={!(config.storageTemplate.enabled === savedConfig.storageTemplate.enabled)}
|
isEdited={!(config.storageTemplate.enabled === savedConfig.storageTemplate.enabled)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if !minified}
|
{#if !minified}
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="HASH VERIFICATION ENABLED"
|
title={$t('admin.storage_template_hash_verification_enabled').toUpperCase()}
|
||||||
{disabled}
|
{disabled}
|
||||||
subtitle="Enables hash verification, don't disable this unless you're certain of the implications"
|
subtitle={$t('admin.storage_template_hash_verification_enabled_description')}
|
||||||
bind:checked={config.storageTemplate.hashVerificationEnabled}
|
bind:checked={config.storageTemplate.hashVerificationEnabled}
|
||||||
isEdited={!(
|
isEdited={!(
|
||||||
config.storageTemplate.hashVerificationEnabled === savedConfig.storageTemplate.hashVerificationEnabled
|
config.storageTemplate.hashVerificationEnabled === savedConfig.storageTemplate.hashVerificationEnabled
|
||||||
@ -129,7 +130,7 @@
|
|||||||
{#if config.storageTemplate.enabled}
|
{#if config.storageTemplate.enabled}
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">Variables</h3>
|
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">{$t('variables')}</h3>
|
||||||
|
|
||||||
<section class="support-date">
|
<section class="support-date">
|
||||||
{#await getSupportDateTimeFormat()}
|
{#await getSupportDateTimeFormat()}
|
||||||
@ -146,10 +147,10 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="flex flex-col mt-4">
|
<div class="flex flex-col mt-4">
|
||||||
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">Template</h3>
|
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">{$t('template')}</h3>
|
||||||
|
|
||||||
<div class="my-2 text-sm">
|
<div class="my-2 text-sm">
|
||||||
<h4>PREVIEW</h4>
|
<h4>{$t('preview').toUpperCase()}</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-sm">
|
<p class="text-sm">
|
||||||
@ -172,7 +173,7 @@
|
|||||||
|
|
||||||
<form autocomplete="off" class="flex flex-col" on:submit|preventDefault>
|
<form autocomplete="off" class="flex flex-col" on:submit|preventDefault>
|
||||||
<div class="flex flex-col my-2">
|
<div class="flex flex-col my-2">
|
||||||
<label class="text-sm" for="preset-select">PRESET</label>
|
<label class="text-sm" for="preset-select">{$t('preset').toUpperCase()}</label>
|
||||||
<select
|
<select
|
||||||
class="immich-form-input p-2 mt-2 text-sm rounded-lg bg-slate-200 hover:cursor-pointer dark:bg-gray-600"
|
class="immich-form-input p-2 mt-2 text-sm rounded-lg bg-slate-200 hover:cursor-pointer dark:bg-gray-600"
|
||||||
disabled={disabled || !config.storageTemplate.enabled}
|
disabled={disabled || !config.storageTemplate.enabled}
|
||||||
@ -188,7 +189,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 align-bottom">
|
<div class="flex gap-2 align-bottom">
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
label="TEMPLATE"
|
label={$t('template').toUpperCase()}
|
||||||
disabled={disabled || !config.storageTemplate.enabled}
|
disabled={disabled || !config.storageTemplate.enabled}
|
||||||
required
|
required
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
@ -197,19 +198,24 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex-0">
|
<div class="flex-0">
|
||||||
<SettingInputField label="EXTENSION" inputType={SettingInputFieldType.TEXT} value={'.jpg'} disabled />
|
<SettingInputField
|
||||||
|
label={$t('extension')}
|
||||||
|
inputType={SettingInputFieldType.TEXT}
|
||||||
|
value={'.jpg'}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !minified}
|
{#if !minified}
|
||||||
<div id="migration-info" class="mt-2 text-sm">
|
<div id="migration-info" class="mt-2 text-sm">
|
||||||
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">Notes</h3>
|
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">{$t('notes')}</h3>
|
||||||
<section class="flex flex-col gap-2">
|
<section class="flex flex-col gap-2">
|
||||||
<p>
|
<p>
|
||||||
Template changes will only apply to new assets. To retroactively apply the template to previously
|
Template changes will only apply to new assets. To retroactively apply the template to previously
|
||||||
uploaded assets, run the
|
uploaded assets, run the
|
||||||
<a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary"
|
<a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary"
|
||||||
>Storage Migration Job</a
|
>{$t('admin.storage_template_migration_job')}</a
|
||||||
>.
|
>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
@ -217,7 +223,7 @@
|
|||||||
assets, so manually running the
|
assets, so manually running the
|
||||||
|
|
||||||
<a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary"
|
<a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary"
|
||||||
>Storage Migration Job</a
|
>{$t('admin.storage_template_migration_job')}</a
|
||||||
>
|
>
|
||||||
is required in order to successfully use the variable.
|
is required in order to successfully use the variable.
|
||||||
</p>
|
</p>
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import type { SystemConfigTemplateStorageOptionDto } from '@immich/sdk';
|
import type { SystemConfigTemplateStorageOptionDto } from '@immich/sdk';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let options: SystemConfigTemplateStorageOptionDto;
|
export let options: SystemConfigTemplateStorageOptionDto;
|
||||||
|
|
||||||
@ -21,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex gap-[40px]">
|
<div class="flex gap-[40px]">
|
||||||
<div>
|
<div>
|
||||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">YEAR</p>
|
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('year').toUpperCase()}</p>
|
||||||
<ul>
|
<ul>
|
||||||
{#each options.yearOptions as yearFormat}
|
{#each options.yearOptions as yearFormat}
|
||||||
<li>{'{{'}{yearFormat}{'}}'} - {getLuxonExample(yearFormat)}</li>
|
<li>{'{{'}{yearFormat}{'}}'} - {getLuxonExample(yearFormat)}</li>
|
||||||
@ -30,7 +31,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">MONTH</p>
|
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('month').toUpperCase()}</p>
|
||||||
<ul>
|
<ul>
|
||||||
{#each options.monthOptions as monthFormat}
|
{#each options.monthOptions as monthFormat}
|
||||||
<li>{'{{'}{monthFormat}{'}}'} - {getLuxonExample(monthFormat)}</li>
|
<li>{'{{'}{monthFormat}{'}}'} - {getLuxonExample(monthFormat)}</li>
|
||||||
@ -39,7 +40,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">WEEK</p>
|
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('week').toUpperCase()}</p>
|
||||||
<ul>
|
<ul>
|
||||||
{#each options.weekOptions as weekFormat}
|
{#each options.weekOptions as weekFormat}
|
||||||
<li>{'{{'}{weekFormat}{'}}'} - {getLuxonExample(weekFormat)}</li>
|
<li>{'{{'}{weekFormat}{'}}'} - {getLuxonExample(weekFormat)}</li>
|
||||||
@ -48,7 +49,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">DAY</p>
|
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('day').toUpperCase()}</p>
|
||||||
<ul>
|
<ul>
|
||||||
{#each options.dayOptions as dayFormat}
|
{#each options.dayOptions as dayFormat}
|
||||||
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
|
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
|
||||||
@ -57,7 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">HOUR</p>
|
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('hour').toUpperCase()}</p>
|
||||||
<ul>
|
<ul>
|
||||||
{#each options.hourOptions as dayFormat}
|
{#each options.hourOptions as dayFormat}
|
||||||
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
|
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
|
||||||
@ -66,7 +67,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">MINUTE</p>
|
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('minute').toUpperCase()}</p>
|
||||||
<ul>
|
<ul>
|
||||||
{#each options.minuteOptions as dayFormat}
|
{#each options.minuteOptions as dayFormat}
|
||||||
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
|
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
|
||||||
@ -75,7 +76,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">SECOND</p>
|
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('second').toUpperCase()}</p>
|
||||||
<ul>
|
<ul>
|
||||||
{#each options.secondOptions as dayFormat}
|
{#each options.secondOptions as dayFormat}
|
||||||
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
|
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
</script>
|
||||||
|
|
||||||
<div class="mt-4 text-sm">
|
<div class="mt-4 text-sm">
|
||||||
<h4>OTHER VARIABLES</h4>
|
<h4>{$t('other_variables').toUpperCase()}</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4 mt-2 text-xs bg-gray-200 rounded-lg dark:bg-gray-700 dark:text-immich-dark-fg">
|
<div class="p-4 mt-2 text-xs bg-gray-200 rounded-lg dark:bg-gray-700 dark:text-immich-dark-fg">
|
||||||
<div class="flex gap-[50px]">
|
<div class="flex gap-[50px]">
|
||||||
<div>
|
<div>
|
||||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">FILENAME</p>
|
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('filename').toUpperCase()}</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>{`{{filename}}`} - IMG_123</li>
|
<li>{`{{filename}}`} - IMG_123</li>
|
||||||
<li>{`{{ext}}`} - jpg</li>
|
<li>{`{{ext}}`} - jpg</li>
|
||||||
@ -13,14 +17,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">FILETYPE</p>
|
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('filetype').toUpperCase()}</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>{`{{filetype}}`} - VID or IMG</li>
|
<li>{`{{filetype}}`} - VID or IMG</li>
|
||||||
<li>{`{{filetypefull}}`} - VIDEO or IMAGE</li>
|
<li>{`{{filetypefull}}`} - VIDEO or IMAGE</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary uppercase">OTHER</p>
|
<p class="font-medium text-immich-primary dark:text-immich-dark-primary uppercase">{$t('other').toUpperCase()}</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>{`{{assetId}}`} - Asset ID</li>
|
<li>{`{{assetId}}`} - Asset ID</li>
|
||||||
<li>{`{{album}}`} - Album Name</li>
|
<li>{`{{album}}`} - Album Name</li>
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
import type { SettingsEventType } from '../admin-settings';
|
import type { SettingsEventType } from '../admin-settings';
|
||||||
import SettingTextarea from '$lib/components/shared-components/settings/setting-textarea.svelte';
|
import SettingTextarea from '$lib/components/shared-components/settings/setting-textarea.svelte';
|
||||||
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let savedConfig: SystemConfigDto;
|
export let savedConfig: SystemConfigDto;
|
||||||
export let defaultConfig: SystemConfigDto;
|
export let defaultConfig: SystemConfigDto;
|
||||||
@ -21,8 +22,8 @@
|
|||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingTextarea
|
<SettingTextarea
|
||||||
{disabled}
|
{disabled}
|
||||||
label="Custom CSS"
|
label={$t('admin.theme_custom_css_settings')}
|
||||||
desc="Cascading Style Sheets allow the design of Immich to be customized."
|
desc={$t('admin.theme_custom_css_settings_description')}
|
||||||
bind:value={config.theme.customCss}
|
bind:value={config.theme.customCss}
|
||||||
required={true}
|
required={true}
|
||||||
isEdited={config.theme.customCss !== savedConfig.theme.customCss}
|
isEdited={config.theme.customCss !== savedConfig.theme.customCss}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
SettingInputFieldType,
|
SettingInputFieldType,
|
||||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||||
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let savedConfig: SystemConfigDto;
|
export let savedConfig: SystemConfigDto;
|
||||||
export let defaultConfig: SystemConfigDto;
|
export let defaultConfig: SystemConfigDto;
|
||||||
@ -23,9 +24,9 @@
|
|||||||
<form autocomplete="off" on:submit|preventDefault>
|
<form autocomplete="off" on:submit|preventDefault>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="ENABLED"
|
title={$t('enabled').toUpperCase()}
|
||||||
{disabled}
|
{disabled}
|
||||||
subtitle="Enable Trash features"
|
subtitle={$t('admin.trash_enabled_description')}
|
||||||
bind:checked={config.trash.enabled}
|
bind:checked={config.trash.enabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -33,8 +34,8 @@
|
|||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
label="Number of days"
|
label={$t('admin.trash_number_of_days')}
|
||||||
desc="Number of days to keep the assets in trash before permanently removing them"
|
desc={$t('admin.trash_number_of_days_description')}
|
||||||
bind:value={config.trash.days}
|
bind:value={config.trash.days}
|
||||||
required={true}
|
required={true}
|
||||||
disabled={disabled || !config.trash.enabled}
|
disabled={disabled || !config.trash.enabled}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
import SettingInputField, {
|
import SettingInputField, {
|
||||||
SettingInputFieldType,
|
SettingInputFieldType,
|
||||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let savedConfig: SystemConfigDto;
|
export let savedConfig: SystemConfigDto;
|
||||||
export let defaultConfig: SystemConfigDto;
|
export let defaultConfig: SystemConfigDto;
|
||||||
@ -25,8 +26,8 @@
|
|||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
min={1}
|
min={1}
|
||||||
label="DELETE DELAY"
|
label={$t('admin.user_delete_delay_settings').toUpperCase()}
|
||||||
desc="Number of days after removal to permanently delete a user's account and assets. The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution."
|
desc={$t('admin.user_delete_delay_settings_description')}
|
||||||
bind:value={config.user.deleteDelay}
|
bind:value={config.user.deleteDelay}
|
||||||
isEdited={config.user.deleteDelay !== savedConfig.user.deleteDelay}
|
isEdited={config.user.deleteDelay !== savedConfig.user.deleteDelay}
|
||||||
/>
|
/>
|
||||||
|
@ -2,6 +2,7 @@ import { sdkMock } from '$lib/__mocks__/sdk.mock';
|
|||||||
import { albumFactory } from '@test-data';
|
import { albumFactory } from '@test-data';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { fireEvent, render, waitFor, type RenderResult } from '@testing-library/svelte';
|
import { fireEvent, render, waitFor, type RenderResult } from '@testing-library/svelte';
|
||||||
|
import { init, register, waitLocale } from 'svelte-i18n';
|
||||||
import AlbumCard from '../album-card.svelte';
|
import AlbumCard from '../album-card.svelte';
|
||||||
|
|
||||||
const onShowContextMenu = vi.fn();
|
const onShowContextMenu = vi.fn();
|
||||||
@ -9,6 +10,12 @@ const onShowContextMenu = vi.fn();
|
|||||||
describe('AlbumCard component', () => {
|
describe('AlbumCard component', () => {
|
||||||
let sut: RenderResult<AlbumCard>;
|
let sut: RenderResult<AlbumCard>;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await init({ fallbackLocale: 'en-US' });
|
||||||
|
register('en-US', () => import('$lib/i18n/en-US.json'));
|
||||||
|
await waitLocale('en-US');
|
||||||
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
{
|
{
|
||||||
album: albumFactory.build({ albumThumbnailAssetId: null, shared: false, assetCount: 0 }),
|
album: albumFactory.build({ albumThumbnailAssetId: null, shared: false, assetCount: 0 }),
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
import AlbumCover from '$lib/components/album-page/album-cover.svelte';
|
import AlbumCover from '$lib/components/album-page/album-cover.svelte';
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
import { s } from '$lib/utils';
|
import { s } from '$lib/utils';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let album: AlbumResponseDto;
|
export let album: AlbumResponseDto;
|
||||||
export let showOwner = false;
|
export let showOwner = false;
|
||||||
@ -35,7 +36,7 @@
|
|||||||
>
|
>
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
color="opaque"
|
color="opaque"
|
||||||
title="Show album options"
|
title={$t('show_album_options')}
|
||||||
icon={mdiDotsVertical}
|
icon={mdiDotsVertical}
|
||||||
size="20"
|
size="20"
|
||||||
padding="2"
|
padding="2"
|
||||||
@ -76,14 +77,14 @@
|
|||||||
|
|
||||||
{#if showOwner}
|
{#if showOwner}
|
||||||
{#if $user.id === album.ownerId}
|
{#if $user.id === album.ownerId}
|
||||||
<p>Owned</p>
|
<p>{$t('owned')}</p>
|
||||||
{:else if album.owner}
|
{:else if album.owner}
|
||||||
<p>Shared by {album.owner.name}</p>
|
<p>Shared by {album.owner.name}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p>Shared</p>
|
<p>{$t('shared')}</p>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if album.shared}
|
{:else if album.shared}
|
||||||
<p>Shared</p>
|
<p>{$t('shared')}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getAssetThumbnailUrl } from '$lib/utils';
|
import { getAssetThumbnailUrl } from '$lib/utils';
|
||||||
import { type AlbumResponseDto } from '@immich/sdk';
|
import { type AlbumResponseDto } from '@immich/sdk';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let album: AlbumResponseDto | undefined;
|
export let album: AlbumResponseDto | undefined;
|
||||||
export let preload = false;
|
export let preload = false;
|
||||||
@ -15,7 +16,7 @@
|
|||||||
<img
|
<img
|
||||||
loading={preload ? 'eager' : 'lazy'}
|
loading={preload ? 'eager' : 'lazy'}
|
||||||
src={thumbnailUrl}
|
src={thumbnailUrl}
|
||||||
alt={album?.albumName ?? 'Unknown Album'}
|
alt={album?.albumName ?? $t('unknown_album')}
|
||||||
class="z-0 rounded-xl object-cover {css}"
|
class="z-0 rounded-xl object-cover {css}"
|
||||||
data-testid="album-image"
|
data-testid="album-image"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
@ -25,7 +26,7 @@
|
|||||||
loading={preload ? 'eager' : 'lazy'}
|
loading={preload ? 'eager' : 'lazy'}
|
||||||
src="$lib/assets/no-thumbnail.png"
|
src="$lib/assets/no-thumbnail.png"
|
||||||
sizes="min(271px,186px)"
|
sizes="min(271px,186px)"
|
||||||
alt={album?.albumName ?? 'Empty Album'}
|
alt={album?.albumName ?? $t('empty_album')}
|
||||||
class="z-0 rounded-xl object-cover {css}"
|
class="z-0 rounded-xl object-cover {css}"
|
||||||
data-testid="album-image"
|
data-testid="album-image"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
import AlbumDescription from '$lib/components/album-page/album-description.svelte';
|
import AlbumDescription from '$lib/components/album-page/album-description.svelte';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { render, screen } from '@testing-library/svelte';
|
import { render, screen } from '@testing-library/svelte';
|
||||||
|
import { init } from 'svelte-i18n';
|
||||||
import { describe } from 'vitest';
|
import { describe } from 'vitest';
|
||||||
|
|
||||||
describe('AlbumDescription component', () => {
|
describe('AlbumDescription component', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await init({ fallbackLocale: 'en-US' });
|
||||||
|
});
|
||||||
|
|
||||||
it('shows an AutogrowTextarea component when isOwned is true', () => {
|
it('shows an AutogrowTextarea component when isOwned is true', () => {
|
||||||
render(AlbumDescription, { isOwned: true, id: '', description: '' });
|
render(AlbumDescription, { isOwned: true, id: '', description: '' });
|
||||||
const autogrowTextarea = screen.getByTestId('autogrow-textarea');
|
const autogrowTextarea = screen.getByTestId('autogrow-textarea');
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import { updateAlbumInfo } from '@immich/sdk';
|
import { updateAlbumInfo } from '@immich/sdk';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import AutogrowTextarea from '$lib/components/shared-components/autogrow-textarea.svelte';
|
import AutogrowTextarea from '$lib/components/shared-components/autogrow-textarea.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let id: string;
|
export let id: string;
|
||||||
export let description: string;
|
export let description: string;
|
||||||
@ -16,7 +17,7 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Error updating album description');
|
handleError(error, $t('errors.unable_to_save_album'));
|
||||||
}
|
}
|
||||||
description = newDescription;
|
description = newDescription;
|
||||||
};
|
};
|
||||||
@ -27,7 +28,7 @@
|
|||||||
content={description}
|
content={description}
|
||||||
class="w-full mt-2 text-black dark:text-white border-b-2 border-transparent border-gray-500 bg-transparent text-base outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:focus:border-immich-dark-primary hover:border-gray-400"
|
class="w-full mt-2 text-black dark:text-white border-b-2 border-transparent border-gray-500 bg-transparent text-base outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:focus:border-immich-dark-primary hover:border-gray-400"
|
||||||
onContentUpdate={handleUpdateDescription}
|
onContentUpdate={handleUpdateDescription}
|
||||||
placeholder="Add a description"
|
placeholder={$t('add_a_description')}
|
||||||
/>
|
/>
|
||||||
{:else if description}
|
{:else if description}
|
||||||
<p class="break-words whitespace-pre-line w-full text-black dark:text-white text-base">
|
<p class="break-words whitespace-pre-line w-full text-black dark:text-white text-base">
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
import type { RenderedOption } from '../elements/dropdown.svelte';
|
import type { RenderedOption } from '../elements/dropdown.svelte';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { findKey } from 'lodash-es';
|
import { findKey } from 'lodash-es';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let album: AlbumResponseDto;
|
export let album: AlbumResponseDto;
|
||||||
export let order: AssetOrder | undefined;
|
export let order: AssetOrder | undefined;
|
||||||
@ -17,8 +18,8 @@
|
|||||||
export let onChangeOrder: (order: AssetOrder) => void;
|
export let onChangeOrder: (order: AssetOrder) => void;
|
||||||
|
|
||||||
const options: Record<AssetOrder, RenderedOption> = {
|
const options: Record<AssetOrder, RenderedOption> = {
|
||||||
[AssetOrder.Asc]: { icon: mdiArrowUpThin, title: 'Oldest first' },
|
[AssetOrder.Asc]: { icon: mdiArrowUpThin, title: $t('oldest_first') },
|
||||||
[AssetOrder.Desc]: { icon: mdiArrowDownThin, title: 'Newest first' },
|
[AssetOrder.Desc]: { icon: mdiArrowDownThin, title: $t('newest_first') },
|
||||||
};
|
};
|
||||||
|
|
||||||
$: selectedOption = order ? options[order] : options[AssetOrder.Desc];
|
$: selectedOption = order ? options[order] : options[AssetOrder.Desc];
|
||||||
@ -45,19 +46,19 @@
|
|||||||
});
|
});
|
||||||
onChangeOrder(order);
|
onChangeOrder(order);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Error updating album order');
|
handleError(error, $t('errors.unable_to_save_album'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FullScreenModal title="Options" onClose={() => dispatch('close')}>
|
<FullScreenModal title={$t('options')} onClose={() => dispatch('close')}>
|
||||||
<div class="items-center justify-center">
|
<div class="items-center justify-center">
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<h2 class="text-gray text-sm mb-2">SETTINGS</h2>
|
<h2 class="text-gray text-sm mb-2">{$t('settings').toUpperCase()}</h2>
|
||||||
<div class="grid p-2 gap-y-2">
|
<div class="grid p-2 gap-y-2">
|
||||||
{#if order}
|
{#if order}
|
||||||
<SettingDropdown
|
<SettingDropdown
|
||||||
title="Display order"
|
title={$t('display_order')}
|
||||||
options={Object.values(options)}
|
options={Object.values(options)}
|
||||||
selectedOption={options[order]}
|
selectedOption={options[order]}
|
||||||
onToggle={handleToggle}
|
onToggle={handleToggle}
|
||||||
@ -65,27 +66,27 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="Comments & likes"
|
title="Comments & likes"
|
||||||
subtitle="Let others respond"
|
subtitle={$t('let_others_respond')}
|
||||||
checked={album.isActivityEnabled}
|
checked={album.isActivityEnabled}
|
||||||
on:toggle={() => dispatch('toggleEnableActivity')}
|
on:toggle={() => dispatch('toggleEnableActivity')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<div class="text-gray text-sm mb-3">PEOPLE</div>
|
<div class="text-gray text-sm mb-3">{$t('people').toUpperCase()}</div>
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<button type="button" class="flex items-center gap-2" on:click={() => dispatch('showSelectSharedUser')}>
|
<button type="button" class="flex items-center gap-2" on:click={() => dispatch('showSelectSharedUser')}>
|
||||||
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
|
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
|
||||||
<div><Icon path={mdiPlus} size="25" /></div>
|
<div><Icon path={mdiPlus} size="25" /></div>
|
||||||
</div>
|
</div>
|
||||||
<div>Invite People</div>
|
<div>{$t('invite_people')}</div>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex items-center gap-2 py-2 mt-2">
|
<div class="flex items-center gap-2 py-2 mt-2">
|
||||||
<div>
|
<div>
|
||||||
<UserAvatar {user} size="md" />
|
<UserAvatar {user} size="md" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full">{user.name}</div>
|
<div class="w-full">{user.name}</div>
|
||||||
<div>Owner</div>
|
<div>{$t('owner')}</div>
|
||||||
</div>
|
</div>
|
||||||
{#each album.albumUsers as { user } (user.id)}
|
{#each album.albumUsers as { user } (user.id)}
|
||||||
<div class="flex items-center gap-2 py-2">
|
<div class="flex items-center gap-2 py-2">
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import { updateAlbumInfo } from '@immich/sdk';
|
import { updateAlbumInfo } from '@immich/sdk';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { shortcut } from '$lib/actions/shortcut';
|
import { shortcut } from '$lib/actions/shortcut';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let id: string;
|
export let id: string;
|
||||||
export let albumName: string;
|
export let albumName: string;
|
||||||
@ -22,7 +23,7 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to update album name');
|
handleError(error, $t('errors.unable_to_save_album'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
albumName = newAlbumName;
|
albumName = newAlbumName;
|
||||||
@ -38,6 +39,6 @@
|
|||||||
type="text"
|
type="text"
|
||||||
bind:value={newAlbumName}
|
bind:value={newAlbumName}
|
||||||
disabled={!isOwned}
|
disabled={!isOwned}
|
||||||
title="Edit Title"
|
title={$t('edit_title')}
|
||||||
placeholder="Add a title"
|
placeholder={$t('add_a_title')}
|
||||||
/>
|
/>
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js';
|
import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import AlbumSummary from './album-summary.svelte';
|
import AlbumSummary from './album-summary.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let sharedLink: SharedLinkResponseDto;
|
export let sharedLink: SharedLinkResponseDto;
|
||||||
export let user: UserResponseDto | undefined = undefined;
|
export let user: UserResponseDto | undefined = undefined;
|
||||||
@ -72,14 +73,18 @@
|
|||||||
<svelte:fragment slot="trailing">
|
<svelte:fragment slot="trailing">
|
||||||
{#if sharedLink.allowUpload}
|
{#if sharedLink.allowUpload}
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
title="Add Photos"
|
title={$t('add_photos')}
|
||||||
on:click={() => openFileUploadDialog({ albumId: album.id })}
|
on:click={() => openFileUploadDialog({ albumId: album.id })}
|
||||||
icon={mdiFileImagePlusOutline}
|
icon={mdiFileImagePlusOutline}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if album.assetCount > 0 && sharedLink.allowDownload}
|
{#if album.assetCount > 0 && sharedLink.allowDownload}
|
||||||
<CircleIconButton title="Download" on:click={() => downloadAlbum(album)} icon={mdiFolderDownloadOutline} />
|
<CircleIconButton
|
||||||
|
title={$t('download')}
|
||||||
|
on:click={() => downloadAlbum(album)}
|
||||||
|
icon={mdiFolderDownloadOutline}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<ThemeButton />
|
<ThemeButton />
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
import GroupTab from '$lib/components/elements/group-tab.svelte';
|
import GroupTab from '$lib/components/elements/group-tab.svelte';
|
||||||
import { createAlbumAndRedirect, collapseAllAlbumGroups, expandAllAlbumGroups } from '$lib/utils/album-utils';
|
import { createAlbumAndRedirect, collapseAllAlbumGroups, expandAllAlbumGroups } from '$lib/utils/album-utils';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let albumGroups: string[];
|
export let albumGroups: string[];
|
||||||
export let searchQuery: string;
|
export let searchQuery: string;
|
||||||
@ -100,20 +101,20 @@
|
|||||||
|
|
||||||
<!-- Search Albums -->
|
<!-- Search Albums -->
|
||||||
<div class="hidden xl:block h-10 xl:w-60 2xl:w-80">
|
<div class="hidden xl:block h-10 xl:w-60 2xl:w-80">
|
||||||
<SearchBar placeholder="Search albums" bind:name={searchQuery} showLoadingSpinner={false} />
|
<SearchBar placeholder={$t('search_albums')} bind:name={searchQuery} showLoadingSpinner={false} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Create Album -->
|
<!-- Create Album -->
|
||||||
<LinkButton on:click={() => createAlbumAndRedirect()}>
|
<LinkButton on:click={() => createAlbumAndRedirect()}>
|
||||||
<div class="flex place-items-center gap-2 text-sm">
|
<div class="flex place-items-center gap-2 text-sm">
|
||||||
<Icon path={mdiPlusBoxOutline} size="18" />
|
<Icon path={mdiPlusBoxOutline} size="18" />
|
||||||
<p class="hidden md:block">Create album</p>
|
<p class="hidden md:block">{$t('create_album')}</p>
|
||||||
</div>
|
</div>
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
|
|
||||||
<!-- Sort Albums -->
|
<!-- Sort Albums -->
|
||||||
<Dropdown
|
<Dropdown
|
||||||
title="Sort albums by..."
|
title={$t('sort_albums_by')}
|
||||||
options={Object.values(sortOptionsMetadata)}
|
options={Object.values(sortOptionsMetadata)}
|
||||||
selectedOption={selectedSortOption}
|
selectedOption={selectedSortOption}
|
||||||
on:select={({ detail }) => handleChangeSortBy(detail)}
|
on:select={({ detail }) => handleChangeSortBy(detail)}
|
||||||
@ -125,7 +126,7 @@
|
|||||||
|
|
||||||
<!-- Group Albums -->
|
<!-- Group Albums -->
|
||||||
<Dropdown
|
<Dropdown
|
||||||
title="Group albums by..."
|
title={$t('group_albums_by')}
|
||||||
options={Object.values(groupOptionsMetadata)}
|
options={Object.values(groupOptionsMetadata)}
|
||||||
selectedOption={selectedGroupOption}
|
selectedOption={selectedGroupOption}
|
||||||
on:select={({ detail }) => handleChangeGroupBy(detail)}
|
on:select={({ detail }) => handleChangeGroupBy(detail)}
|
||||||
@ -141,7 +142,7 @@
|
|||||||
<!-- Expand Album Groups -->
|
<!-- Expand Album Groups -->
|
||||||
<div class="hidden xl:flex gap-0">
|
<div class="hidden xl:flex gap-0">
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<LinkButton title="Expand all" on:click={() => expandAllAlbumGroups()}>
|
<LinkButton title={$t('expand_all')} on:click={() => expandAllAlbumGroups()}>
|
||||||
<div class="flex place-items-center gap-2 text-sm">
|
<div class="flex place-items-center gap-2 text-sm">
|
||||||
<Icon path={mdiUnfoldMoreHorizontal} size="18" />
|
<Icon path={mdiUnfoldMoreHorizontal} size="18" />
|
||||||
</div>
|
</div>
|
||||||
@ -150,7 +151,7 @@
|
|||||||
|
|
||||||
<!-- Collapse Album Groups -->
|
<!-- Collapse Album Groups -->
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<LinkButton title="Collapse all" on:click={() => collapseAllAlbumGroups(albumGroups)}>
|
<LinkButton title={$t('collapse_all')} on:click={() => collapseAllAlbumGroups(albumGroups)}>
|
||||||
<div class="flex place-items-center gap-2 text-sm">
|
<div class="flex place-items-center gap-2 text-sm">
|
||||||
<Icon path={mdiUnfoldLessHorizontal} size="18" />
|
<Icon path={mdiUnfoldLessHorizontal} size="18" />
|
||||||
</div>
|
</div>
|
||||||
@ -165,10 +166,10 @@
|
|||||||
<div class="flex place-items-center gap-2 text-sm">
|
<div class="flex place-items-center gap-2 text-sm">
|
||||||
{#if $albumViewSettings.view === AlbumViewMode.List}
|
{#if $albumViewSettings.view === AlbumViewMode.List}
|
||||||
<Icon path={mdiViewGridOutline} size="18" />
|
<Icon path={mdiViewGridOutline} size="18" />
|
||||||
<p class="hidden md:block">Covers</p>
|
<p class="hidden md:block">{$t('covers')}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<Icon path={mdiFormatListBulletedSquare} size="18" />
|
<Icon path={mdiFormatListBulletedSquare} size="18" />
|
||||||
<p class="hidden md:block">List</p>
|
<p class="hidden md:block">{$t('list')}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let ownedAlbums: AlbumResponseDto[] = [];
|
export let ownedAlbums: AlbumResponseDto[] = [];
|
||||||
export let sharedAlbums: AlbumResponseDto[] = [];
|
export let sharedAlbums: AlbumResponseDto[] = [];
|
||||||
@ -55,8 +56,8 @@
|
|||||||
[AlbumGroupBy.None]: (order, albums): AlbumGroup[] => {
|
[AlbumGroupBy.None]: (order, albums): AlbumGroup[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'Albums',
|
id: $t('albums'),
|
||||||
name: 'Albums',
|
name: $t('albums'),
|
||||||
albums,
|
albums,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -64,7 +65,7 @@
|
|||||||
|
|
||||||
/** Group by year */
|
/** Group by year */
|
||||||
[AlbumGroupBy.Year]: (order, albums): AlbumGroup[] => {
|
[AlbumGroupBy.Year]: (order, albums): AlbumGroup[] => {
|
||||||
const unknownYear = 'Unknown Year';
|
const unknownYear = $t('unknown_year');
|
||||||
const useStartDate = userSettings.sortBy === AlbumSortBy.OldestPhoto;
|
const useStartDate = userSettings.sortBy === AlbumSortBy.OldestPhoto;
|
||||||
|
|
||||||
const groupedByYear = groupBy(albums, (album) => {
|
const groupedByYear = groupBy(albums, (album) => {
|
||||||
@ -111,7 +112,7 @@
|
|||||||
|
|
||||||
return sortedByOwnerNames.map(([ownerId, albums]) => ({
|
return sortedByOwnerNames.map(([ownerId, albums]) => ({
|
||||||
id: ownerId,
|
id: ownerId,
|
||||||
name: ownerId === currentUserId ? 'My albums' : albums[0].owner.name,
|
name: ownerId === currentUserId ? $t('my_albums') : albums[0].owner.name,
|
||||||
albums,
|
albums,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
@ -314,7 +315,7 @@
|
|||||||
await handleDeleteAlbum(albumToDelete);
|
await handleDeleteAlbum(albumToDelete);
|
||||||
} catch {
|
} catch {
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Error deleting album',
|
message: $t('errors.errors.unable_to_delete_album'),
|
||||||
type: NotificationType.Error,
|
type: NotificationType.Error,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
@ -336,7 +337,7 @@
|
|||||||
albumToEdit = null;
|
albumToEdit = null;
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Album info updated',
|
message: $t('album_info_updated'),
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
button: {
|
button: {
|
||||||
text: 'View Album',
|
text: 'View Album',
|
||||||
@ -362,7 +363,7 @@
|
|||||||
});
|
});
|
||||||
updateAlbumInfo(album);
|
updateAlbumInfo(album);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Error adding users to album');
|
handleError(error, $t('errors.unable_to_add_album_users'));
|
||||||
} finally {
|
} finally {
|
||||||
albumToShare = null;
|
albumToShare = null;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { mdiShareVariantOutline } from '@mdi/js';
|
import { mdiShareVariantOutline } from '@mdi/js';
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let album: AlbumResponseDto;
|
export let album: AlbumResponseDto;
|
||||||
export let onShowContextMenu: ((position: ContextMenuPosition, album: AlbumResponseDto) => unknown) | undefined =
|
export let onShowContextMenu: ((position: ContextMenuPosition, album: AlbumResponseDto) => unknown) | undefined =
|
||||||
@ -33,7 +34,7 @@
|
|||||||
path={mdiShareVariantOutline}
|
path={mdiShareVariantOutline}
|
||||||
size="16"
|
size="16"
|
||||||
class="inline ml-1 opacity-70"
|
class="inline ml-1 opacity-70"
|
||||||
title={album.ownerId === $user.id ? 'Shared by you' : `Shared by ${album.owner.name}`}
|
title={album.ownerId === $user.id ? $t('shared_by_you') : `Shared by ${album.owner.name}`}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||||
import UserAvatar from '../shared-components/user-avatar.svelte';
|
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let album: AlbumResponseDto;
|
export let album: AlbumResponseDto;
|
||||||
export let onClose: () => void;
|
export let onClose: () => void;
|
||||||
@ -38,7 +39,7 @@
|
|||||||
try {
|
try {
|
||||||
currentUser = await getMyUser();
|
currentUser = await getMyUser();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to refresh user');
|
handleError(error, $t('errors.unable_to_refresh_user'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -66,7 +67,7 @@
|
|||||||
const message = userId === 'me' ? `Left ${album.albumName}` : `Removed ${selectedRemoveUser.name}`;
|
const message = userId === 'me' ? `Left ${album.albumName}` : `Removed ${selectedRemoveUser.name}`;
|
||||||
notificationController.show({ type: NotificationType.Info, message });
|
notificationController.show({ type: NotificationType.Info, message });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to remove user');
|
handleError(error, $t('errors.unable_to_remove_album_users'));
|
||||||
} finally {
|
} finally {
|
||||||
selectedRemoveUser = null;
|
selectedRemoveUser = null;
|
||||||
}
|
}
|
||||||
@ -79,7 +80,7 @@
|
|||||||
dispatch('refreshAlbum');
|
dispatch('refreshAlbum');
|
||||||
notificationController.show({ type: NotificationType.Info, message });
|
notificationController.show({ type: NotificationType.Info, message });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to set user role');
|
handleError(error, $t('errors.unable_to_change_album_user_role'));
|
||||||
} finally {
|
} finally {
|
||||||
selectedRemoveUser = null;
|
selectedRemoveUser = null;
|
||||||
}
|
}
|
||||||
@ -87,7 +88,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !selectedRemoveUser}
|
{#if !selectedRemoveUser}
|
||||||
<FullScreenModal title="Options" {onClose}>
|
<FullScreenModal title={$t('options')} {onClose}>
|
||||||
<section class="immich-scrollbar max-h-[400px] overflow-y-auto pb-4">
|
<section class="immich-scrollbar max-h-[400px] overflow-y-auto pb-4">
|
||||||
<div class="flex w-full place-items-center justify-between gap-4 p-5">
|
<div class="flex w-full place-items-center justify-between gap-4 p-5">
|
||||||
<div class="flex place-items-center gap-4">
|
<div class="flex place-items-center gap-4">
|
||||||
@ -96,7 +97,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="icon-{album.owner.id}" class="flex place-items-center">
|
<div id="icon-{album.owner.id}" class="flex place-items-center">
|
||||||
<p class="text-sm">Owner</p>
|
<p class="text-sm">{$t('owner')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#each album.albumUsers as { user, role }}
|
{#each album.albumUsers as { user, role }}
|
||||||
@ -119,7 +120,7 @@
|
|||||||
{#if isOwned}
|
{#if isOwned}
|
||||||
<div>
|
<div>
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
title="Options"
|
title={$t('options')}
|
||||||
on:click={(event) => showContextMenu(event, user)}
|
on:click={(event) => showContextMenu(event, user)}
|
||||||
icon={mdiDotsVertical}
|
icon={mdiDotsVertical}
|
||||||
size="20"
|
size="20"
|
||||||
@ -128,14 +129,17 @@
|
|||||||
{#if selectedMenuUser === user}
|
{#if selectedMenuUser === user}
|
||||||
<ContextMenu {...position} onClose={() => (selectedMenuUser = null)}>
|
<ContextMenu {...position} onClose={() => (selectedMenuUser = null)}>
|
||||||
{#if role === AlbumUserRole.Viewer}
|
{#if role === AlbumUserRole.Viewer}
|
||||||
<MenuOption on:click={() => handleSetReadonly(user, AlbumUserRole.Editor)} text="Allow edits" />
|
<MenuOption
|
||||||
|
on:click={() => handleSetReadonly(user, AlbumUserRole.Editor)}
|
||||||
|
text={$t('allow_edits')}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<MenuOption
|
<MenuOption
|
||||||
on:click={() => handleSetReadonly(user, AlbumUserRole.Viewer)}
|
on:click={() => handleSetReadonly(user, AlbumUserRole.Viewer)}
|
||||||
text="Disallow edits"
|
text={$t('disallow_edits')}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<MenuOption on:click={handleMenuRemove} text="Remove" />
|
<MenuOption on:click={handleMenuRemove} text={$t('remove')} />
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@ -144,7 +148,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
on:click={() => (selectedRemoveUser = user)}
|
on:click={() => (selectedRemoveUser = user)}
|
||||||
class="text-sm font-medium text-immich-primary transition-colors hover:text-immich-primary/75 dark:text-immich-dark-primary"
|
class="text-sm font-medium text-immich-primary transition-colors hover:text-immich-primary/75 dark:text-immich-dark-primary"
|
||||||
>Leave</button
|
>{$t('leave')}</button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@ -158,7 +162,7 @@
|
|||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
title="Leave album?"
|
title="Leave album?"
|
||||||
prompt="Are you sure you want to leave {album.albumName}?"
|
prompt="Are you sure you want to leave {album.albumName}?"
|
||||||
confirmText="Leave"
|
confirmText={$t('leave')}
|
||||||
onConfirm={handleRemoveUser}
|
onConfirm={handleRemoveUser}
|
||||||
onCancel={() => (selectedRemoveUser = null)}
|
onCancel={() => (selectedRemoveUser = null)}
|
||||||
/>
|
/>
|
||||||
@ -168,7 +172,7 @@
|
|||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
title="Remove user?"
|
title="Remove user?"
|
||||||
prompt="Are you sure you want to remove {selectedRemoveUser.name}?"
|
prompt="Are you sure you want to remove {selectedRemoveUser.name}?"
|
||||||
confirmText="Remove"
|
confirmText={$t('remove')}
|
||||||
onConfirm={handleRemoveUser}
|
onConfirm={handleRemoveUser}
|
||||||
onCancel={() => (selectedRemoveUser = null)}
|
onCancel={() => (selectedRemoveUser = null)}
|
||||||
/>
|
/>
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
|
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let album: AlbumResponseDto;
|
export let album: AlbumResponseDto;
|
||||||
|
|
||||||
@ -26,7 +27,7 @@
|
|||||||
>
|
>
|
||||||
<ControlAppBar on:close={() => dispatch('close')}>
|
<ControlAppBar on:close={() => dispatch('close')}>
|
||||||
<svelte:fragment slot="leading">
|
<svelte:fragment slot="leading">
|
||||||
<p class="text-lg">Select album cover</p>
|
<p class="text-lg">{$t('select_album_cover')}</p>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
<svelte:fragment slot="trailing">
|
<svelte:fragment slot="trailing">
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
import { createEventDispatcher, onMount } from 'svelte';
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
import UserAvatar from '../shared-components/user-avatar.svelte';
|
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let album: AlbumResponseDto;
|
export let album: AlbumResponseDto;
|
||||||
export let onClose: () => void;
|
export let onClose: () => void;
|
||||||
@ -23,9 +24,9 @@
|
|||||||
let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = {};
|
let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = {};
|
||||||
|
|
||||||
const roleOptions: Array<{ title: string; value: AlbumUserRole | 'none'; icon?: string }> = [
|
const roleOptions: Array<{ title: string; value: AlbumUserRole | 'none'; icon?: string }> = [
|
||||||
{ title: 'Editor', value: AlbumUserRole.Editor, icon: mdiPencil },
|
{ title: $t('editor'), value: AlbumUserRole.Editor, icon: mdiPencil },
|
||||||
{ title: 'Viewer', value: AlbumUserRole.Viewer, icon: mdiEye },
|
{ title: $t('viewer'), value: AlbumUserRole.Viewer, icon: mdiEye },
|
||||||
{ title: 'Remove', value: 'none' },
|
{ title: $t('remove'), value: 'none' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
@ -70,10 +71,10 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FullScreenModal title="Invite to album" showLogo {onClose}>
|
<FullScreenModal title={$t('invite_to_album')} showLogo {onClose}>
|
||||||
{#if Object.keys(selectedUsers).length > 0}
|
{#if Object.keys(selectedUsers).length > 0}
|
||||||
<div class="mb-2 py-2 sticky">
|
<div class="mb-2 py-2 sticky">
|
||||||
<p class="text-xs font-medium">SELECTED</p>
|
<p class="text-xs font-medium">{$t('selected').toUpperCase()}</p>
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
{#each Object.values(selectedUsers) as { user }}
|
{#each Object.values(selectedUsers) as { user }}
|
||||||
{#key user.id}
|
{#key user.id}
|
||||||
@ -95,7 +96,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
title="Role"
|
title={$t('role')}
|
||||||
options={roleOptions}
|
options={roleOptions}
|
||||||
render={({ title, icon }) => ({ title, icon })}
|
render={({ title, icon }) => ({ title, icon })}
|
||||||
on:select={({ detail: { value } }) => handleChangeRole(user, value)}
|
on:select={({ detail: { value } }) => handleChangeRole(user, value)}
|
||||||
@ -115,7 +116,7 @@
|
|||||||
|
|
||||||
<div class="immich-scrollbar max-h-[500px] overflow-y-auto">
|
<div class="immich-scrollbar max-h-[500px] overflow-y-auto">
|
||||||
{#if users.length > 0 && users.length !== Object.keys(selectedUsers).length}
|
{#if users.length > 0 && users.length !== Object.keys(selectedUsers).length}
|
||||||
<p class="text-xs font-medium">SUGGESTIONS</p>
|
<p class="text-xs font-medium">{$t('suggestions').toUpperCase()}</p>
|
||||||
|
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
{#each users as user}
|
{#each users as user}
|
||||||
@ -154,7 +155,7 @@
|
|||||||
dispatch(
|
dispatch(
|
||||||
'select',
|
'select',
|
||||||
Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })),
|
Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })),
|
||||||
)}>Add</Button
|
)}>{$t('add')}</Button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -168,7 +169,7 @@
|
|||||||
on:click={() => dispatch('share')}
|
on:click={() => dispatch('share')}
|
||||||
>
|
>
|
||||||
<Icon path={mdiLink} size={24} />
|
<Icon path={mdiLink} size={24} />
|
||||||
<p class="text-sm">Create link</p>
|
<p class="text-sm">{$t('create_link')}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if sharedLinks.length}
|
{#if sharedLinks.length}
|
||||||
@ -177,7 +178,7 @@
|
|||||||
class="flex flex-col place-content-center place-items-center gap-2 hover:cursor-pointer"
|
class="flex flex-col place-content-center place-items-center gap-2 hover:cursor-pointer"
|
||||||
>
|
>
|
||||||
<Icon path={mdiShareCircle} size={24} />
|
<Icon path={mdiShareCircle} size={24} />
|
||||||
<p class="text-sm">View links</p>
|
<p class="text-sm">{$t('view_links')}</p>
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
import { mdiCommentOutline, mdiHeart, mdiHeartOutline } from '@mdi/js';
|
import { mdiCommentOutline, mdiHeart, mdiHeartOutline } from '@mdi/js';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import Icon from '../elements/icon.svelte';
|
import Icon from '../elements/icon.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let isLiked: ActivityResponseDto | null;
|
export let isLiked: ActivityResponseDto | null;
|
||||||
export let numberOfComments: number | undefined;
|
export let numberOfComments: number | undefined;
|
||||||
@ -29,7 +30,7 @@
|
|||||||
{#if numberOfComments}
|
{#if numberOfComments}
|
||||||
<div class="text-xl">{numberOfComments}</div>
|
<div class="text-xl">{numberOfComments}</div>
|
||||||
{:else if !isShowActivity}
|
{:else if !isShowActivity}
|
||||||
<div class="text-lg">Say something</div>
|
<div class="text-lg">{$t('say_something')}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
import UserAvatar from '../shared-components/user-avatar.svelte';
|
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { shortcut } from '$lib/actions/shortcut';
|
import { shortcut } from '$lib/actions/shortcut';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
const units: Intl.RelativeTimeFormatUnit[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'];
|
const units: Intl.RelativeTimeFormatUnit[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'];
|
||||||
|
|
||||||
@ -91,7 +92,7 @@
|
|||||||
try {
|
try {
|
||||||
reactions = await getActivities({ assetId, albumId });
|
reactions = await getActivities({ assetId, albumId });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Error when fetching reactions');
|
handleError(error, $t('errors.unable_to_load_asset_activity'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -120,7 +121,7 @@
|
|||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Can't remove ${reaction.type}`);
|
handleError(error, $t('errors.unable_to_remove_reaction'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -140,7 +141,7 @@
|
|||||||
// Re-render the activity feed
|
// Re-render the activity feed
|
||||||
reactions = reactions;
|
reactions = reactions;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "Can't add your comment");
|
handleError(error, $t('errors.unable_to_add_comment'));
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
}
|
}
|
||||||
@ -159,9 +160,9 @@
|
|||||||
bind:clientHeight={activityHeight}
|
bind:clientHeight={activityHeight}
|
||||||
>
|
>
|
||||||
<div class="flex place-items-center gap-2">
|
<div class="flex place-items-center gap-2">
|
||||||
<CircleIconButton on:click={() => dispatch('close')} icon={mdiClose} title="Close" />
|
<CircleIconButton on:click={() => dispatch('close')} icon={mdiClose} title={$t('close')} />
|
||||||
|
|
||||||
<p class="text-lg text-immich-fg dark:text-immich-dark-fg">Activity</p>
|
<p class="text-lg text-immich-fg dark:text-immich-dark-fg">{$t('activity')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if innerHeight}
|
{#if innerHeight}
|
||||||
@ -190,7 +191,7 @@
|
|||||||
<div class="flex items-start w-fit pt-[5px]">
|
<div class="flex items-start w-fit pt-[5px]">
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
icon={mdiDotsVertical}
|
icon={mdiDotsVertical}
|
||||||
title="Comment options"
|
title={$t('comment_options')}
|
||||||
size="16"
|
size="16"
|
||||||
on:click={() => (showDeleteReaction[index] ? '' : showOptionsMenu(index))}
|
on:click={() => (showDeleteReaction[index] ? '' : showOptionsMenu(index))}
|
||||||
/>
|
/>
|
||||||
@ -242,7 +243,7 @@
|
|||||||
<div class="flex items-start w-fit">
|
<div class="flex items-start w-fit">
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
icon={mdiDotsVertical}
|
icon={mdiDotsVertical}
|
||||||
title="Reaction options"
|
title={$t('reaction_options')}
|
||||||
size="16"
|
size="16"
|
||||||
on:click={() => (showDeleteReaction[index] ? '' : showOptionsMenu(index))}
|
on:click={() => (showDeleteReaction[index] ? '' : showOptionsMenu(index))}
|
||||||
/>
|
/>
|
||||||
@ -289,7 +290,7 @@
|
|||||||
bind:this={textArea}
|
bind:this={textArea}
|
||||||
bind:value={message}
|
bind:value={message}
|
||||||
use:autoGrowHeight={'5px'}
|
use:autoGrowHeight={'5px'}
|
||||||
placeholder={disabled ? 'Comments are disabled' : 'Say something'}
|
placeholder={disabled ? $t('comments_are_disabled') : $t('say_something')}
|
||||||
on:input={() => autoGrowHeight(textArea, '5px')}
|
on:input={() => autoGrowHeight(textArea, '5px')}
|
||||||
use:shortcut={{
|
use:shortcut={{
|
||||||
shortcut: { key: 'Enter' },
|
shortcut: { key: 'Enter' },
|
||||||
@ -308,7 +309,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else if message}
|
{:else if message}
|
||||||
<div class="flex items-end w-fit ml-0">
|
<div class="flex items-end w-fit ml-0">
|
||||||
<CircleIconButton title="Send message" size="15" icon={mdiSend} class="dark:text-immich-dark-gray" />
|
<CircleIconButton
|
||||||
|
title={$t('send_message')}
|
||||||
|
size="15"
|
||||||
|
icon={mdiSend}
|
||||||
|
class="dark:text-immich-dark-gray"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from '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 { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let asset: AssetResponseDto;
|
export let asset: AssetResponseDto;
|
||||||
export let album: AlbumResponseDto | null = null;
|
export let album: AlbumResponseDto | null = null;
|
||||||
@ -107,7 +108,7 @@
|
|||||||
class="z-[1001] flex h-16 place-items-center justify-between bg-gradient-to-b from-black/40 px-3 transition-transform duration-200"
|
class="z-[1001] flex h-16 place-items-center justify-between bg-gradient-to-b from-black/40 px-3 transition-transform duration-200"
|
||||||
>
|
>
|
||||||
<div class="text-white">
|
<div class="text-white">
|
||||||
<CircleIconButton color="opaque" icon={mdiArrowLeft} title="Go back" on:click={() => dispatch('back')} />
|
<CircleIconButton color="opaque" icon={mdiArrowLeft} title={$t('go_back')} on:click={() => dispatch('back')} />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex w-[calc(100%-3rem)] justify-end gap-2 overflow-hidden text-white"
|
class="flex w-[calc(100%-3rem)] justify-end gap-2 overflow-hidden text-white"
|
||||||
@ -118,7 +119,7 @@
|
|||||||
color="opaque"
|
color="opaque"
|
||||||
icon={mdiShareVariantOutline}
|
icon={mdiShareVariantOutline}
|
||||||
on:click={() => dispatch('showShareModal')}
|
on:click={() => dispatch('showShareModal')}
|
||||||
title="Share"
|
title={$t('share')}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if asset.isOffline}
|
{#if asset.isOffline}
|
||||||
@ -126,7 +127,7 @@
|
|||||||
color="opaque"
|
color="opaque"
|
||||||
icon={mdiAlertOutline}
|
icon={mdiAlertOutline}
|
||||||
on:click={() => dispatch('showDetail')}
|
on:click={() => dispatch('showDetail')}
|
||||||
title="Asset Offline"
|
title={$t('asset_offline')}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if showMotionPlayButton}
|
{#if showMotionPlayButton}
|
||||||
@ -134,14 +135,14 @@
|
|||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
color="opaque"
|
color="opaque"
|
||||||
icon={mdiMotionPauseOutline}
|
icon={mdiMotionPauseOutline}
|
||||||
title="Stop Motion Photo"
|
title={$t('stop_motion_photo')}
|
||||||
on:click={() => dispatch('stopMotionPhoto')}
|
on:click={() => dispatch('stopMotionPhoto')}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
color="opaque"
|
color="opaque"
|
||||||
icon={mdiPlaySpeed}
|
icon={mdiPlaySpeed}
|
||||||
title="Play Motion Photo"
|
title={$t('play_motion_photo')}
|
||||||
on:click={() => dispatch('playMotionPhoto')}
|
on:click={() => dispatch('playMotionPhoto')}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
@ -151,7 +152,7 @@
|
|||||||
color="opaque"
|
color="opaque"
|
||||||
hideMobile={true}
|
hideMobile={true}
|
||||||
icon={$photoZoomState && $photoZoomState.currentZoom > 1 ? mdiMagnifyMinusOutline : mdiMagnifyPlusOutline}
|
icon={$photoZoomState && $photoZoomState.currentZoom > 1 ? mdiMagnifyMinusOutline : mdiMagnifyPlusOutline}
|
||||||
title="Zoom Image"
|
title={$t('zoom_image')}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
const zoomImage = new CustomEvent('zoomImage');
|
const zoomImage = new CustomEvent('zoomImage');
|
||||||
window.dispatchEvent(zoomImage);
|
window.dispatchEvent(zoomImage);
|
||||||
@ -162,7 +163,7 @@
|
|||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
color="opaque"
|
color="opaque"
|
||||||
icon={mdiContentCopy}
|
icon={mdiContentCopy}
|
||||||
title="Copy Image"
|
title={$t('copy_image')}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
const copyEvent = new CustomEvent('copyImage');
|
const copyEvent = new CustomEvent('copyImage');
|
||||||
window.dispatchEvent(copyEvent);
|
window.dispatchEvent(copyEvent);
|
||||||
@ -175,7 +176,7 @@
|
|||||||
color="opaque"
|
color="opaque"
|
||||||
icon={mdiFolderDownloadOutline}
|
icon={mdiFolderDownloadOutline}
|
||||||
on:click={() => dispatch('download')}
|
on:click={() => dispatch('download')}
|
||||||
title="Download"
|
title={$t('download')}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@ -184,7 +185,7 @@
|
|||||||
color="opaque"
|
color="opaque"
|
||||||
icon={mdiInformationOutline}
|
icon={mdiInformationOutline}
|
||||||
on:click={() => dispatch('showDetail')}
|
on:click={() => dispatch('showDetail')}
|
||||||
title="Info"
|
title={$t('info')}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@ -193,45 +194,58 @@
|
|||||||
color="opaque"
|
color="opaque"
|
||||||
icon={asset.isFavorite ? mdiHeart : mdiHeartOutline}
|
icon={asset.isFavorite ? mdiHeart : mdiHeartOutline}
|
||||||
on:click={() => dispatch('favorite')}
|
on:click={() => dispatch('favorite')}
|
||||||
title={asset.isFavorite ? 'Unfavorite' : 'Favorite'}
|
title={asset.isFavorite ? $t('unfavorite') : $t('favorite')}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isOwner}
|
{#if isOwner}
|
||||||
<CircleIconButton color="opaque" icon={mdiDeleteOutline} on:click={() => dispatch('delete')} title="Delete" />
|
<CircleIconButton
|
||||||
|
color="opaque"
|
||||||
|
icon={mdiDeleteOutline}
|
||||||
|
on:click={() => dispatch('delete')}
|
||||||
|
title={$t('delete')}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
use:clickOutside={{
|
use:clickOutside={{
|
||||||
onOutclick: () => (isShowAssetOptions = false),
|
onOutclick: () => (isShowAssetOptions = false),
|
||||||
onEscape: () => (isShowAssetOptions = false),
|
onEscape: () => (isShowAssetOptions = false),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CircleIconButton color="opaque" icon={mdiDotsVertical} on:click={showOptionsMenu} title="More" />
|
<CircleIconButton color="opaque" icon={mdiDotsVertical} on:click={showOptionsMenu} title={$t('more')} />
|
||||||
{#if isShowAssetOptions}
|
{#if isShowAssetOptions}
|
||||||
<ContextMenu {...contextMenuPosition} direction="left">
|
<ContextMenu {...contextMenuPosition} direction="left">
|
||||||
{#if showSlideshow}
|
{#if showSlideshow}
|
||||||
<MenuOption icon={mdiPresentationPlay} on:click={() => onMenuClick('playSlideShow')} text="Slideshow" />
|
<MenuOption
|
||||||
|
icon={mdiPresentationPlay}
|
||||||
|
on:click={() => onMenuClick('playSlideShow')}
|
||||||
|
text={$t('slideshow')}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if showDownloadButton}
|
{#if showDownloadButton}
|
||||||
<MenuOption icon={mdiFolderDownloadOutline} on:click={() => onMenuClick('download')} text="Download" />
|
<MenuOption
|
||||||
|
icon={mdiFolderDownloadOutline}
|
||||||
|
on:click={() => onMenuClick('download')}
|
||||||
|
text={$t('download')}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if asset.isTrashed}
|
{#if asset.isTrashed}
|
||||||
<MenuOption icon={mdiHistory} on:click={() => onMenuClick('restoreAsset')} text="Restore" />
|
<MenuOption icon={mdiHistory} on:click={() => onMenuClick('restoreAsset')} text={$t('restore')} />
|
||||||
{:else}
|
{:else}
|
||||||
<MenuOption icon={mdiImageAlbum} on:click={() => onMenuClick('addToAlbum')} text="Add to album" />
|
<MenuOption icon={mdiImageAlbum} on:click={() => onMenuClick('addToAlbum')} text={$t('add_to_album')} />
|
||||||
<MenuOption
|
<MenuOption
|
||||||
icon={mdiShareVariantOutline}
|
icon={mdiShareVariantOutline}
|
||||||
on:click={() => onMenuClick('addToSharedAlbum')}
|
on:click={() => onMenuClick('addToSharedAlbum')}
|
||||||
text="Add to shared album"
|
text={$t('add_to_shared_album')}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isOwner}
|
{#if isOwner}
|
||||||
{#if hasStackChildren}
|
{#if hasStackChildren}
|
||||||
<MenuOption icon={mdiImageMinusOutline} on:click={() => onMenuClick('unstack')} text="Un-stack" />
|
<MenuOption icon={mdiImageMinusOutline} on:click={() => onMenuClick('unstack')} text={$t('unstack')} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if album}
|
{#if album}
|
||||||
<MenuOption
|
<MenuOption
|
||||||
text="Set as album cover"
|
text={$t('set_as_album_cover')}
|
||||||
icon={mdiImageOutline}
|
icon={mdiImageOutline}
|
||||||
on:click={() => onMenuClick('setAsAlbumCover')}
|
on:click={() => onMenuClick('setAsAlbumCover')}
|
||||||
/>
|
/>
|
||||||
@ -240,18 +254,18 @@
|
|||||||
<MenuOption
|
<MenuOption
|
||||||
icon={mdiAccountCircleOutline}
|
icon={mdiAccountCircleOutline}
|
||||||
on:click={() => onMenuClick('asProfileImage')}
|
on:click={() => onMenuClick('asProfileImage')}
|
||||||
text="Set as profile picture"
|
text={$t('set_as_profile_picture')}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<MenuOption
|
<MenuOption
|
||||||
on:click={() => dispatch('toggleArchive')}
|
on:click={() => dispatch('toggleArchive')}
|
||||||
icon={asset.isArchived ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline}
|
icon={asset.isArchived ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline}
|
||||||
text={asset.isArchived ? 'Unarchive' : 'Archive'}
|
text={asset.isArchived ? $t('unarchive') : $t('archive')}
|
||||||
/>
|
/>
|
||||||
<MenuOption
|
<MenuOption
|
||||||
icon={mdiUpload}
|
icon={mdiUpload}
|
||||||
on:click={() => openFileUploadDialog({ multiple: false, assetId: asset.id })}
|
on:click={() => openFileUploadDialog({ multiple: false, assetId: asset.id })}
|
||||||
text="Replace with upload"
|
text={$t('replace_with_upload')}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr />
|
||||||
<MenuOption
|
<MenuOption
|
||||||
|
@ -53,6 +53,7 @@
|
|||||||
import VideoViewer from './video-wrapper-viewer.svelte';
|
import VideoViewer from './video-wrapper-viewer.svelte';
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
import { websocketEvents } from '$lib/stores/websocket';
|
import { websocketEvents } from '$lib/stores/websocket';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let assetStore: AssetStore | null = null;
|
export let assetStore: AssetStore | null = null;
|
||||||
export let asset: AssetResponseDto;
|
export let asset: AssetResponseDto;
|
||||||
@ -169,7 +170,7 @@
|
|||||||
});
|
});
|
||||||
isLiked = data.length > 0 ? data[0] : null;
|
isLiked = data.length > 0 ? data[0] : null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "Can't get Favorite");
|
handleError(error, $t('errors.unable_to_load_liked_status'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -352,11 +353,11 @@
|
|||||||
dispatch('action', { type: AssetAction.TRASH, asset });
|
dispatch('action', { type: AssetAction.TRASH, asset });
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Moved to trash',
|
message: $t('moved_to_trash'),
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to trash asset');
|
handleError(error, $t('errors.unable_to_trash_asset'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -367,11 +368,11 @@
|
|||||||
dispatch('action', { type: AssetAction.DELETE, asset });
|
dispatch('action', { type: AssetAction.DELETE, asset });
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Permanently deleted asset',
|
message: $t('permanently_deleted_asset'),
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to delete asset');
|
handleError(error, $t('errors.unable_to_delete_asset'));
|
||||||
} finally {
|
} finally {
|
||||||
isShowDeleteConfirmation = false;
|
isShowDeleteConfirmation = false;
|
||||||
}
|
}
|
||||||
@ -428,7 +429,7 @@
|
|||||||
message: `Restored asset`,
|
message: `Restored asset`,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Error restoring asset');
|
handleError(error, $t('errors.unable_to_restore_assets'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -483,7 +484,7 @@
|
|||||||
try {
|
try {
|
||||||
await assetViewerHtmlElement.requestFullscreen();
|
await assetViewerHtmlElement.requestFullscreen();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error entering fullscreen', error);
|
handleError(error, $t('errors.unable_to_enter_fullscreen'));
|
||||||
$slideshowState = SlideshowState.StopSlideshow;
|
$slideshowState = SlideshowState.StopSlideshow;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -495,7 +496,7 @@
|
|||||||
await document.exitFullscreen();
|
await document.exitFullscreen();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error exiting fullscreen', error);
|
handleError(error, $t('errors.unable_to_exit_fullscreen'));
|
||||||
} finally {
|
} finally {
|
||||||
$stopSlideshowProgress = true;
|
$stopSlideshowProgress = true;
|
||||||
$slideshowState = SlideshowState.None;
|
$slideshowState = SlideshowState.None;
|
||||||
@ -534,7 +535,7 @@
|
|||||||
});
|
});
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
message: 'Album cover updated',
|
message: $t('album_cover_updated'),
|
||||||
timeout: 1500,
|
timeout: 1500,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -606,7 +607,7 @@
|
|||||||
|
|
||||||
{#if $slideshowState === SlideshowState.None && showNavigation}
|
{#if $slideshowState === SlideshowState.None && showNavigation}
|
||||||
<div class="z-[1001] my-auto column-span-1 col-start-1 row-span-full row-start-1 justify-self-start">
|
<div class="z-[1001] my-auto column-span-1 col-start-1 row-span-full row-start-1 justify-self-start">
|
||||||
<NavigationArea onClick={(e) => navigateAsset('previous', e)} label="View previous asset">
|
<NavigationArea onClick={(e) => navigateAsset('previous', e)} label={$t('view_previous_asset')}>
|
||||||
<Icon path={mdiChevronLeft} size="36" ariaHidden />
|
<Icon path={mdiChevronLeft} size="36" ariaHidden />
|
||||||
</NavigationArea>
|
</NavigationArea>
|
||||||
</div>
|
</div>
|
||||||
@ -703,7 +704,7 @@
|
|||||||
|
|
||||||
{#if $slideshowState === SlideshowState.None && showNavigation}
|
{#if $slideshowState === SlideshowState.None && showNavigation}
|
||||||
<div class="z-[1001] my-auto col-span-1 col-start-4 row-span-full row-start-1 justify-self-end">
|
<div class="z-[1001] my-auto col-span-1 col-start-4 row-span-full row-start-1 justify-self-end">
|
||||||
<NavigationArea onClick={(e) => navigateAsset('next', e)} label="View next asset">
|
<NavigationArea onClick={(e) => navigateAsset('next', e)} label={$t('view_next_asset')}>
|
||||||
<Icon path={mdiChevronRight} size="36" ariaHidden />
|
<Icon path={mdiChevronRight} size="36" ariaHidden />
|
||||||
</NavigationArea>
|
</NavigationArea>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { updateAsset, type AssetResponseDto } from '@immich/sdk';
|
import { updateAsset, type AssetResponseDto } from '@immich/sdk';
|
||||||
import AutogrowTextarea from '$lib/components/shared-components/autogrow-textarea.svelte';
|
import AutogrowTextarea from '$lib/components/shared-components/autogrow-textarea.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let asset: AssetResponseDto;
|
export let asset: AssetResponseDto;
|
||||||
export let isOwner: boolean;
|
export let isOwner: boolean;
|
||||||
@ -20,7 +21,7 @@
|
|||||||
message: 'Asset description has been updated',
|
message: 'Asset description has been updated',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Cannot update the description');
|
handleError(error, $t('cannot_update_the_description'));
|
||||||
}
|
}
|
||||||
description = newDescription;
|
description = newDescription;
|
||||||
};
|
};
|
||||||
@ -32,7 +33,7 @@
|
|||||||
content={description}
|
content={description}
|
||||||
class="max-h-[500px] w-full border-b border-gray-500 bg-transparent text-base text-black outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:text-white dark:focus:border-immich-dark-primary immich-scrollbar"
|
class="max-h-[500px] w-full border-b border-gray-500 bg-transparent text-base text-black outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:text-white dark:focus:border-immich-dark-primary immich-scrollbar"
|
||||||
onContentUpdate={handleFocusOut}
|
onContentUpdate={handleFocusOut}
|
||||||
placeholder="Add a description"
|
placeholder={$t('add_a_description')}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
{:else if description}
|
{:else if description}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { updateAsset, type AssetResponseDto } from '@immich/sdk';
|
import { updateAsset, type AssetResponseDto } from '@immich/sdk';
|
||||||
import { mdiMapMarkerOutline, mdiPencil } from '@mdi/js';
|
import { mdiMapMarkerOutline, mdiPencil } from '@mdi/js';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let isOwner: boolean;
|
export let isOwner: boolean;
|
||||||
export let asset: AssetResponseDto;
|
export let asset: AssetResponseDto;
|
||||||
@ -20,7 +21,7 @@
|
|||||||
updateAssetDto: { latitude: gps.lat, longitude: gps.lng },
|
updateAssetDto: { latitude: gps.lat, longitude: gps.lng },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to change location');
|
handleError(error, $t('errors.unable_to_change_location'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -30,7 +31,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="flex w-full text-left justify-between place-items-start gap-4 py-4"
|
class="flex w-full text-left justify-between place-items-start gap-4 py-4"
|
||||||
on:click={() => (isOwner ? (isShowChangeLocation = true) : null)}
|
on:click={() => (isOwner ? (isShowChangeLocation = true) : null)}
|
||||||
title={isOwner ? 'Edit location' : ''}
|
title={isOwner ? $t('edit_location') : ''}
|
||||||
class:hover:dark:text-immich-dark-primary={isOwner}
|
class:hover:dark:text-immich-dark-primary={isOwner}
|
||||||
class:hover:text-immich-primary={isOwner}
|
class:hover:text-immich-primary={isOwner}
|
||||||
>
|
>
|
||||||
@ -63,12 +64,12 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="flex w-full text-left justify-between place-items-start gap-4 py-4 rounded-lg hover:dark:text-immich-dark-primary hover:text-immich-primary"
|
class="flex w-full text-left justify-between place-items-start gap-4 py-4 rounded-lg hover:dark:text-immich-dark-primary hover:text-immich-primary"
|
||||||
on:click={() => (isShowChangeLocation = true)}
|
on:click={() => (isShowChangeLocation = true)}
|
||||||
title="Add location"
|
title={$t('add_location')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<div><Icon path={mdiMapMarkerOutline} size="24" /></div>
|
<div><Icon path={mdiMapMarkerOutline} size="24" /></div>
|
||||||
|
|
||||||
<p>Add a location</p>
|
<p>{$t('add_a_location')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="focus:outline-none p-1">
|
<div class="focus:outline-none p-1">
|
||||||
<Icon path={mdiPencil} size="20" />
|
<Icon path={mdiPencil} size="20" />
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||||
import AlbumListItemDetails from './album-list-item-details.svelte';
|
import AlbumListItemDetails from './album-list-item-details.svelte';
|
||||||
import DetailPanelDescription from '$lib/components/asset-viewer/detail-panel-description.svelte';
|
import DetailPanelDescription from '$lib/components/asset-viewer/detail-panel-description.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let asset: AssetResponseDto;
|
export let asset: AssetResponseDto;
|
||||||
export let albums: AlbumResponseDto[] = [];
|
export let albums: AlbumResponseDto[] = [];
|
||||||
@ -130,21 +131,21 @@
|
|||||||
try {
|
try {
|
||||||
await updateAsset({ id: asset.id, updateAssetDto: { dateTimeOriginal } });
|
await updateAsset({ id: asset.id, updateAssetDto: { dateTimeOriginal } });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to change date');
|
handleError(error, $t('errors.unable_to_change_date'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="relative p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
|
<section class="relative p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
|
||||||
<div class="flex place-items-center gap-2">
|
<div class="flex place-items-center gap-2">
|
||||||
<CircleIconButton icon={mdiClose} title="Close" on:click={() => dispatch('close')} />
|
<CircleIconButton icon={mdiClose} title={$t('close')} on:click={() => dispatch('close')} />
|
||||||
<p class="text-lg text-immich-fg dark:text-immich-dark-fg">Info</p>
|
<p class="text-lg text-immich-fg dark:text-immich-dark-fg">{$t('info')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if asset.isOffline}
|
{#if asset.isOffline}
|
||||||
<section class="px-4 py-4">
|
<section class="px-4 py-4">
|
||||||
<div role="alert">
|
<div role="alert">
|
||||||
<div class="rounded-t bg-red-500 px-4 py-2 font-bold text-white">Asset offline</div>
|
<div class="rounded-t bg-red-500 px-4 py-2 font-bold text-white">{$t('asset_offline')}</div>
|
||||||
<div class="rounded-b border border-t-0 border-red-400 bg-red-100 px-4 py-3 text-red-700">
|
<div class="rounded-b border border-t-0 border-red-400 bg-red-100 px-4 py-3 text-red-700">
|
||||||
<p>
|
<p>
|
||||||
This asset is offline. Immich can not access its file location. Please ensure the asset is available and
|
This asset is offline. Immich can not access its file location. Please ensure the asset is available and
|
||||||
@ -160,11 +161,11 @@
|
|||||||
{#if !isSharedLink() && people.length > 0}
|
{#if !isSharedLink() && people.length > 0}
|
||||||
<section class="px-4 py-4 text-sm">
|
<section class="px-4 py-4 text-sm">
|
||||||
<div class="flex h-10 w-full items-center justify-between">
|
<div class="flex h-10 w-full items-center justify-between">
|
||||||
<h2>PEOPLE</h2>
|
<h2>{$t('people').toUpperCase()}</h2>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
{#if people.some((person) => person.isHidden)}
|
{#if people.some((person) => person.isHidden)}
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
title="Show hidden people"
|
title={$t('show_hidden_people')}
|
||||||
icon={showingHiddenPeople ? mdiEyeOff : mdiEye}
|
icon={showingHiddenPeople ? mdiEyeOff : mdiEye}
|
||||||
padding="1"
|
padding="1"
|
||||||
buttonSize="32"
|
buttonSize="32"
|
||||||
@ -172,7 +173,7 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
title="Edit people"
|
title={$t('edit_people')}
|
||||||
icon={mdiPencil}
|
icon={mdiPencil}
|
||||||
padding="1"
|
padding="1"
|
||||||
size="20"
|
size="20"
|
||||||
@ -247,10 +248,10 @@
|
|||||||
<div class="px-4 py-4">
|
<div class="px-4 py-4">
|
||||||
{#if asset.exifInfo}
|
{#if asset.exifInfo}
|
||||||
<div class="flex h-10 w-full items-center justify-between text-sm">
|
<div class="flex h-10 w-full items-center justify-between text-sm">
|
||||||
<h2>DETAILS</h2>
|
<h2>{$t('details').toUpperCase()}</h2>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="text-sm">NO EXIF INFO AVAILABLE</p>
|
<p class="text-sm">{$t('no_exif_info_available').toUpperCase()}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if asset.exifInfo?.dateTimeOriginal}
|
{#if asset.exifInfo?.dateTimeOriginal}
|
||||||
@ -261,7 +262,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="flex w-full text-left justify-between place-items-start gap-4 py-4"
|
class="flex w-full text-left justify-between place-items-start gap-4 py-4"
|
||||||
on:click={() => (isOwner ? (isShowChangeDate = true) : null)}
|
on:click={() => (isOwner ? (isShowChangeDate = true) : null)}
|
||||||
title={isOwner ? 'Edit date' : ''}
|
title={isOwner ? $t('edit_date') : ''}
|
||||||
class:hover:dark:text-immich-dark-primary={isOwner}
|
class:hover:dark:text-immich-dark-primary={isOwner}
|
||||||
class:hover:text-immich-primary={isOwner}
|
class:hover:text-immich-primary={isOwner}
|
||||||
>
|
>
|
||||||
@ -340,7 +341,7 @@
|
|||||||
{#if isOwner}
|
{#if isOwner}
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
icon={mdiInformationOutline}
|
icon={mdiInformationOutline}
|
||||||
title="Show file location"
|
title={$t('show_file_location')}
|
||||||
size="16"
|
size="16"
|
||||||
padding="2"
|
padding="2"
|
||||||
on:click={toggleAssetPath}
|
on:click={toggleAssetPath}
|
||||||
@ -448,7 +449,7 @@
|
|||||||
|
|
||||||
{#if currentAlbum && currentAlbum.albumUsers.length > 0 && asset.owner}
|
{#if currentAlbum && currentAlbum.albumUsers.length > 0 && asset.owner}
|
||||||
<section class="px-6 dark:text-immich-dark-fg mt-4">
|
<section class="px-6 dark:text-immich-dark-fg mt-4">
|
||||||
<p class="text-sm">SHARED BY</p>
|
<p class="text-sm">{$t('shared_by').toUpperCase()}</p>
|
||||||
<div class="flex gap-4 pt-4">
|
<div class="flex gap-4 pt-4">
|
||||||
<div>
|
<div>
|
||||||
<UserAvatar user={asset.owner} size="md" />
|
<UserAvatar user={asset.owner} size="md" />
|
||||||
@ -465,7 +466,7 @@
|
|||||||
|
|
||||||
{#if albums.length > 0}
|
{#if albums.length > 0}
|
||||||
<section class="p-6 dark:text-immich-dark-fg">
|
<section class="p-6 dark:text-immich-dark-fg">
|
||||||
<p class="pb-4 text-sm">APPEARS IN</p>
|
<p class="pb-4 text-sm">{$t('appears_in').toUpperCase()}</p>
|
||||||
{#each albums as album}
|
{#each albums as album}
|
||||||
<a data-sveltekit-preload-data="hover" href={`/albums/${album.id}`}>
|
<a data-sveltekit-preload-data="hover" href={`/albums/${album.id}`}>
|
||||||
<div class="flex gap-4 py-2 hover:cursor-pointer items-center">
|
<div class="flex gap-4 py-2 hover:cursor-pointer items-center">
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import { asByteUnitString } from '../../utils/byte-units';
|
import { asByteUnitString } from '../../utils/byte-units';
|
||||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||||
import { mdiClose } from '@mdi/js';
|
import { mdiClose } from '@mdi/js';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
const abort = (downloadKey: string, download: DownloadProgress) => {
|
const abort = (downloadKey: string, download: DownloadProgress) => {
|
||||||
download.abort?.abort();
|
download.abort?.abort();
|
||||||
@ -17,7 +18,7 @@
|
|||||||
transition:fly={{ x: -100, duration: 350 }}
|
transition:fly={{ x: -100, duration: 350 }}
|
||||||
class="absolute bottom-10 left-2 z-[10000] max-h-[270px] w-[315px] rounded-2xl border bg-immich-bg p-4 text-sm shadow-sm"
|
class="absolute bottom-10 left-2 z-[10000] max-h-[270px] w-[315px] rounded-2xl border bg-immich-bg p-4 text-sm shadow-sm"
|
||||||
>
|
>
|
||||||
<p class="mb-2 text-xs text-gray-500">DOWNLOADING</p>
|
<p class="mb-2 text-xs text-gray-500">{$t('downloading').toUpperCase()}</p>
|
||||||
<div class="my-2 mb-2 flex max-h-[200px] flex-col overflow-y-auto text-sm">
|
<div class="my-2 mb-2 flex max-h-[200px] flex-col overflow-y-auto text-sm">
|
||||||
{#each Object.keys($downloadAssets) as downloadKey (downloadKey)}
|
{#each Object.keys($downloadAssets) as downloadKey (downloadKey)}
|
||||||
{@const download = $downloadAssets[downloadKey]}
|
{@const download = $downloadAssets[downloadKey]}
|
||||||
@ -40,7 +41,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="absolute right-2">
|
<div class="absolute right-2">
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
title="Close"
|
title={$t('close')}
|
||||||
on:click={() => abort(downloadKey, download)}
|
on:click={() => abort(downloadKey, download)}
|
||||||
size="20"
|
size="20"
|
||||||
icon={mdiClose}
|
icon={mdiClose}
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||||
import { SlideshowLook, slideshowLookCssMapping, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
import { SlideshowLook, slideshowLookCssMapping, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
const { slideshowState, slideshowLook } = slideshowStore;
|
const { slideshowState, slideshowLook } = slideshowStore;
|
||||||
|
|
||||||
@ -99,7 +100,7 @@
|
|||||||
await copyImageToClipboard(assetData);
|
await copyImageToClipboard(assetData);
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
message: 'Copied image to clipboard.',
|
message: $t('copied_image_to_clipboard'),
|
||||||
timeout: 3000,
|
timeout: 3000,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -134,7 +135,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onCopyShortcut = (event: KeyboardEvent) => {
|
const onCopyShortcut = (event: KeyboardEvent) => {
|
||||||
if (window.getSelection()?.type === 'Range') {
|
if (window.getSelection()?.type === $t('range')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
import { mdiChevronLeft, mdiChevronRight, mdiClose, mdiCog, mdiFullscreen, mdiPause, mdiPlay } from '@mdi/js';
|
import { mdiChevronLeft, mdiChevronRight, mdiClose, mdiCog, mdiFullscreen, mdiPause, mdiPlay } from '@mdi/js';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let isFullScreen: boolean;
|
export let isFullScreen: boolean;
|
||||||
export let onNext = () => {};
|
export let onNext = () => {};
|
||||||
@ -94,23 +95,28 @@
|
|||||||
transition:fly={{ duration: 150 }}
|
transition:fly={{ duration: 150 }}
|
||||||
role="navigation"
|
role="navigation"
|
||||||
>
|
>
|
||||||
<CircleIconButton buttonSize="50" icon={mdiClose} on:click={onClose} title="Exit Slideshow" />
|
<CircleIconButton buttonSize="50" icon={mdiClose} on:click={onClose} title={$t('exit_slideshow')} />
|
||||||
|
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
buttonSize="50"
|
buttonSize="50"
|
||||||
icon={progressBarStatus === ProgressBarStatus.Paused ? mdiPlay : mdiPause}
|
icon={progressBarStatus === ProgressBarStatus.Paused ? mdiPlay : mdiPause}
|
||||||
on:click={() => (progressBarStatus === ProgressBarStatus.Paused ? progressBar.play() : progressBar.pause())}
|
on:click={() => (progressBarStatus === ProgressBarStatus.Paused ? progressBar.play() : progressBar.pause())}
|
||||||
title={progressBarStatus === ProgressBarStatus.Paused ? 'Play' : 'Pause'}
|
title={progressBarStatus === ProgressBarStatus.Paused ? $t('play') : $t('pause')}
|
||||||
|
/>
|
||||||
|
<CircleIconButton buttonSize="50" icon={mdiChevronLeft} on:click={onPrevious} title={$t('previous')} />
|
||||||
|
<CircleIconButton buttonSize="50" icon={mdiChevronRight} on:click={onNext} title={$t('next')} />
|
||||||
|
<CircleIconButton
|
||||||
|
buttonSize="50"
|
||||||
|
icon={mdiCog}
|
||||||
|
on:click={() => (showSettings = !showSettings)}
|
||||||
|
title={$t('next')}
|
||||||
/>
|
/>
|
||||||
<CircleIconButton buttonSize="50" icon={mdiChevronLeft} on:click={onPrevious} title="Previous" />
|
|
||||||
<CircleIconButton buttonSize="50" icon={mdiChevronRight} on:click={onNext} title="Next" />
|
|
||||||
<CircleIconButton buttonSize="50" icon={mdiCog} on:click={() => (showSettings = !showSettings)} title="Next" />
|
|
||||||
{#if !isFullScreen}
|
{#if !isFullScreen}
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
buttonSize="50"
|
buttonSize="50"
|
||||||
icon={mdiFullscreen}
|
icon={mdiFullscreen}
|
||||||
on:click={onSetToFullScreen}
|
on:click={onSetToFullScreen}
|
||||||
title="Set Slideshow to fullscreen"
|
title={$t('set_slideshow_to_fullscreen')}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
import { AssetMediaSize } from '@immich/sdk';
|
import { AssetMediaSize } from '@immich/sdk';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let assetId: string;
|
export let assetId: string;
|
||||||
export let loopVideo: boolean;
|
export let loopVideo: boolean;
|
||||||
@ -31,7 +32,7 @@
|
|||||||
await video.play();
|
await video.play();
|
||||||
dispatch('onVideoStarted');
|
dispatch('onVideoStarted');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to play video');
|
handleError(error, $t('errors.unable_to_play_video'));
|
||||||
} finally {
|
} finally {
|
||||||
isVideoLoading = false;
|
isVideoLoading = false;
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import type { SearchOptions } from '$lib/utils/dipatch';
|
import type { SearchOptions } from '$lib/utils/dipatch';
|
||||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let name: string;
|
export let name: string;
|
||||||
export let roundedBottom = true;
|
export let roundedBottom = true;
|
||||||
@ -34,7 +35,7 @@
|
|||||||
>
|
>
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
icon={mdiMagnify}
|
icon={mdiMagnify}
|
||||||
title="Search"
|
title={$t('search')}
|
||||||
size="16"
|
size="16"
|
||||||
padding="2"
|
padding="2"
|
||||||
on:click={() => dispatch('search', { force: true })}
|
on:click={() => dispatch('search', { force: true })}
|
||||||
@ -54,6 +55,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if name}
|
{#if name}
|
||||||
<CircleIconButton icon={mdiClose} title="Clear value" size="16" padding="2" on:click={resetSearch} />
|
<CircleIconButton icon={mdiClose} title={$t('clear_value')} size="16" padding="2" on:click={resetSearch} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||||
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
|
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let peopleWithFaces: AssetFaceResponseDto[];
|
export let peopleWithFaces: AssetFaceResponseDto[];
|
||||||
export let allPeople: PersonResponseDto[];
|
export let allPeople: PersonResponseDto[];
|
||||||
@ -119,19 +120,19 @@
|
|||||||
<div class="flex place-items-center justify-between gap-2">
|
<div class="flex place-items-center justify-between gap-2">
|
||||||
{#if !searchFaces}
|
{#if !searchFaces}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<CircleIconButton icon={mdiArrowLeftThin} title="Back" on:click={handleBackButton} />
|
<CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={handleBackButton} />
|
||||||
<p class="flex text-lg text-immich-fg dark:text-immich-dark-fg">Select face</p>
|
<p class="flex text-lg text-immich-fg dark:text-immich-dark-fg">{$t('select_face')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
icon={mdiMagnify}
|
icon={mdiMagnify}
|
||||||
title="Search for existing person"
|
title={$t('search_for_existing_person')}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
searchFaces = true;
|
searchFaces = true;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{#if !isShowLoadingNewPerson}
|
{#if !isShowLoadingNewPerson}
|
||||||
<CircleIconButton icon={mdiPlus} title="Create new person" on:click={handleCreatePerson} />
|
<CircleIconButton icon={mdiPlus} title={$t('create_new_person')} on:click={handleCreatePerson} />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex place-content-center place-items-center">
|
<div class="flex place-content-center place-items-center">
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
@ -139,7 +140,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<CircleIconButton icon={mdiArrowLeftThin} title="Back" on:click={handleBackButton} />
|
<CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={handleBackButton} />
|
||||||
<div class="w-full flex">
|
<div class="w-full flex">
|
||||||
<SearchPeople
|
<SearchPeople
|
||||||
type="input"
|
type="input"
|
||||||
@ -153,11 +154,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<CircleIconButton icon={mdiClose} title="Cancel search" on:click={() => (searchFaces = false)} />
|
<CircleIconButton icon={mdiClose} title={$t('cancel_search')} on:click={() => (searchFaces = false)} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="px-4 py-4 text-sm">
|
<div class="px-4 py-4 text-sm">
|
||||||
<h2 class="mb-8 mt-4 uppercase">All people</h2>
|
<h2 class="mb-8 mt-4 uppercase">{$t('all_people')}</h2>
|
||||||
<div class="immich-scrollbar mt-4 flex flex-wrap gap-2 overflow-y-auto">
|
<div class="immich-scrollbar mt-4 flex flex-wrap gap-2 overflow-y-auto">
|
||||||
{#each showPeople as person (person.id)}
|
{#each showPeople as person (person.id)}
|
||||||
{#if person.id !== editedPerson.id}
|
{#if person.id !== editedPerson.id}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
|
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let person: PersonResponseDto;
|
export let person: PersonResponseDto;
|
||||||
export let name: string;
|
export let name: string;
|
||||||
@ -35,6 +36,6 @@
|
|||||||
inputClass="w-full gap-2 bg-gray-100 dark:bg-gray-700 dark:text-white"
|
inputClass="w-full gap-2 bg-gray-100 dark:bg-gray-700 dark:text-white"
|
||||||
bind:showLoadingSpinner={isSearchingPeople}
|
bind:showLoadingSpinner={isSearchingPeople}
|
||||||
/>
|
/>
|
||||||
<Button size="sm" type="submit">Done</Button>
|
<Button size="sm" type="submit">{$t('done')}</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
import FaceThumbnail from './face-thumbnail.svelte';
|
import FaceThumbnail from './face-thumbnail.svelte';
|
||||||
import PeopleList from './people-list.svelte';
|
import PeopleList from './people-list.svelte';
|
||||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let person: PersonResponseDto;
|
export let person: PersonResponseDto;
|
||||||
let people: PersonResponseDto[] = [];
|
let people: PersonResponseDto[] = [];
|
||||||
@ -86,7 +87,7 @@
|
|||||||
});
|
});
|
||||||
dispatch('merge', mergedPerson);
|
dispatch('merge', mergedPerson);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Cannot merge people');
|
handleError(error, $t('cannot_merge_people'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -134,7 +135,7 @@
|
|||||||
{#if selectedPeople.length === 1}
|
{#if selectedPeople.length === 1}
|
||||||
<div class="absolute bottom-2">
|
<div class="absolute bottom-2">
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
title="Swap merge direction"
|
title={$t('swap_merge_direction')}
|
||||||
icon={mdiSwapHorizontal}
|
icon={mdiSwapHorizontal}
|
||||||
size="24"
|
size="24"
|
||||||
on:click={handleSwapPeople}
|
on:click={handleSwapPeople}
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let personMerge1: PersonResponseDto;
|
export let personMerge1: PersonResponseDto;
|
||||||
export let personMerge2: PersonResponseDto;
|
export let personMerge2: PersonResponseDto;
|
||||||
@ -44,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mx-0.5 flex md:mx-2">
|
<div class="mx-0.5 flex md:mx-2">
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
title="Swap merge direction"
|
title={$t('swap_merge_direction')}
|
||||||
icon={mdiMerge}
|
icon={mdiMerge}
|
||||||
on:click={() => ([personMerge1, personMerge2] = [personMerge2, personMerge1])}
|
on:click={() => ([personMerge1, personMerge2] = [personMerge2, personMerge1])}
|
||||||
/>
|
/>
|
||||||
@ -104,7 +105,7 @@
|
|||||||
<p class="text-sm text-gray-500 dark:text-gray-300">They will be merged together</p>
|
<p class="text-sm text-gray-500 dark:text-gray-300">They will be merged together</p>
|
||||||
</div>
|
</div>
|
||||||
<svelte:fragment slot="sticky-bottom">
|
<svelte:fragment slot="sticky-bottom">
|
||||||
<Button fullwidth color="gray" on:click={() => dispatch('reject')}>No</Button>
|
<Button fullwidth color="gray" on:click={() => dispatch('reject')}>{$t('no')}</Button>
|
||||||
<Button fullwidth on:click={() => dispatch('confirm', [personMerge1, personMerge2])}>Yes</Button>
|
<Button fullwidth on:click={() => dispatch('confirm', [personMerge1, personMerge2])}>{$t('yes')}</Button>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</FullScreenModal>
|
</FullScreenModal>
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
|
||||||
import Portal from '../shared-components/portal/portal.svelte';
|
import Portal from '../shared-components/portal/portal.svelte';
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let person: PersonResponseDto;
|
export let person: PersonResponseDto;
|
||||||
export let preload = false;
|
export let preload = false;
|
||||||
@ -76,7 +77,7 @@
|
|||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
color="opaque"
|
color="opaque"
|
||||||
icon={mdiDotsVertical}
|
icon={mdiDotsVertical}
|
||||||
title="Show person options"
|
title={$t('show_person_options')}
|
||||||
size="20"
|
size="20"
|
||||||
padding="2"
|
padding="2"
|
||||||
class="icon-white-drop-shadow"
|
class="icon-white-drop-shadow"
|
||||||
@ -88,17 +89,17 @@
|
|||||||
{#if showContextMenu}
|
{#if showContextMenu}
|
||||||
<Portal target="body">
|
<Portal target="body">
|
||||||
<ContextMenu {...contextMenuPosition} onClose={() => onMenuExit()}>
|
<ContextMenu {...contextMenuPosition} onClose={() => onMenuExit()}>
|
||||||
<MenuOption on:click={() => onMenuClick('hide-person')} icon={mdiEyeOffOutline} text="Hide person" />
|
<MenuOption on:click={() => onMenuClick('hide-person')} icon={mdiEyeOffOutline} text={$t('hide_person')} />
|
||||||
<MenuOption on:click={() => onMenuClick('change-name')} icon={mdiAccountEditOutline} text="Change name" />
|
<MenuOption on:click={() => onMenuClick('change-name')} icon={mdiAccountEditOutline} text={$t('change_name')} />
|
||||||
<MenuOption
|
<MenuOption
|
||||||
on:click={() => onMenuClick('set-birth-date')}
|
on:click={() => onMenuClick('set-birth-date')}
|
||||||
icon={mdiCalendarEditOutline}
|
icon={mdiCalendarEditOutline}
|
||||||
text="Set date of birth"
|
text={$t('set_date_of_birth')}
|
||||||
/>
|
/>
|
||||||
<MenuOption
|
<MenuOption
|
||||||
on:click={() => onMenuClick('merge-people')}
|
on:click={() => onMenuClick('merge-people')}
|
||||||
icon={mdiAccountMultipleCheckOutline}
|
icon={mdiAccountMultipleCheckOutline}
|
||||||
text="Merge people"
|
text={$t('merge_people')}
|
||||||
/>
|
/>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</Portal>
|
</Portal>
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import FaceThumbnail from './face-thumbnail.svelte';
|
import FaceThumbnail from './face-thumbnail.svelte';
|
||||||
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
|
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let screenHeight: number;
|
export let screenHeight: number;
|
||||||
export let people: PersonResponseDto[];
|
export let people: PersonResponseDto[];
|
||||||
@ -25,7 +26,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class=" w-40 sm:w-48 md:w-96 h-14 mb-8">
|
<div class=" w-40 sm:w-48 md:w-96 h-14 mb-8">
|
||||||
<SearchPeople type="searchBar" placeholder="Search people" bind:searchName={name} bind:searchedPeopleLocal />
|
<SearchPeople type="searchBar" placeholder={$t('search_people')} bind:searchName={name} bind:searchedPeopleLocal />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { searchNameLocal } from '$lib/utils/person';
|
import { searchNameLocal } from '$lib/utils/person';
|
||||||
import { searchPerson, type PersonResponseDto } from '@immich/sdk';
|
import { searchPerson, type PersonResponseDto } from '@immich/sdk';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let searchName: string;
|
export let searchName: string;
|
||||||
export let searchedPeopleLocal: PersonResponseDto[];
|
export let searchedPeopleLocal: PersonResponseDto[];
|
||||||
@ -12,7 +13,7 @@
|
|||||||
export let numberPeopleToSearch: number = maximumLengthSearchPeople;
|
export let numberPeopleToSearch: number = maximumLengthSearchPeople;
|
||||||
export let inputClass: string = 'w-full gap-2 bg-immich-bg dark:bg-immich-dark-bg';
|
export let inputClass: string = 'w-full gap-2 bg-immich-bg dark:bg-immich-dark-bg';
|
||||||
export let showLoadingSpinner: boolean = false;
|
export let showLoadingSpinner: boolean = false;
|
||||||
export let placeholder: string = 'Name or nickname';
|
export let placeholder: string = $t('name_or_nickname');
|
||||||
export let onReset = () => {};
|
export let onReset = () => {};
|
||||||
export let onSearch = () => {};
|
export let onSearch = () => {};
|
||||||
|
|
||||||
@ -61,7 +62,7 @@
|
|||||||
searchedPeople = data;
|
searchedPeople = data;
|
||||||
searchWord = searchName;
|
searchWord = searchName;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "Can't search people");
|
handleError(error, $t('cant_search_people'));
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
timeout = null;
|
timeout = null;
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||||
import AssignFaceSidePanel from './assign-face-side-panel.svelte';
|
import AssignFaceSidePanel from './assign-face-side-panel.svelte';
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let assetId: string;
|
export let assetId: string;
|
||||||
export let assetType: AssetTypeEnum;
|
export let assetType: AssetTypeEnum;
|
||||||
@ -64,7 +65,7 @@
|
|||||||
allPeople = people;
|
allPeople = people;
|
||||||
peopleWithFaces = await getFaces({ id: assetId });
|
peopleWithFaces = await getFaces({ id: assetId });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "Can't get faces");
|
handleError(error, $t('cant_get_faces'));
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
}
|
}
|
||||||
@ -142,7 +143,7 @@
|
|||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "Can't apply changes");
|
handleError(error, $t('cant_apply_changes'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,8 +185,8 @@
|
|||||||
>
|
>
|
||||||
<div class="flex place-items-center justify-between gap-2">
|
<div class="flex place-items-center justify-between gap-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<CircleIconButton icon={mdiArrowLeftThin} title="Back" on:click={handleBackButton} />
|
<CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={handleBackButton} />
|
||||||
<p class="flex text-lg text-immich-fg dark:text-immich-dark-fg">Edit faces</p>
|
<p class="flex text-lg text-immich-fg dark:text-immich-dark-fg">{$t('edit_faces')}</p>
|
||||||
</div>
|
</div>
|
||||||
{#if !isShowLoadingDone}
|
{#if !isShowLoadingDone}
|
||||||
<button
|
<button
|
||||||
@ -225,7 +226,7 @@
|
|||||||
shadow
|
shadow
|
||||||
url={selectedPersonToCreate[face.id]}
|
url={selectedPersonToCreate[face.id]}
|
||||||
altText={selectedPersonToCreate[face.id]}
|
altText={selectedPersonToCreate[face.id]}
|
||||||
title={'New person'}
|
title={$t('new_person')}
|
||||||
widthStyle={thumbnailWidth}
|
widthStyle={thumbnailWidth}
|
||||||
heightStyle={thumbnailWidth}
|
heightStyle={thumbnailWidth}
|
||||||
/>
|
/>
|
||||||
@ -272,7 +273,7 @@
|
|||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
icon={mdiRestart}
|
icon={mdiRestart}
|
||||||
title="Reset"
|
title={$t('reset')}
|
||||||
size="18"
|
size="18"
|
||||||
padding="1"
|
padding="1"
|
||||||
class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
|
class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
|
||||||
@ -282,7 +283,7 @@
|
|||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
icon={mdiMinus}
|
icon={mdiMinus}
|
||||||
title="Select new face"
|
title={$t('select_new_face')}
|
||||||
size="18"
|
size="18"
|
||||||
padding="1"
|
padding="1"
|
||||||
class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
|
class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
||||||
import { mdiCake } from '@mdi/js';
|
import { mdiCake } from '@mdi/js';
|
||||||
import DateInput from '../elements/date-input.svelte';
|
import DateInput from '../elements/date-input.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let birthDate: string;
|
export let birthDate: string;
|
||||||
|
|
||||||
@ -20,7 +21,7 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FullScreenModal title="Set date of birth" icon={mdiCake} onClose={handleCancel}>
|
<FullScreenModal title={$t('set_date_of_birth')} icon={mdiCake} onClose={handleCancel}>
|
||||||
<div class="text-immich-primary dark:text-immich-dark-primary">
|
<div class="text-immich-primary dark:text-immich-dark-primary">
|
||||||
<p class="text-sm dark:text-immich-dark-fg">
|
<p class="text-sm dark:text-immich-dark-fg">
|
||||||
Date of birth is used to calculate the age of this person at the time of a photo.
|
Date of birth is used to calculate the age of this person at the time of a photo.
|
||||||
@ -40,7 +41,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<svelte:fragment slot="sticky-bottom">
|
<svelte:fragment slot="sticky-bottom">
|
||||||
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
|
<Button color="gray" fullwidth on:click={() => handleCancel()}>{$t('cancel')}</Button>
|
||||||
<Button type="submit" fullwidth form="set-birth-date-form">Set</Button>
|
<Button type="submit" fullwidth form="set-birth-date-form">{$t('set')}</Button>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</FullScreenModal>
|
</FullScreenModal>
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
import { mdiClose, mdiEye, mdiEyeOff, mdiEyeSettings, mdiRestart } from '@mdi/js';
|
import { mdiClose, mdiEye, mdiEyeOff, mdiEyeSettings, mdiRestart } from '@mdi/js';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let showLoadingSpinner: boolean;
|
export let showLoadingSpinner: boolean;
|
||||||
export let toggleVisibility: ToggleVisibilty = ToggleVisibilty.VIEW_ALL;
|
export let toggleVisibility: ToggleVisibilty = ToggleVisibilty.VIEW_ALL;
|
||||||
@ -51,7 +52,7 @@
|
|||||||
class="fixed top-0 z-10 flex h-16 w-full items-center justify-between border-b bg-white p-1 dark:border-immich-dark-gray dark:bg-black dark:text-immich-dark-fg md:p-8"
|
class="fixed top-0 z-10 flex h-16 w-full items-center justify-between border-b bg-white p-1 dark:border-immich-dark-gray dark:bg-black dark:text-immich-dark-fg md:p-8"
|
||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<CircleIconButton title="Close" icon={mdiClose} on:click={onClose} />
|
<CircleIconButton title={$t('close')} icon={mdiClose} on:click={onClose} />
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<p class="ml-2">Show & hide people</p>
|
<p class="ml-2">Show & hide people</p>
|
||||||
<p class="text-sm text-gray-400 dark:text-gray-600">({countTotalPeople.toLocaleString($locale)})</p>
|
<p class="text-sm text-gray-400 dark:text-gray-600">({countTotalPeople.toLocaleString($locale)})</p>
|
||||||
@ -59,15 +60,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-end">
|
<div class="flex items-center justify-end">
|
||||||
<div class="flex items-center md:mr-8">
|
<div class="flex items-center md:mr-8">
|
||||||
<CircleIconButton title="Reset people visibility" icon={mdiRestart} on:click={onReset} />
|
<CircleIconButton title={$t('reset_people_visibility')} icon={mdiRestart} on:click={onReset} />
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
title="Toggle visibility"
|
title={$t('toggle_visibility')}
|
||||||
icon={toggleIcon}
|
icon={toggleIcon}
|
||||||
on:click={() => onChange(getNextVisibility(toggleVisibility))}
|
on:click={() => onChange(getNextVisibility(toggleVisibility))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if !showLoadingSpinner}
|
{#if !showLoadingSpinner}
|
||||||
<Button on:click={onDone} size="sm" rounded="lg">Done</Button>
|
<Button on:click={onDone} size="sm" rounded="lg">{$t('done')}</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import { handleError } from '../../utils/handle-error';
|
import { handleError } from '../../utils/handle-error';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
import PasswordField from '../shared-components/password-field.svelte';
|
import PasswordField from '../shared-components/password-field.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
let email = '';
|
let email = '';
|
||||||
let password = '';
|
let password = '';
|
||||||
@ -16,7 +17,7 @@
|
|||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (password !== confirmPassword && confirmPassword.length > 0) {
|
if (password !== confirmPassword && confirmPassword.length > 0) {
|
||||||
errorMessage = 'Password does not match';
|
errorMessage = $t('password_does_not_match');
|
||||||
canRegister = false;
|
canRegister = false;
|
||||||
} else {
|
} else {
|
||||||
errorMessage = '';
|
errorMessage = '';
|
||||||
@ -32,8 +33,8 @@
|
|||||||
await signUpAdmin({ signUpDto: { email, password, name } });
|
await signUpAdmin({ signUpDto: { email, password, name } });
|
||||||
await goto(AppRoute.AUTH_LOGIN);
|
await goto(AppRoute.AUTH_LOGIN);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to create admin account');
|
handleError(error, 'errors.errors.unable_to_create_admin_account');
|
||||||
errorMessage = 'Error create admin account';
|
errorMessage = $t('errors.errors.unable_to_create_admin_account');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -41,22 +42,22 @@
|
|||||||
|
|
||||||
<form on:submit|preventDefault={registerAdmin} method="post" class="mt-5 flex flex-col gap-5">
|
<form on:submit|preventDefault={registerAdmin} method="post" class="mt-5 flex flex-col gap-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">{$t('admin_email')}</label>
|
||||||
<input class="immich-form-input" id="email" bind:value={email} type="email" autocomplete="email" required />
|
<input class="immich-form-input" id="email" bind:value={email} type="email" autocomplete="email" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="password">Admin Password</label>
|
<label class="immich-form-label" for="password">{$t('admin_password')}</label>
|
||||||
<PasswordField id="password" bind:password autocomplete="new-password" />
|
<PasswordField id="password" bind:password autocomplete="new-password" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="confirmPassword">Confirm Admin Password</label>
|
<label class="immich-form-label" for="confirmPassword">{$t('confirm_admin_password')}</label>
|
||||||
<PasswordField id="confirmPassword" bind:password={confirmPassword} autocomplete="new-password" />
|
<PasswordField id="confirmPassword" bind:password={confirmPassword} autocomplete="new-password" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="name">Name</label>
|
<label class="immich-form-label" for="name">{$t('name')}</label>
|
||||||
<input class="immich-form-input" id="name" bind:value={name} type="text" autocomplete="name" required />
|
<input class="immich-form-input" id="name" bind:value={name} type="text" autocomplete="name" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -65,6 +66,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="my-5 flex w-full">
|
<div class="my-5 flex w-full">
|
||||||
<Button type="submit" size="lg" fullwidth>Sign up</Button>
|
<Button type="submit" size="lg" fullwidth>{$t('sign_up')}</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -5,11 +5,12 @@
|
|||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
||||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let apiKey: Partial<ApiKeyResponseDto>;
|
export let apiKey: Partial<ApiKeyResponseDto>;
|
||||||
export let title: string;
|
export let title: string;
|
||||||
export let cancelText = 'Cancel';
|
export let cancelText = $t('cancel');
|
||||||
export let submitText = 'Save';
|
export let submitText = $t('save');
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
cancel: void;
|
cancel: void;
|
||||||
@ -31,7 +32,7 @@
|
|||||||
<FullScreenModal {title} icon={mdiKeyVariant} onClose={handleCancel}>
|
<FullScreenModal {title} icon={mdiKeyVariant} onClose={handleCancel}>
|
||||||
<form on:submit|preventDefault={handleSubmit} autocomplete="off" id="api-key-form">
|
<form on:submit|preventDefault={handleSubmit} autocomplete="off" id="api-key-form">
|
||||||
<div class="mb-4 flex flex-col gap-2">
|
<div class="mb-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="name">Name</label>
|
<label class="immich-form-label" for="name">{$t('name')}</label>
|
||||||
<input class="immich-form-input" id="name" name="name" type="text" bind:value={apiKey.name} />
|
<input class="immich-form-input" id="name" name="name" type="text" bind:value={apiKey.name} />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let secret = '';
|
export let secret = '';
|
||||||
|
|
||||||
@ -13,7 +14,7 @@
|
|||||||
const handleDone = () => dispatch('done');
|
const handleDone = () => dispatch('done');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FullScreenModal title="API key" icon={mdiKeyVariant} onClose={() => handleDone()}>
|
<FullScreenModal title={$t('api_key')} icon={mdiKeyVariant} onClose={() => handleDone()}>
|
||||||
<div class="text-immich-primary dark:text-immich-dark-primary">
|
<div class="text-immich-primary dark:text-immich-dark-primary">
|
||||||
<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.
|
||||||
@ -21,12 +22,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 flex flex-col gap-2">
|
<div class="my-4 flex flex-col gap-2">
|
||||||
<!-- <label class="immich-form-label" for="secret">API Key</label> -->
|
<!-- <label class="immich-form-label" for="secret">{ $t("api_key") }</label> -->
|
||||||
<textarea class="immich-form-input" id="secret" name="secret" readonly={true} value={secret} />
|
<textarea class="immich-form-input" id="secret" name="secret" readonly={true} value={secret} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<svelte:fragment slot="sticky-bottom">
|
<svelte:fragment slot="sticky-bottom">
|
||||||
<Button on:click={() => copyToClipboard(secret)} fullwidth>Copy to Clipboard</Button>
|
<Button on:click={() => copyToClipboard(secret)} fullwidth>{$t('copy_to_clipboard')}</Button>
|
||||||
<Button on:click={() => handleDone()} fullwidth>Done</Button>
|
<Button on:click={() => handleDone()} fullwidth>{$t('done')}</Button>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</FullScreenModal>
|
</FullScreenModal>
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
import PasswordField from '../shared-components/password-field.svelte';
|
import PasswordField from '../shared-components/password-field.svelte';
|
||||||
import { updateMyUser } from '@immich/sdk';
|
import { updateMyUser } from '@immich/sdk';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
let errorMessage: string;
|
let errorMessage: string;
|
||||||
let success: string;
|
let success: string;
|
||||||
@ -14,7 +15,7 @@
|
|||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (password !== passwordConfirm && passwordConfirm.length > 0) {
|
if (password !== passwordConfirm && passwordConfirm.length > 0) {
|
||||||
errorMessage = 'Password does not match';
|
errorMessage = $t('password_does_not_match');
|
||||||
valid = false;
|
valid = false;
|
||||||
} else {
|
} else {
|
||||||
errorMessage = '';
|
errorMessage = '';
|
||||||
@ -39,12 +40,12 @@
|
|||||||
|
|
||||||
<form on:submit|preventDefault={changePassword} method="post" class="mt-5 flex flex-col gap-5">
|
<form on:submit|preventDefault={changePassword} method="post" class="mt-5 flex flex-col gap-5">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="password">New Password</label>
|
<label class="immich-form-label" for="password">{$t('new_password')}</label>
|
||||||
<PasswordField id="password" bind:password autocomplete="new-password" />
|
<PasswordField id="password" bind:password autocomplete="new-password" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="confirmPassword">Confirm Password</label>
|
<label class="immich-form-label" for="confirmPassword">{$t('confirm_password')}</label>
|
||||||
<PasswordField id="confirmPassword" bind:password={passwordConfirm} autocomplete="new-password" />
|
<PasswordField id="confirmPassword" bind:password={passwordConfirm} autocomplete="new-password" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -56,6 +57,6 @@
|
|||||||
<p class="text-sm text-immich-primary">{success}</p>
|
<p class="text-sm text-immich-primary">{success}</p>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="my-5 flex w-full">
|
<div class="my-5 flex w-full">
|
||||||
<Button type="submit" size="lg" fullwidth>Change password</Button>
|
<Button type="submit" size="lg" fullwidth>{$t('change_password')}</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
import Slider from '../elements/slider.svelte';
|
import Slider from '../elements/slider.svelte';
|
||||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let onClose: () => void;
|
export let onClose: () => void;
|
||||||
|
|
||||||
@ -31,7 +32,7 @@
|
|||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (password !== confirmPassword && confirmPassword.length > 0) {
|
if (password !== confirmPassword && confirmPassword.length > 0) {
|
||||||
error = 'Password does not match';
|
error = $t('password_does_not_match');
|
||||||
canCreateUser = false;
|
canCreateUser = false;
|
||||||
} else {
|
} else {
|
||||||
error = '';
|
error = '';
|
||||||
@ -60,13 +61,13 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
success = 'New user created';
|
success = $t('new_user_created');
|
||||||
|
|
||||||
dispatch('submit');
|
dispatch('submit');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to create user');
|
handleError(error, $t('errors.unable_to_create_user'));
|
||||||
} finally {
|
} finally {
|
||||||
isCreatingUser = false;
|
isCreatingUser = false;
|
||||||
}
|
}
|
||||||
@ -74,10 +75,10 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FullScreenModal title="Create new user" showLogo {onClose}>
|
<FullScreenModal title={$t('create_new_user')} showLogo {onClose}>
|
||||||
<form on:submit|preventDefault={registerUser} autocomplete="off" id="create-new-user-form">
|
<form on:submit|preventDefault={registerUser} autocomplete="off" id="create-new-user-form">
|
||||||
<div class="my-4 flex flex-col gap-2">
|
<div class="my-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="email">Email</label>
|
<label class="immich-form-label" for="email">{$t('email')}</label>
|
||||||
<input class="immich-form-input" id="email" bind:value={email} type="email" required />
|
<input class="immich-form-input" id="email" bind:value={email} type="email" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -89,12 +90,12 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="my-4 flex flex-col gap-2">
|
<div class="my-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="password">Password</label>
|
<label class="immich-form-label" for="password">{$t('password')}</label>
|
||||||
<PasswordField id="password" bind:password autocomplete="new-password" />
|
<PasswordField id="password" bind:password autocomplete="new-password" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 flex flex-col gap-2">
|
<div class="my-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="confirmPassword">Confirm Password</label>
|
<label class="immich-form-label" for="confirmPassword">{$t('confirm_password')}</label>
|
||||||
<PasswordField id="confirmPassword" bind:password={confirmPassword} autocomplete="new-password" />
|
<PasswordField id="confirmPassword" bind:password={confirmPassword} autocomplete="new-password" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -106,7 +107,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 flex flex-col gap-2">
|
<div class="my-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="name">Name</label>
|
<label class="immich-form-label" for="name">{$t('name')}</label>
|
||||||
<input class="immich-form-input" id="name" bind:value={name} type="text" required />
|
<input class="immich-form-input" id="name" bind:value={name} type="text" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -129,7 +130,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
<svelte:fragment slot="sticky-bottom">
|
<svelte:fragment slot="sticky-bottom">
|
||||||
<Button color="gray" fullwidth on:click={() => dispatch('cancel')}>Cancel</Button>
|
<Button color="gray" fullwidth on:click={() => dispatch('cancel')}>{$t('cancel')}</Button>
|
||||||
<Button type="submit" disabled={isCreatingUser} fullwidth form="create-new-user-form">Create</Button>
|
<Button type="submit" disabled={isCreatingUser} fullwidth form="create-new-user-form">{$t('create')}</Button>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</FullScreenModal>
|
</FullScreenModal>
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||||
import AlbumCover from '$lib/components/album-page/album-cover.svelte';
|
import AlbumCover from '$lib/components/album-page/album-cover.svelte';
|
||||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let album: AlbumResponseDto;
|
export let album: AlbumResponseDto;
|
||||||
export let onEditSuccess: ((album: AlbumResponseDto) => unknown) | undefined = undefined;
|
export let onEditSuccess: ((album: AlbumResponseDto) => unknown) | undefined = undefined;
|
||||||
@ -36,7 +37,7 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FullScreenModal title="Edit album" width="wide" {onClose}>
|
<FullScreenModal title={$t('edit_album')} width="wide" {onClose}>
|
||||||
<form on:submit|preventDefault={handleUpdateAlbumInfo} autocomplete="off" id="edit-album-form">
|
<form on:submit|preventDefault={handleUpdateAlbumInfo} autocomplete="off" id="edit-album-form">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="hidden sm:flex">
|
<div class="hidden sm:flex">
|
||||||
@ -45,19 +46,19 @@
|
|||||||
|
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="name">Name</label>
|
<label class="immich-form-label" for="name">{$t('name')}</label>
|
||||||
<input class="immich-form-input" id="name" type="text" bind:value={albumName} />
|
<input class="immich-form-input" id="name" type="text" bind:value={albumName} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="description">Description</label>
|
<label class="immich-form-label" for="description">{$t('description')}</label>
|
||||||
<textarea class="immich-form-input" id="description" bind:value={description} />
|
<textarea class="immich-form-input" id="description" bind:value={description} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<svelte:fragment slot="sticky-bottom">
|
<svelte:fragment slot="sticky-bottom">
|
||||||
<Button color="gray" fullwidth on:click={() => onCancel?.()}>Cancel</Button>
|
<Button color="gray" fullwidth on:click={() => onCancel?.()}>{$t('cancel')}</Button>
|
||||||
<Button type="submit" fullwidth disabled={isSubmitting} form="edit-album-form">OK</Button>
|
<Button type="submit" fullwidth disabled={isSubmitting} form="edit-album-form">{$t('ok')}</Button>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</FullScreenModal>
|
</FullScreenModal>
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let user: UserAdminResponseDto;
|
export let user: UserAdminResponseDto;
|
||||||
export let canResetPassword = true;
|
export let canResetPassword = true;
|
||||||
@ -47,7 +48,7 @@
|
|||||||
|
|
||||||
dispatch('editSuccess');
|
dispatch('editSuccess');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to update user');
|
handleError(error, $t('errors.unable_to_update_user'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -74,7 +75,7 @@
|
|||||||
|
|
||||||
dispatch('resetPasswordSuccess');
|
dispatch('resetPasswordSuccess');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to reset password');
|
handleError(error, $t('errors.unable_to_reset_password'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -96,15 +97,15 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FullScreenModal title="Edit user" icon={mdiAccountEditOutline} {onClose}>
|
<FullScreenModal title={$t('edit_user')} icon={mdiAccountEditOutline} {onClose}>
|
||||||
<form on:submit|preventDefault={editUser} autocomplete="off" id="edit-user-form">
|
<form on:submit|preventDefault={editUser} autocomplete="off" id="edit-user-form">
|
||||||
<div class="my-4 flex flex-col gap-2">
|
<div class="my-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="email">Email</label>
|
<label class="immich-form-label" for="email">{$t('email')}</label>
|
||||||
<input class="immich-form-input" id="email" name="email" type="email" bind:value={user.email} />
|
<input class="immich-form-input" id="email" name="email" type="email" bind:value={user.email} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 flex flex-col gap-2">
|
<div class="my-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="name">Name</label>
|
<label class="immich-form-label" for="name">{$t('name')}</label>
|
||||||
<input class="immich-form-input" id="name" name="name" type="text" required bind:value={user.name} />
|
<input class="immich-form-input" id="name" name="name" type="text" required bind:value={user.name} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -119,7 +120,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 flex flex-col gap-2">
|
<div class="my-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="storage-label">Storage Label</label>
|
<label class="immich-form-label" for="storage-label">{$t('storage_label')}</label>
|
||||||
<input
|
<input
|
||||||
class="immich-form-input"
|
class="immich-form-input"
|
||||||
id="storage-label"
|
id="storage-label"
|
||||||
@ -146,8 +147,8 @@
|
|||||||
</form>
|
</form>
|
||||||
<svelte:fragment slot="sticky-bottom">
|
<svelte:fragment slot="sticky-bottom">
|
||||||
{#if canResetPassword}
|
{#if canResetPassword}
|
||||||
<Button color="light-red" fullwidth on:click={resetPassword}>Reset password</Button>
|
<Button color="light-red" fullwidth on:click={resetPassword}>{$t('reset_password')}</Button>
|
||||||
{/if}
|
{/if}
|
||||||
<Button type="submit" fullwidth form="edit-user-form">Confirm</Button>
|
<Button type="submit" fullwidth form="edit-user-form">{$t('confirm')}</Button>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</FullScreenModal>
|
</FullScreenModal>
|
||||||
|
@ -4,11 +4,12 @@
|
|||||||
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
||||||
import { mdiFolderRemove } from '@mdi/js';
|
import { mdiFolderRemove } from '@mdi/js';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let exclusionPattern: string;
|
export let exclusionPattern: string;
|
||||||
export let exclusionPatterns: string[] = [];
|
export let exclusionPatterns: string[] = [];
|
||||||
export let isEditing = false;
|
export let isEditing = false;
|
||||||
export let submitText = 'Submit';
|
export let submitText = $t('submit');
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
@ -28,7 +29,7 @@
|
|||||||
const handleSubmit = () => dispatch('submit', { excludePattern: exclusionPattern });
|
const handleSubmit = () => dispatch('submit', { excludePattern: exclusionPattern });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FullScreenModal title="Add exclusion pattern" icon={mdiFolderRemove} onClose={handleCancel}>
|
<FullScreenModal title={$t('add_exclusion_pattern')} icon={mdiFolderRemove} onClose={handleCancel}>
|
||||||
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="add-exclusion-pattern-form">
|
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="add-exclusion-pattern-form">
|
||||||
<p class="py-5 text-sm">
|
<p class="py-5 text-sm">
|
||||||
Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have
|
Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have
|
||||||
@ -38,7 +39,7 @@
|
|||||||
use "**/Raw/**". To ignore all files ending in ".tif", use "**/*.tif". To ignore an absolute path, use "/path/to/ignore/**".
|
use "**/Raw/**". To ignore all files ending in ".tif", use "**/*.tif". To ignore an absolute path, use "/path/to/ignore/**".
|
||||||
</p>
|
</p>
|
||||||
<div class="my-4 flex flex-col gap-2">
|
<div class="my-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="exclusionPattern">Pattern</label>
|
<label class="immich-form-label" for="exclusionPattern">{$t('pattern')}</label>
|
||||||
<input
|
<input
|
||||||
class="immich-form-input"
|
class="immich-form-input"
|
||||||
id="exclusionPattern"
|
id="exclusionPattern"
|
||||||
@ -54,9 +55,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<svelte:fragment slot="sticky-bottom">
|
<svelte:fragment slot="sticky-bottom">
|
||||||
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
|
<Button color="gray" fullwidth on:click={() => handleCancel()}>{$t('cancel')}</Button>
|
||||||
{#if isEditing}
|
{#if isEditing}
|
||||||
<Button color="red" fullwidth on:click={() => dispatch('delete')}>Delete</Button>
|
<Button color="red" fullwidth on:click={() => dispatch('delete')}>{$t('delete')}</Button>
|
||||||
{/if}
|
{/if}
|
||||||
<Button type="submit" disabled={!canSubmit} fullwidth form="add-exclusion-pattern-form">{submitText}</Button>
|
<Button type="submit" disabled={!canSubmit} fullwidth form="add-exclusion-pattern-form">{submitText}</Button>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
@ -4,12 +4,13 @@
|
|||||||
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
||||||
import { mdiFolderSync } from '@mdi/js';
|
import { mdiFolderSync } from '@mdi/js';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let importPath: string | null;
|
export let importPath: string | null;
|
||||||
export let importPaths: string[] = [];
|
export let importPaths: string[] = [];
|
||||||
export let title = 'Import path';
|
export let title = $t('import_path');
|
||||||
export let cancelText = 'Cancel';
|
export let cancelText = $t('cancel');
|
||||||
export let submitText = 'Save';
|
export let submitText = $t('save');
|
||||||
export let isEditing = false;
|
export let isEditing = false;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@ -37,7 +38,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="my-4 flex flex-col gap-2">
|
<div class="my-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="path">Path</label>
|
<label class="immich-form-label" for="path">{$t('path')}</label>
|
||||||
<input class="immich-form-input" id="path" name="path" type="text" bind:value={importPath} />
|
<input class="immich-form-input" id="path" name="path" type="text" bind:value={importPath} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -50,7 +51,7 @@
|
|||||||
<svelte:fragment slot="sticky-bottom">
|
<svelte:fragment slot="sticky-bottom">
|
||||||
<Button color="gray" fullwidth on:click={() => handleCancel()}>{cancelText}</Button>
|
<Button color="gray" fullwidth on:click={() => handleCancel()}>{cancelText}</Button>
|
||||||
{#if isEditing}
|
{#if isEditing}
|
||||||
<Button color="red" fullwidth on:click={() => dispatch('delete')}>Delete</Button>
|
<Button color="red" fullwidth on:click={() => dispatch('delete')}>{$t('delete')}</Button>
|
||||||
{/if}
|
{/if}
|
||||||
<Button type="submit" disabled={!canSubmit} fullwidth form="library-import-path-form">{submitText}</Button>
|
<Button type="submit" disabled={!canSubmit} fullwidth form="library-import-path-form">{submitText}</Button>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
import { s } from '$lib/utils';
|
import { s } from '$lib/utils';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let library: LibraryResponseDto;
|
export let library: LibraryResponseDto;
|
||||||
|
|
||||||
@ -149,8 +150,8 @@
|
|||||||
|
|
||||||
{#if addImportPath}
|
{#if addImportPath}
|
||||||
<LibraryImportPathForm
|
<LibraryImportPathForm
|
||||||
title="Add import path"
|
title={$t('add_import_path')}
|
||||||
submitText="Add"
|
submitText={$t('add')}
|
||||||
bind:importPath={importPathToAdd}
|
bind:importPath={importPathToAdd}
|
||||||
{importPaths}
|
{importPaths}
|
||||||
on:submit={handleAddImportPath}
|
on:submit={handleAddImportPath}
|
||||||
@ -163,8 +164,8 @@
|
|||||||
|
|
||||||
{#if editImportPath != undefined}
|
{#if editImportPath != undefined}
|
||||||
<LibraryImportPathForm
|
<LibraryImportPathForm
|
||||||
title="Edit import path"
|
title={$t('edit_import_path')}
|
||||||
submitText="Save"
|
submitText={$t('save')}
|
||||||
isEditing={true}
|
isEditing={true}
|
||||||
bind:importPath={editedImportPath}
|
bind:importPath={editedImportPath}
|
||||||
{importPaths}
|
{importPaths}
|
||||||
@ -210,7 +211,7 @@
|
|||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
icon={mdiPencilOutline}
|
icon={mdiPencilOutline}
|
||||||
title="Edit import path"
|
title={$t('edit_import_path')}
|
||||||
size="16"
|
size="16"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
editImportPath = listIndex;
|
editImportPath = listIndex;
|
||||||
@ -238,7 +239,7 @@
|
|||||||
size="sm"
|
size="sm"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
addImportPath = true;
|
addImportPath = true;
|
||||||
}}>Add path</Button
|
}}>{$t('add_path')}</Button
|
||||||
></td
|
></td
|
||||||
>
|
>
|
||||||
</tr>
|
</tr>
|
||||||
@ -246,11 +247,13 @@
|
|||||||
</table>
|
</table>
|
||||||
<div class="flex justify-between w-full">
|
<div class="flex justify-between w-full">
|
||||||
<div class="justify-end gap-2">
|
<div class="justify-end gap-2">
|
||||||
<Button size="sm" color="gray" on:click={() => revalidate()}><Icon path={mdiRefresh} size={20} />Validate</Button>
|
<Button size="sm" color="gray" on:click={() => revalidate()}
|
||||||
|
><Icon path={mdiRefresh} size={20} />{$t('validate')}</Button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="justify-end gap-2">
|
<div class="justify-end gap-2">
|
||||||
<Button size="sm" color="gray" on:click={() => handleCancel()}>Cancel</Button>
|
<Button size="sm" color="gray" on:click={() => handleCancel()}>{$t('cancel')}</Button>
|
||||||
<Button size="sm" type="submit">Save</Button>
|
<Button size="sm" type="submit">{$t('save')}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import type { LibraryResponseDto } from '@immich/sdk';
|
import type { LibraryResponseDto } from '@immich/sdk';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let library: Partial<LibraryResponseDto>;
|
export let library: Partial<LibraryResponseDto>;
|
||||||
|
|
||||||
@ -20,11 +21,11 @@
|
|||||||
|
|
||||||
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" class="m-4 flex flex-col gap-2">
|
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" class="m-4 flex flex-col gap-2">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="path">Name</label>
|
<label class="immich-form-label" for="path">{$t('name')}</label>
|
||||||
<input class="immich-form-input" id="name" name="name" type="text" bind:value={library.name} />
|
<input class="immich-form-input" id="name" name="name" type="text" bind:value={library.name} />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full justify-end gap-2 pt-2">
|
<div class="flex w-full justify-end gap-2 pt-2">
|
||||||
<Button size="sm" color="gray" on:click={() => handleCancel()}>Cancel</Button>
|
<Button size="sm" color="gray" on:click={() => handleCancel()}>{$t('cancel')}</Button>
|
||||||
<Button size="sm" type="submit">Save</Button>
|
<Button size="sm" type="submit">{$t('save')}</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
import LibraryExclusionPatternForm from './library-exclusion-pattern-form.svelte';
|
import LibraryExclusionPatternForm from './library-exclusion-pattern-form.svelte';
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let library: Partial<LibraryResponseDto>;
|
export let library: Partial<LibraryResponseDto>;
|
||||||
|
|
||||||
@ -102,7 +103,7 @@
|
|||||||
|
|
||||||
{#if addExclusionPattern}
|
{#if addExclusionPattern}
|
||||||
<LibraryExclusionPatternForm
|
<LibraryExclusionPatternForm
|
||||||
submitText="Add"
|
submitText={$t('add')}
|
||||||
bind:exclusionPattern={exclusionPatternToAdd}
|
bind:exclusionPattern={exclusionPatternToAdd}
|
||||||
{exclusionPatterns}
|
{exclusionPatterns}
|
||||||
on:submit={handleAddExclusionPattern}
|
on:submit={handleAddExclusionPattern}
|
||||||
@ -114,7 +115,7 @@
|
|||||||
|
|
||||||
{#if editExclusionPattern != undefined}
|
{#if editExclusionPattern != undefined}
|
||||||
<LibraryExclusionPatternForm
|
<LibraryExclusionPatternForm
|
||||||
submitText="Save"
|
submitText={$t('save')}
|
||||||
isEditing={true}
|
isEditing={true}
|
||||||
bind:exclusionPattern={editedExclusionPattern}
|
bind:exclusionPattern={editedExclusionPattern}
|
||||||
{exclusionPatterns}
|
{exclusionPatterns}
|
||||||
@ -142,7 +143,7 @@
|
|||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
icon={mdiPencilOutline}
|
icon={mdiPencilOutline}
|
||||||
title="Edit exclusion pattern"
|
title={$t('edit_exclusion_pattern')}
|
||||||
size="16"
|
size="16"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
editExclusionPattern = listIndex;
|
editExclusionPattern = listIndex;
|
||||||
@ -169,7 +170,7 @@
|
|||||||
size="sm"
|
size="sm"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
addExclusionPattern = true;
|
addExclusionPattern = true;
|
||||||
}}>Add exclusion pattern</Button
|
}}>{$t('add_exclusion_pattern')}</Button
|
||||||
></td
|
></td
|
||||||
></tr
|
></tr
|
||||||
>
|
>
|
||||||
@ -177,7 +178,7 @@
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="flex w-full justify-end gap-4">
|
<div class="flex w-full justify-end gap-4">
|
||||||
<Button size="sm" color="gray" on:click={() => handleCancel()}>Cancel</Button>
|
<Button size="sm" color="gray" on:click={() => handleCancel()}>{$t('cancel')}</Button>
|
||||||
<Button size="sm" type="submit">Save</Button>
|
<Button size="sm" type="submit">{$t('save')}</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
import { searchUsersAdmin } from '@immich/sdk';
|
import { searchUsersAdmin } from '@immich/sdk';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
let ownerId: string = $user.id;
|
let ownerId: string = $user.id;
|
||||||
|
|
||||||
@ -27,14 +28,14 @@
|
|||||||
const handleSubmit = () => dispatch('submit', { ownerId });
|
const handleSubmit = () => dispatch('submit', { ownerId });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FullScreenModal title="Select library owner" icon={mdiFolderSync} onClose={handleCancel}>
|
<FullScreenModal title={$t('select_library_owner')} icon={mdiFolderSync} onClose={handleCancel}>
|
||||||
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="select-library-owner-form">
|
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="select-library-owner-form">
|
||||||
<p class="p-5 text-sm">NOTE: This cannot be changed later!</p>
|
<p class="p-5 text-sm">NOTE: This cannot be changed later!</p>
|
||||||
|
|
||||||
<SettingSelect bind:value={ownerId} options={userOptions} name="user" />
|
<SettingSelect bind:value={ownerId} options={userOptions} name="user" />
|
||||||
</form>
|
</form>
|
||||||
<svelte:fragment slot="sticky-bottom">
|
<svelte:fragment slot="sticky-bottom">
|
||||||
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
|
<Button color="gray" fullwidth on:click={() => handleCancel()}>{$t('cancel')}</Button>
|
||||||
<Button type="submit" fullwidth form="select-library-owner-form">Create</Button>
|
<Button type="submit" fullwidth form="select-library-owner-form">{$t('create')}</Button>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</FullScreenModal>
|
</FullScreenModal>
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
import PasswordField from '../shared-components/password-field.svelte';
|
import PasswordField from '../shared-components/password-field.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let onSuccess: () => unknown | Promise<unknown>;
|
export let onSuccess: () => unknown | Promise<unknown>;
|
||||||
export let onFirstLogin: () => unknown | Promise<unknown>;
|
export let onFirstLogin: () => unknown | Promise<unknown>;
|
||||||
@ -99,7 +100,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="email">Email</label>
|
<label class="immich-form-label" for="email">{$t('email')}</label>
|
||||||
<input
|
<input
|
||||||
class="immich-form-input"
|
class="immich-form-input"
|
||||||
id="email"
|
id="email"
|
||||||
@ -112,7 +113,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="password">Password</label>
|
<label class="immich-form-label" for="password">{$t('password')}</label>
|
||||||
<PasswordField id="password" bind:password autocomplete="current-password" />
|
<PasswordField id="password" bind:password autocomplete="current-password" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -165,5 +166,5 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !$featureFlags.passwordLogin && !$featureFlags.oauth}
|
{#if !$featureFlags.passwordLogin && !$featureFlags.oauth}
|
||||||
<p class="p-4 text-center dark:text-immich-dark-fg">Login has been disabled.</p>
|
<p class="p-4 text-center dark:text-immich-dark-fg">{$t('login_has_been_disabled')}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||||
|
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||||
|
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||||
import type { MapSettings } from '$lib/stores/preferences.store';
|
import type { MapSettings } from '$lib/stores/preferences.store';
|
||||||
import { Duration } from 'luxon';
|
import { Duration } from 'luxon';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
import LinkButton from '../elements/buttons/link-button.svelte';
|
import LinkButton from '../elements/buttons/link-button.svelte';
|
||||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
|
||||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
|
||||||
import DateInput from '../elements/date-input.svelte';
|
import DateInput from '../elements/date-input.svelte';
|
||||||
|
|
||||||
export let settings: MapSettings;
|
export let settings: MapSettings;
|
||||||
@ -21,21 +22,21 @@
|
|||||||
const handleClose = () => dispatch('close');
|
const handleClose = () => dispatch('close');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FullScreenModal title="Map settings" onClose={handleClose}>
|
<FullScreenModal title={$t('map_settings')} onClose={handleClose}>
|
||||||
<form
|
<form
|
||||||
on:submit|preventDefault={() => dispatch('save', settings)}
|
on:submit|preventDefault={() => dispatch('save', settings)}
|
||||||
class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary"
|
class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary"
|
||||||
id="map-settings-form"
|
id="map-settings-form"
|
||||||
>
|
>
|
||||||
<SettingSwitch title="Allow dark mode" bind:checked={settings.allowDarkMode} />
|
<SettingSwitch title={$t('allow_dark_mode')} bind:checked={settings.allowDarkMode} />
|
||||||
<SettingSwitch title="Only favorites" bind:checked={settings.onlyFavorites} />
|
<SettingSwitch title={$t('only_favorites')} bind:checked={settings.onlyFavorites} />
|
||||||
<SettingSwitch title="Include archived" bind:checked={settings.includeArchived} />
|
<SettingSwitch title={$t('include_archived')} bind:checked={settings.includeArchived} />
|
||||||
<SettingSwitch title="Include shared partner assets" bind:checked={settings.withPartners} />
|
<SettingSwitch title={$t('include_shared_partner_assets')} bind:checked={settings.withPartners} />
|
||||||
<SettingSwitch title="Include shared albums" bind:checked={settings.withSharedAlbums} />
|
<SettingSwitch title={$t('include_shared_albums')} bind:checked={settings.withSharedAlbums} />
|
||||||
{#if customDateRange}
|
{#if customDateRange}
|
||||||
<div in:fly={{ y: 10, duration: 200 }} class="flex flex-col gap-4">
|
<div in:fly={{ y: 10, duration: 200 }} class="flex flex-col gap-4">
|
||||||
<div class="flex items-center justify-between gap-8">
|
<div class="flex items-center justify-between gap-8">
|
||||||
<label class="immich-form-label shrink-0 text-sm" for="date-after">Date after</label>
|
<label class="immich-form-label shrink-0 text-sm" for="date-after">{$t('date_after')}</label>
|
||||||
<DateInput
|
<DateInput
|
||||||
class="immich-form-input w-40"
|
class="immich-form-input w-40"
|
||||||
type="date"
|
type="date"
|
||||||
@ -45,7 +46,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between gap-8">
|
<div class="flex items-center justify-between gap-8">
|
||||||
<label class="immich-form-label shrink-0 text-sm" for="date-before">Date before</label>
|
<label class="immich-form-label shrink-0 text-sm" for="date-before">{$t('date_before')}</label>
|
||||||
<DateInput class="immich-form-input w-40" type="date" id="date-before" bind:value={settings.dateBefore} />
|
<DateInput 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">
|
||||||
@ -63,7 +64,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div in:fly={{ y: -10, duration: 200 }} class="flex flex-col gap-1">
|
<div in:fly={{ y: -10, duration: 200 }} class="flex flex-col gap-1">
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="Date range"
|
label={$t('date_range')}
|
||||||
name="date-range"
|
name="date-range"
|
||||||
bind:value={settings.relativeDate}
|
bind:value={settings.relativeDate}
|
||||||
options={[
|
options={[
|
||||||
@ -73,23 +74,23 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: Duration.fromObject({ hours: 24 }).toISO() || '',
|
value: Duration.fromObject({ hours: 24 }).toISO() || '',
|
||||||
text: 'Past 24 hours',
|
text: $t('past_durations.hours', { values: { hours: 24 } }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: Duration.fromObject({ days: 7 }).toISO() || '',
|
value: Duration.fromObject({ days: 7 }).toISO() || '',
|
||||||
text: 'Past 7 days',
|
text: $t('past_durations.days', { values: { days: 7 } }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: Duration.fromObject({ days: 30 }).toISO() || '',
|
value: Duration.fromObject({ days: 30 }).toISO() || '',
|
||||||
text: 'Past 30 days',
|
text: $t('past_durations.days', { values: { days: 30 } }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: Duration.fromObject({ years: 1 }).toISO() || '',
|
value: Duration.fromObject({ years: 1 }).toISO() || '',
|
||||||
text: 'Past year',
|
text: $t('past_durations.years', { values: { years: 1 } }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: Duration.fromObject({ years: 3 }).toISO() || '',
|
value: Duration.fromObject({ years: 3 }).toISO() || '',
|
||||||
text: 'Past 3 years',
|
text: $t('past_durations.years', { values: { years: 3 } }),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@ -107,7 +108,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
<svelte:fragment slot="sticky-bottom">
|
<svelte:fragment slot="sticky-bottom">
|
||||||
<Button color="gray" size="sm" fullwidth on:click={handleClose}>Cancel</Button>
|
<Button color="gray" size="sm" fullwidth on:click={handleClose}>{$t('cancel')}</Button>
|
||||||
<Button type="submit" size="sm" fullwidth form="map-settings-form">Save</Button>
|
<Button type="submit" size="sm" fullwidth form="map-settings-form">{$t('save')}</Button>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</FullScreenModal>
|
</FullScreenModal>
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { tweened } from 'svelte/motion';
|
import { tweened } from 'svelte/motion';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
const parseIndex = (s: string | null, max: number | null) =>
|
const parseIndex = (s: string | null, max: number | null) =>
|
||||||
Math.max(Math.min(Number.parseInt(s ?? '') || 0, max ?? 0), 0);
|
Math.max(Math.min(Number.parseInt(s ?? '') || 0, max ?? 0), 0);
|
||||||
@ -143,16 +144,16 @@
|
|||||||
<div class="sticky top-0 z-[90]">
|
<div class="sticky top-0 z-[90]">
|
||||||
<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
|
<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
|
||||||
<CreateSharedLink />
|
<CreateSharedLink />
|
||||||
<CircleIconButton title="Select all" icon={mdiSelectAll} on:click={handleSelectAll} />
|
<CircleIconButton title={$t('select_all')} icon={mdiSelectAll} on:click={handleSelectAll} />
|
||||||
|
|
||||||
<AssetSelectContextMenu icon={mdiPlus} title="Add to...">
|
<AssetSelectContextMenu icon={mdiPlus} title={$t('add_to')}>
|
||||||
<AddToAlbum />
|
<AddToAlbum />
|
||||||
<AddToAlbum shared />
|
<AddToAlbum shared />
|
||||||
</AssetSelectContextMenu>
|
</AssetSelectContextMenu>
|
||||||
|
|
||||||
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={triggerAssetUpdate} />
|
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={triggerAssetUpdate} />
|
||||||
|
|
||||||
<AssetSelectContextMenu icon={mdiDotsVertical} title="Add">
|
<AssetSelectContextMenu icon={mdiDotsVertical} title={$t('add')}>
|
||||||
<DownloadAction menuItem />
|
<DownloadAction menuItem />
|
||||||
<ChangeDate menuItem />
|
<ChangeDate menuItem />
|
||||||
<ChangeLocation menuItem />
|
<ChangeLocation menuItem />
|
||||||
@ -175,7 +176,7 @@
|
|||||||
{#if canGoForward}
|
{#if canGoForward}
|
||||||
<div class="flex place-content-center place-items-center gap-2 overflow-hidden">
|
<div class="flex place-content-center place-items-center gap-2 overflow-hidden">
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
title={paused ? 'Play memories' : 'Pause memories'}
|
title={paused ? $t('play_memories') : $t('pause_memories')}
|
||||||
icon={paused ? mdiPlay : mdiPause}
|
icon={paused ? mdiPlay : mdiPause}
|
||||||
on:click={() => (paused = !paused)}
|
on:click={() => (paused = !paused)}
|
||||||
class="hover:text-black"
|
class="hover:text-black"
|
||||||
@ -218,7 +219,7 @@
|
|||||||
on:click={() => memoryWrapper.scrollIntoView({ behavior: 'smooth' })}
|
on:click={() => memoryWrapper.scrollIntoView({ behavior: 'smooth' })}
|
||||||
disabled={!galleryInView}
|
disabled={!galleryInView}
|
||||||
>
|
>
|
||||||
<CircleIconButton title="Hide gallery" icon={mdiChevronUp} color="light" />
|
<CircleIconButton title={$t('hide_gallery')} icon={mdiChevronUp} color="light" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -244,7 +245,7 @@
|
|||||||
<img
|
<img
|
||||||
class="h-full w-full rounded-2xl object-cover"
|
class="h-full w-full rounded-2xl object-cover"
|
||||||
src={getAssetThumbnailUrl({ id: previousMemory.assets[0].id, size: AssetMediaSize.Preview })}
|
src={getAssetThumbnailUrl({ id: previousMemory.assets[0].id, size: AssetMediaSize.Preview })}
|
||||||
alt="Previous memory"
|
alt={$t('previous_memory')}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
@ -252,14 +253,14 @@
|
|||||||
class="h-full w-full rounded-2xl object-cover"
|
class="h-full w-full rounded-2xl object-cover"
|
||||||
src="$lib/assets/no-thumbnail.png"
|
src="$lib/assets/no-thumbnail.png"
|
||||||
sizes="min(271px,186px)"
|
sizes="min(271px,186px)"
|
||||||
alt="Previous memory"
|
alt={$t('previous_memory')}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if previousMemory}
|
{#if previousMemory}
|
||||||
<div class="absolute bottom-4 right-4 text-left text-white">
|
<div class="absolute bottom-4 right-4 text-left text-white">
|
||||||
<p class="text-xs font-semibold text-gray-200">PREVIOUS</p>
|
<p class="text-xs font-semibold text-gray-200">{$t('previous').toUpperCase()}</p>
|
||||||
<p class="text-xl">{memoryLaneTitle(previousMemory.yearsAgo)}</p>
|
<p class="text-xl">{memoryLaneTitle(previousMemory.yearsAgo)}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -283,13 +284,18 @@
|
|||||||
<!-- CONTROL BUTTONS -->
|
<!-- CONTROL BUTTONS -->
|
||||||
{#if canGoBack}
|
{#if canGoBack}
|
||||||
<div class="absolute top-1/2 left-0 ml-4">
|
<div class="absolute top-1/2 left-0 ml-4">
|
||||||
<CircleIconButton title="Previous memory" icon={mdiChevronLeft} color="dark" on:click={toPrevious} />
|
<CircleIconButton
|
||||||
|
title={$t('previous_memory')}
|
||||||
|
icon={mdiChevronLeft}
|
||||||
|
color="dark"
|
||||||
|
on:click={toPrevious}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if canGoForward}
|
{#if canGoForward}
|
||||||
<div class="absolute top-1/2 right-0 mr-4">
|
<div class="absolute top-1/2 right-0 mr-4">
|
||||||
<CircleIconButton title="Next memory" icon={mdiChevronRight} color="dark" on:click={toNext} />
|
<CircleIconButton title={$t('next_memory')} icon={mdiChevronRight} color="dark" on:click={toNext} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@ -322,7 +328,7 @@
|
|||||||
<img
|
<img
|
||||||
class="h-full w-full rounded-2xl object-cover"
|
class="h-full w-full rounded-2xl object-cover"
|
||||||
src={getAssetThumbnailUrl({ id: nextMemory.assets[0].id, size: AssetMediaSize.Preview })}
|
src={getAssetThumbnailUrl({ id: nextMemory.assets[0].id, size: AssetMediaSize.Preview })}
|
||||||
alt="Next memory"
|
alt={$t('next_memory')}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
@ -330,14 +336,14 @@
|
|||||||
class="h-full w-full rounded-2xl object-cover"
|
class="h-full w-full rounded-2xl object-cover"
|
||||||
src="$lib/assets/no-thumbnail.png"
|
src="$lib/assets/no-thumbnail.png"
|
||||||
sizes="min(271px,186px)"
|
sizes="min(271px,186px)"
|
||||||
alt="Next memory"
|
alt={$t('next_memory')}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if nextMemory}
|
{#if nextMemory}
|
||||||
<div class="absolute bottom-4 left-4 text-left text-white">
|
<div class="absolute bottom-4 left-4 text-left text-white">
|
||||||
<p class="text-xs font-semibold text-gray-200">UP NEXT</p>
|
<p class="text-xs font-semibold text-gray-200">{$t('up_next').toUpperCase()}</p>
|
||||||
<p class="text-xl">{memoryLaneTitle(nextMemory.yearsAgo)}</p>
|
<p class="text-xl">{memoryLaneTitle(nextMemory.yearsAgo)}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -354,7 +360,7 @@
|
|||||||
class:opacity-100={!galleryInView}
|
class:opacity-100={!galleryInView}
|
||||||
>
|
>
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
title="Show gallery"
|
title={$t('show_gallery')}
|
||||||
icon={mdiChevronDown}
|
icon={mdiChevronDown}
|
||||||
color="light"
|
color="light"
|
||||||
on:click={() => memoryGallery.scrollIntoView({ behavior: 'smooth' })}
|
on:click={() => memoryGallery.scrollIntoView({ behavior: 'smooth' })}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
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 { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
done: void;
|
done: void;
|
||||||
@ -21,7 +22,7 @@
|
|||||||
|
|
||||||
<div class="w-full flex place-content-end">
|
<div class="w-full flex place-content-end">
|
||||||
<Button class="flex gap-2 place-content-center" on:click={() => dispatch('done')}>
|
<Button class="flex gap-2 place-content-center" on:click={() => dispatch('done')}>
|
||||||
<p>Theme</p>
|
<p>{$t('theme')}</p>
|
||||||
<Icon path={mdiArrowRight} size="18" />
|
<Icon path={mdiArrowRight} size="18" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
import Icon from '../elements/icon.svelte';
|
import Icon from '../elements/icon.svelte';
|
||||||
import OnboardingCard from './onboarding-card.svelte';
|
import OnboardingCard from './onboarding-card.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
done: void;
|
done: void;
|
||||||
@ -23,7 +24,9 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<OnboardingCard>
|
<OnboardingCard>
|
||||||
<p class="text-xl text-immich-primary dark:text-immich-dark-primary">STORAGE TEMPLATE</p>
|
<p class="text-xl text-immich-primary dark:text-immich-dark-primary">
|
||||||
|
{$t('storage_template_settings').toUpperCase()}
|
||||||
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
When enabled, this feature will auto-organize files based on a user-defined template. Due to stability issues the
|
When enabled, this feature will auto-organize files based on a user-defined template. Due to stability issues the
|
||||||
@ -46,7 +49,7 @@
|
|||||||
<div class="w-full flex place-content-start">
|
<div class="w-full flex place-content-start">
|
||||||
<Button class="flex gap-2 place-content-center" on:click={() => dispatch('previous')}>
|
<Button class="flex gap-2 place-content-center" on:click={() => dispatch('previous')}>
|
||||||
<Icon path={mdiArrowLeft} size="18" />
|
<Icon path={mdiArrowLeft} size="18" />
|
||||||
<p>Theme</p>
|
<p>{$t('theme')}</p>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full place-content-end">
|
<div class="flex w-full place-content-end">
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
import { colorTheme } from '$lib/stores/preferences.store';
|
import { colorTheme } from '$lib/stores/preferences.store';
|
||||||
import { moonPath, moonViewBox, sunPath, sunViewBox } from '$lib/assets/svg-paths';
|
import { moonPath, moonViewBox, sunPath, sunViewBox } from '$lib/assets/svg-paths';
|
||||||
import { Theme } from '$lib/constants';
|
import { Theme } from '$lib/constants';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
done: void;
|
done: void;
|
||||||
@ -15,7 +16,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<OnboardingCard>
|
<OnboardingCard>
|
||||||
<p class="text-xl text-immich-primary dark:text-immich-dark-primary">COLOR THEME</p>
|
<p class="text-xl text-immich-primary dark:text-immich-dark-primary">{$t('color_theme').toUpperCase()}</p>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="pb-6 font-light">Choose a color theme for your instance. You can change this later in your settings.</p>
|
<p class="pb-6 font-light">Choose a color theme for your instance. You can change this later in your settings.</p>
|
||||||
@ -31,7 +32,7 @@
|
|||||||
class="flex flex-col place-items-center place-content-center justify-around h-full w-full text-immich-primary"
|
class="flex flex-col place-items-center place-content-center justify-around h-full w-full text-immich-primary"
|
||||||
>
|
>
|
||||||
<Icon path={sunPath} viewBox={sunViewBox} size="96" />
|
<Icon path={sunPath} viewBox={sunViewBox} size="96" />
|
||||||
<p class="font-semibold text-4xl">LIGHT</p>
|
<p class="font-semibold text-4xl">{$t('light').toUpperCase()}</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@ -43,7 +44,7 @@
|
|||||||
class="flex flex-col place-items-center place-content-center justify-around h-full w-full text-immich-dark-primary"
|
class="flex flex-col place-items-center place-content-center justify-around h-full w-full text-immich-dark-primary"
|
||||||
>
|
>
|
||||||
<Icon path={moonPath} viewBox={moonViewBox} size="96" />
|
<Icon path={moonPath} viewBox={moonViewBox} size="96" />
|
||||||
<p class="font-semibold text-4xl">DARK</p>
|
<p class="font-semibold text-4xl">{$t('dark').toUpperCase()}</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -51,7 +52,7 @@
|
|||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="w-full flex place-content-end">
|
<div class="w-full flex place-content-end">
|
||||||
<Button class="flex gap-2 place-content-center" on:click={() => dispatch('done')}>
|
<Button class="flex gap-2 place-content-center" on:click={() => dispatch('done')}>
|
||||||
<p>Storage Template</p>
|
<p>{$t('admin.storage_template_settings')}</p>
|
||||||
<Icon path={mdiArrowRight} size="18" />
|
<Icon path={mdiArrowRight} size="18" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
import { getMenuContext } from '../asset-select-context-menu.svelte';
|
import { getMenuContext } from '../asset-select-context-menu.svelte';
|
||||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
import { mdiImageAlbum, mdiShareVariantOutline } from '@mdi/js';
|
import { mdiImageAlbum, mdiShareVariantOutline } from '@mdi/js';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let shared = false;
|
export let shared = false;
|
||||||
|
|
||||||
@ -37,7 +38,7 @@
|
|||||||
|
|
||||||
<MenuOption
|
<MenuOption
|
||||||
on:click={() => (showAlbumPicker = true)}
|
on:click={() => (showAlbumPicker = true)}
|
||||||
text={shared ? 'Add to shared album' : 'Add to album'}
|
text={shared ? $t('add_to_shared_album') : $t('add_to_album')}
|
||||||
icon={shared ? mdiShareVariantOutline : mdiImageAlbum}
|
icon={shared ? mdiShareVariantOutline : mdiImageAlbum}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -10,13 +10,14 @@
|
|||||||
import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline, mdiTimerSand } from '@mdi/js';
|
import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline, mdiTimerSand } from '@mdi/js';
|
||||||
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let onArchive: OnArchive;
|
export let onArchive: OnArchive;
|
||||||
|
|
||||||
export let menuItem = false;
|
export let menuItem = false;
|
||||||
export let unarchive = false;
|
export let unarchive = false;
|
||||||
|
|
||||||
$: text = unarchive ? 'Unarchive' : 'Archive';
|
$: text = unarchive ? $t('unarchive') : $t('archive');
|
||||||
$: icon = unarchive ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline;
|
$: icon = unarchive ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline;
|
||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
@ -42,7 +43,7 @@
|
|||||||
onArchive(ids, isArchived);
|
onArchive(ids, isArchived);
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: `${isArchived ? 'Archived' : 'Unarchived'} ${ids.length}`,
|
message: `${isArchived ? $t('archived') : $t('unarchived')} ${ids.length}`,
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -61,7 +62,7 @@
|
|||||||
|
|
||||||
{#if !menuItem}
|
{#if !menuItem}
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<CircleIconButton title="Loading" icon={mdiTimerSand} />
|
<CircleIconButton title={$t('loading')} icon={mdiTimerSand} />
|
||||||
{:else}
|
{:else}
|
||||||
<CircleIconButton title={text} {icon} on:click={handleArchive} />
|
<CircleIconButton title={text} {icon} on:click={handleArchive} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { AssetJobName, AssetTypeEnum, runAssetJobs } from '@immich/sdk';
|
import { AssetJobName, AssetTypeEnum, runAssetJobs } from '@immich/sdk';
|
||||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let jobs: AssetJobName[] = [
|
export let jobs: AssetJobName[] = [
|
||||||
AssetJobName.RegenerateThumbnail,
|
AssetJobName.RegenerateThumbnail,
|
||||||
@ -26,7 +27,7 @@
|
|||||||
notificationController.show({ message: getAssetJobMessage(name), type: NotificationType.Info });
|
notificationController.show({ message: getAssetJobMessage(name), type: NotificationType.Info });
|
||||||
clearSelect();
|
clearSelect();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to submit job');
|
handleError(error, $t('errors.unable_to_submit_job'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
import { mdiCalendarEditOutline } from '@mdi/js';
|
import { mdiCalendarEditOutline } from '@mdi/js';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
export let menuItem = false;
|
export let menuItem = false;
|
||||||
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
||||||
|
|
||||||
@ -20,14 +21,14 @@
|
|||||||
try {
|
try {
|
||||||
await updateAssets({ assetBulkUpdateDto: { ids, dateTimeOriginal } });
|
await updateAssets({ assetBulkUpdateDto: { ids, dateTimeOriginal } });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to change date');
|
handleError(error, $t('errors.unable_to_change_date'));
|
||||||
}
|
}
|
||||||
clearSelect();
|
clearSelect();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if menuItem}
|
{#if menuItem}
|
||||||
<MenuOption text="Change date" icon={mdiCalendarEditOutline} on:click={() => (isShowChangeDate = true)} />
|
<MenuOption text={$t('change_date')} icon={mdiCalendarEditOutline} on:click={() => (isShowChangeDate = true)} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if isShowChangeDate}
|
{#if isShowChangeDate}
|
||||||
<ChangeDate
|
<ChangeDate
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
import { mdiMapMarkerMultipleOutline } from '@mdi/js';
|
import { mdiMapMarkerMultipleOutline } from '@mdi/js';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let menuItem = false;
|
export let menuItem = false;
|
||||||
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
||||||
@ -20,7 +21,7 @@
|
|||||||
try {
|
try {
|
||||||
await updateAssets({ assetBulkUpdateDto: { ids, latitude: point.lat, longitude: point.lng } });
|
await updateAssets({ assetBulkUpdateDto: { ids, latitude: point.lat, longitude: point.lng } });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to update location');
|
handleError(error, $t('errors.unable_to_update_location'));
|
||||||
}
|
}
|
||||||
clearSelect();
|
clearSelect();
|
||||||
}
|
}
|
||||||
@ -28,7 +29,7 @@
|
|||||||
|
|
||||||
{#if menuItem}
|
{#if menuItem}
|
||||||
<MenuOption
|
<MenuOption
|
||||||
text="Change location"
|
text={$t('change_location')}
|
||||||
icon={mdiMapMarkerMultipleOutline}
|
icon={mdiMapMarkerMultipleOutline}
|
||||||
on:click={() => (isShowChangeLocation = true)}
|
on:click={() => (isShowChangeLocation = true)}
|
||||||
/>
|
/>
|
||||||
|
@ -3,12 +3,13 @@
|
|||||||
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
|
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
|
||||||
import { mdiShareVariantOutline } from '@mdi/js';
|
import { mdiShareVariantOutline } from '@mdi/js';
|
||||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
let showModal = false;
|
let showModal = false;
|
||||||
const { getAssets } = getAssetControlContext();
|
const { getAssets } = getAssetControlContext();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CircleIconButton title="Share" icon={mdiShareVariantOutline} on:click={() => (showModal = true)} />
|
<CircleIconButton title={$t('share')} icon={mdiShareVariantOutline} on:click={() => (showModal = true)} />
|
||||||
|
|
||||||
{#if showModal}
|
{#if showModal}
|
||||||
<CreateSharedLinkModal assetIds={[...getAssets()].map(({ id }) => id)} onClose={() => (showModal = false)} />
|
<CreateSharedLinkModal assetIds={[...getAssets()].map(({ id }) => id)} onClose={() => (showModal = false)} />
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
import { mdiTimerSand, mdiDeleteOutline } from '@mdi/js';
|
import { mdiTimerSand, mdiDeleteOutline } from '@mdi/js';
|
||||||
import { type OnDelete, deleteAssets } from '$lib/utils/actions';
|
import { type OnDelete, deleteAssets } from '$lib/utils/actions';
|
||||||
import DeleteAssetDialog from '../delete-asset-dialog.svelte';
|
import DeleteAssetDialog from '../delete-asset-dialog.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let onAssetDelete: OnDelete;
|
export let onAssetDelete: OnDelete;
|
||||||
export let menuItem = false;
|
export let menuItem = false;
|
||||||
@ -16,7 +17,7 @@
|
|||||||
let isShowConfirmation = false;
|
let isShowConfirmation = false;
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
|
||||||
$: label = force ? 'Permanently delete' : 'Delete';
|
$: label = force ? $t('permanently_delete') : $t('delete');
|
||||||
|
|
||||||
const handleTrash = async () => {
|
const handleTrash = async () => {
|
||||||
if (force) {
|
if (force) {
|
||||||
@ -40,7 +41,7 @@
|
|||||||
{#if menuItem}
|
{#if menuItem}
|
||||||
<MenuOption text={label} icon={mdiDeleteOutline} on:click={handleTrash} />
|
<MenuOption text={label} icon={mdiDeleteOutline} on:click={handleTrash} />
|
||||||
{:else if loading}
|
{:else if loading}
|
||||||
<CircleIconButton title="Loading" icon={mdiTimerSand} />
|
<CircleIconButton title={$t('loading')} icon={mdiTimerSand} />
|
||||||
{:else}
|
{:else}
|
||||||
<CircleIconButton title={label} icon={mdiDeleteOutline} on:click={handleTrash} />
|
<CircleIconButton title={label} icon={mdiDeleteOutline} on:click={handleTrash} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
import { mdiCloudDownloadOutline, mdiFileDownloadOutline, mdiFolderDownloadOutline } from '@mdi/js';
|
import { mdiCloudDownloadOutline, mdiFileDownloadOutline, mdiFolderDownloadOutline } from '@mdi/js';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let filename = 'immich.zip';
|
export let filename = 'immich.zip';
|
||||||
export let menuItem = false;
|
export let menuItem = false;
|
||||||
@ -26,7 +27,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if menuItem}
|
{#if menuItem}
|
||||||
<MenuOption text="Download" icon={menuItemIcon} on:click={handleDownloadFiles} />
|
<MenuOption text={$t('download')} icon={menuItemIcon} on:click={handleDownloadFiles} />
|
||||||
{:else}
|
{:else}
|
||||||
<CircleIconButton title="Download" icon={mdiCloudDownloadOutline} on:click={handleDownloadFiles} />
|
<CircleIconButton title={$t('download')} icon={mdiCloudDownloadOutline} on:click={handleDownloadFiles} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -10,13 +10,14 @@
|
|||||||
import { updateAssets } from '@immich/sdk';
|
import { updateAssets } from '@immich/sdk';
|
||||||
import { mdiHeartMinusOutline, mdiHeartOutline, mdiTimerSand } from '@mdi/js';
|
import { mdiHeartMinusOutline, mdiHeartOutline, mdiTimerSand } from '@mdi/js';
|
||||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let onFavorite: OnFavorite;
|
export let onFavorite: OnFavorite;
|
||||||
|
|
||||||
export let menuItem = false;
|
export let menuItem = false;
|
||||||
export let removeFavorite: boolean;
|
export let removeFavorite: boolean;
|
||||||
|
|
||||||
$: text = removeFavorite ? 'Remove from favorites' : 'Favorite';
|
$: text = removeFavorite ? $t('remove_from_favorites') : $t('favorite');
|
||||||
$: icon = removeFavorite ? mdiHeartMinusOutline : mdiHeartOutline;
|
$: icon = removeFavorite ? mdiHeartMinusOutline : mdiHeartOutline;
|
||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
@ -62,7 +63,7 @@
|
|||||||
|
|
||||||
{#if !menuItem}
|
{#if !menuItem}
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<CircleIconButton title="Loading" icon={mdiTimerSand} />
|
<CircleIconButton title={$t('loading')} icon={mdiTimerSand} />
|
||||||
{:else}
|
{:else}
|
||||||
<CircleIconButton title={text} {icon} on:click={handleFavorite} />
|
<CircleIconButton title={text} {icon} on:click={handleFavorite} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
import { s } from '$lib/utils';
|
import { s } from '$lib/utils';
|
||||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let album: AlbumResponseDto;
|
export let album: AlbumResponseDto;
|
||||||
export let onRemove: ((assetIds: string[]) => void) | undefined;
|
export let onRemove: ((assetIds: string[]) => void) | undefined;
|
||||||
@ -56,7 +57,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if menuItem}
|
{#if menuItem}
|
||||||
<MenuOption text="Remove from album" icon={mdiImageRemoveOutline} on:click={removeFromAlbum} />
|
<MenuOption text={$t('remove_from_album')} icon={mdiImageRemoveOutline} on:click={removeFromAlbum} />
|
||||||
{:else}
|
{:else}
|
||||||
<CircleIconButton title="Remove from album" icon={mdiDeleteOutline} on:click={removeFromAlbum} />
|
<CircleIconButton title={$t('remove_from_album')} icon={mdiDeleteOutline} on:click={removeFromAlbum} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
import { NotificationType, notificationController } from '../../shared-components/notification/notification';
|
import { NotificationType, notificationController } from '../../shared-components/notification/notification';
|
||||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let sharedLink: SharedLinkResponseDto;
|
export let sharedLink: SharedLinkResponseDto;
|
||||||
|
|
||||||
@ -55,4 +56,4 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CircleIconButton title="Remove from shared link" on:click={handleRemove} icon={mdiDeleteOutline} />
|
<CircleIconButton title={$t('remove_from_shared_link')} on:click={handleRemove} icon={mdiDeleteOutline} />
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
import { mdiHistory } from '@mdi/js';
|
import { mdiHistory } from '@mdi/js';
|
||||||
import Button from '../../elements/buttons/button.svelte';
|
import Button from '../../elements/buttons/button.svelte';
|
||||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let onRestore: OnRestore | undefined;
|
export let onRestore: OnRestore | undefined;
|
||||||
|
|
||||||
@ -32,7 +33,7 @@
|
|||||||
|
|
||||||
clearSelect();
|
clearSelect();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Error restoring assets');
|
handleError(error, $t('errors.unable_to_restore_assets'));
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
@ -41,5 +42,5 @@
|
|||||||
|
|
||||||
<Button disabled={loading} size="sm" color="transparent-gray" shadow={false} rounded="lg" on:click={handleRestore}>
|
<Button disabled={loading} size="sm" color="transparent-gray" shadow={false} rounded="lg" on:click={handleRestore}>
|
||||||
<Icon path={mdiHistory} size="24" />
|
<Icon path={mdiHistory} size="24" />
|
||||||
<span class="ml-2">Restore</span>
|
<span class="ml-2">{$t('restore')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import { type AssetStore, isSelectingAllAssets } from '$lib/stores/assets.store';
|
import { type AssetStore, isSelectingAllAssets } from '$lib/stores/assets.store';
|
||||||
import { mdiSelectAll, mdiSelectRemove } from '@mdi/js';
|
import { mdiSelectAll, mdiSelectRemove } from '@mdi/js';
|
||||||
import { selectAllAssets } from '$lib/utils/asset-utils';
|
import { selectAllAssets } from '$lib/utils/asset-utils';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let assetStore: AssetStore;
|
export let assetStore: AssetStore;
|
||||||
export let assetInteractionStore: AssetInteractionStore;
|
export let assetInteractionStore: AssetInteractionStore;
|
||||||
@ -19,7 +20,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $isSelectingAllAssets}
|
{#if $isSelectingAllAssets}
|
||||||
<CircleIconButton title="Unselect all" icon={mdiSelectRemove} on:click={handleCancel} />
|
<CircleIconButton title={$t('unselect_all')} icon={mdiSelectRemove} on:click={handleCancel} />
|
||||||
{:else}
|
{:else}
|
||||||
<CircleIconButton title="Select all" icon={mdiSelectAll} on:click={handleSelectAll} />
|
<CircleIconButton title={$t('select_all')} icon={mdiSelectAll} on:click={handleSelectAll} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import { mdiImageMinusOutline, mdiImageMultipleOutline } from '@mdi/js';
|
import { mdiImageMinusOutline, mdiImageMultipleOutline } from '@mdi/js';
|
||||||
import { stackAssets, unstackAssets } from '$lib/utils/asset-utils';
|
import { stackAssets, unstackAssets } from '$lib/utils/asset-utils';
|
||||||
import type { OnStack, OnUnstack } from '$lib/utils/actions';
|
import type { OnStack, OnUnstack } from '$lib/utils/actions';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let unstack = false;
|
export let unstack = false;
|
||||||
export let onStack: OnStack | undefined;
|
export let onStack: OnStack | undefined;
|
||||||
@ -39,7 +40,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if unstack}
|
{#if unstack}
|
||||||
<MenuOption text="Un-stack" icon={mdiImageMinusOutline} on:click={handleUnstack} />
|
<MenuOption text={$t('un-stack')} icon={mdiImageMinusOutline} on:click={handleUnstack} />
|
||||||
{:else}
|
{:else}
|
||||||
<MenuOption text="Stack" icon={mdiImageMultipleOutline} on:click={handleStack} />
|
<MenuOption text={$t('stack')} icon={mdiImageMultipleOutline} on:click={handleStack} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||||
import Checkbox from '$lib/components/elements/checkbox.svelte';
|
import Checkbox from '$lib/components/elements/checkbox.svelte';
|
||||||
import { s } from '$lib/utils';
|
import { s } from '$lib/utils';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let size: number;
|
export let size: number;
|
||||||
|
|
||||||
@ -24,7 +25,7 @@
|
|||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
title="Permanently delete asset{s(size)}"
|
title="Permanently delete asset{s(size)}"
|
||||||
confirmText="Delete"
|
confirmText={$t('delete')}
|
||||||
onConfirm={handleConfirm}
|
onConfirm={handleConfirm}
|
||||||
onCancel={() => dispatch('cancel')}
|
onCancel={() => dispatch('cancel')}
|
||||||
>
|
>
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte';
|
import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte';
|
||||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||||
import type { Viewport } from '$lib/stores/assets.store';
|
import type { Viewport } from '$lib/stores/assets.store';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let sharedLink: SharedLinkResponseDto;
|
export let sharedLink: SharedLinkResponseDto;
|
||||||
export let isOwned: boolean;
|
export let isOwned: boolean;
|
||||||
@ -74,7 +75,7 @@
|
|||||||
<section class="bg-immich-bg dark:bg-immich-dark-bg">
|
<section class="bg-immich-bg dark:bg-immich-dark-bg">
|
||||||
{#if isMultiSelectionMode}
|
{#if isMultiSelectionMode}
|
||||||
<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
|
<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
|
||||||
<CircleIconButton title="Select all" icon={mdiSelectAll} on:click={handleSelectAll} />
|
<CircleIconButton title={$t('select_all')} icon={mdiSelectAll} on:click={handleSelectAll} />
|
||||||
{#if sharedLink?.allowDownload}
|
{#if sharedLink?.allowDownload}
|
||||||
<DownloadAction filename="immich-shared.zip" />
|
<DownloadAction filename="immich-shared.zip" />
|
||||||
{/if}
|
{/if}
|
||||||
@ -90,11 +91,15 @@
|
|||||||
|
|
||||||
<svelte:fragment slot="trailing">
|
<svelte:fragment slot="trailing">
|
||||||
{#if sharedLink?.allowUpload}
|
{#if sharedLink?.allowUpload}
|
||||||
<CircleIconButton title="Add Photos" on:click={() => handleUploadAssets()} icon={mdiFileImagePlusOutline} />
|
<CircleIconButton
|
||||||
|
title={$t('add_photos')}
|
||||||
|
on:click={() => handleUploadAssets()}
|
||||||
|
icon={mdiFileImagePlusOutline}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if sharedLink?.allowDownload}
|
{#if sharedLink?.allowDownload}
|
||||||
<CircleIconButton title="Download" on:click={downloadAssets} icon={mdiFolderDownloadOutline} />
|
<CircleIconButton title={$t('download')} on:click={downloadAssets} icon={mdiFolderDownloadOutline} />
|
||||||
{/if}
|
{/if}
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</ControlAppBar>
|
</ControlAppBar>
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
import { normalizeSearchString } from '$lib/utils/string-utils';
|
import { normalizeSearchString } from '$lib/utils/string-utils';
|
||||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||||
import { initInput } from '$lib/actions/focus';
|
import { initInput } from '$lib/actions/focus';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
let albums: AlbumResponseDto[] = [];
|
let albums: AlbumResponseDto[] = [];
|
||||||
let recentAlbums: AlbumResponseDto[] = [];
|
let recentAlbums: AlbumResponseDto[] = [];
|
||||||
@ -47,9 +48,9 @@
|
|||||||
|
|
||||||
const getTitle = () => {
|
const getTitle = () => {
|
||||||
if (shared) {
|
if (shared) {
|
||||||
return 'Add to shared album';
|
return $t('add_to_shared_album');
|
||||||
}
|
}
|
||||||
return 'Add to album';
|
return $t('add_to_album');
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -71,7 +72,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<input
|
<input
|
||||||
class="border-b-4 border-immich-bg bg-immich-bg px-6 py-2 text-2xl focus:border-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:focus:border-immich-dark-primary"
|
class="border-b-4 border-immich-bg bg-immich-bg px-6 py-2 text-2xl focus:border-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:focus:border-immich-dark-primary"
|
||||||
placeholder="Search"
|
placeholder={$t('search')}
|
||||||
bind:value={search}
|
bind:value={search}
|
||||||
use:initInput
|
use:initInput
|
||||||
/>
|
/>
|
||||||
@ -90,7 +91,7 @@
|
|||||||
</button>
|
</button>
|
||||||
{#if filteredAlbums.length > 0}
|
{#if filteredAlbums.length > 0}
|
||||||
{#if !shared && search.length === 0}
|
{#if !shared && search.length === 0}
|
||||||
<p class="px-5 py-3 text-xs">RECENT</p>
|
<p class="px-5 py-3 text-xs">{$t('recent').toUpperCase()}</p>
|
||||||
{#each recentAlbums as album (album.id)}
|
{#each recentAlbums as album (album.id)}
|
||||||
<AlbumListItem {album} on:album={() => handleSelect(album)} />
|
<AlbumListItem {album} on:album={() => handleSelect(album)} />
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import ConfirmDialog from './dialog/confirm-dialog.svelte';
|
import ConfirmDialog from './dialog/confirm-dialog.svelte';
|
||||||
import Combobox from './combobox.svelte';
|
import Combobox from './combobox.svelte';
|
||||||
import DateInput from '../elements/date-input.svelte';
|
import DateInput from '../elements/date-input.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let initialDate: DateTime = DateTime.now();
|
export let initialDate: DateTime = DateTime.now();
|
||||||
|
|
||||||
@ -57,7 +58,7 @@
|
|||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
confirmColor="primary"
|
confirmColor="primary"
|
||||||
title="Edit date and time"
|
title={$t('edit_date_and_time')}
|
||||||
prompt="Please select a new date:"
|
prompt="Please select a new date:"
|
||||||
disabled={!date.isValid}
|
disabled={!date.isValid}
|
||||||
onConfirm={handleConfirm}
|
onConfirm={handleConfirm}
|
||||||
@ -65,7 +66,7 @@
|
|||||||
>
|
>
|
||||||
<div class="flex flex-col text-md px-4 text-center gap-2" slot="prompt">
|
<div class="flex flex-col text-md px-4 text-center gap-2" slot="prompt">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<label for="datetime">Date and Time</label>
|
<label for="datetime">{$t('date_and_time')}</label>
|
||||||
<DateInput
|
<DateInput
|
||||||
class="immich-form-input text-sm my-4 w-full"
|
class="immich-form-input text-sm my-4 w-full"
|
||||||
id="datetime"
|
id="datetime"
|
||||||
@ -74,7 +75,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col w-full mt-2">
|
<div class="flex flex-col w-full mt-2">
|
||||||
<Combobox bind:selectedOption label="Timezone" options={timezones} placeholder="Search timezone..." />
|
<Combobox bind:selectedOption label={$t('timezone')} options={timezones} placeholder={$t('search_timezone')} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
import { searchPlaces, type AssetResponseDto, type PlacesResponseDto } from '@immich/sdk';
|
import { searchPlaces, type AssetResponseDto, type PlacesResponseDto } from '@immich/sdk';
|
||||||
import SearchBar from '../elements/search-bar.svelte';
|
import SearchBar from '../elements/search-bar.svelte';
|
||||||
import { listNavigation } from '$lib/actions/list-navigation';
|
import { listNavigation } from '$lib/actions/list-navigation';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let asset: AssetResponseDto | undefined = undefined;
|
export let asset: AssetResponseDto | undefined = undefined;
|
||||||
|
|
||||||
@ -88,7 +89,7 @@
|
|||||||
// skip error when a newer search is happening
|
// skip error when a newer search is happening
|
||||||
if (latestSearchTimeout === searchTimeout) {
|
if (latestSearchTimeout === searchTimeout) {
|
||||||
places = [];
|
places = [];
|
||||||
handleError(error, "Can't search places");
|
handleError(error, $t('cant_search_places'));
|
||||||
showLoadingSpinner = false;
|
showLoadingSpinner = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -105,7 +106,7 @@
|
|||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
confirmColor="primary"
|
confirmColor="primary"
|
||||||
title="Change location"
|
title={$t('change_location')}
|
||||||
width="wide"
|
width="wide"
|
||||||
onConfirm={handleConfirm}
|
onConfirm={handleConfirm}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
@ -118,7 +119,7 @@
|
|||||||
>
|
>
|
||||||
<button type="button" class="w-full" on:click={() => (hideSuggestion = false)}>
|
<button type="button" class="w-full" on:click={() => (hideSuggestion = false)}>
|
||||||
<SearchBar
|
<SearchBar
|
||||||
placeholder="Search places"
|
placeholder={$t('search_places')}
|
||||||
bind:name={searchWord}
|
bind:name={searchWord}
|
||||||
{showLoadingSpinner}
|
{showLoadingSpinner}
|
||||||
on:reset={() => {
|
on:reset={() => {
|
||||||
@ -147,7 +148,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label for="datetime">Pick a location</label>
|
<label for="datetime">{$t('pick_a_location')}</label>
|
||||||
<div class="h-[500px] min-h-[300px] w-full">
|
<div class="h-[500px] min-h-[300px] w-full">
|
||||||
{#await import('../shared-components/map/map.svelte')}
|
{#await import('../shared-components/map/map.svelte')}
|
||||||
{#await delay(timeToLoadTheMap) then}
|
{#await delay(timeToLoadTheMap) then}
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
import { focusOutside } from '$lib/actions/focus-outside';
|
import { focusOutside } from '$lib/actions/focus-outside';
|
||||||
import { generateId } from '$lib/utils/generate-id';
|
import { generateId } from '$lib/utils/generate-id';
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let label: string;
|
export let label: string;
|
||||||
export let hideLabel = false;
|
export let hideLabel = false;
|
||||||
@ -200,7 +201,7 @@
|
|||||||
class:pointer-events-none={!selectedOption}
|
class:pointer-events-none={!selectedOption}
|
||||||
>
|
>
|
||||||
{#if selectedOption}
|
{#if selectedOption}
|
||||||
<CircleIconButton on:click={onClear} title="Clear value" icon={mdiClose} size="16" padding="2" />
|
<CircleIconButton on:click={onClear} title={$t('clear_value')} icon={mdiClose} size="16" padding="2" />
|
||||||
{:else if !isOpen}
|
{:else if !isOpen}
|
||||||
<Icon path={mdiUnfoldMoreHorizontal} ariaHidden={true} />
|
<Icon path={mdiUnfoldMoreHorizontal} ariaHidden={true} />
|
||||||
{/if}
|
{/if}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user