From 52247c36505405c8cd2bf5e5ab2373be4ea24a39 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Mon, 2 Dec 2024 20:28:50 -0500 Subject: [PATCH 01/19] fix(server): always set transcoding device, prefer renderD* (#14455) always set device, prefer renderD* --- server/src/services/media.service.spec.ts | 63 +++++++----- server/src/utils/media.ts | 115 +++++++--------------- 2 files changed, 73 insertions(+), 105 deletions(-) diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 5fd947e86097f..909b9d02e32d3 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -1637,7 +1637,10 @@ describe(MediaService.name, () => { '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', expect.objectContaining({ - inputOptions: expect.arrayContaining(['-init_hw_device qsv=hw', '-filter_hw_device hw']), + inputOptions: expect.arrayContaining([ + '-init_hw_device qsv=hw,child_device=/dev/dri/renderD128', + '-filter_hw_device hw', + ]), outputOptions: expect.arrayContaining([ `-c:v h264_qsv`, '-c:a copy', @@ -1696,7 +1699,10 @@ describe(MediaService.name, () => { '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', expect.objectContaining({ - inputOptions: expect.arrayContaining(['-init_hw_device qsv=hw', '-filter_hw_device hw']), + inputOptions: expect.arrayContaining([ + '-init_hw_device qsv=hw,child_device=/dev/dri/renderD128', + '-filter_hw_device hw', + ]), outputOptions: expect.not.arrayContaining([expect.stringContaining('-preset')]), twoPass: false, }), @@ -1713,7 +1719,10 @@ describe(MediaService.name, () => { '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', expect.objectContaining({ - inputOptions: expect.arrayContaining(['-init_hw_device qsv=hw', '-filter_hw_device hw']), + inputOptions: expect.arrayContaining([ + '-init_hw_device qsv=hw,child_device=/dev/dri/renderD128', + '-filter_hw_device hw', + ]), outputOptions: expect.arrayContaining(['-low_power 1']), twoPass: false, }), @@ -1730,6 +1739,26 @@ describe(MediaService.name, () => { expect(loggerMock.debug).toHaveBeenCalledWith('No devices found in /dev/dri.'); }); + it('should prefer higher index renderD* device for qsv', async () => { + storageMock.readdir.mockResolvedValue(['card1', 'renderD129', 'card0', 'renderD128']); + mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); + systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV } }); + assetMock.getByIds.mockResolvedValue([assetStub.video]); + await sut.handleVideoConversion({ id: assetStub.video.id }); + expect(mediaMock.transcode).toHaveBeenCalledWith( + '/original/path.ext', + 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.objectContaining({ + inputOptions: expect.arrayContaining([ + '-init_hw_device qsv=hw,child_device=/dev/dri/renderD129', + '-filter_hw_device hw', + ]), + outputOptions: expect.arrayContaining([`-c:v h264_qsv`]), + twoPass: false, + }), + ); + }); + it('should use hardware decoding for qsv if enabled', async () => { storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); @@ -1750,6 +1779,7 @@ describe(MediaService.name, () => { '-async_depth 4', '-noautorotate', '-threads 1', + '-qsv_device /dev/dri/renderD128', ]), outputOptions: expect.arrayContaining([ expect.stringContaining('scale_qsv=-1:720:async_depth=4:mode=hq:format=nv12'), @@ -1939,28 +1969,8 @@ describe(MediaService.name, () => { ); }); - it('should prefer gpu for vaapi if available', async () => { - storageMock.readdir.mockResolvedValue(['renderD129', 'card1', 'card0', 'renderD128']); - mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); - systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } }); - assetMock.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleVideoConversion({ id: assetStub.video.id }); - expect(mediaMock.transcode).toHaveBeenCalledWith( - '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', - expect.objectContaining({ - inputOptions: expect.arrayContaining([ - '-init_hw_device vaapi=accel:/dev/dri/card1', - '-filter_hw_device accel', - ]), - outputOptions: expect.arrayContaining([`-c:v h264_vaapi`]), - twoPass: false, - }), - ); - }); - - it('should prefer higher index gpu node', async () => { - storageMock.readdir.mockResolvedValue(['renderD129', 'renderD130', 'renderD128']); + it('should prefer higher index renderD* device for vaapi', async () => { + storageMock.readdir.mockResolvedValue(['card1', 'renderD129', 'card0', 'renderD128']); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } }); assetMock.getByIds.mockResolvedValue([assetStub.video]); @@ -1970,7 +1980,7 @@ describe(MediaService.name, () => { 'upload/encoded-video/user-id/as/se/asset-id.mp4', expect.objectContaining({ inputOptions: expect.arrayContaining([ - '-init_hw_device vaapi=accel:/dev/dri/renderD130', + '-init_hw_device vaapi=accel:/dev/dri/renderD129', '-filter_hw_device accel', ]), outputOptions: expect.arrayContaining([`-c:v h264_vaapi`]), @@ -2020,6 +2030,7 @@ describe(MediaService.name, () => { '-hwaccel_output_format vaapi', '-noautorotate', '-threads 1', + '-hwaccel_device /dev/dri/renderD128', ]), outputOptions: expect.arrayContaining([ expect.stringContaining('scale_vaapi=-2:720:mode=hq:out_range=pc:format=nv12'), diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index c7df4d27a76a4..226f95b4bb69e 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -322,14 +322,14 @@ export class BaseConfig implements VideoCodecSWConfig { } export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig { - protected devices: string[]; + protected device: string; constructor( protected config: SystemConfigFFmpegDto, devices: string[] = [], ) { super(config); - this.devices = this.validateDevices(devices); + this.device = this.getDevice(devices); } getSupportedCodecs() { @@ -337,18 +337,29 @@ export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig { } validateDevices(devices: string[]) { - return devices - .filter((device) => device.startsWith('renderD') || device.startsWith('card')) - .sort((a, b) => { - // order GPU devices first - if (a.startsWith('card') && b.startsWith('renderD')) { - return -1; - } - if (a.startsWith('renderD') && b.startsWith('card')) { - return 1; - } - return -a.localeCompare(b); - }); + if (devices.length === 0) { + throw new Error('No /dev/dri devices found. If using Docker, make sure at least one /dev/dri device is mounted'); + } + + return devices.filter(function (device) { + return device.startsWith('renderD') || device.startsWith('card'); + }); + } + + getDevice(devices: string[]) { + if (this.config.preferredHwDevice === 'auto') { + // eslint-disable-next-line unicorn/no-array-reduce + return `/dev/dri/${this.validateDevices(devices).reduce(function (a, b) { + return a.localeCompare(b) < 0 ? b : a; + })}`; + } + + const deviceName = this.config.preferredHwDevice.replace('/dev/dri/', ''); + if (!devices.includes(deviceName)) { + throw new Error(`Device '${deviceName}' does not exist. If using Docker, make sure this device is mounted`); + } + + return `/dev/dri/${deviceName}`; } getVideoCodec(): string { @@ -361,20 +372,6 @@ export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig { } return this.config.gopSize; } - - getPreferredHardwareDevice(): string | undefined { - const device = this.config.preferredHwDevice; - if (device === 'auto') { - return; - } - - const deviceName = device.replace('/dev/dri/', ''); - if (!this.devices.includes(deviceName)) { - throw new Error(`Device '${device}' does not exist`); - } - - return `/dev/dri/${deviceName}`; - } } export class ThumbnailConfig extends BaseConfig { @@ -513,12 +510,16 @@ export class AV1Config extends BaseConfig { } export class NvencSwDecodeConfig extends BaseHWConfig { + getDevice() { + return '0'; + } + getSupportedCodecs() { return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.AV1]; } getBaseInputOptions() { - return ['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']; + return [`-init_hw_device cuda=cuda:${this.device}`, '-filter_hw_device cuda']; } getBaseOutputOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) { @@ -641,17 +642,7 @@ export class NvencHwDecodeConfig extends NvencSwDecodeConfig { export class QsvSwDecodeConfig extends BaseHWConfig { getBaseInputOptions() { - if (this.devices.length === 0) { - throw new Error('No QSV device found'); - } - - let qsvString = ''; - const hwDevice = this.getPreferredHardwareDevice(); - if (hwDevice) { - qsvString = `,child_device=${hwDevice}`; - } - - return [`-init_hw_device qsv=hw${qsvString}`, '-filter_hw_device hw']; + return [`-init_hw_device qsv=hw,child_device=${this.device}`, '-filter_hw_device hw']; } getBaseOutputOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) { @@ -721,23 +712,14 @@ export class QsvSwDecodeConfig extends BaseHWConfig { export class QsvHwDecodeConfig extends QsvSwDecodeConfig { getBaseInputOptions() { - if (this.devices.length === 0) { - throw new Error('No QSV device found'); - } - - const options = [ + return [ '-hwaccel qsv', '-hwaccel_output_format qsv', '-async_depth 4', '-noautorotate', + `-qsv_device ${this.device}`, ...this.getInputThreadOptions(), ]; - const hwDevice = this.getPreferredHardwareDevice(); - if (hwDevice) { - options.push(`-qsv_device ${hwDevice}`); - } - - return options; } getFilterOptions(videoStream: VideoStreamInfo) { @@ -789,16 +771,7 @@ export class QsvHwDecodeConfig extends QsvSwDecodeConfig { export class VaapiSwDecodeConfig extends BaseHWConfig { getBaseInputOptions() { - if (this.devices.length === 0) { - throw new Error('No VAAPI device found'); - } - - let hwDevice = this.getPreferredHardwareDevice(); - if (!hwDevice) { - hwDevice = `/dev/dri/${this.devices[0]}`; - } - - return [`-init_hw_device vaapi=accel:${hwDevice}`, '-filter_hw_device accel']; + return [`-init_hw_device vaapi=accel:${this.device}`, '-filter_hw_device accel']; } getFilterOptions(videoStream: VideoStreamInfo) { @@ -856,22 +829,13 @@ export class VaapiSwDecodeConfig extends BaseHWConfig { export class VaapiHwDecodeConfig extends VaapiSwDecodeConfig { getBaseInputOptions() { - if (this.devices.length === 0) { - throw new Error('No VAAPI device found'); - } - - const options = [ + return [ '-hwaccel vaapi', '-hwaccel_output_format vaapi', '-noautorotate', + `-hwaccel_device ${this.device}`, ...this.getInputThreadOptions(), ]; - const hwDevice = this.getPreferredHardwareDevice(); - if (hwDevice) { - options.push(`-hwaccel_device ${hwDevice}`); - } - - return options; } getFilterOptions(videoStream: VideoStreamInfo) { @@ -934,9 +898,6 @@ export class RkmppSwDecodeConfig extends BaseHWConfig { } getBaseInputOptions(): string[] { - if (this.devices.length === 0) { - throw new Error('No RKMPP device found'); - } return []; } @@ -987,10 +948,6 @@ export class RkmppHwDecodeConfig extends RkmppSwDecodeConfig { } getBaseInputOptions() { - if (this.devices.length === 0) { - throw new Error('No RKMPP device found'); - } - return ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga', '-noautorotate']; } From ba9b9353bc6fb33ea961c5f6c7e1367eb6dd6d1e Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:04:42 -0500 Subject: [PATCH 02/19] fix(server): show people without thumbnails (#14460) * show people without thumbnails * redundant clause * updated sql --- server/src/queries/person.repository.sql | 8 ++------ server/src/repositories/person.repository.ts | 7 ++----- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index 5616559d7d06d..a7e683fca1e72 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -20,13 +20,12 @@ SELECT "person"."isHidden" AS "person_isHidden" FROM "person" "person" - LEFT JOIN "asset_faces" "face" ON "face"."personId" = "person"."id" + INNER JOIN "asset_faces" "face" ON "face"."personId" = "person"."id" INNER JOIN "assets" "asset" ON "asset"."id" = "face"."assetId" AND ("asset"."deletedAt" IS NULL) WHERE "person"."ownerId" = $1 AND "asset"."isArchived" = false - AND "person"."thumbnailPath" != '' AND "person"."isHidden" = false GROUP BY "person"."id" @@ -257,15 +256,12 @@ SELECT ) AS "hidden" FROM "person" "person" - LEFT JOIN "asset_faces" "face" ON "face"."personId" = "person"."id" + INNER JOIN "asset_faces" "face" ON "face"."personId" = "person"."id" INNER JOIN "assets" "asset" ON "asset"."id" = "face"."assetId" AND ("asset"."deletedAt" IS NULL) WHERE "person"."ownerId" = $1 AND "asset"."isArchived" = false - AND "person"."thumbnailPath" != '' -HAVING - COUNT("face"."assetId") != 0 -- PersonRepository.getFacesByIds SELECT diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 56116d7b3bf97..81958d269d27b 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -86,7 +86,7 @@ export class PersonRepository implements IPersonRepository { getAllForUser(pagination: PaginationOptions, userId: string, options?: PersonSearchOptions): Paginated { const queryBuilder = this.personRepository .createQueryBuilder('person') - .leftJoin('person.faces', 'face') + .innerJoin('person.faces', 'face') .where('person.ownerId = :userId', { userId }) .innerJoin('face.asset', 'asset') .andWhere('asset.isArchived = false') @@ -95,7 +95,6 @@ export class PersonRepository implements IPersonRepository { .addOrderBy('COUNT(face.assetId)', 'DESC') .addOrderBy("NULLIF(person.name, '')", 'ASC', 'NULLS LAST') .addOrderBy('person.createdAt') - .andWhere("person.thumbnailPath != ''") .having("person.name != '' OR COUNT(face.assetId) >= :faces", { faces: options?.minimumFaceCount || 1 }) .groupBy('person.id'); if (!options?.withHidden) { @@ -232,14 +231,12 @@ export class PersonRepository implements IPersonRepository { async getNumberOfPeople(userId: string): Promise { const items = await this.personRepository .createQueryBuilder('person') - .leftJoin('person.faces', 'face') + .innerJoin('person.faces', 'face') .where('person.ownerId = :userId', { userId }) .innerJoin('face.asset', 'asset') .andWhere('asset.isArchived = false') - .andWhere("person.thumbnailPath != ''") .select('COUNT(DISTINCT(person.id))', 'total') .addSelect('COUNT(DISTINCT(person.id)) FILTER (WHERE person.isHidden = true)', 'hidden') - .having('COUNT(face.assetId) != 0') .getRawOne(); if (items == undefined) { From 411878c0aac9e7094cb404e7eb8907d5d85630a6 Mon Sep 17 00:00:00 2001 From: Alessandro Piccin <117726828+alessandrv@users.noreply.github.com> Date: Wed, 4 Dec 2024 20:53:55 +0100 Subject: [PATCH 03/19] fix(mobile): album most recent sorting on mobile (#13766) * Fix album most recent sorting on mobile * fix: format * fix: format --------- Co-authored-by: Alex --- .../album/album_sort_by_options.provider.dart | 19 +++++--- mobile/test/fixtures/album.stub.dart | 45 +++++++++++++++++++ .../album_sort_by_options_provider_test.dart | 40 ++++++++++++----- 3 files changed, 87 insertions(+), 17 deletions(-) diff --git a/mobile/lib/providers/album/album_sort_by_options.provider.dart b/mobile/lib/providers/album/album_sort_by_options.provider.dart index 216688ee15804..cafde372530c9 100644 --- a/mobile/lib/providers/album/album_sort_by_options.provider.dart +++ b/mobile/lib/providers/album/album_sort_by_options.provider.dart @@ -39,12 +39,21 @@ class _AlbumSortHandlers { static const AlbumSortFn mostRecent = _sortByMostRecent; static List _sortByMostRecent(List albums, bool isReverse) { final sorted = albums.sorted((a, b) { - if (a.endDate != null && b.endDate != null) { - return a.endDate!.compareTo(b.endDate!); + if (a.endDate == null && b.endDate == null) { + return 0; } - if (a.endDate == null) return 1; - if (b.endDate == null) return -1; - return 0; + + if (a.endDate == null) { + // Put nulls at the end for recent sorting + return 1; + } + + if (b.endDate == null) { + return -1; + } + + // Sort by descending recent date + return b.endDate!.compareTo(a.endDate!); }); return (isReverse ? sorted.reversed : sorted).toList(); } diff --git a/mobile/test/fixtures/album.stub.dart b/mobile/test/fixtures/album.stub.dart index 4fa0dac1d243b..e820f193d5450 100644 --- a/mobile/test/fixtures/album.stub.dart +++ b/mobile/test/fixtures/album.stub.dart @@ -54,4 +54,49 @@ final class AlbumStub { ..assets.addAll([AssetStub.image1, AssetStub.image2]) ..activityEnabled = true ..owner.value = UserStub.admin; + + static final create2020end2020Album = Album( + name: "create2020update2020Album", + localId: "create2020update2020Album-local", + remoteId: "create2020update2020Album-remote", + createdAt: DateTime(2020), + modifiedAt: DateTime(2020), + shared: false, + activityEnabled: false, + startDate: DateTime(2020), + endDate: DateTime(2020), + ); + static final create2020end2022Album = Album( + name: "create2020update2021Album", + localId: "create2020update2021Album-local", + remoteId: "create2020update2021Album-remote", + createdAt: DateTime(2020), + modifiedAt: DateTime(2022), + shared: false, + activityEnabled: false, + startDate: DateTime(2020), + endDate: DateTime(2022), + ); + static final create2020end2024Album = Album( + name: "create2020update2022Album", + localId: "create2020update2022Album-local", + remoteId: "create2020update2022Album-remote", + createdAt: DateTime(2020), + modifiedAt: DateTime(2024), + shared: false, + activityEnabled: false, + startDate: DateTime(2020), + endDate: DateTime(2024), + ); + static final create2020end2026Album = Album( + name: "create2020update2023Album", + localId: "create2020update2023Album-local", + remoteId: "create2020update2023Album-remote", + createdAt: DateTime(2020), + modifiedAt: DateTime(2026), + shared: false, + activityEnabled: false, + startDate: DateTime(2020), + endDate: DateTime(2026), + ); } diff --git a/mobile/test/modules/album/album_sort_by_options_provider_test.dart b/mobile/test/modules/album/album_sort_by_options_provider_test.dart index 84a7e6e9b8465..bfb61ef4029f7 100644 --- a/mobile/test/modules/album/album_sort_by_options_provider_test.dart +++ b/mobile/test/modules/album/album_sort_by_options_provider_test.dart @@ -147,24 +147,40 @@ void main() { group("Album sort - Most Recent", () { const mostRecent = AlbumSortMode.mostRecent; - test("Most Recent - ASC", () { - final sorted = mostRecent.sortFn(albums, false); + test("Most Recent - DESC", () { + final sorted = mostRecent.sortFn( + [ + AlbumStub.create2020end2020Album, + AlbumStub.create2020end2022Album, + AlbumStub.create2020end2024Album, + AlbumStub.create2020end2026Album, + ], + false, + ); final sortedList = [ - AlbumStub.sharedWithUser, - AlbumStub.twoAsset, - AlbumStub.oneAsset, - AlbumStub.emptyAlbum, + AlbumStub.create2020end2026Album, + AlbumStub.create2020end2024Album, + AlbumStub.create2020end2022Album, + AlbumStub.create2020end2020Album, ]; expect(sorted, orderedEquals(sortedList)); }); - test("Most Recent - DESC", () { - final sorted = mostRecent.sortFn(albums, true); + test("Most Recent - ASC", () { + final sorted = mostRecent.sortFn( + [ + AlbumStub.create2020end2020Album, + AlbumStub.create2020end2022Album, + AlbumStub.create2020end2024Album, + AlbumStub.create2020end2026Album, + ], + true, + ); final sortedList = [ - AlbumStub.emptyAlbum, - AlbumStub.oneAsset, - AlbumStub.twoAsset, - AlbumStub.sharedWithUser, + AlbumStub.create2020end2020Album, + AlbumStub.create2020end2022Album, + AlbumStub.create2020end2024Album, + AlbumStub.create2020end2026Album, ]; expect(sorted, orderedEquals(sortedList)); }); From 4bf1b84cc2b2a3f796dd9bc34fe507ecb512dc71 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Wed, 4 Dec 2024 15:17:47 -0500 Subject: [PATCH 04/19] feat(ml): support multiple urls (#14347) * support multiple url * update api * styling unnecessary `?.` * update docs, make new url field go first add load balancing section * update tests doc formatting wording wording linting * small styling * `url` -> `urls` * fix tests * update docs * make docusaurus happy --------- Co-authored-by: Alex --- docs/docs/guides/remote-machine-learning.md | 42 +++++++++++----- docs/docs/install/config-file.md | 2 +- i18n/en.json | 4 +- .../system_config_machine_learning_dto.dart | 34 ++++++++++--- open-api/immich-openapi-specs.json | 12 ++++- open-api/typescript-sdk/src/fetch-client.ts | 4 +- server/src/config.ts | 4 +- server/src/cores/storage.core.spec.ts | 2 + server/src/dtos/system-config.dto.ts | 15 ++++-- .../interfaces/machine-learning.interface.ts | 6 +-- ...39482860-RenameMachineLearningUrlToUrls.ts | 19 ++++++++ server/src/repositories/event.repository.ts | 2 +- .../machine-learning.repository.ts | 48 +++++++++++-------- server/src/services/person.service.spec.ts | 2 +- server/src/services/person.service.ts | 2 +- server/src/services/search.service.ts | 2 +- .../src/services/smart-info.service.spec.ts | 4 +- server/src/services/smart-info.service.ts | 2 +- .../services/system-config.service.spec.ts | 6 +-- .../machine-learning-settings.svelte | 48 +++++++++++++++---- .../buttons/circle-icon-button.svelte | 3 +- .../settings/setting-input-field.svelte | 6 ++- 22 files changed, 199 insertions(+), 70 deletions(-) create mode 100644 server/src/migrations/1733339482860-RenameMachineLearningUrlToUrls.ts diff --git a/docs/docs/guides/remote-machine-learning.md b/docs/docs/guides/remote-machine-learning.md index 4dbb72a408f16..1abf7d4e54d15 100644 --- a/docs/docs/guides/remote-machine-learning.md +++ b/docs/docs/guides/remote-machine-learning.md @@ -1,18 +1,20 @@ # Remote Machine Learning -To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-immich-slow-on-low-memory-systems-like-the-raspberry-pi) like the Raspberry Pi, you may also host Immich's machine-learning container on a more powerful system (e.g. your laptop or desktop computer): - -- Set the URL in Machine Learning Settings on the Admin Settings page to point to the designated ML system, e.g. `http://workstation:3003`. -- Copy the following `docker-compose.yml` to your ML system. - - If using [hardware acceleration](/docs/features/ml-hardware-acceleration), the [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml) file also needs to be added -- Start the container by running `docker compose up -d`. +To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-immich-slow-on-low-memory-systems-like-the-raspberry-pi) like the Raspberry Pi, you may also host Immich's machine learning container on a more powerful system, such as your laptop or desktop computer. The server container will send requests containing the image preview to the remote machine learning container for processing. The machine learning container does not persist this data or associate it with a particular user. :::info -Smart Search and Face Detection will use this feature, but Facial Recognition is handled in the server. +Smart Search and Face Detection will use this feature, but Facial Recognition will not. This is because Facial Recognition uses the _outputs_ of these models that have already been saved to the database. As such, its processing is between the server container and the database. ::: :::danger -When using remote machine learning, the thumbnails are sent to the remote machine learning container. Use this option carefully when running this on a public computer or a paid processing cloud. +Image previews are sent to the remote machine learning container. Use this option carefully when running this on a public computer or a paid processing cloud. Additionally, as an internal service, the machine learning container has no security measures whatsoever. Please be mindful of where it's deployed and who can access it. +::: + +1. Ensure the remote server has Docker installed +2. Copy the following `docker-compose.yml` to the remote server + +:::info +If using hardware acceleration, the [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml) file also needs to be added and the `docker-compose.yml` needs to be configured as described in the [hardware acceleration documentation](/docs/features/ml-hardware-acceleration) ::: ```yaml @@ -37,8 +39,26 @@ volumes: model-cache: ``` -Please note that version mismatches between both hosts may cause instabilities and bugs, so make sure to always perform updates together. +3. Start the remote machine learning container by running `docker compose up -d` + +:::info +Version mismatches between both hosts may cause bugs and instability, so remember to update this container as well when updating the local Immich instance. +::: + +4. Navigate to the [Machine Learning Settings](https://my.immich.app/admin/system-settings?isOpen=machine-learning) +5. Click _Add URL_ +6. Fill the new field with the URL to the remote machine learning container, e.g. `http://ip:port` + +## Forcing remote processing + +Adding a new URL to the settings is recommended over replacing the existing URL. This is because it will allow machine learning tasks to be processed successfully when the remote server is down by falling back to the local machine learning container. If you do not want machine learning tasks to be processed locally when the remote server is not available, you can instead replace the existing URL and only provide the remote container's URL. If doing this, you can remove the `immich-machine-learning` section of the local `docker-compose.yml` file to save resources, as this service will never be used. + +Do note that this will mean that Smart Search and Face Detection jobs will fail to be processed when the remote instance is not available. This in turn means that tasks dependent on these features—Duplicate Detection and Facial Recognition—will not run for affected assets. If this occurs, you must manually click the _Missing_ button next to Smart Search and Face Detection in the [Job Status](http://my.immich.app/admin/jobs-status) page for the jobs to be retried. + +## Load balancing + +While several URLs can be provided in the settings, they are tried sequentially; there is no attempt to distribute load across multiple containers. It is recommended to use a dedicated load balancer for such use-cases and specify it as the only URL. Among other things, it may enable the use of different APIs on the same server by running multiple containers with different configurations. For example, one might run an OpenVINO container in addition to a CUDA container, or run a standard release container to maximize both CPU and GPU utilization. -:::caution -As an internal service, the machine learning container has no security measures whatsoever. Please be mindful of where it's deployed and who can access it. +:::tip +The machine learning container can be shared among several Immich instances regardless of the models a particular instance uses. However, using different models will lead to higher peak memory usage. ::: diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md index 9d86b8dad77d2..d3d7133254ad2 100644 --- a/docs/docs/install/config-file.md +++ b/docs/docs/install/config-file.md @@ -83,7 +83,7 @@ The default configuration looks like this: }, "machineLearning": { "enabled": true, - "url": "http://immich-machine-learning:3003", + "url": ["http://immich-machine-learning:3003"], "clip": { "enabled": true, "modelName": "ViT-B-32__openai" diff --git a/i18n/en.json b/i18n/en.json index 277db70a23f29..907f5df182e42 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -25,6 +25,7 @@ "add_to": "Add to...", "add_to_album": "Add to album", "add_to_shared_album": "Add to shared album", + "add_url": "Add URL", "added_to_archive": "Added to archive", "added_to_favorites": "Added to favorites", "added_to_favorites_count": "Added {count, number} to favorites", @@ -132,7 +133,7 @@ "machine_learning_smart_search_description": "Search for images semantically using CLIP embeddings", "machine_learning_smart_search_enabled": "Enable smart search", "machine_learning_smart_search_enabled_description": "If disabled, images will not be encoded for smart search.", - "machine_learning_url_description": "URL of the machine learning server", + "machine_learning_url_description": "The URL of the machine learning server. If more than one URL is provided, each server will be attempted one-at-a-time until one responds successfully, in order from first to last.", "manage_concurrency": "Manage Concurrency", "manage_log_settings": "Manage log settings", "map_dark_style": "Dark style", @@ -1045,6 +1046,7 @@ "remove_from_album": "Remove from album", "remove_from_favorites": "Remove from favorites", "remove_from_shared_link": "Remove from shared link", + "remove_url": "Remove URL", "remove_user": "Remove user", "removed_api_key": "Removed API Key: {name}", "removed_from_archive": "Removed from archive", diff --git a/mobile/openapi/lib/model/system_config_machine_learning_dto.dart b/mobile/openapi/lib/model/system_config_machine_learning_dto.dart index d665f0bfa56a7..a4a9ca7d82bdf 100644 --- a/mobile/openapi/lib/model/system_config_machine_learning_dto.dart +++ b/mobile/openapi/lib/model/system_config_machine_learning_dto.dart @@ -17,7 +17,8 @@ class SystemConfigMachineLearningDto { required this.duplicateDetection, required this.enabled, required this.facialRecognition, - required this.url, + this.url, + this.urls = const [], }); CLIPConfig clip; @@ -28,7 +29,16 @@ class SystemConfigMachineLearningDto { FacialRecognitionConfig facialRecognition; - String url; + /// This property was deprecated in v1.122.0 + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? url; + + List urls; @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigMachineLearningDto && @@ -36,7 +46,8 @@ class SystemConfigMachineLearningDto { other.duplicateDetection == duplicateDetection && other.enabled == enabled && other.facialRecognition == facialRecognition && - other.url == url; + other.url == url && + _deepEquality.equals(other.urls, urls); @override int get hashCode => @@ -45,10 +56,11 @@ class SystemConfigMachineLearningDto { (duplicateDetection.hashCode) + (enabled.hashCode) + (facialRecognition.hashCode) + - (url.hashCode); + (url == null ? 0 : url!.hashCode) + + (urls.hashCode); @override - String toString() => 'SystemConfigMachineLearningDto[clip=$clip, duplicateDetection=$duplicateDetection, enabled=$enabled, facialRecognition=$facialRecognition, url=$url]'; + String toString() => 'SystemConfigMachineLearningDto[clip=$clip, duplicateDetection=$duplicateDetection, enabled=$enabled, facialRecognition=$facialRecognition, url=$url, urls=$urls]'; Map toJson() { final json = {}; @@ -56,7 +68,12 @@ class SystemConfigMachineLearningDto { json[r'duplicateDetection'] = this.duplicateDetection; json[r'enabled'] = this.enabled; json[r'facialRecognition'] = this.facialRecognition; + if (this.url != null) { json[r'url'] = this.url; + } else { + // json[r'url'] = null; + } + json[r'urls'] = this.urls; return json; } @@ -73,7 +90,10 @@ class SystemConfigMachineLearningDto { duplicateDetection: DuplicateDetectionConfig.fromJson(json[r'duplicateDetection'])!, enabled: mapValueOfType(json, r'enabled')!, facialRecognition: FacialRecognitionConfig.fromJson(json[r'facialRecognition'])!, - url: mapValueOfType(json, r'url')!, + url: mapValueOfType(json, r'url'), + urls: json[r'urls'] is Iterable + ? (json[r'urls'] as Iterable).cast().toList(growable: false) + : const [], ); } return null; @@ -125,7 +145,7 @@ class SystemConfigMachineLearningDto { 'duplicateDetection', 'enabled', 'facialRecognition', - 'url', + 'urls', }; } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 20ebe607a41e5..bc32a32e04f01 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -11857,7 +11857,17 @@ "$ref": "#/components/schemas/FacialRecognitionConfig" }, "url": { + "deprecated": true, + "description": "This property was deprecated in v1.122.0", "type": "string" + }, + "urls": { + "items": { + "format": "uri", + "type": "string" + }, + "minItems": 1, + "type": "array" } }, "required": [ @@ -11865,7 +11875,7 @@ "duplicateDetection", "enabled", "facialRecognition", - "url" + "urls" ], "type": "object" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 9b79816091fc5..d786139ab51f4 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1178,7 +1178,9 @@ export type SystemConfigMachineLearningDto = { duplicateDetection: DuplicateDetectionConfig; enabled: boolean; facialRecognition: FacialRecognitionConfig; - url: string; + /** This property was deprecated in v1.122.0 */ + url?: string; + urls: string[]; }; export type SystemConfigMapDto = { darkStyle: string; diff --git a/server/src/config.ts b/server/src/config.ts index f5e78ab01bc50..dd850e063f0da 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -52,7 +52,7 @@ export interface SystemConfig { }; machineLearning: { enabled: boolean; - url: string; + urls: string[]; clip: { enabled: boolean; modelName: string; @@ -206,7 +206,7 @@ export const defaults = Object.freeze({ }, machineLearning: { enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false', - url: process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003', + urls: [process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003'], clip: { enabled: true, modelName: 'ViT-B-32__openai', diff --git a/server/src/cores/storage.core.spec.ts b/server/src/cores/storage.core.spec.ts index 6ff6ca61bf3d3..a6636733066d8 100644 --- a/server/src/cores/storage.core.spec.ts +++ b/server/src/cores/storage.core.spec.ts @@ -3,6 +3,8 @@ import { vitest } from 'vitest'; vitest.mock('src/constants', () => ({ APP_MEDIA_LOCATION: '/photos', + ADDED_IN_PREFIX: 'This property was added in ', + DEPRECATED_IN_PREFIX: 'This property was deprecated in ', })); describe('StorageCore', () => { diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 8d79fecb22b20..894f4c7948cab 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; +import { Exclude, Transform, Type } from 'class-transformer'; import { + ArrayMinSize, IsBoolean, IsEnum, IsInt, @@ -16,6 +17,7 @@ import { ValidateNested, } from 'class-validator'; import { SystemConfig } from 'src/config'; +import { PropertyLifecycle } from 'src/decorators'; import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto'; import { AudioCodec, @@ -269,9 +271,16 @@ class SystemConfigMachineLearningDto { @ValidateBoolean() enabled!: boolean; - @IsUrl({ require_tld: false, allow_underscores: true }) + @PropertyLifecycle({ deprecatedAt: 'v1.122.0' }) + @Exclude() + url?: string; + + @IsUrl({ require_tld: false, allow_underscores: true }, { each: true }) + @ArrayMinSize(1) + @Transform(({ obj, value }) => (obj.url ? [obj.url] : value)) @ValidateIf((dto) => dto.enabled) - url!: string; + @ApiProperty({ type: 'array', items: { type: 'string', format: 'uri' }, minItems: 1 }) + urls!: string[]; @Type(() => CLIPConfig) @ValidateNested() diff --git a/server/src/interfaces/machine-learning.interface.ts b/server/src/interfaces/machine-learning.interface.ts index 5342030c8fde7..372aa0c7cde25 100644 --- a/server/src/interfaces/machine-learning.interface.ts +++ b/server/src/interfaces/machine-learning.interface.ts @@ -51,7 +51,7 @@ export type DetectedFaces = { faces: Face[] } & VisualResponse; export type MachineLearningRequest = ClipVisualRequest | ClipTextualRequest | FacialRecognitionRequest; export interface IMachineLearningRepository { - encodeImage(url: string, imagePath: string, config: ModelOptions): Promise; - encodeText(url: string, text: string, config: ModelOptions): Promise; - detectFaces(url: string, imagePath: string, config: FaceDetectionOptions): Promise; + encodeImage(urls: string[], imagePath: string, config: ModelOptions): Promise; + encodeText(urls: string[], text: string, config: ModelOptions): Promise; + detectFaces(urls: string[], imagePath: string, config: FaceDetectionOptions): Promise; } diff --git a/server/src/migrations/1733339482860-RenameMachineLearningUrlToUrls.ts b/server/src/migrations/1733339482860-RenameMachineLearningUrlToUrls.ts new file mode 100644 index 0000000000000..65bb02c8e2971 --- /dev/null +++ b/server/src/migrations/1733339482860-RenameMachineLearningUrlToUrls.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenameMachineLearningUrlToUrls1733339482860 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE system_metadata + SET value = jsonb_insert(value #- '{machineLearning,url}', '{machineLearning,urls}'::text[], jsonb_build_array(value->'machineLearning'->'url')) + WHERE key = 'system-config' AND value->'machineLearning'->'url' IS NOT NULL + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE system_metadata + SET value = jsonb_insert(value #- '{machineLearning,urls}', '{machineLearning,url}'::text[], to_jsonb(value->'machineLearning'->'urls'->>0)) + WHERE key = 'system-config' AND value->'machineLearning'->'urls' IS NOT NULL AND jsonb_array_length(value->'machineLearning'->'urls') >= 1 + `); + } +} diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts index 96df72e43f364..7de8defe6e9b0 100644 --- a/server/src/repositories/event.repository.ts +++ b/server/src/repositories/event.repository.ts @@ -155,7 +155,7 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect this.emitHandlers[event].push(item); } - async emit(event: T, ...args: ArgsOf): Promise { + emit(event: T, ...args: ArgsOf): Promise { return this.onEvent({ name: event, args, server: false }); } diff --git a/server/src/repositories/machine-learning.repository.ts b/server/src/repositories/machine-learning.repository.ts index 74b17ca6a754f..56cdf30a1e48d 100644 --- a/server/src/repositories/machine-learning.repository.ts +++ b/server/src/repositories/machine-learning.repository.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { readFile } from 'node:fs/promises'; import { CLIPConfig } from 'src/dtos/model-config.dto'; +import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ClipTextualResponse, ClipVisualResponse, @@ -13,33 +14,42 @@ import { ModelType, } from 'src/interfaces/machine-learning.interface'; -const errorPrefix = 'Machine learning request'; - @Injectable() export class MachineLearningRepository implements IMachineLearningRepository { - private async predict(url: string, payload: ModelPayload, config: MachineLearningRequest): Promise { - const formData = await this.getFormData(payload, config); + constructor(@Inject(ILoggerRepository) private logger: ILoggerRepository) { + this.logger.setContext(MachineLearningRepository.name); + } - const res = await fetch(new URL('/predict', url), { method: 'POST', body: formData }).catch( - (error: Error | any) => { - throw new Error(`${errorPrefix} to "${url}" failed with ${error?.cause || error}`); - }, - ); + private async predict(urls: string[], payload: ModelPayload, config: MachineLearningRequest): Promise { + const formData = await this.getFormData(payload, config); + for (const url of urls) { + try { + const response = await fetch(new URL('/predict', url), { method: 'POST', body: formData }); + if (response.ok) { + return response.json(); + } - if (res.status >= 400) { - throw new Error(`${errorPrefix} '${JSON.stringify(config)}' failed with status ${res.status}: ${res.statusText}`); + this.logger.warn( + `Machine learning request to "${url}" failed with status ${response.status}: ${response.statusText}`, + ); + } catch (error: Error | unknown) { + this.logger.warn( + `Machine learning request to "${url}" failed: ${error instanceof Error ? error.message : error}`, + ); + } } - return res.json(); + + throw new Error(`Machine learning request '${JSON.stringify(config)}' failed for all URLs`); } - async detectFaces(url: string, imagePath: string, { modelName, minScore }: FaceDetectionOptions) { + async detectFaces(urls: string[], imagePath: string, { modelName, minScore }: FaceDetectionOptions) { const request = { [ModelTask.FACIAL_RECOGNITION]: { [ModelType.DETECTION]: { modelName, options: { minScore } }, [ModelType.RECOGNITION]: { modelName }, }, }; - const response = await this.predict(url, { imagePath }, request); + const response = await this.predict(urls, { imagePath }, request); return { imageHeight: response.imageHeight, imageWidth: response.imageWidth, @@ -47,15 +57,15 @@ export class MachineLearningRepository implements IMachineLearningRepository { }; } - async encodeImage(url: string, imagePath: string, { modelName }: CLIPConfig) { + async encodeImage(urls: string[], imagePath: string, { modelName }: CLIPConfig) { const request = { [ModelTask.SEARCH]: { [ModelType.VISUAL]: { modelName } } }; - const response = await this.predict(url, { imagePath }, request); + const response = await this.predict(urls, { imagePath }, request); return response[ModelTask.SEARCH]; } - async encodeText(url: string, text: string, { modelName }: CLIPConfig) { + async encodeText(urls: string[], text: string, { modelName }: CLIPConfig) { const request = { [ModelTask.SEARCH]: { [ModelType.TEXTUAL]: { modelName } } }; - const response = await this.predict(url, { text }, request); + const response = await this.predict(urls, { text }, request); return response[ModelTask.SEARCH]; } diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index da4656be021a8..3b749c0ab65cc 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -717,7 +717,7 @@ describe(PersonService.name, () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); await sut.handleDetectFaces({ id: assetStub.image.id }); expect(machineLearningMock.detectFaces).toHaveBeenCalledWith( - 'http://immich-machine-learning:3003', + ['http://immich-machine-learning:3003'], '/uploads/user-id/thumbs/path.jpg', expect.objectContaining({ minScore: 0.7, modelName: 'buffalo_l' }), ); diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 5b6e721eab0bc..79e82bb74289d 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -297,7 +297,7 @@ export class PersonService extends BaseService { } const { imageHeight, imageWidth, faces } = await this.machineLearningRepository.detectFaces( - machineLearning.url, + machineLearning.urls, previewFile.path, machineLearning.facialRecognition, ); diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index 04d3addb6353e..bf5bf9e3111a3 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -86,7 +86,7 @@ export class SearchService extends BaseService { const userIds = await this.getUserIdsToSearch(auth); const embedding = await this.machineLearningRepository.encodeText( - machineLearning.url, + machineLearning.urls, dto.query, machineLearning.clip, ); diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts index 250f9326f983f..0b0ee6b20f30b 100644 --- a/server/src/services/smart-info.service.spec.ts +++ b/server/src/services/smart-info.service.spec.ts @@ -289,7 +289,7 @@ describe(SmartInfoService.name, () => { expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.SUCCESS); expect(machineLearningMock.encodeImage).toHaveBeenCalledWith( - 'http://immich-machine-learning:3003', + ['http://immich-machine-learning:3003'], '/uploads/user-id/thumbs/path.jpg', expect.objectContaining({ modelName: 'ViT-B-32__openai' }), ); @@ -322,7 +322,7 @@ describe(SmartInfoService.name, () => { expect(databaseMock.wait).toHaveBeenCalledWith(512); expect(machineLearningMock.encodeImage).toHaveBeenCalledWith( - 'http://immich-machine-learning:3003', + ['http://immich-machine-learning:3003'], '/uploads/user-id/thumbs/path.jpg', expect.objectContaining({ modelName: 'ViT-B-32__openai' }), ); diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index 9122a48658726..8fef961fe1f32 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -122,7 +122,7 @@ export class SmartInfoService extends BaseService { } const embedding = await this.machineLearningRepository.encodeImage( - machineLearning.url, + machineLearning.urls, previewFile.path, machineLearning.clip, ); diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 4d5a29e8a89e8..2550c15de25ca 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -85,7 +85,7 @@ const updatedConfig = Object.freeze({ }, machineLearning: { enabled: true, - url: 'http://immich-machine-learning:3003', + urls: ['http://immich-machine-learning:3003'], clip: { enabled: true, modelName: 'ViT-B-32__openai', @@ -330,11 +330,11 @@ describe(SystemConfigService.name, () => { it('should allow underscores in the machine learning url', async () => { configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' })); - const partialConfig = { machineLearning: { url: 'immich_machine_learning' } }; + const partialConfig = { machineLearning: { urls: ['immich_machine_learning'] } }; systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig)); const config = await sut.getSystemConfig(); - expect(config.machineLearning.url).toEqual('immich_machine_learning'); + expect(config.machineLearning.urls).toEqual(['immich_machine_learning']); }); const externalDomainTests = [ diff --git a/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte b/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte index 13678a31c1b63..90131d7238d5f 100644 --- a/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte +++ b/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte @@ -12,6 +12,9 @@ import { t } from 'svelte-i18n'; import FormatMessage from '$lib/components/i18n/format-message.svelte'; import { SettingInputFieldType } from '$lib/constants'; + import Button from '$lib/components/elements/buttons/button.svelte'; + import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; + import { mdiMinusCircle } from '@mdi/js'; interface Props { savedConfig: SystemConfigDto; @@ -42,15 +45,42 @@
- +
+ {#each config.machineLearning.urls as _, i} + {#snippet removeButton()} + {#if config.machineLearning.urls.length > 1} + config.machineLearning.urls.splice(i, 1)} + icon={mdiMinusCircle} + /> + {/if} + {/snippet} + + + {/each} +
+ + - export type Color = 'transparent' | 'light' | 'dark' | 'gray' | 'primary' | 'opaque' | 'alert'; + export type Color = 'transparent' | 'light' | 'dark' | 'red' | 'gray' | 'primary' | 'opaque' | 'alert'; export type Padding = '1' | '2' | '3'; @@ -64,6 +64,7 @@ transparent: 'bg-transparent hover:bg-[#d3d3d3] dark:text-immich-dark-fg', opaque: 'bg-transparent hover:bg-immich-bg/30 text-white hover:dark:text-white', light: 'bg-white hover:bg-[#d3d3d3]', + red: 'text-red-400 hover:bg-[#d3d3d3]', dark: 'bg-[#202123] hover:bg-[#d3d3d3]', alert: 'text-[#ff0000] hover:text-white', gray: 'bg-[#d3d3d3] hover:bg-[#e2e7e9] text-immich-dark-gray hover:text-black', diff --git a/web/src/lib/components/shared-components/settings/setting-input-field.svelte b/web/src/lib/components/shared-components/settings/setting-input-field.svelte index 1463cc48407b8..a04f521773d58 100644 --- a/web/src/lib/components/shared-components/settings/setting-input-field.svelte +++ b/web/src/lib/components/shared-components/settings/setting-input-field.svelte @@ -22,6 +22,7 @@ autofocus?: boolean; passwordAutocomplete?: AutoFill; descriptionSnippet?: Snippet; + trailingSnippet?: Snippet; } let { @@ -39,6 +40,7 @@ autofocus = false, passwordAutocomplete = 'current-password', descriptionSnippet, + trailingSnippet, }: Props = $props(); let input: HTMLInputElement | undefined = $state(); @@ -68,7 +70,7 @@
-
+
{#if required}
*
@@ -132,6 +134,8 @@ {disabled} {title} /> + + {@render trailingSnippet?.()}
{:else} Date: Wed, 4 Dec 2024 21:26:02 +0100 Subject: [PATCH 05/19] feat: Notification Email Templates (#13940) --- .../administration/email-notification.mdx | 6 + .../img/user-notifications-templates.png | Bin 0 -> 199469 bytes docs/docs/administration/system-settings.md | 4 + i18n/en.json | 12 +- i18n/nl.json | 10 ++ mobile/openapi/README.md | 5 + mobile/openapi/lib/api.dart | 4 + mobile/openapi/lib/api/notifications_api.dart | 52 +++++++ mobile/openapi/lib/api_client.dart | 8 ++ .../openapi/lib/model/system_config_dto.dart | 10 +- .../system_config_template_emails_dto.dart | 115 +++++++++++++++ .../model/system_config_templates_dto.dart | 99 +++++++++++++ mobile/openapi/lib/model/template_dto.dart | 99 +++++++++++++ .../lib/model/template_response_dto.dart | 107 ++++++++++++++ open-api/immich-openapi-specs.json | 111 +++++++++++++++ open-api/typescript-sdk/src/fetch-client.ts | 29 ++++ server/src/config.ts | 14 ++ .../controllers/notification.controller.ts | 16 ++- server/src/dtos/notification.dto.ts | 10 ++ server/src/dtos/system-config.dto.ts | 23 +++ server/src/emails/album-invite.email.tsx | 90 +++++++----- server/src/emails/album-update.email.tsx | 97 ++++++++----- server/src/emails/welcome.email.tsx | 82 +++++++---- .../src/interfaces/notification.interface.ts | 5 + .../notification.repository.spec.ts | 4 + .../repositories/notification.repository.ts | 10 +- server/src/services/notification.service.ts | 76 +++++++++- .../services/system-config.service.spec.ts | 7 + server/src/utils/replace-template-tags.ts | 5 + web/package-lock.json | 2 +- .../notification-settings.svelte | 16 ++- .../template-settings.svelte | 131 ++++++++++++++++++ 32 files changed, 1145 insertions(+), 114 deletions(-) create mode 100644 docs/docs/administration/img/user-notifications-templates.png create mode 100644 mobile/openapi/lib/model/system_config_template_emails_dto.dart create mode 100644 mobile/openapi/lib/model/system_config_templates_dto.dart create mode 100644 mobile/openapi/lib/model/template_dto.dart create mode 100644 mobile/openapi/lib/model/template_response_dto.dart create mode 100644 server/src/utils/replace-template-tags.ts create mode 100644 web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte diff --git a/docs/docs/administration/email-notification.mdx b/docs/docs/administration/email-notification.mdx index 93b1051053069..2f244f33521a7 100644 --- a/docs/docs/administration/email-notification.mdx +++ b/docs/docs/administration/email-notification.mdx @@ -19,3 +19,9 @@ You can use [this guide](/docs/guides/smtp-gmail) to use Gmail's SMTP server. Users can manage their email notification settings from their account settings page on the web. They can choose to turn email notifications on or off for the following events: + +## Notification templates + +You can override the default notification text with custom templates in HTML format. You can use tags to show dynamic tags in your templates. + + diff --git a/docs/docs/administration/img/user-notifications-templates.png b/docs/docs/administration/img/user-notifications-templates.png new file mode 100644 index 0000000000000000000000000000000000000000..150d39b7a6a2a0e499bd7d5e793f07179495afd7 GIT binary patch literal 199469 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

6~#=|P$bc} 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 is the user's Storage Label", "system_settings": "System Settings", "tag_cleanup_job": "Tag cleanup", + "template_email_preview": "Preview", + "template_email_settings": "Email Templates", + "template_email_settings_description": "Manage custom email notification templates", + "template_email_welcome": "Welcome email template", + "template_email_invite_album": "Invite Album Template", + "template_email_update_album": "Update Album Template", + "template_settings": "Notification Templates", + "template_settings_description": "Manage custom templates for notifications.", + "template_email_if_empty": "If the template is empty, the default email will be used.", + "template_email_available_tags": "You can use the following variables in your template: {tags}", "theme_custom_css_settings": "Custom CSS", "theme_custom_css_settings_description": "Cascading Style Sheets allow the design of Immich to be customized.", "theme_settings": "Theme Settings", @@ -1325,4 +1335,4 @@ "zoom_image": "Zoom Image", "timeline": "Timeline", "total": "Total" -} +} \ No newline at end of file diff --git a/i18n/nl.json b/i18n/nl.json index ade7a50925ed3..3420c5d10585f 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -247,6 +247,16 @@ "storage_template_user_label": "{label} is het opslaglabel van de gebruiker", "system_settings": "Systeeminstellingen", "tag_cleanup_job": "Tag opschoning", + "template_email_settings": "Email", + "template_email_settings_description": "Beheer aangepaste email melding sjablonen", + "template_email_preview": "Voorbeeld", + "template_email_welcome": "Welkom email sjabloon", + "template_email_invite_album": "Uitgenodigd in album sjabloon", + "template_email_update_album": "Update in album sjabloon", + "template_settings": "Melding sjablonen", + "template_settings_description": "Beheer aangepast sjablonen voor meldingen.", + "template_email_if_empty": "Wanneer het sjabloon leeg is, wordt de standaard mail gebruikt.", + "template_email_available_tags": "Je kan de volgende tags gebruiken in een template: {tags}", "theme_custom_css_settings": "Aangepaste CSS", "theme_custom_css_settings_description": "Met Cascading Style Sheets kan het ontwerp van Immich worden aangepast.", "theme_settings": "Thema instellingen", diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 77809359023ba..b97ff5411cbba 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -144,6 +144,7 @@ Class | Method | HTTP request | Description *MemoriesApi* | [**removeMemoryAssets**](doc//MemoriesApi.md#removememoryassets) | **DELETE** /memories/{id}/assets | *MemoriesApi* | [**searchMemories**](doc//MemoriesApi.md#searchmemories) | **GET** /memories | *MemoriesApi* | [**updateMemory**](doc//MemoriesApi.md#updatememory) | **PUT** /memories/{id} | +*NotificationsApi* | [**getNotificationTemplate**](doc//NotificationsApi.md#getnotificationtemplate) | **POST** /notifications/templates/{name} | *NotificationsApi* | [**sendTestEmail**](doc//NotificationsApi.md#sendtestemail) | **POST** /notifications/test-email | *OAuthApi* | [**finishOAuth**](doc//OAuthApi.md#finishoauth) | **POST** /oauth/callback | *OAuthApi* | [**linkOAuthAccount**](doc//OAuthApi.md#linkoauthaccount) | **POST** /oauth/link | @@ -436,7 +437,9 @@ Class | Method | HTTP request | Description - [SystemConfigSmtpDto](doc//SystemConfigSmtpDto.md) - [SystemConfigSmtpTransportDto](doc//SystemConfigSmtpTransportDto.md) - [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md) + - [SystemConfigTemplateEmailsDto](doc//SystemConfigTemplateEmailsDto.md) - [SystemConfigTemplateStorageOptionDto](doc//SystemConfigTemplateStorageOptionDto.md) + - [SystemConfigTemplatesDto](doc//SystemConfigTemplatesDto.md) - [SystemConfigThemeDto](doc//SystemConfigThemeDto.md) - [SystemConfigTrashDto](doc//SystemConfigTrashDto.md) - [SystemConfigUserDto](doc//SystemConfigUserDto.md) @@ -448,6 +451,8 @@ Class | Method | HTTP request | Description - [TagUpsertDto](doc//TagUpsertDto.md) - [TagsResponse](doc//TagsResponse.md) - [TagsUpdate](doc//TagsUpdate.md) + - [TemplateDto](doc//TemplateDto.md) + - [TemplateResponseDto](doc//TemplateResponseDto.md) - [TestEmailResponseDto](doc//TestEmailResponseDto.md) - [TimeBucketResponseDto](doc//TimeBucketResponseDto.md) - [TimeBucketSize](doc//TimeBucketSize.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index e1c343ad50d1d..73eb02d89ed7a 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -250,7 +250,9 @@ part 'model/system_config_server_dto.dart'; part 'model/system_config_smtp_dto.dart'; part 'model/system_config_smtp_transport_dto.dart'; part 'model/system_config_storage_template_dto.dart'; +part 'model/system_config_template_emails_dto.dart'; part 'model/system_config_template_storage_option_dto.dart'; +part 'model/system_config_templates_dto.dart'; part 'model/system_config_theme_dto.dart'; part 'model/system_config_trash_dto.dart'; part 'model/system_config_user_dto.dart'; @@ -262,6 +264,8 @@ part 'model/tag_update_dto.dart'; part 'model/tag_upsert_dto.dart'; part 'model/tags_response.dart'; part 'model/tags_update.dart'; +part 'model/template_dto.dart'; +part 'model/template_response_dto.dart'; part 'model/test_email_response_dto.dart'; part 'model/time_bucket_response_dto.dart'; part 'model/time_bucket_size.dart'; diff --git a/mobile/openapi/lib/api/notifications_api.dart b/mobile/openapi/lib/api/notifications_api.dart index 0681d582479ad..323fbcc3d6bc0 100644 --- a/mobile/openapi/lib/api/notifications_api.dart +++ b/mobile/openapi/lib/api/notifications_api.dart @@ -16,6 +16,58 @@ class NotificationsApi { final ApiClient apiClient; + /// Performs an HTTP 'POST /notifications/templates/{name}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] name (required): + /// + /// * [TemplateDto] templateDto (required): + Future getNotificationTemplateWithHttpInfo(String name, TemplateDto templateDto,) async { + // ignore: prefer_const_declarations + final path = r'/notifications/templates/{name}' + .replaceAll('{name}', name); + + // ignore: prefer_final_locals + Object? postBody = templateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] name (required): + /// + /// * [TemplateDto] templateDto (required): + Future getNotificationTemplate(String name, TemplateDto templateDto,) async { + final response = await getNotificationTemplateWithHttpInfo(name, templateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'TemplateResponseDto',) as TemplateResponseDto; + + } + return null; + } + /// Performs an HTTP 'POST /notifications/test-email' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index b71e6f45f71cb..a6f8d551da81c 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -554,8 +554,12 @@ class ApiClient { return SystemConfigSmtpTransportDto.fromJson(value); case 'SystemConfigStorageTemplateDto': return SystemConfigStorageTemplateDto.fromJson(value); + case 'SystemConfigTemplateEmailsDto': + return SystemConfigTemplateEmailsDto.fromJson(value); case 'SystemConfigTemplateStorageOptionDto': return SystemConfigTemplateStorageOptionDto.fromJson(value); + case 'SystemConfigTemplatesDto': + return SystemConfigTemplatesDto.fromJson(value); case 'SystemConfigThemeDto': return SystemConfigThemeDto.fromJson(value); case 'SystemConfigTrashDto': @@ -578,6 +582,10 @@ class ApiClient { return TagsResponse.fromJson(value); case 'TagsUpdate': return TagsUpdate.fromJson(value); + case 'TemplateDto': + return TemplateDto.fromJson(value); + case 'TemplateResponseDto': + return TemplateResponseDto.fromJson(value); case 'TestEmailResponseDto': return TestEmailResponseDto.fromJson(value); case 'TimeBucketResponseDto': diff --git a/mobile/openapi/lib/model/system_config_dto.dart b/mobile/openapi/lib/model/system_config_dto.dart index 4215953906601..59d5f09fc9467 100644 --- a/mobile/openapi/lib/model/system_config_dto.dart +++ b/mobile/openapi/lib/model/system_config_dto.dart @@ -29,6 +29,7 @@ class SystemConfigDto { required this.reverseGeocoding, required this.server, required this.storageTemplate, + required this.templates, required this.theme, required this.trash, required this.user, @@ -66,6 +67,8 @@ class SystemConfigDto { SystemConfigStorageTemplateDto storageTemplate; + SystemConfigTemplatesDto templates; + SystemConfigThemeDto theme; SystemConfigTrashDto trash; @@ -90,6 +93,7 @@ class SystemConfigDto { other.reverseGeocoding == reverseGeocoding && other.server == server && other.storageTemplate == storageTemplate && + other.templates == templates && other.theme == theme && other.trash == trash && other.user == user; @@ -113,12 +117,13 @@ class SystemConfigDto { (reverseGeocoding.hashCode) + (server.hashCode) + (storageTemplate.hashCode) + + (templates.hashCode) + (theme.hashCode) + (trash.hashCode) + (user.hashCode); @override - String toString() => 'SystemConfigDto[backup=$backup, ffmpeg=$ffmpeg, image=$image, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, metadata=$metadata, newVersionCheck=$newVersionCheck, notifications=$notifications, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, theme=$theme, trash=$trash, user=$user]'; + String toString() => 'SystemConfigDto[backup=$backup, ffmpeg=$ffmpeg, image=$image, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, metadata=$metadata, newVersionCheck=$newVersionCheck, notifications=$notifications, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, templates=$templates, theme=$theme, trash=$trash, user=$user]'; Map toJson() { final json = {}; @@ -138,6 +143,7 @@ class SystemConfigDto { json[r'reverseGeocoding'] = this.reverseGeocoding; json[r'server'] = this.server; json[r'storageTemplate'] = this.storageTemplate; + json[r'templates'] = this.templates; json[r'theme'] = this.theme; json[r'trash'] = this.trash; json[r'user'] = this.user; @@ -169,6 +175,7 @@ class SystemConfigDto { reverseGeocoding: SystemConfigReverseGeocodingDto.fromJson(json[r'reverseGeocoding'])!, server: SystemConfigServerDto.fromJson(json[r'server'])!, storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!, + templates: SystemConfigTemplatesDto.fromJson(json[r'templates'])!, theme: SystemConfigThemeDto.fromJson(json[r'theme'])!, trash: SystemConfigTrashDto.fromJson(json[r'trash'])!, user: SystemConfigUserDto.fromJson(json[r'user'])!, @@ -235,6 +242,7 @@ class SystemConfigDto { 'reverseGeocoding', 'server', 'storageTemplate', + 'templates', 'theme', 'trash', 'user', diff --git a/mobile/openapi/lib/model/system_config_template_emails_dto.dart b/mobile/openapi/lib/model/system_config_template_emails_dto.dart new file mode 100644 index 0000000000000..9db85509f58eb --- /dev/null +++ b/mobile/openapi/lib/model/system_config_template_emails_dto.dart @@ -0,0 +1,115 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SystemConfigTemplateEmailsDto { + /// Returns a new [SystemConfigTemplateEmailsDto] instance. + SystemConfigTemplateEmailsDto({ + required this.albumInviteTemplate, + required this.albumUpdateTemplate, + required this.welcomeTemplate, + }); + + String albumInviteTemplate; + + String albumUpdateTemplate; + + String welcomeTemplate; + + @override + bool operator ==(Object other) => identical(this, other) || other is SystemConfigTemplateEmailsDto && + other.albumInviteTemplate == albumInviteTemplate && + other.albumUpdateTemplate == albumUpdateTemplate && + other.welcomeTemplate == welcomeTemplate; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (albumInviteTemplate.hashCode) + + (albumUpdateTemplate.hashCode) + + (welcomeTemplate.hashCode); + + @override + String toString() => 'SystemConfigTemplateEmailsDto[albumInviteTemplate=$albumInviteTemplate, albumUpdateTemplate=$albumUpdateTemplate, welcomeTemplate=$welcomeTemplate]'; + + Map toJson() { + final json = {}; + json[r'albumInviteTemplate'] = this.albumInviteTemplate; + json[r'albumUpdateTemplate'] = this.albumUpdateTemplate; + json[r'welcomeTemplate'] = this.welcomeTemplate; + return json; + } + + /// Returns a new [SystemConfigTemplateEmailsDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SystemConfigTemplateEmailsDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigTemplateEmailsDto"); + if (value is Map) { + final json = value.cast(); + + return SystemConfigTemplateEmailsDto( + albumInviteTemplate: mapValueOfType(json, r'albumInviteTemplate')!, + albumUpdateTemplate: mapValueOfType(json, r'albumUpdateTemplate')!, + welcomeTemplate: mapValueOfType(json, r'welcomeTemplate')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SystemConfigTemplateEmailsDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SystemConfigTemplateEmailsDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SystemConfigTemplateEmailsDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SystemConfigTemplateEmailsDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'albumInviteTemplate', + 'albumUpdateTemplate', + 'welcomeTemplate', + }; +} + diff --git a/mobile/openapi/lib/model/system_config_templates_dto.dart b/mobile/openapi/lib/model/system_config_templates_dto.dart new file mode 100644 index 0000000000000..a5e883497881f --- /dev/null +++ b/mobile/openapi/lib/model/system_config_templates_dto.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SystemConfigTemplatesDto { + /// Returns a new [SystemConfigTemplatesDto] instance. + SystemConfigTemplatesDto({ + required this.email, + }); + + SystemConfigTemplateEmailsDto email; + + @override + bool operator ==(Object other) => identical(this, other) || other is SystemConfigTemplatesDto && + other.email == email; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (email.hashCode); + + @override + String toString() => 'SystemConfigTemplatesDto[email=$email]'; + + Map toJson() { + final json = {}; + json[r'email'] = this.email; + return json; + } + + /// Returns a new [SystemConfigTemplatesDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SystemConfigTemplatesDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigTemplatesDto"); + if (value is Map) { + final json = value.cast(); + + return SystemConfigTemplatesDto( + email: SystemConfigTemplateEmailsDto.fromJson(json[r'email'])!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SystemConfigTemplatesDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SystemConfigTemplatesDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SystemConfigTemplatesDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SystemConfigTemplatesDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'email', + }; +} + diff --git a/mobile/openapi/lib/model/template_dto.dart b/mobile/openapi/lib/model/template_dto.dart new file mode 100644 index 0000000000000..f818e0508acab --- /dev/null +++ b/mobile/openapi/lib/model/template_dto.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class TemplateDto { + /// Returns a new [TemplateDto] instance. + TemplateDto({ + required this.template, + }); + + String template; + + @override + bool operator ==(Object other) => identical(this, other) || other is TemplateDto && + other.template == template; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (template.hashCode); + + @override + String toString() => 'TemplateDto[template=$template]'; + + Map toJson() { + final json = {}; + json[r'template'] = this.template; + return json; + } + + /// Returns a new [TemplateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static TemplateDto? fromJson(dynamic value) { + upgradeDto(value, "TemplateDto"); + if (value is Map) { + final json = value.cast(); + + return TemplateDto( + template: mapValueOfType(json, r'template')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = TemplateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = TemplateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of TemplateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = TemplateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'template', + }; +} + diff --git a/mobile/openapi/lib/model/template_response_dto.dart b/mobile/openapi/lib/model/template_response_dto.dart new file mode 100644 index 0000000000000..3c3224a54beb0 --- /dev/null +++ b/mobile/openapi/lib/model/template_response_dto.dart @@ -0,0 +1,107 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class TemplateResponseDto { + /// Returns a new [TemplateResponseDto] instance. + TemplateResponseDto({ + required this.html, + required this.name, + }); + + String html; + + String name; + + @override + bool operator ==(Object other) => identical(this, other) || other is TemplateResponseDto && + other.html == html && + other.name == name; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (html.hashCode) + + (name.hashCode); + + @override + String toString() => 'TemplateResponseDto[html=$html, name=$name]'; + + Map toJson() { + final json = {}; + json[r'html'] = this.html; + json[r'name'] = this.name; + return json; + } + + /// Returns a new [TemplateResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static TemplateResponseDto? fromJson(dynamic value) { + upgradeDto(value, "TemplateResponseDto"); + if (value is Map) { + final json = value.cast(); + + return TemplateResponseDto( + html: mapValueOfType(json, r'html')!, + name: mapValueOfType(json, r'name')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = TemplateResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = TemplateResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of TemplateResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = TemplateResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'html', + 'name', + }; +} + diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index bc32a32e04f01..43985cae8141d 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -3430,6 +3430,57 @@ ] } }, + "/notifications/templates/{name}": { + "post": { + "operationId": "getNotificationTemplate", + "parameters": [ + { + "name": "name", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Notifications" + ] + } + }, "/notifications/test-email": { "post": { "operationId": "sendTestEmail", @@ -11538,6 +11589,9 @@ "storageTemplate": { "$ref": "#/components/schemas/SystemConfigStorageTemplateDto" }, + "templates": { + "$ref": "#/components/schemas/SystemConfigTemplatesDto" + }, "theme": { "$ref": "#/components/schemas/SystemConfigThemeDto" }, @@ -11565,6 +11619,7 @@ "reverseGeocoding", "server", "storageTemplate", + "templates", "theme", "trash", "user" @@ -12111,6 +12166,25 @@ ], "type": "object" }, + "SystemConfigTemplateEmailsDto": { + "properties": { + "albumInviteTemplate": { + "type": "string" + }, + "albumUpdateTemplate": { + "type": "string" + }, + "welcomeTemplate": { + "type": "string" + } + }, + "required": [ + "albumInviteTemplate", + "albumUpdateTemplate", + "welcomeTemplate" + ], + "type": "object" + }, "SystemConfigTemplateStorageOptionDto": { "properties": { "dayOptions": { @@ -12174,6 +12248,17 @@ ], "type": "object" }, + "SystemConfigTemplatesDto": { + "properties": { + "email": { + "$ref": "#/components/schemas/SystemConfigTemplateEmailsDto" + } + }, + "required": [ + "email" + ], + "type": "object" + }, "SystemConfigThemeDto": { "properties": { "customCss": { @@ -12352,6 +12437,32 @@ }, "type": "object" }, + "TemplateDto": { + "properties": { + "template": { + "type": "string" + } + }, + "required": [ + "template" + ], + "type": "object" + }, + "TemplateResponseDto": { + "properties": { + "html": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "html", + "name" + ], + "type": "object" + }, "TestEmailResponseDto": { "properties": { "messageId": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index d786139ab51f4..20d0c5715fa91 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -634,6 +634,13 @@ export type MemoryUpdateDto = { memoryAt?: string; seenAt?: string; }; +export type TemplateDto = { + template: string; +}; +export type TemplateResponseDto = { + html: string; + name: string; +}; export type SystemConfigSmtpTransportDto = { host: string; ignoreCert: boolean; @@ -1232,6 +1239,14 @@ export type SystemConfigStorageTemplateDto = { hashVerificationEnabled: boolean; template: string; }; +export type SystemConfigTemplateEmailsDto = { + albumInviteTemplate: string; + albumUpdateTemplate: string; + welcomeTemplate: string; +}; +export type SystemConfigTemplatesDto = { + email: SystemConfigTemplateEmailsDto; +}; export type SystemConfigThemeDto = { customCss: string; }; @@ -1259,6 +1274,7 @@ export type SystemConfigDto = { reverseGeocoding: SystemConfigReverseGeocodingDto; server: SystemConfigServerDto; storageTemplate: SystemConfigStorageTemplateDto; + templates: SystemConfigTemplatesDto; theme: SystemConfigThemeDto; trash: SystemConfigTrashDto; user: SystemConfigUserDto; @@ -2227,6 +2243,19 @@ export function addMemoryAssets({ id, bulkIdsDto }: { body: bulkIdsDto }))); } +export function getNotificationTemplate({ name, templateDto }: { + name: string; + templateDto: TemplateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: TemplateResponseDto; + }>(`/notifications/templates/${encodeURIComponent(name)}`, oazapfts.json({ + ...opts, + method: "POST", + body: templateDto + }))); +} export function sendTestEmail({ systemConfigSmtpDto }: { systemConfigSmtpDto: SystemConfigSmtpDto; }, opts?: Oazapfts.RequestOpts) { diff --git a/server/src/config.ts b/server/src/config.ts index dd850e063f0da..26589742003e7 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -146,6 +146,13 @@ export interface SystemConfig { }; }; }; + templates: { + email: { + welcomeTemplate: string; + albumInviteTemplate: string; + albumUpdateTemplate: string; + }; + }; server: { externalDomain: string; loginPageMessage: string; @@ -313,6 +320,13 @@ export const defaults = Object.freeze({ }, }, }, + templates: { + email: { + welcomeTemplate: '', + albumInviteTemplate: '', + albumUpdateTemplate: '', + }, + }, user: { deleteDelay: 7, }, diff --git a/server/src/controllers/notification.controller.ts b/server/src/controllers/notification.controller.ts index 3dd72dd73a91d..27034fd63a873 100644 --- a/server/src/controllers/notification.controller.ts +++ b/server/src/controllers/notification.controller.ts @@ -1,8 +1,9 @@ -import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { Body, Controller, HttpCode, HttpStatus, Param, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AuthDto } from 'src/dtos/auth.dto'; -import { TestEmailResponseDto } from 'src/dtos/notification.dto'; +import { TemplateDto, TemplateResponseDto, TestEmailResponseDto } from 'src/dtos/notification.dto'; import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; +import { EmailTemplate } from 'src/interfaces/notification.interface'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { NotificationService } from 'src/services/notification.service'; @@ -17,4 +18,15 @@ export class NotificationController { sendTestEmail(@Auth() auth: AuthDto, @Body() dto: SystemConfigSmtpDto): Promise { return this.service.sendTestEmail(auth.user.id, dto); } + + @Post('templates/:name') + @HttpCode(HttpStatus.OK) + @Authenticated({ admin: true }) + getNotificationTemplate( + @Auth() auth: AuthDto, + @Param('name') name: EmailTemplate, + @Body() dto: TemplateDto, + ): Promise { + return this.service.getTemplate(name, dto.template); + } } diff --git a/server/src/dtos/notification.dto.ts b/server/src/dtos/notification.dto.ts index 34b3923580830..c1a09c801c892 100644 --- a/server/src/dtos/notification.dto.ts +++ b/server/src/dtos/notification.dto.ts @@ -1,3 +1,13 @@ +import { IsString } from 'class-validator'; + export class TestEmailResponseDto { messageId!: string; } +export class TemplateResponseDto { + name!: string; + html!: string; +} +export class TemplateDto { + @IsString() + template!: string; +} diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 894f4c7948cab..350918254542a 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -465,6 +465,24 @@ class SystemConfigNotificationsDto { smtp!: SystemConfigSmtpDto; } +class SystemConfigTemplateEmailsDto { + @IsString() + albumInviteTemplate!: string; + + @IsString() + welcomeTemplate!: string; + + @IsString() + albumUpdateTemplate!: string; +} + +class SystemConfigTemplatesDto { + @Type(() => SystemConfigTemplateEmailsDto) + @ValidateNested() + @IsObject() + email!: SystemConfigTemplateEmailsDto; +} + class SystemConfigStorageTemplateDto { @ValidateBoolean() enabled!: boolean; @@ -636,6 +654,11 @@ export class SystemConfigDto implements SystemConfig { @IsObject() notifications!: SystemConfigNotificationsDto; + @Type(() => SystemConfigTemplatesDto) + @ValidateNested() + @IsObject() + templates!: SystemConfigTemplatesDto; + @Type(() => SystemConfigServerDto) @ValidateNested() @IsObject() diff --git a/server/src/emails/album-invite.email.tsx b/server/src/emails/album-invite.email.tsx index 232ef5290d6db..0b3819b332b5d 100644 --- a/server/src/emails/album-invite.email.tsx +++ b/server/src/emails/album-invite.email.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import { ImmichButton } from 'src/emails/components/button.component'; import ImmichLayout from 'src/emails/components/immich.layout'; import { AlbumInviteEmailProps } from 'src/interfaces/notification.interface'; +import { replaceTemplateTags } from 'src/utils/replace-template-tags'; export const AlbumInviteEmail = ({ baseUrl, @@ -11,39 +12,64 @@ export const AlbumInviteEmail = ({ senderName, albumId, cid, -}: AlbumInviteEmailProps) => ( - - - Hey {recipientName}! - - - - {senderName} has added you to the album {albumName}. - - - {cid && ( -

- + customTemplate, +}: AlbumInviteEmailProps) => { + const variables = { + albumName, + recipientName, + senderName, + albumId, + baseUrl, + }; + + const emailContent = customTemplate ? ( + replaceTemplateTags(customTemplate, variables) + ) : ( + <> + + Hey {recipientName}! + + + + {senderName} has added you to the album {albumName}. + + + ); + + return ( + + {customTemplate && ( + +
+
+ )} + + {!customTemplate && emailContent} + + {cid && ( +
+ +
+ )} + +
+ View Album
- )} - -
- View Album -
- - - If you cannot click the button use the link below to view the album. -
- {`${baseUrl}/albums/${albumId}`} -
-
-); + + + If you cannot click the button use the link below to view the album. +
+ {`${baseUrl}/albums/${albumId}`} +
+ + ); +}; AlbumInviteEmail.PreviewProps = { baseUrl: 'https://demo.immich.app', diff --git a/server/src/emails/album-update.email.tsx b/server/src/emails/album-update.email.tsx index 0fb5ad931c9f5..9dcd858e93e03 100644 --- a/server/src/emails/album-update.email.tsx +++ b/server/src/emails/album-update.email.tsx @@ -3,47 +3,80 @@ import * as React from 'react'; import { ImmichButton } from 'src/emails/components/button.component'; import ImmichLayout from 'src/emails/components/immich.layout'; import { AlbumUpdateEmailProps } from 'src/interfaces/notification.interface'; +import { replaceTemplateTags } from 'src/utils/replace-template-tags'; -export const AlbumUpdateEmail = ({ baseUrl, albumName, recipientName, albumId, cid }: AlbumUpdateEmailProps) => ( - - - Hey {recipientName}! - - - - New media has been added to {albumName}, -
check it out! -
- - {cid && ( -
- -
- )} +export const AlbumUpdateEmail = ({ + baseUrl, + albumName, + recipientName, + albumId, + cid, + customTemplate, +}: AlbumUpdateEmailProps) => { + const usableTemplateVariables = { + albumName, + recipientName, + albumId, + baseUrl, + }; + + const emailContent = customTemplate ? ( + replaceTemplateTags(customTemplate, usableTemplateVariables) + ) : ( + <> + + Hey {recipientName}! + + + + New media has been added to {albumName}, +
check it out! +
+ + ); + + return ( + + {customTemplate && ( + +
+
+ )} -
- View Album -
+ {!customTemplate && emailContent} + + {cid && ( +
+ +
+ )} + +
+ View Album +
- - If you cannot click the button use the link below to view the album. -
- {`${baseUrl}/albums/${albumId}`} -
-
-); + + If you cannot click the button use the link below to view the album. +
+ {`${baseUrl}/albums/${albumId}`} +
+
+ ); +}; AlbumUpdateEmail.PreviewProps = { baseUrl: 'https://demo.immich.app', albumName: 'Trip to Europe', albumId: 'b63f6dae-e1c9-401b-9a85-9dbbf5612539', recipientName: 'Alan Turing', + cid: '', + customTemplate: '', } as AlbumUpdateEmailProps; export default AlbumUpdateEmail; diff --git a/server/src/emails/welcome.email.tsx b/server/src/emails/welcome.email.tsx index e031ac6b97137..ced0b77698836 100644 --- a/server/src/emails/welcome.email.tsx +++ b/server/src/emails/welcome.email.tsx @@ -3,36 +3,62 @@ import * as React from 'react'; import { ImmichButton } from 'src/emails/components/button.component'; import ImmichLayout from 'src/emails/components/immich.layout'; import { WelcomeEmailProps } from 'src/interfaces/notification.interface'; +import { replaceTemplateTags } from 'src/utils/replace-template-tags'; -export const WelcomeEmail = ({ baseUrl, displayName, username, password }: WelcomeEmailProps) => ( - - - Hey {displayName}! - - - A new account has been created for you. - - - Username: {username} - {password && ( - <> -
- Password: {password} - +export const WelcomeEmail = ({ baseUrl, displayName, username, password, customTemplate }: WelcomeEmailProps) => { + const usableTemplateVariables = { + displayName, + username, + password, + baseUrl, + }; + + const emailContent = customTemplate ? ( + replaceTemplateTags(customTemplate, usableTemplateVariables) + ) : ( + <> + + Hey {displayName}! + + + A new account has been created for you. + + + Username: {username} + {password && ( + <> +
+ Password: {password} + + )} +
+ + ); + + return ( + + {customTemplate && ( + +
+
)} -
- -
- Login -
- - - If you cannot click the button use the link below to proceed with first login. -
- {baseUrl} -
-
-); + + {!customTemplate && emailContent} + +
+ Login +
+ + + If you cannot click the button use the link below to proceed with first login. +
+ {baseUrl} +
+ + ); +}; WelcomeEmail.PreviewProps = { baseUrl: 'https://demo.immich.app/auth/login', diff --git a/server/src/interfaces/notification.interface.ts b/server/src/interfaces/notification.interface.ts index ec0ecc534b6b1..b20b3c50aee8f 100644 --- a/server/src/interfaces/notification.interface.ts +++ b/server/src/interfaces/notification.interface.ts @@ -39,6 +39,7 @@ export enum EmailTemplate { interface BaseEmailProps { baseUrl: string; + customTemplate?: string; } export interface TestEmailProps extends BaseEmailProps { @@ -70,18 +71,22 @@ export type EmailRenderRequest = | { template: EmailTemplate.TEST_EMAIL; data: TestEmailProps; + customTemplate: string; } | { template: EmailTemplate.WELCOME; data: WelcomeEmailProps; + customTemplate: string; } | { template: EmailTemplate.ALBUM_INVITE; data: AlbumInviteEmailProps; + customTemplate: string; } | { template: EmailTemplate.ALBUM_UPDATE; data: AlbumUpdateEmailProps; + customTemplate: string; }; export type SendEmailResponse = { diff --git a/server/src/repositories/notification.repository.spec.ts b/server/src/repositories/notification.repository.spec.ts index 983be21d2b905..368ba3f0ce3ff 100644 --- a/server/src/repositories/notification.repository.spec.ts +++ b/server/src/repositories/notification.repository.spec.ts @@ -21,6 +21,7 @@ describe(NotificationRepository.name, () => { const request: EmailRenderRequest = { template: EmailTemplate.TEST_EMAIL, data: { displayName: 'Alen Turing', baseUrl: 'http://localhost' }, + customTemplate: '', }; const result = await sut.renderEmail(request); @@ -33,6 +34,7 @@ describe(NotificationRepository.name, () => { const request: EmailRenderRequest = { template: EmailTemplate.WELCOME, data: { displayName: 'Alen Turing', username: 'turing', baseUrl: 'http://localhost' }, + customTemplate: '', }; const result = await sut.renderEmail(request); @@ -51,6 +53,7 @@ describe(NotificationRepository.name, () => { recipientName: 'Jane', baseUrl: 'http://localhost', }, + customTemplate: '', }; const result = await sut.renderEmail(request); @@ -63,6 +66,7 @@ describe(NotificationRepository.name, () => { const request: EmailRenderRequest = { template: EmailTemplate.ALBUM_UPDATE, data: { albumName: 'Holiday', albumId: '123', recipientName: 'Jane', baseUrl: 'http://localhost' }, + customTemplate: '', }; const result = await sut.renderEmail(request); diff --git a/server/src/repositories/notification.repository.ts b/server/src/repositories/notification.repository.ts index 293a80576fa70..b2444301e5335 100644 --- a/server/src/repositories/notification.repository.ts +++ b/server/src/repositories/notification.repository.ts @@ -55,22 +55,22 @@ export class NotificationRepository implements INotificationRepository { } } - private render({ template, data }: EmailRenderRequest): React.FunctionComponentElement { + private render({ template, data, customTemplate }: EmailRenderRequest): React.FunctionComponentElement { switch (template) { case EmailTemplate.TEST_EMAIL: { - return React.createElement(TestEmail, data); + return React.createElement(TestEmail, { ...data, customTemplate }); } case EmailTemplate.WELCOME: { - return React.createElement(WelcomeEmail, data); + return React.createElement(WelcomeEmail, { ...data, customTemplate }); } case EmailTemplate.ALBUM_INVITE: { - return React.createElement(AlbumInviteEmail, data); + return React.createElement(AlbumInviteEmail, { ...data, customTemplate }); } case EmailTemplate.ALBUM_UPDATE: { - return React.createElement(AlbumUpdateEmail, data); + return React.createElement(AlbumUpdateEmail, { ...data, customTemplate }); } } } diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index e7c0201963036..37b265c6ae741 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -140,7 +140,7 @@ export class NotificationService extends BaseService { setTimeout(() => this.eventRepository.clientSend('on_session_delete', sessionId, sessionId), 500); } - async sendTestEmail(id: string, dto: SystemConfigSmtpDto) { + async sendTestEmail(id: string, dto: SystemConfigSmtpDto, tempTemplate?: string) { const user = await this.userRepository.get(id, { withDeleted: false }); if (!user) { throw new Error('User not found'); @@ -160,8 +160,8 @@ export class NotificationService extends BaseService { baseUrl: getExternalDomain(server, port), displayName: user.name, }, + customTemplate: tempTemplate!, }); - const { messageId } = await this.notificationRepository.sendEmail({ to: user.email, subject: 'Test email from Immich', @@ -175,6 +175,69 @@ export class NotificationService extends BaseService { return { messageId }; } + async getTemplate(name: EmailTemplate, customTemplate: string) { + const { server, templates } = await this.getConfig({ withCache: false }); + const { port } = this.configRepository.getEnv(); + + let templateResponse = ''; + + switch (name) { + case EmailTemplate.WELCOME: { + const { html: _welcomeHtml } = await this.notificationRepository.renderEmail({ + template: EmailTemplate.WELCOME, + data: { + baseUrl: getExternalDomain(server, port), + displayName: 'John Doe', + username: 'john@doe.com', + password: 'thisIsAPassword123', + }, + customTemplate: customTemplate || templates.email.welcomeTemplate, + }); + + templateResponse = _welcomeHtml; + break; + } + case EmailTemplate.ALBUM_UPDATE: { + const { html: _updateAlbumHtml } = await this.notificationRepository.renderEmail({ + template: EmailTemplate.ALBUM_UPDATE, + data: { + baseUrl: getExternalDomain(server, port), + albumId: '1', + albumName: 'Favorite Photos', + recipientName: 'Jane Doe', + cid: undefined, + }, + customTemplate: customTemplate || templates.email.albumInviteTemplate, + }); + templateResponse = _updateAlbumHtml; + break; + } + + case EmailTemplate.ALBUM_INVITE: { + const { html } = await this.notificationRepository.renderEmail({ + template: EmailTemplate.ALBUM_INVITE, + data: { + baseUrl: getExternalDomain(server, port), + albumId: '1', + albumName: "John Doe's Favorites", + senderName: 'John Doe', + recipientName: 'Jane Doe', + cid: undefined, + }, + customTemplate: customTemplate || templates.email.albumInviteTemplate, + }); + templateResponse = html; + break; + } + default: { + templateResponse = ''; + break; + } + } + + return { name, html: templateResponse }; + } + @OnJob({ name: JobName.NOTIFY_SIGNUP, queue: QueueName.NOTIFICATION }) async handleUserSignup({ id, tempPassword }: JobOf) { const user = await this.userRepository.get(id, { withDeleted: false }); @@ -182,7 +245,7 @@ export class NotificationService extends BaseService { return JobStatus.SKIPPED; } - const { server } = await this.getConfig({ withCache: true }); + const { server, templates } = await this.getConfig({ withCache: true }); const { port } = this.configRepository.getEnv(); const { html, text } = await this.notificationRepository.renderEmail({ template: EmailTemplate.WELCOME, @@ -192,6 +255,7 @@ export class NotificationService extends BaseService { username: user.email, password: tempPassword, }, + customTemplate: templates.email.welcomeTemplate, }); await this.jobRepository.queue({ @@ -227,7 +291,7 @@ export class NotificationService extends BaseService { const attachment = await this.getAlbumThumbnailAttachment(album); - const { server } = await this.getConfig({ withCache: false }); + const { server, templates } = await this.getConfig({ withCache: false }); const { port } = this.configRepository.getEnv(); const { html, text } = await this.notificationRepository.renderEmail({ template: EmailTemplate.ALBUM_INVITE, @@ -239,6 +303,7 @@ export class NotificationService extends BaseService { recipientName: recipient.name, cid: attachment ? attachment.cid : undefined, }, + customTemplate: templates.email.albumInviteTemplate, }); await this.jobRepository.queue({ @@ -273,7 +338,7 @@ export class NotificationService extends BaseService { ); const attachment = await this.getAlbumThumbnailAttachment(album); - const { server } = await this.getConfig({ withCache: false }); + const { server, templates } = await this.getConfig({ withCache: false }); const { port } = this.configRepository.getEnv(); for (const recipient of recipients) { @@ -297,6 +362,7 @@ export class NotificationService extends BaseService { recipientName: recipient.name, cid: attachment ? attachment.cid : undefined, }, + customTemplate: templates.email.albumUpdateTemplate, }); await this.jobRepository.queue({ diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 2550c15de25ca..2a20f329330ae 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -190,6 +190,13 @@ const updatedConfig = Object.freeze({ }, }, }, + templates: { + email: { + albumInviteTemplate: '', + welcomeTemplate: '', + albumUpdateTemplate: '', + }, + }, }); describe(SystemConfigService.name, () => { diff --git a/server/src/utils/replace-template-tags.ts b/server/src/utils/replace-template-tags.ts new file mode 100644 index 0000000000000..70333d7dfff5b --- /dev/null +++ b/server/src/utils/replace-template-tags.ts @@ -0,0 +1,5 @@ +export const replaceTemplateTags = (template: string, variables: Record) => { + return template.replaceAll(/{(.*?)}/g, (_, key) => { + return variables[key] || `{${key}}`; + }); +}; diff --git a/web/package-lock.json b/web/package-lock.json index f06484fe8f758..15edeb0c289cc 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -23,7 +23,7 @@ "justified-layout": "^4.1.0", "lodash-es": "^4.17.21", "luxon": "^3.4.4", - "socket.io-client": "^4.7.5", + "socket.io-client": "~4.7.5", "svelte-gestures": "^5.0.4", "svelte-i18n": "^4.0.1", "svelte-local-storage-store": "^0.6.4", diff --git a/web/src/lib/components/admin-page/settings/notification-settings/notification-settings.svelte b/web/src/lib/components/admin-page/settings/notification-settings/notification-settings.svelte index 28187978f9581..30a9fbad5c626 100644 --- a/web/src/lib/components/admin-page/settings/notification-settings/notification-settings.svelte +++ b/web/src/lib/components/admin-page/settings/notification-settings/notification-settings.svelte @@ -17,6 +17,7 @@ import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import { handleError } from '$lib/utils/handle-error'; import { SettingInputFieldType } from '$lib/constants'; + import TemplateSettings from '$lib/components/admin-page/settings/template-settings/template-settings.svelte'; interface Props { savedConfig: SystemConfigDto; @@ -162,13 +163,14 @@
- - onReset({ ...options, configKeys: ['notifications'] })} - onSave={() => onSave({ notifications: config.notifications })} - showResetToDefault={!isEqual(savedConfig, defaultConfig)} - {disabled} - /> + + + onReset({ ...options, configKeys: ['notifications', 'templates'] })} + onSave={() => onSave({ notifications: config.notifications, templates: config.templates })} + showResetToDefault={!isEqual(savedConfig, defaultConfig)} + {disabled} + /> diff --git a/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte b/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte new file mode 100644 index 0000000000000..c27df817c2914 --- /dev/null +++ b/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte @@ -0,0 +1,131 @@ + + +
+
+
+
+ +
+

+ + {$t('admin.template_email_if_empty')} + +

+
+ {#if loadingPreview} + + {/if} + + {#each templateConfigs as { label, templateKey, descriptionTags, templateName }} + +
+ +
+ {/each} +
+
+
+ + {#if htmlPreview} + +
+ +
+
+ {/if} +
+
+
From 5060ee95c28221bfeb7cd10181493b6cd6c957d6 Mon Sep 17 00:00:00 2001 From: Tim Van Onckelen <2817556+TimVanOnckelen@users.noreply.github.com> Date: Wed, 4 Dec 2024 21:38:55 +0100 Subject: [PATCH 06/19] feat(web): Album preview overview in menu (#13981) --- i18n/en.json | 2 + .../side-bar/recent-albums.svelte | 40 ++++++++++++ .../side-bar/side-bar-link.svelte | 61 ++++++++++++++----- .../side-bar/side-bar.svelte | 17 +++++- web/src/lib/stores/preferences.store.ts | 2 + 5 files changed, 106 insertions(+), 16 deletions(-) create mode 100644 web/src/lib/components/shared-components/side-bar/recent-albums.svelte diff --git a/i18n/en.json b/i18n/en.json index 9741c10b2970f..073d4ba893eaf 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -736,6 +736,7 @@ "external": "External", "external_libraries": "External Libraries", "face_unassigned": "Unassigned", + "failed_to_load_assets": "Failed to load assets", "favorite": "Favorite", "favorite_or_unfavorite_photo": "Favorite or unfavorite photo", "favorites": "Favorites", @@ -1036,6 +1037,7 @@ "reassing_hint": "Assign selected assets to an existing person", "recent": "Recent", "recent_searches": "Recent searches", + "recent-albums": "Recent albums", "refresh": "Refresh", "refresh_encoded_videos": "Refresh encoded videos", "refresh_faces": "Refresh faces", diff --git a/web/src/lib/components/shared-components/side-bar/recent-albums.svelte b/web/src/lib/components/shared-components/side-bar/recent-albums.svelte new file mode 100644 index 0000000000000..a412d5cc42ed5 --- /dev/null +++ b/web/src/lib/components/shared-components/side-bar/recent-albums.svelte @@ -0,0 +1,40 @@ + + +{#each albums as album} + +
+
+
+
+ {album.albumName} +
+
+{/each} diff --git a/web/src/lib/components/shared-components/side-bar/side-bar-link.svelte b/web/src/lib/components/shared-components/side-bar/side-bar-link.svelte index 13f08533c5262..4da73b6288408 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar-link.svelte +++ b/web/src/lib/components/shared-components/side-bar/side-bar-link.svelte @@ -1,7 +1,10 @@ - + {#if hasDropdown} + + {/if} + -
- - {title} -
-
-
+ > +
+ + {title} +
+
+ + +{#if hasDropdown && dropdownOpen} + {@render hasDropdown?.()} +{/if} diff --git a/web/src/lib/components/shared-components/side-bar/side-bar.svelte b/web/src/lib/components/shared-components/side-bar/side-bar.svelte index 000afa5d1a302..9c49b971bae0e 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar.svelte +++ b/web/src/lib/components/shared-components/side-bar/side-bar.svelte @@ -27,6 +27,9 @@ import { t } from 'svelte-i18n'; import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte'; import { preferences } from '$lib/stores/user.store'; + import { recentAlbumsDropdown } from '$lib/stores/preferences.store'; + import RecentAlbums from '$lib/components/shared-components/side-bar/recent-albums.svelte'; + import { fly } from 'svelte/transition'; let isArchiveSelected: boolean = $state(false); let isFavoritesSelected: boolean = $state(false); @@ -88,7 +91,19 @@ bind:isSelected={isFavoritesSelected} > - + + {#snippet dropDownContent()} + + {/snippet} + {#if $preferences.tags.enabled && $preferences.tags.sidebarWeb} diff --git a/web/src/lib/stores/preferences.store.ts b/web/src/lib/stores/preferences.store.ts index 87f4a7ba44708..2b3ff86c2f7e8 100644 --- a/web/src/lib/stores/preferences.store.ts +++ b/web/src/lib/stores/preferences.store.ts @@ -144,3 +144,5 @@ export const alwaysLoadOriginalFile = persisted('always-load-original-f export const playVideoThumbnailOnHover = persisted('play-video-thumbnail-on-hover', true, {}); export const loopVideo = persisted('loop-video', true, {}); + +export const recentAlbumsDropdown = persisted('recent-albums-open', true, {}); From 3c38851d5095baa7ba1baf93abcea1d14a8b0f8b Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Thu, 5 Dec 2024 02:33:46 +0530 Subject: [PATCH 07/19] feat(mobile): native_video_player (#12104) * add native player library * splitup the player * stateful widget * refactor: native_video_player * fix: handle buffering * turn on volume when video plays * fix: aspect ratio * fix: handle remote asset orientation * refinements and fixes fix orientation for remote assets wip separate widget separate video loader widget fixed memory leak optimized seeking, cleanup debug context pop use global key back to one widget fixed rebuild wait for swipe animation to finish smooth hero animation for remote videos faster scroll animation * clean up logging * refactor aspect ratio calculation * removed unnecessary import * transitive dependencies * fixed referencing uninitialized orientation * use correct ref to build android * higher res placeholder for local videos * slightly lower delay * await things * fix controls when swiping between image and video * linting * extra smooth seeking, add comments * chore: generate router page * use current asset provider and loadAsset * fix stack handling * improved motion photo handling * use visibility for motion videos * error handling for async calls * fix duplicate key error * maybe fix duplicate key error * increase delay for hero animation * faster initialization for remote videos * ensure dimensions for memory cards * make aspect ratio logic reusable, optimizations * refactor: move exif search from aspect ratio to orientation * local orientation on ios is unreliable; prefer remote * fix no audio in silent mode on ios * increase bottom bar opacity to account for hdr * remove unused import * fix live photo play button not updating * fix map marker -> galleryviewer * remove video_player * fix hdr playback on android * fix looping * remove unused dependencies * update to latest player commit * fix player controls hiding when video is not playing * fix restart video * stop showing motion video after ending when looping is disabled * delay video initialization to avoid placeholder flicker * faster animation * shorter delay * small delay for image -> video on android * fix: lint * hide stacked children when controls are hidden, avoid bottom bar dropping --------- Co-authored-by: Alex Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com> --- mobile/android/app/build.gradle | 4 +- .../android/app/src/main/AndroidManifest.xml | 2 +- mobile/android/build.gradle | 4 +- mobile/ios/Podfile.lock | 19 +- mobile/ios/Runner/AppDelegate.swift | 75 ++-- mobile/lib/constants/immich_colors.dart | 2 +- mobile/lib/entities/asset.entity.dart | 115 +++-- mobile/lib/entities/exif_info.entity.dart | 31 +- mobile/lib/entities/exif_info.entity.g.dart | 213 ++++++++- mobile/lib/extensions/scroll_extensions.dart | 38 ++ .../common/gallery_stacked_children.dart | 91 ++++ .../lib/pages/common/gallery_viewer.page.dart | 415 ++++++++---------- .../common/native_video_viewer.page.dart | 411 +++++++++++++++++ .../lib/pages/common/video_viewer.page.dart | 167 ------- mobile/lib/pages/photos/memory.page.dart | 4 + mobile/lib/pages/search/map/map.page.dart | 11 +- .../asset_viewer/asset_stack.provider.dart | 40 +- .../is_motion_video_playing.provider.dart | 23 + .../video_player_controller_provider.dart | 46 -- .../video_player_controller_provider.g.dart | 164 ------- .../video_player_controls_provider.dart | 96 ++-- .../video_player_value_provider.dart | 81 ++-- .../image/immich_local_image_provider.dart | 76 ++-- mobile/lib/routing/router.dart | 5 + mobile/lib/routing/router.gr.dart | 58 +++ mobile/lib/services/asset.service.dart | 26 ++ mobile/lib/utils/debounce.dart | 57 ++- .../utils/hooks/chewiew_controller_hook.dart | 161 ------- mobile/lib/utils/hooks/interval_hook.dart | 18 + mobile/lib/utils/migration.dart | 2 +- mobile/lib/utils/throttle.dart | 9 +- .../asset_grid/immich_asset_grid_view.dart | 32 +- .../asset_viewer/bottom_gallery_bar.dart | 34 +- .../custom_video_player_controls.dart | 53 +-- .../asset_viewer/detail_panel/file_info.dart | 7 +- .../widgets/asset_viewer/gallery_app_bar.dart | 23 +- .../asset_viewer/motion_photo_button.dart | 22 + .../asset_viewer/top_control_app_bar.dart | 28 +- .../widgets/asset_viewer/video_player.dart | 48 -- .../widgets/asset_viewer/video_position.dart | 8 +- mobile/lib/widgets/common/immich_image.dart | 9 +- mobile/lib/widgets/memories/memory_card.dart | 24 +- mobile/pubspec.lock | 99 +---- mobile/pubspec.yaml | 9 +- 44 files changed, 1621 insertions(+), 1239 deletions(-) create mode 100644 mobile/lib/extensions/scroll_extensions.dart create mode 100644 mobile/lib/pages/common/gallery_stacked_children.dart create mode 100644 mobile/lib/pages/common/native_video_viewer.page.dart delete mode 100644 mobile/lib/pages/common/video_viewer.page.dart create mode 100644 mobile/lib/providers/asset_viewer/is_motion_video_playing.provider.dart delete mode 100644 mobile/lib/providers/asset_viewer/video_player_controller_provider.dart delete mode 100644 mobile/lib/providers/asset_viewer/video_player_controller_provider.g.dart delete mode 100644 mobile/lib/utils/hooks/chewiew_controller_hook.dart create mode 100644 mobile/lib/utils/hooks/interval_hook.dart create mode 100644 mobile/lib/widgets/asset_viewer/motion_photo_button.dart delete mode 100644 mobile/lib/widgets/asset_viewer/video_player.dart diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 506ee9d1a4b0a..0ec511d9f125e 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -28,7 +28,7 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 34 + compileSdkVersion 35 compileOptions { sourceCompatibility JavaVersion.VERSION_17 @@ -47,7 +47,7 @@ android { defaultConfig { applicationId "app.alextran.immich" minSdkVersion 26 - targetSdkVersion 34 + targetSdkVersion 35 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index c85ce136844bc..8f239015dd62a 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -35,7 +35,7 @@ + android:value="true" /> Bool { - // Required for flutter_local_notification - if #available(iOS 10.0, *) { - UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate + // Required for flutter_local_notification + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate + } + + do { + try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default) + try AVAudioSession.sharedInstance().setActive(true) + } catch { + print("Failed to set audio session category. Error: \(error)") + } + + GeneratedPluginRegistrant.register(with: self) + BackgroundServicePlugin.registerBackgroundProcessing() + + BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!) + + BackgroundServicePlugin.setPluginRegistrantCallback { registry in + if !registry.hasPlugin("org.cocoapods.path-provider-ios") { + FLTPathProviderPlugin.register( + with: registry.registrar(forPlugin: "org.cocoapods.path-provider-ios")!) + } + + if !registry.hasPlugin("org.cocoapods.photo-manager") { + PhotoManagerPlugin.register( + with: registry.registrar(forPlugin: "org.cocoapods.photo-manager")!) } - GeneratedPluginRegistrant.register(with: self) - BackgroundServicePlugin.registerBackgroundProcessing() - - BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!) - - BackgroundServicePlugin.setPluginRegistrantCallback { registry in - if !registry.hasPlugin("org.cocoapods.path-provider-ios") { - FLTPathProviderPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.path-provider-ios")!) - } - - if !registry.hasPlugin("org.cocoapods.photo-manager") { - PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.photo-manager")!) - } - - if !registry.hasPlugin("org.cocoapods.shared-preferences-foundation") { - SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.shared-preferences-foundation")!) - } - - if !registry.hasPlugin("org.cocoapods.permission-handler-apple") { - PermissionHandlerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.permission-handler-apple")!) - } + if !registry.hasPlugin("org.cocoapods.shared-preferences-foundation") { + SharedPreferencesPlugin.register( + with: registry.registrar(forPlugin: "org.cocoapods.shared-preferences-foundation")!) } - - return super.application(application, didFinishLaunchingWithOptions: launchOptions) + + if !registry.hasPlugin("org.cocoapods.permission-handler-apple") { + PermissionHandlerPlugin.register( + with: registry.registrar(forPlugin: "org.cocoapods.permission-handler-apple")!) + } + } + + return super.application(application, didFinishLaunchingWithOptions: launchOptions) } - + } diff --git a/mobile/lib/constants/immich_colors.dart b/mobile/lib/constants/immich_colors.dart index a49e783602b4d..847887de8c6b6 100644 --- a/mobile/lib/constants/immich_colors.dart +++ b/mobile/lib/constants/immich_colors.dart @@ -20,8 +20,8 @@ const String defaultColorPresetName = "indigo"; const Color immichBrandColorLight = Color(0xFF4150AF); const Color immichBrandColorDark = Color(0xFFACCBFA); const Color whiteOpacity75 = Color.fromARGB((0.75 * 255) ~/ 1, 255, 255, 255); -const Color blackOpacity90 = Color.fromARGB((0.90 * 255) ~/ 1, 0, 0, 0); const Color red400 = Color(0xFFEF5350); +const Color grey200 = Color(0xFFEEEEEE); final Map _themePresetsMap = { ImmichColorPreset.indigo: ImmichTheme( diff --git a/mobile/lib/entities/asset.entity.dart b/mobile/lib/entities/asset.entity.dart index 182c10307fdef..4bec35970a44f 100644 --- a/mobile/lib/entities/asset.entity.dart +++ b/mobile/lib/entities/asset.entity.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:immich_mobile/entities/exif_info.entity.dart'; import 'package:immich_mobile/utils/hash.dart'; @@ -22,12 +23,8 @@ class Asset { durationInSeconds = remote.duration.toDuration()?.inSeconds ?? 0, type = remote.type.toAssetType(), fileName = remote.originalFileName, - height = isFlipped(remote) - ? remote.exifInfo?.exifImageWidth?.toInt() - : remote.exifInfo?.exifImageHeight?.toInt(), - width = isFlipped(remote) - ? remote.exifInfo?.exifImageHeight?.toInt() - : remote.exifInfo?.exifImageWidth?.toInt(), + height = remote.exifInfo?.exifImageHeight?.toInt(), + width = remote.exifInfo?.exifImageWidth?.toInt(), livePhotoVideoId = remote.livePhotoVideoId, ownerId = fastHash(remote.ownerId), exifInfo = @@ -93,6 +90,27 @@ class Asset { set local(AssetEntity? assetEntity) => _local = assetEntity; + @ignore + bool _didUpdateLocal = false; + + @ignore + Future get localAsync async { + final local = this.local; + if (local == null) { + throw Exception('Asset $fileName has no local data'); + } + + final updatedLocal = + _didUpdateLocal ? local : await local.obtainForNewProperties(); + if (updatedLocal == null) { + throw Exception('Could not fetch local data for $fileName'); + } + + this.local = updatedLocal; + _didUpdateLocal = true; + return updatedLocal; + } + Id id = Isar.autoIncrement; /// stores the raw SHA1 bytes as a base64 String @@ -150,10 +168,21 @@ class Asset { int stackCount; - /// Aspect ratio of the asset + /// Returns null if the asset has no sync access to the exif info @ignore - double? get aspectRatio => - width == null || height == null ? 0 : width! / height!; + double? get aspectRatio { + final orientatedWidth = this.orientatedWidth; + final orientatedHeight = this.orientatedHeight; + + if (orientatedWidth != null && + orientatedHeight != null && + orientatedWidth > 0 && + orientatedHeight > 0) { + return orientatedWidth.toDouble() / orientatedHeight.toDouble(); + } + + return null; + } /// `true` if this [Asset] is present on the device @ignore @@ -172,6 +201,12 @@ class Asset { @ignore bool get isImage => type == AssetType.image; + @ignore + bool get isVideo => type == AssetType.video; + + @ignore + bool get isMotionPhoto => livePhotoVideoId != null; + @ignore AssetState get storage { if (isRemote && isLocal) { @@ -192,6 +227,50 @@ class Asset { @ignore set byteHash(List hash) => checksum = base64.encode(hash); + /// Returns null if the asset has no sync access to the exif info + @ignore + @pragma('vm:prefer-inline') + bool? get isFlipped { + final exifInfo = this.exifInfo; + if (exifInfo != null) { + return exifInfo.isFlipped; + } + + if (_didUpdateLocal && Platform.isAndroid) { + final local = this.local; + if (local == null) { + throw Exception('Asset $fileName has no local data'); + } + return local.orientation == 90 || local.orientation == 270; + } + + return null; + } + + /// Returns null if the asset has no sync access to the exif info + @ignore + @pragma('vm:prefer-inline') + int? get orientatedHeight { + final isFlipped = this.isFlipped; + if (isFlipped == null) { + return null; + } + + return isFlipped ? width : height; + } + + /// Returns null if the asset has no sync access to the exif info + @ignore + @pragma('vm:prefer-inline') + int? get orientatedWidth { + final isFlipped = this.isFlipped; + if (isFlipped == null) { + return null; + } + + return isFlipped ? height : width; + } + @override bool operator ==(other) { if (other is! Asset) return false; @@ -511,21 +590,3 @@ extension AssetsHelper on IsarCollection { return where().anyOf(ids, (q, String e) => q.localIdEqualTo(e)); } } - -/// Returns `true` if this [int] is flipped 90° clockwise -bool isRotated90CW(int orientation) { - return [7, 8, -90].contains(orientation); -} - -/// Returns `true` if this [int] is flipped 270° clockwise -bool isRotated270CW(int orientation) { - return [5, 6, 90].contains(orientation); -} - -/// Returns `true` if this [Asset] is flipped 90° or 270° clockwise -bool isFlipped(AssetResponseDto response) { - final int orientation = - int.tryParse(response.exifInfo?.orientation ?? '0') ?? 0; - return orientation != 0 && - (isRotated90CW(orientation) || isRotated270CW(orientation)); -} diff --git a/mobile/lib/entities/exif_info.entity.dart b/mobile/lib/entities/exif_info.entity.dart index 63d06f5d2c1aa..c46f3dddc15e0 100644 --- a/mobile/lib/entities/exif_info.entity.dart +++ b/mobile/lib/entities/exif_info.entity.dart @@ -23,6 +23,7 @@ class ExifInfo { String? state; String? country; String? description; + String? orientation; @ignore bool get hasCoordinates => @@ -45,6 +46,13 @@ class ExifInfo { @ignore String get focalLength => mm != null ? mm!.toStringAsFixed(1) : ""; + @ignore + bool? _isFlipped; + + @ignore + @pragma('vm:prefer-inline') + bool get isFlipped => _isFlipped ??= _isOrientationFlipped(orientation); + @ignore double? get latitude => lat; @@ -67,7 +75,8 @@ class ExifInfo { city = dto.city, state = dto.state, country = dto.country, - description = dto.description; + description = dto.description, + orientation = dto.orientation; ExifInfo({ this.id, @@ -87,6 +96,7 @@ class ExifInfo { this.state, this.country, this.description, + this.orientation, }); ExifInfo copyWith({ @@ -107,6 +117,7 @@ class ExifInfo { String? state, String? country, String? description, + String? orientation, }) => ExifInfo( id: id ?? this.id, @@ -126,6 +137,7 @@ class ExifInfo { state: state ?? this.state, country: country ?? this.country, description: description ?? this.description, + orientation: orientation ?? this.orientation, ); @override @@ -147,7 +159,8 @@ class ExifInfo { city == other.city && state == other.state && country == other.country && - description == other.description; + description == other.description && + orientation == other.orientation; } @override @@ -169,7 +182,8 @@ class ExifInfo { city.hashCode ^ state.hashCode ^ country.hashCode ^ - description.hashCode; + description.hashCode ^ + orientation.hashCode; @override String toString() { @@ -192,10 +206,21 @@ class ExifInfo { state: $state, country: $country, description: $description, + orientation: $orientation }"""; } } +bool _isOrientationFlipped(String? orientation) { + final value = orientation != null ? int.tryParse(orientation) : null; + if (value == null) { + return false; + } + final isRotated90CW = value == 5 || value == 6 || value == 90; + final isRotated270CW = value == 7 || value == 8 || value == -90; + return isRotated90CW || isRotated270CW; +} + double? _exposureTimeToSeconds(String? s) { if (s == null) { return null; diff --git a/mobile/lib/entities/exif_info.entity.g.dart b/mobile/lib/entities/exif_info.entity.g.dart index 015983abf289f..0b744e5f20ae6 100644 --- a/mobile/lib/entities/exif_info.entity.g.dart +++ b/mobile/lib/entities/exif_info.entity.g.dart @@ -87,13 +87,18 @@ const ExifInfoSchema = CollectionSchema( name: r'model', type: IsarType.string, ), - r'state': PropertySchema( + r'orientation': PropertySchema( id: 14, + name: r'orientation', + type: IsarType.string, + ), + r'state': PropertySchema( + id: 15, name: r'state', type: IsarType.string, ), r'timeZone': PropertySchema( - id: 15, + id: 16, name: r'timeZone', type: IsarType.string, ) @@ -154,6 +159,12 @@ int _exifInfoEstimateSize( bytesCount += 3 + value.length * 3; } } + { + final value = object.orientation; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } { final value = object.state; if (value != null) { @@ -189,8 +200,9 @@ void _exifInfoSerialize( writer.writeString(offsets[11], object.make); writer.writeFloat(offsets[12], object.mm); writer.writeString(offsets[13], object.model); - writer.writeString(offsets[14], object.state); - writer.writeString(offsets[15], object.timeZone); + writer.writeString(offsets[14], object.orientation); + writer.writeString(offsets[15], object.state); + writer.writeString(offsets[16], object.timeZone); } ExifInfo _exifInfoDeserialize( @@ -215,8 +227,9 @@ ExifInfo _exifInfoDeserialize( make: reader.readStringOrNull(offsets[11]), mm: reader.readFloatOrNull(offsets[12]), model: reader.readStringOrNull(offsets[13]), - state: reader.readStringOrNull(offsets[14]), - timeZone: reader.readStringOrNull(offsets[15]), + orientation: reader.readStringOrNull(offsets[14]), + state: reader.readStringOrNull(offsets[15]), + timeZone: reader.readStringOrNull(offsets[16]), ); return object; } @@ -260,6 +273,8 @@ P _exifInfoDeserializeProp

( return (reader.readStringOrNull(offset)) as P; case 15: return (reader.readStringOrNull(offset)) as P; + case 16: + return (reader.readStringOrNull(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); } @@ -1909,6 +1924,155 @@ extension ExifInfoQueryFilter }); } + QueryBuilder orientationIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'orientation', + )); + }); + } + + QueryBuilder + orientationIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'orientation', + )); + }); + } + + QueryBuilder orientationEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'orientation', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + orientationGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'orientation', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder orientationLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'orientation', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder orientationBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'orientation', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder orientationStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'orientation', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder orientationEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'orientation', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder orientationContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'orientation', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder orientationMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'orientation', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder orientationIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'orientation', + value: '', + )); + }); + } + + QueryBuilder + orientationIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'orientation', + value: '', + )); + }); + } + QueryBuilder stateIsNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNull( @@ -2377,6 +2541,18 @@ extension ExifInfoQuerySortBy on QueryBuilder { }); } + QueryBuilder sortByOrientation() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'orientation', Sort.asc); + }); + } + + QueryBuilder sortByOrientationDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'orientation', Sort.desc); + }); + } + QueryBuilder sortByState() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'state', Sort.asc); @@ -2584,6 +2760,18 @@ extension ExifInfoQuerySortThenBy }); } + QueryBuilder thenByOrientation() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'orientation', Sort.asc); + }); + } + + QueryBuilder thenByOrientationDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'orientation', Sort.desc); + }); + } + QueryBuilder thenByState() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'state', Sort.asc); @@ -2701,6 +2889,13 @@ extension ExifInfoQueryWhereDistinct }); } + QueryBuilder distinctByOrientation( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'orientation', caseSensitive: caseSensitive); + }); + } + QueryBuilder distinctByState( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -2809,6 +3004,12 @@ extension ExifInfoQueryProperty }); } + QueryBuilder orientationProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'orientation'); + }); + } + QueryBuilder stateProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'state'); diff --git a/mobile/lib/extensions/scroll_extensions.dart b/mobile/lib/extensions/scroll_extensions.dart new file mode 100644 index 0000000000000..5bbd73163a6b8 --- /dev/null +++ b/mobile/lib/extensions/scroll_extensions.dart @@ -0,0 +1,38 @@ +import 'package:flutter/cupertino.dart'; + +// https://stackoverflow.com/a/74453792 +class FastScrollPhysics extends ScrollPhysics { + const FastScrollPhysics({super.parent}); + + @override + FastScrollPhysics applyTo(ScrollPhysics? ancestor) { + return FastScrollPhysics(parent: buildParent(ancestor)); + } + + @override + SpringDescription get spring => const SpringDescription( + mass: 40, + stiffness: 100, + damping: 1, + ); +} + +class FastClampingScrollPhysics extends ClampingScrollPhysics { + const FastClampingScrollPhysics({super.parent}); + + @override + FastClampingScrollPhysics applyTo(ScrollPhysics? ancestor) { + return FastClampingScrollPhysics(parent: buildParent(ancestor)); + } + + @override + SpringDescription get spring => const SpringDescription( + // When swiping between videos on Android, the placeholder of the first opened video + // can briefly be seen and cause a flicker effect if the video begins to initialize + // before the animation finishes - probably a bug in PhotoViewGallery's animation handling + // Making the animation faster is not just stylistic, but also helps to avoid this flicker + mass: 80, + stiffness: 100, + damping: 1, + ); +} diff --git a/mobile/lib/pages/common/gallery_stacked_children.dart b/mobile/lib/pages/common/gallery_stacked_children.dart new file mode 100644 index 0000000000000..eafc3250494f9 --- /dev/null +++ b/mobile/lib/pages/common/gallery_stacked_children.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; +import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart'; + +class GalleryStackedChildren extends HookConsumerWidget { + final ValueNotifier stackIndex; + + const GalleryStackedChildren(this.stackIndex, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asset = ref.watch(currentAssetProvider); + if (asset == null) { + return const SizedBox(); + } + + final stackId = asset.stackId; + if (stackId == null) { + return const SizedBox(); + } + + final stackElements = ref.watch(assetStackStateProvider(stackId)); + final showControls = ref.watch(showControlsProvider); + + return IgnorePointer( + ignoring: !showControls, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 100), + opacity: showControls ? 1.0 : 0.0, + child: SizedBox( + height: 80, + child: ListView.builder( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + itemCount: stackElements.length, + padding: const EdgeInsets.only( + left: 5, + right: 5, + bottom: 30, + ), + itemBuilder: (context, index) { + final currentAsset = stackElements.elementAt(index); + final assetId = currentAsset.remoteId; + if (assetId == null) { + return const SizedBox(); + } + + return Padding( + key: ValueKey(currentAsset.id), + padding: const EdgeInsets.only(right: 5), + child: GestureDetector( + onTap: () { + stackIndex.value = index; + ref.read(currentAssetProvider.notifier).set(currentAsset); + }, + child: Container( + width: 60, + height: 60, + decoration: index == stackIndex.value + ? const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(6)), + border: Border.fromBorderSide( + BorderSide(color: Colors.white, width: 2), + ), + ) + : const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(6)), + border: null, + ), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(4)), + child: Image( + fit: BoxFit.cover, + image: ImmichRemoteImageProvider(assetId: assetId), + ), + ), + ), + ), + ); + }, + ), + ), + ), + ); + } +} diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index 5747332587a91..2ea446ea71cfb 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -8,18 +8,19 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/scroll_extensions.dart'; import 'package:immich_mobile/pages/common/download_panel.dart'; -import 'package:immich_mobile/pages/common/video_viewer.page.dart'; +import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; +import 'package:immich_mobile/pages/common/gallery_stacked_children.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/widgets/asset_viewer/advanced_bottom_sheet.dart'; @@ -35,6 +36,7 @@ import 'package:immich_mobile/widgets/photo_view/src/utils/photo_view_hero_attri @RoutePage() // ignore: must_be_immutable +/// Expects [currentAssetProvider] to be set before navigating to this page class GalleryViewerPage extends HookConsumerWidget { final int initialIndex; final int heroOffset; @@ -53,79 +55,66 @@ class GalleryViewerPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final settings = ref.watch(appSettingsServiceProvider); - final loadAsset = renderList.loadAsset; final totalAssets = useState(renderList.totalAssets); - final shouldLoopVideo = useState(AppSettingsEnum.loopVideo.defaultValue); final isZoomed = useState(false); - final isPlayingVideo = useState(false); - final localPosition = useState(null); - final currentIndex = useState(initialIndex); - final currentAsset = loadAsset(currentIndex.value); - - // Update is playing motion video - ref.listen(videoPlaybackValueProvider.select((v) => v.state), (_, state) { - isPlayingVideo.value = state == VideoPlaybackState.playing; - }); - - final stackIndex = useState(-1); - final stack = showStack && currentAsset.stackCount > 0 - ? ref.watch(assetStackStateProvider(currentAsset)) - : []; - final stackElements = showStack ? [currentAsset, ...stack] : []; - // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id - final isFromDto = currentAsset.id == noDbId; - - Asset asset = stackIndex.value == -1 - ? currentAsset - : stackElements.elementAt(stackIndex.value); - - final isMotionPhoto = asset.livePhotoVideoId != null; - // Listen provider to prevent autoDispose when navigating to other routes from within the gallery page - ref.listen(currentAssetProvider, (_, __) {}); - useEffect( - () { - // Delay state update to after the execution of build method - Future.microtask( - () => ref.read(currentAssetProvider.notifier).set(asset), - ); - return null; - }, - [asset], - ); - - useEffect( - () { - shouldLoopVideo.value = - settings.getSetting(AppSettingsEnum.loopVideo); - return null; - }, - [], - ); + final stackIndex = useState(0); + final localPosition = useRef(null); + final currentIndex = useValueNotifier(initialIndex); + final loadAsset = renderList.loadAsset; Future precacheNextImage(int index) async { + if (!context.mounted) { + return; + } + void onError(Object exception, StackTrace? stackTrace) { // swallow error silently - debugPrint('Error precaching next image: $exception, $stackTrace'); + log.severe('Error precaching next image: $exception, $stackTrace'); } try { if (index < totalAssets.value && index >= 0) { final asset = loadAsset(index); await precacheImage( - ImmichImage.imageProvider(asset: asset), + ImmichImage.imageProvider( + asset: asset, + width: context.width, + height: context.height, + ), context, onError: onError, ); } } catch (e) { // swallow error silently - debugPrint('Error precaching next image: $e'); + log.severe('Error precaching next image: $e'); context.maybePop(); } } + useEffect( + () { + if (ref.read(showControlsProvider)) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + } else { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + } + + // Delay this a bit so we can finish loading the page + Timer(const Duration(milliseconds: 400), () { + precacheNextImage(currentIndex.value + 1); + }); + + return null; + }, + const [], + ); + void showInfo() { + final asset = ref.read(currentAssetProvider); + if (asset == null) { + return; + } showModalBottomSheet( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(15.0)), @@ -183,86 +172,100 @@ class GalleryViewerPage extends HookConsumerWidget { } } - useEffect( - () { - if (ref.read(showControlsProvider)) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - } else { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); - } - isPlayingVideo.value = false; - return null; - }, - [], - ); - - useEffect( - () { - // No need to await this - unawaited( - // Delay this a bit so we can finish loading the page - Future.delayed(const Duration(milliseconds: 400)).then( - // Precache the next image - (_) => precacheNextImage(currentIndex.value + 1), - ), - ); - return null; - }, - [], - ); - ref.listen(showControlsProvider, (_, show) { - if (show) { + if (show || Platform.isIOS) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - } else { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + return; } + + // This prevents the bottom bar from "dropping" while the controls are being hidden + Timer(const Duration(milliseconds: 100), () { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + }); }); - Widget buildStackedChildren() { - return ListView.builder( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - itemCount: stackElements.length, - padding: const EdgeInsets.only( - left: 5, - right: 5, - bottom: 30, + PhotoViewGalleryPageOptions buildImage(BuildContext context, Asset asset) { + return PhotoViewGalleryPageOptions( + onDragStart: (_, details, __) { + localPosition.value = details.localPosition; + }, + onDragUpdate: (_, details, __) { + handleSwipeUpDown(details); + }, + onTapDown: (_, __, ___) { + ref.read(showControlsProvider.notifier).toggle(); + }, + onLongPressStart: asset.isMotionPhoto + ? (_, __, ___) { + ref.read(isPlayingMotionVideoProvider.notifier).playing = true; + } + : null, + imageProvider: ImmichImage.imageProvider(asset: asset), + heroAttributes: _getHeroAttributes(asset), + filterQuality: FilterQuality.high, + tightMode: true, + minScale: PhotoViewComputedScale.contained, + errorBuilder: (context, error, stackTrace) => ImmichImage( + asset, + fit: BoxFit.contain, ), - itemBuilder: (context, index) { - final assetId = stackElements.elementAt(index).remoteId; - return Padding( - padding: const EdgeInsets.only(right: 5), - child: GestureDetector( - onTap: () => stackIndex.value = index, - child: Container( - width: 60, - height: 60, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(6), - border: (stackIndex.value == -1 && index == 0) || - index == stackIndex.value - ? Border.all( - color: Colors.white, - width: 2, - ) - : null, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Image( - fit: BoxFit.cover, - image: ImmichRemoteImageProvider(assetId: assetId!), - ), - ), + ); + } + + PhotoViewGalleryPageOptions buildVideo(BuildContext context, Asset asset) { + // This key is to prevent the video player from being re-initialized during the hero animation + final key = GlobalKey(); + return PhotoViewGalleryPageOptions.customChild( + onDragStart: (_, details, __) => + localPosition.value = details.localPosition, + onDragUpdate: (_, details, __) => handleSwipeUpDown(details), + heroAttributes: _getHeroAttributes(asset), + filterQuality: FilterQuality.high, + initialScale: 1.0, + maxScale: 1.0, + minScale: 1.0, + basePosition: Alignment.center, + child: SizedBox( + width: context.width, + height: context.height, + child: NativeVideoViewerPage( + key: key, + asset: asset, + image: Image( + key: ValueKey(asset), + image: ImmichImage.imageProvider( + asset: asset, + width: context.width, + height: context.height, ), + fit: BoxFit.contain, + height: context.height, + width: context.width, + alignment: Alignment.center, ), - ); - }, + ), + ), ); } + PhotoViewGalleryPageOptions buildAsset(BuildContext context, int index) { + ref.read(isPlayingMotionVideoProvider.notifier).playing = false; + var newAsset = loadAsset(index); + final stackId = newAsset.stackId; + if (stackId != null && currentIndex.value == index) { + final stackElements = + ref.read(assetStackStateProvider(newAsset.stackId!)); + if (stackIndex.value < stackElements.length) { + newAsset = stackElements.elementAt(stackIndex.value); + } + } + + if (newAsset.isImage && !newAsset.isMotionPhoto) { + return buildImage(context, newAsset); + } + return buildVideo(context, newAsset); + } + return PopScope( // Change immersive mode back to normal "edgeToEdge" mode onPopInvokedWithResult: (didPop, _) => @@ -272,128 +275,79 @@ class GalleryViewerPage extends HookConsumerWidget { body: Stack( children: [ PhotoViewGallery.builder( + key: const ValueKey('gallery'), scaleStateChangedCallback: (state) { - isZoomed.value = state != PhotoViewScaleState.initial; - ref.read(showControlsProvider.notifier).show = !isZoomed.value; + final asset = ref.read(currentAssetProvider); + if (asset == null) { + return; + } + + if (asset.isImage && !ref.read(isPlayingMotionVideoProvider)) { + isZoomed.value = state != PhotoViewScaleState.initial; + ref.read(showControlsProvider.notifier).show = + !isZoomed.value; + } }, - loadingBuilder: (context, event, index) => ClipRect( - child: Stack( - fit: StackFit.expand, - children: [ - BackdropFilter( - filter: ui.ImageFilter.blur( - sigmaX: 10, - sigmaY: 10, + gaplessPlayback: true, + loadingBuilder: (context, event, index) { + final asset = loadAsset(index); + return ClipRect( + child: Stack( + fit: StackFit.expand, + children: [ + BackdropFilter( + filter: ui.ImageFilter.blur( + sigmaX: 10, + sigmaY: 10, + ), ), - ), - ImmichThumbnail( - asset: asset, - fit: BoxFit.contain, - ), - ], - ), - ), + ImmichThumbnail( + key: ValueKey(asset), + asset: asset, + fit: BoxFit.contain, + ), + ], + ), + ); + }, pageController: controller, scrollPhysics: isZoomed.value ? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in : (Platform.isIOS - ? const ScrollPhysics() // Use bouncing physics for iOS - : const ClampingScrollPhysics() // Use heavy physics for Android + ? const FastScrollPhysics() // Use bouncing physics for iOS + : const FastClampingScrollPhysics() // Use heavy physics for Android ), itemCount: totalAssets.value, scrollDirection: Axis.horizontal, - onPageChanged: (value) async { + onPageChanged: (value) { final next = currentIndex.value < value ? value + 1 : value - 1; ref.read(hapticFeedbackProvider.notifier).selectionClick(); - currentIndex.value = value; - stackIndex.value = -1; - isPlayingVideo.value = false; - - // Wait for page change animation to finish - await Future.delayed(const Duration(milliseconds: 400)); - // Then precache the next image - unawaited(precacheNextImage(next)); - }, - builder: (context, index) { - final a = - index == currentIndex.value ? asset : loadAsset(index); + final newAsset = loadAsset(value); - final ImageProvider provider = - ImmichImage.imageProvider(asset: a); + currentIndex.value = value; + stackIndex.value = 0; - if (a.isImage && !isPlayingVideo.value) { - return PhotoViewGalleryPageOptions( - onDragStart: (_, details, __) => - localPosition.value = details.localPosition, - onDragUpdate: (_, details, __) => - handleSwipeUpDown(details), - onTapDown: (_, __, ___) { - ref.read(showControlsProvider.notifier).toggle(); - }, - onLongPressStart: (_, __, ___) { - if (asset.livePhotoVideoId != null) { - isPlayingVideo.value = true; - } - }, - imageProvider: provider, - heroAttributes: PhotoViewHeroAttributes( - tag: isFromDto - ? '${currentAsset.remoteId}-$heroOffset' - : currentAsset.id + heroOffset, - transitionOnUserGestures: true, - ), - filterQuality: FilterQuality.high, - tightMode: true, - minScale: PhotoViewComputedScale.contained, - errorBuilder: (context, error, stackTrace) => ImmichImage( - a, - fit: BoxFit.contain, - ), - ); - } else { - return PhotoViewGalleryPageOptions.customChild( - onDragStart: (_, details, __) => - localPosition.value = details.localPosition, - onDragUpdate: (_, details, __) => - handleSwipeUpDown(details), - heroAttributes: PhotoViewHeroAttributes( - tag: isFromDto - ? '${currentAsset.remoteId}-$heroOffset' - : currentAsset.id + heroOffset, - ), - filterQuality: FilterQuality.high, - maxScale: 1.0, - minScale: 1.0, - basePosition: Alignment.center, - child: VideoViewerPage( - key: ValueKey(a), - asset: a, - isMotionVideo: a.livePhotoVideoId != null, - loopVideo: shouldLoopVideo.value, - placeholder: Image( - image: provider, - fit: BoxFit.contain, - height: context.height, - width: context.width, - alignment: Alignment.center, - ), - ), - ); + ref.read(currentAssetProvider.notifier).set(newAsset); + if (newAsset.isVideo || newAsset.isMotionPhoto) { + ref.read(videoPlaybackValueProvider.notifier).reset(); } + + // Wait for page change animation to finish, then precache the next image + Timer(const Duration(milliseconds: 400), () { + precacheNextImage(next); + }); }, + builder: buildAsset, ), Positioned( top: 0, left: 0, right: 0, child: GalleryAppBar( - asset: asset, + key: const ValueKey('app-bar'), showInfo: showInfo, - isPlayingVideo: isPlayingVideo.value, - onToggleMotionVideo: () => - isPlayingVideo.value = !isPlayingVideo.value, ), ), Positioned( @@ -402,22 +356,15 @@ class GalleryViewerPage extends HookConsumerWidget { right: 0, child: Column( children: [ - Visibility( - visible: stack.isNotEmpty, - child: SizedBox( - height: 80, - child: buildStackedChildren(), - ), - ), + GalleryStackedChildren(stackIndex), BottomGalleryBar( + key: const ValueKey('bottom-bar'), renderList: renderList, totalAssets: totalAssets, controller: controller, showStack: showStack, - stackIndex: stackIndex.value, - asset: asset, + stackIndex: stackIndex, assetIndex: currentIndex, - showVideoPlayerControls: !asset.isImage && !isMotionPhoto, ), ], ), @@ -428,4 +375,14 @@ class GalleryViewerPage extends HookConsumerWidget { ), ); } + + @pragma('vm:prefer-inline') + PhotoViewHeroAttributes _getHeroAttributes(Asset asset) { + return PhotoViewHeroAttributes( + tag: asset.isInDb + ? asset.id + heroOffset + : '${asset.remoteId}-$heroOffset', + transitionOnUserGestures: true, + ); + } } diff --git a/mobile/lib/pages/common/native_video_viewer.page.dart b/mobile/lib/pages/common/native_video_viewer.page.dart new file mode 100644 index 0000000000000..536c7f6303c6a --- /dev/null +++ b/mobile/lib/pages/common/native_video_viewer.page.dart @@ -0,0 +1,411 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart' hide Store; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/services/asset.service.dart'; +import 'package:immich_mobile/utils/debounce.dart'; +import 'package:immich_mobile/utils/hooks/interval_hook.dart'; +import 'package:immich_mobile/widgets/asset_viewer/custom_video_player_controls.dart'; +import 'package:logging/logging.dart'; +import 'package:native_video_player/native_video_player.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; + +@RoutePage() +class NativeVideoViewerPage extends HookConsumerWidget { + final Asset asset; + final bool showControls; + final Widget image; + + const NativeVideoViewerPage({ + super.key, + required this.asset, + required this.image, + this.showControls = true, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final controller = useState(null); + final lastVideoPosition = useRef(-1); + final isBuffering = useRef(false); + final showMotionVideo = useState(false); + + // When a video is opened through the timeline, `isCurrent` will immediately be true. + // When swiping from video A to video B, `isCurrent` will initially be true for video A and false for video B. + // If the swipe is completed, `isCurrent` will be true for video B after a delay. + // If the swipe is canceled, `currentAsset` will not have changed and video A will continue to play. + final currentAsset = useState(ref.read(currentAssetProvider)); + final isCurrent = currentAsset.value == asset; + + // Used to show the placeholder during hero animations for remote videos to avoid a stutter + final isVisible = + useState((Platform.isIOS && asset.isLocal) || asset.isMotionPhoto); + + final log = Logger('NativeVideoViewerPage'); + + ref.listen(isPlayingMotionVideoProvider, (_, value) async { + final videoController = controller.value; + if (!asset.isMotionPhoto || videoController == null || !context.mounted) { + return; + } + + showMotionVideo.value = value; + try { + if (value) { + await videoController.seekTo(0); + await videoController.play(); + } else { + await videoController.pause(); + } + } catch (error) { + log.severe('Error toggling motion video: $error'); + } + }); + + Future createSource() async { + if (!context.mounted) { + return null; + } + + try { + final local = asset.local; + if (local != null && !asset.isMotionPhoto) { + final file = await local.file; + if (file == null) { + throw Exception('No file found for the video'); + } + + final source = await VideoSource.init( + path: file.path, + type: VideoSourceType.file, + ); + return source; + } + + // Use a network URL for the video player controller + final serverEndpoint = Store.get(StoreKey.serverEndpoint); + final String videoUrl = asset.livePhotoVideoId != null + ? '$serverEndpoint/assets/${asset.livePhotoVideoId}/video/playback' + : '$serverEndpoint/assets/${asset.remoteId}/video/playback'; + + final source = await VideoSource.init( + path: videoUrl, + type: VideoSourceType.network, + headers: ApiService.getRequestHeaders(), + ); + return source; + } catch (error) { + log.severe( + 'Error creating video source for asset ${asset.fileName}: $error', + ); + return null; + } + } + + final videoSource = useMemoized>(() => createSource()); + final aspectRatio = useState(asset.aspectRatio); + useMemoized( + () async { + if (!context.mounted || aspectRatio.value != null) { + return null; + } + + try { + aspectRatio.value = + await ref.read(assetServiceProvider).getAspectRatio(asset); + } catch (error) { + log.severe( + 'Error getting aspect ratio for asset ${asset.fileName}: $error', + ); + } + }, + ); + + void checkIfBuffering() { + if (!context.mounted) { + return; + } + + final videoPlayback = ref.read(videoPlaybackValueProvider); + if ((isBuffering.value || + videoPlayback.state == VideoPlaybackState.initializing) && + videoPlayback.state != VideoPlaybackState.buffering) { + ref.read(videoPlaybackValueProvider.notifier).value = + videoPlayback.copyWith(state: VideoPlaybackState.buffering); + } + } + + // Timer to mark videos as buffering if the position does not change + useInterval(const Duration(seconds: 5), checkIfBuffering); + + // When the position changes, seek to the position + // Debounce the seek to avoid seeking too often + // But also don't delay the seek too much to maintain visual feedback + final seekDebouncer = useDebouncer( + interval: const Duration(milliseconds: 100), + maxWaitTime: const Duration(milliseconds: 200), + ); + ref.listen(videoPlayerControlsProvider, (oldControls, newControls) async { + final playerController = controller.value; + if (playerController == null) { + return; + } + + final playbackInfo = playerController.playbackInfo; + if (playbackInfo == null) { + return; + } + + final oldSeek = (oldControls?.position ?? 0) ~/ 1; + final newSeek = newControls.position ~/ 1; + if (oldSeek != newSeek || newControls.restarted) { + seekDebouncer.run(() => playerController.seekTo(newSeek)); + } + + if (oldControls?.pause != newControls.pause || newControls.restarted) { + // Make sure the last seek is complete before pausing or playing + // Otherwise, `onPlaybackPositionChanged` can receive outdated events + if (seekDebouncer.isActive) { + await seekDebouncer.drain(); + } + + try { + if (newControls.pause) { + await playerController.pause(); + } else { + await playerController.play(); + } + } catch (error) { + log.severe('Error pausing or playing video: $error'); + } + } + }); + + void onPlaybackReady() async { + final videoController = controller.value; + if (videoController == null || !isCurrent || !context.mounted) { + return; + } + + final videoPlayback = + VideoPlaybackValue.fromNativeController(videoController); + ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback; + + try { + if (asset.isVideo || showMotionVideo.value) { + await videoController.play(); + } + await videoController.setVolume(0.9); + } catch (error) { + log.severe('Error playing video: $error'); + } + } + + void onPlaybackStatusChanged() { + final videoController = controller.value; + if (videoController == null || !context.mounted) { + return; + } + + final videoPlayback = + VideoPlaybackValue.fromNativeController(videoController); + if (videoPlayback.state == VideoPlaybackState.playing) { + // Sync with the controls playing + WakelockPlus.enable(); + } else { + // Sync with the controls pause + WakelockPlus.disable(); + } + + ref.read(videoPlaybackValueProvider.notifier).status = + videoPlayback.state; + } + + void onPlaybackPositionChanged() { + // When seeking, these events sometimes move the slider to an older position + if (seekDebouncer.isActive) { + return; + } + + final videoController = controller.value; + if (videoController == null || !context.mounted) { + return; + } + + final playbackInfo = videoController.playbackInfo; + if (playbackInfo == null) { + return; + } + + ref.read(videoPlaybackValueProvider.notifier).position = + Duration(seconds: playbackInfo.position); + + // Check if the video is buffering + if (playbackInfo.status == PlaybackStatus.playing) { + isBuffering.value = lastVideoPosition.value == playbackInfo.position; + lastVideoPosition.value = playbackInfo.position; + } else { + isBuffering.value = false; + lastVideoPosition.value = -1; + } + } + + void onPlaybackEnded() { + final videoController = controller.value; + if (videoController == null || !context.mounted) { + return; + } + + if (showMotionVideo.value && + videoController.playbackInfo?.status == PlaybackStatus.stopped && + !ref + .read(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.loopVideo)) { + ref.read(isPlayingMotionVideoProvider.notifier).playing = false; + } + } + + void removeListeners(NativeVideoPlayerController controller) { + controller.onPlaybackPositionChanged + .removeListener(onPlaybackPositionChanged); + controller.onPlaybackStatusChanged + .removeListener(onPlaybackStatusChanged); + controller.onPlaybackReady.removeListener(onPlaybackReady); + controller.onPlaybackEnded.removeListener(onPlaybackEnded); + } + + void initController(NativeVideoPlayerController nc) async { + if (controller.value != null || !context.mounted) { + return; + } + ref.read(videoPlayerControlsProvider.notifier).reset(); + ref.read(videoPlaybackValueProvider.notifier).reset(); + + final source = await videoSource; + if (source == null) { + return; + } + + nc.onPlaybackPositionChanged.addListener(onPlaybackPositionChanged); + nc.onPlaybackStatusChanged.addListener(onPlaybackStatusChanged); + nc.onPlaybackReady.addListener(onPlaybackReady); + nc.onPlaybackEnded.addListener(onPlaybackEnded); + + nc.loadVideoSource(source).catchError((error) { + log.severe('Error loading video source: $error'); + }); + final loopVideo = ref + .read(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.loopVideo); + nc.setLoop(loopVideo); + + controller.value = nc; + Timer(const Duration(milliseconds: 200), checkIfBuffering); + } + + ref.listen(currentAssetProvider, (_, value) { + final playerController = controller.value; + if (playerController != null && value != asset) { + removeListeners(playerController); + } + + final curAsset = currentAsset.value; + if (curAsset == asset) { + return; + } + + final imageToVideo = curAsset != null && !curAsset.isVideo; + + // No need to delay video playback when swiping from an image to a video + if (imageToVideo && Platform.isIOS) { + currentAsset.value = value; + onPlaybackReady(); + return; + } + + // Delay the video playback to avoid a stutter in the swipe animation + Timer( + Platform.isIOS + ? const Duration(milliseconds: 300) + : imageToVideo + ? const Duration(milliseconds: 200) + : const Duration(milliseconds: 400), () { + if (!context.mounted) { + return; + } + + currentAsset.value = value; + if (currentAsset.value == asset) { + onPlaybackReady(); + } + }); + }); + + useEffect( + () { + // If opening a remote video from a hero animation, delay visibility to avoid a stutter + final timer = isVisible.value + ? null + : Timer( + const Duration(milliseconds: 300), + () => isVisible.value = true, + ); + + return () { + timer?.cancel(); + final playerController = controller.value; + if (playerController == null) { + return; + } + removeListeners(playerController); + playerController.stop().catchError((error) { + log.fine('Error stopping video: $error'); + }); + + WakelockPlus.disable(); + }; + }, + const [], + ); + + return Stack( + children: [ + // This remains under the video to avoid flickering + // For motion videos, this is the image portion of the asset + Center(key: ValueKey(asset.id), child: image), + if (aspectRatio.value != null) + Visibility.maintain( + key: ValueKey(asset), + visible: + (asset.isVideo || showMotionVideo.value) && isVisible.value, + child: Center( + key: ValueKey(asset), + child: AspectRatio( + key: ValueKey(asset), + aspectRatio: aspectRatio.value!, + child: isCurrent + ? NativeVideoPlayerView( + key: ValueKey(asset), + onViewReady: initController, + ) + : null, + ), + ), + ), + if (showControls) const Center(child: CustomVideoPlayerControls()), + ], + ); + } +} diff --git a/mobile/lib/pages/common/video_viewer.page.dart b/mobile/lib/pages/common/video_viewer.page.dart deleted file mode 100644 index 774d4eb31ec6e..0000000000000 --- a/mobile/lib/pages/common/video_viewer.page.dart +++ /dev/null @@ -1,167 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/video_player_controller_provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; -import 'package:immich_mobile/widgets/asset_viewer/video_player.dart'; -import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; - -class VideoViewerPage extends HookConsumerWidget { - final Asset asset; - final bool isMotionVideo; - final Widget? placeholder; - final Duration hideControlsTimer; - final bool showControls; - final bool showDownloadingIndicator; - final bool loopVideo; - - const VideoViewerPage({ - super.key, - required this.asset, - this.isMotionVideo = false, - this.placeholder, - this.showControls = true, - this.hideControlsTimer = const Duration(seconds: 5), - this.showDownloadingIndicator = true, - this.loopVideo = false, - }); - - @override - build(BuildContext context, WidgetRef ref) { - final controller = - ref.watch(videoPlayerControllerProvider(asset: asset)).value; - // The last volume of the video used when mute is toggled - final lastVolume = useState(0.5); - - // When the volume changes, set the volume - ref.listen(videoPlayerControlsProvider.select((value) => value.mute), - (_, mute) { - if (mute) { - controller?.setVolume(0.0); - } else { - controller?.setVolume(lastVolume.value); - } - }); - - // When the position changes, seek to the position - ref.listen(videoPlayerControlsProvider.select((value) => value.position), - (_, position) { - if (controller == null) { - // No seeeking if there is no video - return; - } - - // Find the position to seek to - final Duration seek = controller.value.duration * (position / 100.0); - controller.seekTo(seek); - }); - - // When the custom video controls paus or plays - ref.listen(videoPlayerControlsProvider.select((value) => value.pause), - (lastPause, pause) { - if (pause) { - controller?.pause(); - } else { - controller?.play(); - } - }); - - // Updates the [videoPlaybackValueProvider] with the current - // position and duration of the video from the Chewie [controller] - // Also sets the error if there is an error in the playback - void updateVideoPlayback() { - final videoPlayback = VideoPlaybackValue.fromController(controller); - ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback; - final state = videoPlayback.state; - - // Enable the WakeLock while the video is playing - if (state == VideoPlaybackState.playing) { - // Sync with the controls playing - WakelockPlus.enable(); - } else { - // Sync with the controls pause - WakelockPlus.disable(); - } - } - - // Adds and removes the listener to the video player - useEffect( - () { - Future.microtask( - () => ref.read(videoPlayerControlsProvider.notifier).reset(), - ); - // Guard no controller - if (controller == null) { - return null; - } - - // Hide the controls - // Done in a microtask to avoid setting the state while the is building - if (!isMotionVideo) { - Future.microtask(() { - ref.read(showControlsProvider.notifier).show = false; - }); - } - - // Subscribes to listener - Future.microtask(() { - controller.addListener(updateVideoPlayback); - }); - return () { - // Removes listener when we dispose - controller.removeListener(updateVideoPlayback); - controller.pause(); - }; - }, - [controller], - ); - - return PopScope( - onPopInvokedWithResult: (didPop, _) { - ref.read(videoPlaybackValueProvider.notifier).value = - VideoPlaybackValue.uninitialized(); - }, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 400), - child: Stack( - children: [ - Visibility( - visible: controller == null, - child: Stack( - children: [ - if (placeholder != null) placeholder!, - const Positioned.fill( - child: Center( - child: DelayedLoadingIndicator( - fadeInDuration: Duration(milliseconds: 500), - ), - ), - ), - ], - ), - ), - if (controller != null) - SizedBox( - height: context.height, - width: context.width, - child: VideoPlayerViewer( - controller: controller, - isMotionVideo: isMotionVideo, - placeholder: placeholder, - hideControlsTimer: hideControlsTimer, - showControls: showControls, - showDownloadingIndicator: showDownloadingIndicator, - loopVideo: loopVideo, - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/photos/memory.page.dart b/mobile/lib/pages/photos/memory.page.dart index 3f86f5be082c0..74a94ed6ee084 100644 --- a/mobile/lib/pages/photos/memory.page.dart +++ b/mobile/lib/pages/photos/memory.page.dart @@ -113,11 +113,15 @@ class MemoryPage extends HookConsumerWidget { } // Precache the asset + final size = MediaQuery.sizeOf(context); await precacheImage( ImmichImage.imageProvider( asset: asset, + width: size.width, + height: size.height, ), context, + size: size, ); } diff --git a/mobile/lib/pages/search/map/map.page.dart b/mobile/lib/pages/search/map/map.page.dart index 8000c7e339282..10fe8de541506 100644 --- a/mobile/lib/pages/search/map/map.page.dart +++ b/mobile/lib/pages/search/map/map.page.dart @@ -15,6 +15,8 @@ import 'package:immich_mobile/extensions/latlngbounds_extension.dart'; import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart'; import 'package:immich_mobile/models/map/map_event.model.dart'; import 'package:immich_mobile/models/map/map_marker.model.dart'; +import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/map/map_marker.provider.dart'; import 'package:immich_mobile/providers/map/map_state.provider.dart'; @@ -99,8 +101,11 @@ class MapPage extends HookConsumerWidget { useEffect( () { + final currentAssetLink = + ref.read(currentAssetProvider.notifier).ref.keepAlive(); + loadMarkers(); - return null; + return currentAssetLink.close; }, [], ); @@ -186,6 +191,10 @@ class MapPage extends HookConsumerWidget { GroupAssetsBy.none, ); + ref.read(currentAssetProvider.notifier).set(asset); + if (asset.isVideo) { + ref.read(showControlsProvider.notifier).show = false; + } context.pushRoute( GalleryViewerRoute( initialIndex: 0, diff --git a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart b/mobile/lib/providers/asset_viewer/asset_stack.provider.dart index c3e4414b3935a..407aef16109e1 100644 --- a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart +++ b/mobile/lib/providers/asset_viewer/asset_stack.provider.dart @@ -7,49 +7,49 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'asset_stack.provider.g.dart'; class AssetStackNotifier extends StateNotifier> { - final Asset _asset; + final String _stackId; final Ref _ref; - AssetStackNotifier( - this._asset, - this._ref, - ) : super([]) { - fetchStackChildren(); + AssetStackNotifier(this._stackId, this._ref) : super([]) { + _fetchStack(_stackId); } - void fetchStackChildren() async { - if (mounted) { - state = await _ref.read(assetStackProvider(_asset).future); + void _fetchStack(String stackId) async { + if (!mounted) { + return; + } + + final stack = await _ref.read(assetStackProvider(stackId).future); + if (stack.isNotEmpty) { + state = stack; } } void removeChild(int index) { if (index < state.length) { state.removeAt(index); + state = List.from(state); } } } final assetStackStateProvider = StateNotifierProvider.autoDispose - .family, Asset>( - (ref, asset) => AssetStackNotifier(asset, ref), + .family, String>( + (ref, stackId) => AssetStackNotifier(stackId, ref), ); final assetStackProvider = - FutureProvider.autoDispose.family, Asset>((ref, asset) async { - // Guard [local asset] - if (asset.remoteId == null) { - return []; - } - - return await ref + FutureProvider.autoDispose.family, String>((ref, stackId) { + return ref .watch(dbProvider) .assets .filter() .isArchivedEqualTo(false) .isTrashedEqualTo(false) - .stackPrimaryAssetIdEqualTo(asset.remoteId) - .sortByFileCreatedAtDesc() + .stackIdEqualTo(stackId) + // orders primary asset first as its ID is null + .sortByStackPrimaryAssetId() + .thenByFileCreatedAtDesc() .findAll(); }); diff --git a/mobile/lib/providers/asset_viewer/is_motion_video_playing.provider.dart b/mobile/lib/providers/asset_viewer/is_motion_video_playing.provider.dart new file mode 100644 index 0000000000000..4af061f9548c1 --- /dev/null +++ b/mobile/lib/providers/asset_viewer/is_motion_video_playing.provider.dart @@ -0,0 +1,23 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +/// Whether to display the video part of a motion photo +final isPlayingMotionVideoProvider = + StateNotifierProvider((ref) { + return IsPlayingMotionVideo(ref); +}); + +class IsPlayingMotionVideo extends StateNotifier { + IsPlayingMotionVideo(this.ref) : super(false); + + final Ref ref; + + bool get playing => state; + + set playing(bool value) { + state = value; + } + + void toggle() { + state = !state; + } +} diff --git a/mobile/lib/providers/asset_viewer/video_player_controller_provider.dart b/mobile/lib/providers/asset_viewer/video_player_controller_provider.dart deleted file mode 100644 index 969e181cbb0ad..0000000000000 --- a/mobile/lib/providers/asset_viewer/video_player_controller_provider.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:video_player/video_player.dart'; - -part 'video_player_controller_provider.g.dart'; - -@riverpod -Future videoPlayerController( - VideoPlayerControllerRef ref, { - required Asset asset, -}) async { - late VideoPlayerController controller; - if (asset.isLocal && asset.livePhotoVideoId == null) { - // Use a local file for the video player controller - final file = await asset.local!.file; - if (file == null) { - throw Exception('No file found for the video'); - } - controller = VideoPlayerController.file(file); - } else { - // Use a network URL for the video player controller - final serverEndpoint = Store.get(StoreKey.serverEndpoint); - final String videoUrl = asset.livePhotoVideoId != null - ? '$serverEndpoint/assets/${asset.livePhotoVideoId}/video/playback' - : '$serverEndpoint/assets/${asset.remoteId}/video/playback'; - - final url = Uri.parse(videoUrl); - controller = VideoPlayerController.networkUrl( - url, - httpHeaders: ApiService.getRequestHeaders(), - videoPlayerOptions: asset.livePhotoVideoId != null - ? VideoPlayerOptions(mixWithOthers: true) - : VideoPlayerOptions(mixWithOthers: false), - ); - } - - await controller.initialize(); - - ref.onDispose(() { - controller.dispose(); - }); - - return controller; -} diff --git a/mobile/lib/providers/asset_viewer/video_player_controller_provider.g.dart b/mobile/lib/providers/asset_viewer/video_player_controller_provider.g.dart deleted file mode 100644 index 00ad37648a85e..0000000000000 --- a/mobile/lib/providers/asset_viewer/video_player_controller_provider.g.dart +++ /dev/null @@ -1,164 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'video_player_controller_provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$videoPlayerControllerHash() => - r'84b2961cc2aeaf9d03255dbf9b9484619d0c24f5'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -/// See also [videoPlayerController]. -@ProviderFor(videoPlayerController) -const videoPlayerControllerProvider = VideoPlayerControllerFamily(); - -/// See also [videoPlayerController]. -class VideoPlayerControllerFamily - extends Family> { - /// See also [videoPlayerController]. - const VideoPlayerControllerFamily(); - - /// See also [videoPlayerController]. - VideoPlayerControllerProvider call({ - required Asset asset, - }) { - return VideoPlayerControllerProvider( - asset: asset, - ); - } - - @override - VideoPlayerControllerProvider getProviderOverride( - covariant VideoPlayerControllerProvider provider, - ) { - return call( - asset: provider.asset, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'videoPlayerControllerProvider'; -} - -/// See also [videoPlayerController]. -class VideoPlayerControllerProvider - extends AutoDisposeFutureProvider { - /// See also [videoPlayerController]. - VideoPlayerControllerProvider({ - required Asset asset, - }) : this._internal( - (ref) => videoPlayerController( - ref as VideoPlayerControllerRef, - asset: asset, - ), - from: videoPlayerControllerProvider, - name: r'videoPlayerControllerProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$videoPlayerControllerHash, - dependencies: VideoPlayerControllerFamily._dependencies, - allTransitiveDependencies: - VideoPlayerControllerFamily._allTransitiveDependencies, - asset: asset, - ); - - VideoPlayerControllerProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.asset, - }) : super.internal(); - - final Asset asset; - - @override - Override overrideWith( - FutureOr Function(VideoPlayerControllerRef provider) - create, - ) { - return ProviderOverride( - origin: this, - override: VideoPlayerControllerProvider._internal( - (ref) => create(ref as VideoPlayerControllerRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - asset: asset, - ), - ); - } - - @override - AutoDisposeFutureProviderElement createElement() { - return _VideoPlayerControllerProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is VideoPlayerControllerProvider && other.asset == asset; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, asset.hashCode); - - return _SystemHash.finish(hash); - } -} - -mixin VideoPlayerControllerRef - on AutoDisposeFutureProviderRef { - /// The parameter `asset` of this provider. - Asset get asset; -} - -class _VideoPlayerControllerProviderElement - extends AutoDisposeFutureProviderElement - with VideoPlayerControllerRef { - _VideoPlayerControllerProviderElement(super.provider); - - @override - Asset get asset => (origin as VideoPlayerControllerProvider).asset; -} -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/mobile/lib/providers/asset_viewer/video_player_controls_provider.dart b/mobile/lib/providers/asset_viewer/video_player_controls_provider.dart index d15b26ea20994..69be91480ffc2 100644 --- a/mobile/lib/providers/asset_viewer/video_player_controls_provider.dart +++ b/mobile/lib/providers/asset_viewer/video_player_controls_provider.dart @@ -1,15 +1,16 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; class VideoPlaybackControls { - VideoPlaybackControls({ + const VideoPlaybackControls({ required this.position, - required this.mute, required this.pause, + this.restarted = false, }); final double position; - final bool mute; final bool pause; + final bool restarted; } final videoPlayerControlsProvider = @@ -17,15 +18,11 @@ final videoPlayerControlsProvider = return VideoPlayerControls(ref); }); +const videoPlayerControlsDefault = + VideoPlaybackControls(position: 0, pause: false); + class VideoPlayerControls extends StateNotifier { - VideoPlayerControls(this.ref) - : super( - VideoPlaybackControls( - position: 0, - pause: false, - mute: false, - ), - ); + VideoPlayerControls(this.ref) : super(videoPlayerControlsDefault); final Ref ref; @@ -36,75 +33,48 @@ class VideoPlayerControls extends StateNotifier { } void reset() { - state = VideoPlaybackControls( - position: 0, - pause: false, - mute: false, - ); + state = videoPlayerControlsDefault; } double get position => state.position; - bool get mute => state.mute; + bool get paused => state.pause; set position(double value) { - state = VideoPlaybackControls( - position: value, - mute: state.mute, - pause: state.pause, - ); - } - - set mute(bool value) { - state = VideoPlaybackControls( - position: state.position, - mute: value, - pause: state.pause, - ); - } + if (state.position == value) { + return; + } - void toggleMute() { - state = VideoPlaybackControls( - position: state.position, - mute: !state.mute, - pause: state.pause, - ); + state = VideoPlaybackControls(position: value, pause: state.pause); } void pause() { - state = VideoPlaybackControls( - position: state.position, - mute: state.mute, - pause: true, - ); + if (state.pause) { + return; + } + + state = VideoPlaybackControls(position: state.position, pause: true); } void play() { - state = VideoPlaybackControls( - position: state.position, - mute: state.mute, - pause: false, - ); + if (!state.pause) { + return; + } + + state = VideoPlaybackControls(position: state.position, pause: false); } void togglePlay() { - state = VideoPlaybackControls( - position: state.position, - mute: state.mute, - pause: !state.pause, - ); + state = + VideoPlaybackControls(position: state.position, pause: !state.pause); } void restart() { - state = VideoPlaybackControls( - position: 0, - mute: state.mute, - pause: true, - ); - - state = VideoPlaybackControls( - position: 0, - mute: state.mute, - pause: false, - ); + state = + const VideoPlaybackControls(position: 0, pause: false, restarted: true); + ref.read(videoPlaybackValueProvider.notifier).value = + ref.read(videoPlaybackValueProvider.notifier).value.copyWith( + state: VideoPlaybackState.playing, + position: Duration.zero, + ); } } diff --git a/mobile/lib/providers/asset_viewer/video_player_value_provider.dart b/mobile/lib/providers/asset_viewer/video_player_value_provider.dart index ebdf739ef03de..1a3c54e9e9293 100644 --- a/mobile/lib/providers/asset_viewer/video_player_value_provider.dart +++ b/mobile/lib/providers/asset_viewer/video_player_value_provider.dart @@ -1,5 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:video_player/video_player.dart'; +import 'package:native_video_player/native_video_player.dart'; enum VideoPlaybackState { initializing, @@ -22,56 +22,66 @@ class VideoPlaybackValue { /// The volume of the video final double volume; - VideoPlaybackValue({ + const VideoPlaybackValue({ required this.position, required this.duration, required this.state, required this.volume, }); - factory VideoPlaybackValue.fromController(VideoPlayerController? controller) { - final video = controller?.value; - late VideoPlaybackState s; - if (video == null) { - s = VideoPlaybackState.initializing; - } else if (video.isCompleted) { - s = VideoPlaybackState.completed; - } else if (video.isPlaying) { - s = VideoPlaybackState.playing; - } else if (video.isBuffering) { - s = VideoPlaybackState.buffering; - } else { - s = VideoPlaybackState.paused; + factory VideoPlaybackValue.fromNativeController( + NativeVideoPlayerController controller, + ) { + final playbackInfo = controller.playbackInfo; + final videoInfo = controller.videoInfo; + + if (playbackInfo == null || videoInfo == null) { + return videoPlaybackValueDefault; } + final VideoPlaybackState status = switch (playbackInfo.status) { + PlaybackStatus.playing => VideoPlaybackState.playing, + PlaybackStatus.paused => VideoPlaybackState.paused, + PlaybackStatus.stopped => VideoPlaybackState.completed, + }; + return VideoPlaybackValue( - position: video?.position ?? Duration.zero, - duration: video?.duration ?? Duration.zero, - state: s, - volume: video?.volume ?? 0.0, + position: Duration(seconds: playbackInfo.position), + duration: Duration(seconds: videoInfo.duration), + state: status, + volume: playbackInfo.volume, ); } - factory VideoPlaybackValue.uninitialized() { + VideoPlaybackValue copyWith({ + Duration? position, + Duration? duration, + VideoPlaybackState? state, + double? volume, + }) { return VideoPlaybackValue( - position: Duration.zero, - duration: Duration.zero, - state: VideoPlaybackState.initializing, - volume: 0.0, + position: position ?? this.position, + duration: duration ?? this.duration, + state: state ?? this.state, + volume: volume ?? this.volume, ); } } +const VideoPlaybackValue videoPlaybackValueDefault = VideoPlaybackValue( + position: Duration.zero, + duration: Duration.zero, + state: VideoPlaybackState.initializing, + volume: 0.0, +); + final videoPlaybackValueProvider = StateNotifierProvider((ref) { return VideoPlaybackValueState(ref); }); class VideoPlaybackValueState extends StateNotifier { - VideoPlaybackValueState(this.ref) - : super( - VideoPlaybackValue.uninitialized(), - ); + VideoPlaybackValueState(this.ref) : super(videoPlaybackValueDefault); final Ref ref; @@ -82,6 +92,7 @@ class VideoPlaybackValueState extends StateNotifier { } set position(Duration value) { + if (state.position == value) return; state = VideoPlaybackValue( position: value, duration: state.duration, @@ -89,4 +100,18 @@ class VideoPlaybackValueState extends StateNotifier { volume: state.volume, ); } + + set status(VideoPlaybackState value) { + if (state.state == value) return; + state = VideoPlaybackValue( + position: state.position, + duration: state.duration, + state: value, + volume: state.volume, + ); + } + + void reset() { + state = videoPlaybackValueDefault; + } } diff --git a/mobile/lib/providers/image/immich_local_image_provider.dart b/mobile/lib/providers/image/immich_local_image_provider.dart index bbfaf12a4f445..36fd3334b9442 100644 --- a/mobile/lib/providers/image/immich_local_image_provider.dart +++ b/mobile/lib/providers/image/immich_local_image_provider.dart @@ -7,14 +7,21 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:logging/logging.dart'; import 'package:photo_manager/photo_manager.dart' show ThumbnailSize; /// The local image provider for an asset class ImmichLocalImageProvider extends ImageProvider { final Asset asset; + // only used for videos + final double width; + final double height; + final Logger log = Logger('ImmichLocalImageProvider'); ImmichLocalImageProvider({ required this.asset, + required this.width, + required this.height, }) : assert(asset.local != null, 'Only usable when asset.local is set'); /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key @@ -42,38 +49,57 @@ class ImmichLocalImageProvider extends ImageProvider { // Streams in each stage of the image as we ask for it Stream _codec( - Asset key, + Asset asset, ImageDecoderCallback decode, StreamController chunkEvents, ) async* { - // Load a small thumbnail - final thumbBytes = await asset.local?.thumbnailDataWithSize( - const ThumbnailSize.square(256), - quality: 80, - ); - if (thumbBytes != null) { - final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes); - final codec = await decode(buffer); - yield codec; - } else { - debugPrint("Loading thumb for ${asset.fileName} failed"); - } + ui.ImmutableBuffer? buffer; + try { + final local = asset.local; + if (local == null) { + throw StateError('Asset ${asset.fileName} has no local data'); + } - if (asset.isImage) { - final File? file = await asset.local?.originFile; - if (file == null) { - throw StateError("Opening file for asset ${asset.fileName} failed"); + var thumbBytes = await local + .thumbnailDataWithSize(const ThumbnailSize.square(256), quality: 80); + if (thumbBytes == null) { + throw StateError("Loading thumbnail for ${asset.fileName} failed"); } - try { - final buffer = await ui.ImmutableBuffer.fromFilePath(file.path); - final codec = await decode(buffer); - yield codec; - } catch (error) { - throw StateError("Loading asset ${asset.fileName} failed"); + buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes); + thumbBytes = null; + yield await decode(buffer); + buffer = null; + + switch (asset.type) { + case AssetType.image: + final File? file = await local.originFile; + if (file == null) { + throw StateError("Opening file for asset ${asset.fileName} failed"); + } + buffer = await ui.ImmutableBuffer.fromFilePath(file.path); + yield await decode(buffer); + buffer = null; + break; + case AssetType.video: + final size = ThumbnailSize(width.ceil(), height.ceil()); + thumbBytes = await local.thumbnailDataWithSize(size); + if (thumbBytes == null) { + throw StateError("Failed to load preview for ${asset.fileName}"); + } + buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes); + thumbBytes = null; + yield await decode(buffer); + buffer = null; + break; + default: + throw StateError('Unsupported asset type ${asset.type}'); } + } catch (error, stack) { + log.severe('Error loading local image ${asset.fileName}', error, stack); + buffer?.dispose(); + } finally { + chunkEvents.close(); } - - chunkEvents.close(); } @override diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index b001c6bdd6d33..785d23a7ad83e 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -14,6 +14,7 @@ import 'package:immich_mobile/pages/backup/backup_controller.page.dart'; import 'package:immich_mobile/pages/backup/backup_options.page.dart'; import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart'; import 'package:immich_mobile/pages/albums/albums.page.dart'; +import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; import 'package:immich_mobile/pages/library/local_albums.page.dart'; import 'package:immich_mobile/pages/library/people/people_collection.page.dart'; import 'package:immich_mobile/pages/library/places/places_collection.page.dart'; @@ -272,6 +273,10 @@ class AppRouter extends RootStackRouter { guards: [_authGuard, _duplicateGuard], transitionsBuilder: TransitionsBuilders.slideLeft, ), + AutoRoute( + page: NativeVideoViewerRoute.page, + guards: [_authGuard, _duplicateGuard], + ), ]; } diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index ea7d385e85626..48ee4db5fd2b1 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -1079,6 +1079,64 @@ class MemoryRouteArgs { } } +/// generated route for +/// [NativeVideoViewerPage] +class NativeVideoViewerRoute extends PageRouteInfo { + NativeVideoViewerRoute({ + Key? key, + required Asset asset, + required Widget image, + bool showControls = true, + List? children, + }) : super( + NativeVideoViewerRoute.name, + args: NativeVideoViewerRouteArgs( + key: key, + asset: asset, + image: image, + showControls: showControls, + ), + initialChildren: children, + ); + + static const String name = 'NativeVideoViewerRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return NativeVideoViewerPage( + key: args.key, + asset: args.asset, + image: args.image, + showControls: args.showControls, + ); + }, + ); +} + +class NativeVideoViewerRouteArgs { + const NativeVideoViewerRouteArgs({ + this.key, + required this.asset, + required this.image, + this.showControls = true, + }); + + final Key? key; + + final Asset asset; + + final Widget image; + + final bool showControls; + + @override + String toString() { + return 'NativeVideoViewerRouteArgs{key: $key, asset: $asset, image: $image, showControls: $showControls}'; + } +} + /// generated route for /// [PartnerDetailPage] class PartnerDetailRoute extends PageRouteInfo { diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index b2cad4dc828eb..7d27d1b27b0ea 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -402,4 +403,29 @@ class AssetService { return exifInfo?.description ?? ""; } + + Future getAspectRatio(Asset asset) async { + // platform_manager always returns 0 for orientation on iOS, so only prefer it on Android + if (asset.isLocal && Platform.isAndroid) { + await asset.localAsync; + } else if (asset.isRemote) { + asset = await loadExif(asset); + } else if (asset.isLocal) { + await asset.localAsync; + } + + final aspectRatio = asset.aspectRatio; + if (aspectRatio != null) { + return aspectRatio; + } + + final width = asset.width; + final height = asset.height; + if (width != null && height != null) { + // we don't know the orientation, so assume it's normal + return width / height; + } + + return 1.0; + } } diff --git a/mobile/lib/utils/debounce.dart b/mobile/lib/utils/debounce.dart index ca5f8fc2beef0..78870151a6e78 100644 --- a/mobile/lib/utils/debounce.dart +++ b/mobile/lib/utils/debounce.dart @@ -3,20 +3,52 @@ import 'dart:async'; import 'package:flutter_hooks/flutter_hooks.dart'; /// Used to debounce function calls with the [interval] provided. +/// If [maxWaitTime] is provided, the first [run] call as well as the next call since [maxWaitTime] has passed will be immediately executed, even if [interval] is not satisfied. class Debouncer { - Debouncer({required this.interval}); + Debouncer({required this.interval, this.maxWaitTime}); final Duration interval; + final Duration? maxWaitTime; Timer? _timer; FutureOr Function()? _lastAction; + DateTime? _lastActionTime; + Future? _actionFuture; void run(FutureOr Function() action) { _lastAction = action; _timer?.cancel(); + + if (maxWaitTime != null && + // _actionFuture == null && // TODO: should this check be here? + (_lastActionTime == null || + DateTime.now().difference(_lastActionTime!) > maxWaitTime!)) { + _callAndRest(); + return; + } _timer = Timer(interval, _callAndRest); } + Future? drain() { + if (_timer != null && _timer!.isActive) { + _timer!.cancel(); + if (_lastAction != null) { + _callAndRest(); + } + } + return _actionFuture; + } + + @pragma('vm:prefer-inline') void _callAndRest() { - _lastAction?.call(); + _lastActionTime = DateTime.now(); + final action = _lastAction; + _lastAction = null; + + final result = action!(); + if (result is Future) { + _actionFuture = result.whenComplete(() { + _actionFuture = null; + }); + } _timer = null; } @@ -24,31 +56,48 @@ class Debouncer { _timer?.cancel(); _timer = null; _lastAction = null; + _lastActionTime = null; + _actionFuture = null; } + + bool get isActive => + _actionFuture != null || (_timer != null && _timer!.isActive); } /// Creates a [Debouncer] that will be disposed automatically. If no [interval] is provided, a /// default interval of 300ms is used to debounce the function calls Debouncer useDebouncer({ Duration interval = const Duration(milliseconds: 300), + Duration? maxWaitTime, List? keys, }) => - use(_DebouncerHook(interval: interval, keys: keys)); + use( + _DebouncerHook( + interval: interval, + maxWaitTime: maxWaitTime, + keys: keys, + ), + ); class _DebouncerHook extends Hook { const _DebouncerHook({ required this.interval, + this.maxWaitTime, super.keys, }); final Duration interval; + final Duration? maxWaitTime; @override HookState> createState() => _DebouncerHookState(); } class _DebouncerHookState extends HookState { - late final debouncer = Debouncer(interval: hook.interval); + late final debouncer = Debouncer( + interval: hook.interval, + maxWaitTime: hook.maxWaitTime, + ); @override Debouncer build(_) => debouncer; diff --git a/mobile/lib/utils/hooks/chewiew_controller_hook.dart b/mobile/lib/utils/hooks/chewiew_controller_hook.dart deleted file mode 100644 index 2868e896cf2f4..0000000000000 --- a/mobile/lib/utils/hooks/chewiew_controller_hook.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'package:chewie/chewie.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:video_player/video_player.dart'; - -/// Provides the initialized video player controller -/// If the asset is local, use the local file -/// Otherwise, use a video player with a URL -ChewieController useChewieController({ - required VideoPlayerController controller, - EdgeInsets controlsSafeAreaMinimum = const EdgeInsets.only( - bottom: 100, - ), - bool showOptions = true, - bool showControlsOnInitialize = false, - bool autoPlay = true, - bool allowFullScreen = false, - bool allowedScreenSleep = false, - bool showControls = true, - bool loopVideo = false, - Widget? customControls, - Widget? placeholder, - Duration hideControlsTimer = const Duration(seconds: 1), - VoidCallback? onPlaying, - VoidCallback? onPaused, - VoidCallback? onVideoEnded, -}) { - return use( - _ChewieControllerHook( - controller: controller, - placeholder: placeholder, - showOptions: showOptions, - controlsSafeAreaMinimum: controlsSafeAreaMinimum, - autoPlay: autoPlay, - allowFullScreen: allowFullScreen, - customControls: customControls, - hideControlsTimer: hideControlsTimer, - showControlsOnInitialize: showControlsOnInitialize, - showControls: showControls, - loopVideo: loopVideo, - allowedScreenSleep: allowedScreenSleep, - onPlaying: onPlaying, - onPaused: onPaused, - onVideoEnded: onVideoEnded, - ), - ); -} - -class _ChewieControllerHook extends Hook { - final VideoPlayerController controller; - final EdgeInsets controlsSafeAreaMinimum; - final bool showOptions; - final bool showControlsOnInitialize; - final bool autoPlay; - final bool allowFullScreen; - final bool allowedScreenSleep; - final bool showControls; - final bool loopVideo; - final Widget? customControls; - final Widget? placeholder; - final Duration hideControlsTimer; - final VoidCallback? onPlaying; - final VoidCallback? onPaused; - final VoidCallback? onVideoEnded; - - const _ChewieControllerHook({ - required this.controller, - this.controlsSafeAreaMinimum = const EdgeInsets.only( - bottom: 100, - ), - this.showOptions = true, - this.showControlsOnInitialize = false, - this.autoPlay = true, - this.allowFullScreen = false, - this.allowedScreenSleep = false, - this.showControls = true, - this.loopVideo = false, - this.customControls, - this.placeholder, - this.hideControlsTimer = const Duration(seconds: 3), - this.onPlaying, - this.onPaused, - this.onVideoEnded, - }); - - @override - createState() => _ChewieControllerHookState(); -} - -class _ChewieControllerHookState - extends HookState { - late ChewieController chewieController = ChewieController( - videoPlayerController: hook.controller, - controlsSafeAreaMinimum: hook.controlsSafeAreaMinimum, - showOptions: hook.showOptions, - showControlsOnInitialize: hook.showControlsOnInitialize, - autoPlay: hook.autoPlay, - allowFullScreen: hook.allowFullScreen, - allowedScreenSleep: hook.allowedScreenSleep, - showControls: hook.showControls, - looping: hook.loopVideo, - customControls: hook.customControls, - placeholder: hook.placeholder, - hideControlsTimer: hook.hideControlsTimer, - ); - - @override - void dispose() { - chewieController.dispose(); - super.dispose(); - } - - @override - ChewieController build(BuildContext context) { - return chewieController; - } - - /* - /// Initializes the chewie controller and video player controller - Future _initialize() async { - if (hook.asset.isLocal && hook.asset.livePhotoVideoId == null) { - // Use a local file for the video player controller - final file = await hook.asset.local!.file; - if (file == null) { - throw Exception('No file found for the video'); - } - videoPlayerController = VideoPlayerController.file(file); - } else { - // Use a network URL for the video player controller - final serverEndpoint = store.Store.get(store.StoreKey.serverEndpoint); - final String videoUrl = hook.asset.livePhotoVideoId != null - ? '$serverEndpoint/assets/${hook.asset.livePhotoVideoId}/video/playback' - : '$serverEndpoint/assets/${hook.asset.remoteId}/video/playback'; - - final url = Uri.parse(videoUrl); - final accessToken = store.Store.get(StoreKey.accessToken); - - videoPlayerController = VideoPlayerController.networkUrl( - url, - httpHeaders: {"x-immich-user-token": accessToken}, - ); - } - - await videoPlayerController!.initialize(); - - chewieController = ChewieController( - videoPlayerController: videoPlayerController!, - controlsSafeAreaMinimum: hook.controlsSafeAreaMinimum, - showOptions: hook.showOptions, - showControlsOnInitialize: hook.showControlsOnInitialize, - autoPlay: hook.autoPlay, - allowFullScreen: hook.allowFullScreen, - allowedScreenSleep: hook.allowedScreenSleep, - showControls: hook.showControls, - customControls: hook.customControls, - placeholder: hook.placeholder, - hideControlsTimer: hook.hideControlsTimer, - ); - } - */ -} diff --git a/mobile/lib/utils/hooks/interval_hook.dart b/mobile/lib/utils/hooks/interval_hook.dart new file mode 100644 index 0000000000000..0c346065f7210 --- /dev/null +++ b/mobile/lib/utils/hooks/interval_hook.dart @@ -0,0 +1,18 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter_hooks/flutter_hooks.dart'; + +// https://github.com/rrousselGit/flutter_hooks/issues/233#issuecomment-840416638 +void useInterval(Duration delay, VoidCallback callback) { + final savedCallback = useRef(callback); + savedCallback.value = callback; + + useEffect( + () { + final timer = Timer.periodic(delay, (_) => savedCallback.value()); + return timer.cancel; + }, + [delay], + ); +} diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 2b02a5ff8f290..67ff060075383 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -4,7 +4,7 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/utils/db.dart'; import 'package:isar/isar.dart'; -const int targetVersion = 6; +const int targetVersion = 7; Future migrateDatabaseIfNeeded(Isar db) async { final int version = Store.get(StoreKey.version, 1); diff --git a/mobile/lib/utils/throttle.dart b/mobile/lib/utils/throttle.dart index 9a54e01fc195c..bc0dcf9e2fe2a 100644 --- a/mobile/lib/utils/throttle.dart +++ b/mobile/lib/utils/throttle.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter_hooks/flutter_hooks.dart'; /// Throttles function calls with the [interval] provided. @@ -10,12 +8,15 @@ class Throttler { Throttler({required this.interval}); - void run(FutureOr Function() action) { + T? run(T Function() action) { if (_lastActionTime == null || (DateTime.now().difference(_lastActionTime!) > interval)) { - action(); + final response = action(); _lastActionTime = DateTime.now(); + return response; } + + return null; } void dispose() { diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart index 38e499b5dec8e..5670aa388f9ee 100644 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart +++ b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart @@ -12,7 +12,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/collection_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_drag_region.dart'; import 'package:immich_mobile/widgets/asset_grid/thumbnail_image.dart'; import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; @@ -89,6 +91,7 @@ class ImmichAssetGridViewState extends ConsumerState { ScrollOffsetController(); final ItemPositionsListener _itemPositionsListener = ItemPositionsListener.create(); + late final KeepAliveLink currentAssetLink; /// The timestamp when the haptic feedback was last invoked int _hapticFeedbackTS = 0; @@ -201,6 +204,12 @@ class ImmichAssetGridViewState extends ConsumerState { allAssetsSelected: _allAssetsSelected, showStack: widget.showStack, heroOffset: widget.heroOffset, + onAssetTap: (asset) { + ref.read(currentAssetProvider.notifier).set(asset); + if (asset.isVideo) { + ref.read(showControlsProvider.notifier).show = false; + } + }, ); } @@ -348,6 +357,7 @@ class ImmichAssetGridViewState extends ConsumerState { @override void initState() { super.initState(); + currentAssetLink = ref.read(currentAssetProvider.notifier).ref.keepAlive(); scrollToTopNotifierProvider.addListener(_scrollToTop); scrollToDateNotifierProvider.addListener(_scrollToDate); @@ -369,6 +379,7 @@ class ImmichAssetGridViewState extends ConsumerState { _itemPositionsListener.itemPositions.removeListener(_positionListener); } _itemPositionsListener.itemPositions.removeListener(_hapticsListener); + currentAssetLink.close(); super.dispose(); } @@ -595,12 +606,13 @@ class _Section extends StatelessWidget { final RenderList renderList; final bool selectionActive; final bool dynamicLayout; - final Function(List) selectAssets; - final Function(List) deselectAssets; + final void Function(List) selectAssets; + final void Function(List) deselectAssets; final bool Function(List) allAssetsSelected; final bool showStack; final int heroOffset; final bool showStorageIndicator; + final void Function(Asset) onAssetTap; const _Section({ required this.section, @@ -618,6 +630,7 @@ class _Section extends StatelessWidget { required this.showStack, required this.heroOffset, required this.showStorageIndicator, + required this.onAssetTap, }); @override @@ -683,6 +696,7 @@ class _Section extends StatelessWidget { selectionActive: selectionActive, onSelect: (asset) => selectAssets([asset]), onDeselect: (asset) => deselectAssets([asset]), + onAssetTap: onAssetTap, ), ], ); @@ -724,9 +738,9 @@ class _Title extends StatelessWidget { final String title; final List assets; final bool selectionActive; - final Function(List) selectAssets; - final Function(List) deselectAssets; - final Function(List) allAssetsSelected; + final void Function(List) selectAssets; + final void Function(List) deselectAssets; + final bool Function(List) allAssetsSelected; const _Title({ required this.title, @@ -765,8 +779,9 @@ class _AssetRow extends StatelessWidget { final bool showStorageIndicator; final int heroOffset; final bool showStack; - final Function(Asset)? onSelect; - final Function(Asset)? onDeselect; + final void Function(Asset) onAssetTap; + final void Function(Asset)? onSelect; + final void Function(Asset)? onDeselect; final bool isSelectionActive; const _AssetRow({ @@ -786,6 +801,7 @@ class _AssetRow extends StatelessWidget { required this.showStack, required this.isSelectionActive, required this.selectedAssets, + required this.onAssetTap, this.onSelect, this.onDeselect, }); @@ -838,6 +854,8 @@ class _AssetRow extends StatelessWidget { onSelect?.call(asset); } } else { + final asset = renderList.loadAsset(absoluteOffset + index); + onAssetTap(asset); context.pushRoute( GalleryViewerRoute( renderList: renderList, diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart index 82ca295d8abbf..256141dc7d273 100644 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart @@ -5,11 +5,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; import 'package:immich_mobile/services/stack.service.dart'; @@ -26,12 +26,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/pages/editing/edit.page.dart'; class BottomGalleryBar extends ConsumerWidget { - final Asset asset; final ValueNotifier assetIndex; final bool showStack; - final int stackIndex; + final ValueNotifier stackIndex; final ValueNotifier totalAssets; - final bool showVideoPlayerControls; final PageController controller; final RenderList renderList; @@ -39,20 +37,24 @@ class BottomGalleryBar extends ConsumerWidget { super.key, required this.showStack, required this.stackIndex, - required this.asset, required this.assetIndex, required this.controller, required this.totalAssets, - required this.showVideoPlayerControls, required this.renderList, }); @override Widget build(BuildContext context, WidgetRef ref) { + final asset = ref.watch(currentAssetProvider); + if (asset == null) { + return const SizedBox(); + } final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId; + final showControls = ref.watch(showControlsProvider); + final stackId = asset.stackId; - final stackItems = showStack && asset.stackCount > 0 - ? ref.watch(assetStackStateProvider(asset)) + final stackItems = showStack && stackId != null + ? ref.watch(assetStackStateProvider(stackId)) : []; bool isStackPrimaryAsset = asset.stackPrimaryAssetId == null; final navStack = AutoRouter.of(context).stackData; @@ -64,10 +66,10 @@ class BottomGalleryBar extends ConsumerWidget { final isInAlbum = ref.watch(currentAlbumProvider)?.isRemote ?? false; void removeAssetFromStack() { - if (stackIndex > 0 && showStack) { + if (stackIndex.value > 0 && showStack && stackId != null) { ref - .read(assetStackStateProvider(asset).notifier) - .removeChild(stackIndex - 1); + .read(assetStackStateProvider(stackId).notifier) + .removeChild(stackIndex.value - 1); } } @@ -135,7 +137,7 @@ class BottomGalleryBar extends ConsumerWidget { await ref .read(stackServiceProvider) - .deleteStack(asset.stackId!, [asset, ...stackItems]); + .deleteStack(asset.stackId!, stackItems); } void showStackActionItems() { @@ -324,16 +326,16 @@ class BottomGalleryBar extends ConsumerWidget { }, ]; return IgnorePointer( - ignoring: !ref.watch(showControlsProvider), + ignoring: !showControls, child: AnimatedOpacity( duration: const Duration(milliseconds: 100), - opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0, + opacity: showControls ? 1.0 : 0.0, child: DecoratedBox( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, - colors: [blackOpacity90, Colors.transparent], + colors: [Colors.black, Colors.transparent], ), ), position: DecorationPosition.background, @@ -341,7 +343,7 @@ class BottomGalleryBar extends ConsumerWidget { padding: const EdgeInsets.only(top: 40.0), child: Column( children: [ - if (showVideoPlayerControls) const VideoControls(), + if (asset.isVideo) const VideoControls(), BottomNavigationBar( elevation: 0.0, backgroundColor: Colors.transparent, diff --git a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart b/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart index a34fcb9baf5e0..d759b0d80b3e6 100644 --- a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart +++ b/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart @@ -1,38 +1,48 @@ import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; +import 'package:immich_mobile/utils/hooks/timer_hook.dart'; import 'package:immich_mobile/widgets/asset_viewer/center_play_button.dart'; import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; -import 'package:immich_mobile/utils/hooks/timer_hook.dart'; class CustomVideoPlayerControls extends HookConsumerWidget { final Duration hideTimerDuration; const CustomVideoPlayerControls({ super.key, - this.hideTimerDuration = const Duration(seconds: 3), + this.hideTimerDuration = const Duration(seconds: 5), }); @override Widget build(BuildContext context, WidgetRef ref) { + final assetIsVideo = ref.watch( + currentAssetProvider.select((asset) => asset != null && asset.isVideo), + ); + final showControls = ref.watch(showControlsProvider); + final VideoPlaybackState state = + ref.watch(videoPlaybackValueProvider.select((value) => value.state)); + // A timer to hide the controls final hideTimer = useTimer( hideTimerDuration, () { + if (!context.mounted) { + return; + } final state = ref.read(videoPlaybackValueProvider).state; + // Do not hide on paused - if (state != VideoPlaybackState.paused) { + if (state != VideoPlaybackState.paused && + state != VideoPlaybackState.completed && + assetIsVideo) { ref.read(showControlsProvider.notifier).show = false; } }, ); - - final showBuffering = useState(false); - final VideoPlaybackState state = - ref.watch(videoPlaybackValueProvider).state; + final showBuffering = state == VideoPlaybackState.buffering; /// Shows the controls and starts the timer to hide them void showControlsAndStartHideTimer() { @@ -40,28 +50,15 @@ class CustomVideoPlayerControls extends HookConsumerWidget { ref.read(showControlsProvider.notifier).show = true; } - // When we mute, show the controls - ref.listen(videoPlayerControlsProvider.select((v) => v.mute), - (previous, next) { - showControlsAndStartHideTimer(); - }); - // When we change position, show or hide timer ref.listen(videoPlayerControlsProvider.select((v) => v.position), (previous, next) { showControlsAndStartHideTimer(); }); - ref.listen(videoPlaybackValueProvider.select((value) => value.state), - (_, state) { - // Show buffering - showBuffering.value = state == VideoPlaybackState.buffering; - }); - /// Toggles between playing and pausing depending on the state of the video void togglePlay() { showControlsAndStartHideTimer(); - final state = ref.read(videoPlaybackValueProvider).state; if (state == VideoPlaybackState.playing) { ref.read(videoPlayerControlsProvider.notifier).pause(); } else if (state == VideoPlaybackState.completed) { @@ -75,10 +72,10 @@ class CustomVideoPlayerControls extends HookConsumerWidget { behavior: HitTestBehavior.opaque, onTap: showControlsAndStartHideTimer, child: AbsorbPointer( - absorbing: !ref.watch(showControlsProvider), + absorbing: !showControls, child: Stack( children: [ - if (showBuffering.value) + if (showBuffering) const Center( child: DelayedLoadingIndicator( fadeInDuration: Duration(milliseconds: 400), @@ -86,18 +83,14 @@ class CustomVideoPlayerControls extends HookConsumerWidget { ) else GestureDetector( - onTap: () { - if (state != VideoPlaybackState.playing) { - togglePlay(); - } - ref.read(showControlsProvider.notifier).show = false; - }, + onTap: () => + ref.read(showControlsProvider.notifier).show = false, child: CenterPlayButton( backgroundColor: Colors.black54, iconColor: Colors.white, isFinished: state == VideoPlaybackState.completed, isPlaying: state == VideoPlaybackState.playing, - show: ref.watch(showControlsProvider), + show: assetIsVideo && showControls, onPressed: togglePlay, ), ), diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart index 3c650bdc6a2e3..0dd3305302c41 100644 --- a/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart +++ b/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart @@ -15,9 +15,10 @@ class FileInfo extends StatelessWidget { Widget build(BuildContext context) { final textColor = context.isDarkTheme ? Colors.white : Colors.black; - String resolution = asset.width != null && asset.height != null - ? "${asset.height} x ${asset.width} " - : ""; + final height = asset.orientatedHeight ?? asset.height; + final width = asset.orientatedWidth ?? asset.width; + String resolution = + height != null && width != null ? "$height x $width " : ""; String fileSize = asset.exifInfo?.fileSize != null ? formatBytes(asset.exifInfo!.fileSize!) : ""; diff --git a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart index f400224e0a0be..f7e2158ea981d 100644 --- a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart'; import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; @@ -19,23 +20,19 @@ import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class GalleryAppBar extends ConsumerWidget { - final Asset asset; final void Function() showInfo; - final void Function() onToggleMotionVideo; - final bool isPlayingVideo; - const GalleryAppBar({ - super.key, - required this.asset, - required this.showInfo, - required this.onToggleMotionVideo, - required this.isPlayingVideo, - }); + const GalleryAppBar({super.key, required this.showInfo}); @override Widget build(BuildContext context, WidgetRef ref) { + final asset = ref.watch(currentAssetProvider); + if (asset == null) { + return const SizedBox(); + } final album = ref.watch(currentAlbumProvider); final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId; + final showControls = ref.watch(showControlsProvider); final isPartner = ref .watch(partnerSharedWithProvider) @@ -98,23 +95,21 @@ class GalleryAppBar extends ConsumerWidget { } return IgnorePointer( - ignoring: !ref.watch(showControlsProvider), + ignoring: !showControls, child: AnimatedOpacity( duration: const Duration(milliseconds: 100), - opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0, + opacity: showControls ? 1.0 : 0.0, child: Container( color: Colors.black.withOpacity(0.4), child: TopControlAppBar( isOwner: isOwner, isPartner: isPartner, - isPlayingMotionVideo: isPlayingVideo, asset: asset, onMoreInfoPressed: showInfo, onFavorite: toggleFavorite, onRestorePressed: () => handleRestore(asset), onUploadPressed: asset.isLocal ? () => handleUpload(asset) : null, onDownloadPressed: asset.isLocal ? null : handleDownloadAsset, - onToggleMotionVideo: onToggleMotionVideo, onAddToAlbumPressed: () => addToAlbum(asset), onActivitiesPressed: handleActivities, ), diff --git a/mobile/lib/widgets/asset_viewer/motion_photo_button.dart b/mobile/lib/widgets/asset_viewer/motion_photo_button.dart new file mode 100644 index 0000000000000..e4dd3555545a7 --- /dev/null +++ b/mobile/lib/widgets/asset_viewer/motion_photo_button.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; + +class MotionPhotoButton extends ConsumerWidget { + const MotionPhotoButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isPlaying = ref.watch(isPlayingMotionVideoProvider); + + return IconButton( + onPressed: () { + ref.read(isPlayingMotionVideoProvider.notifier).toggle(); + }, + icon: isPlaying + ? const Icon(Icons.motion_photos_pause_outlined, color: grey200) + : const Icon(Icons.play_circle_outline_rounded, color: grey200), + ); + } +} diff --git a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart index 984b61f50cc05..2bdbb72ec03ac 100644 --- a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/providers/activity_statistics.provider.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; +import 'package:immich_mobile/widgets/asset_viewer/motion_photo_button.dart'; class TopControlAppBar extends HookConsumerWidget { const TopControlAppBar({ @@ -14,8 +15,6 @@ class TopControlAppBar extends HookConsumerWidget { required this.onDownloadPressed, required this.onAddToAlbumPressed, required this.onRestorePressed, - required this.onToggleMotionVideo, - required this.isPlayingMotionVideo, required this.onFavorite, required this.onUploadPressed, required this.isOwner, @@ -27,12 +26,10 @@ class TopControlAppBar extends HookConsumerWidget { final Function onMoreInfoPressed; final VoidCallback? onUploadPressed; final VoidCallback? onDownloadPressed; - final VoidCallback onToggleMotionVideo; final VoidCallback onAddToAlbumPressed; final VoidCallback onRestorePressed; final VoidCallback onActivitiesPressed; final Function(Asset) onFavorite; - final bool isPlayingMotionVideo; final bool isOwner; final bool isPartner; @@ -57,23 +54,6 @@ class TopControlAppBar extends HookConsumerWidget { ); } - Widget buildLivePhotoButton() { - return IconButton( - onPressed: () { - onToggleMotionVideo(); - }, - icon: isPlayingMotionVideo - ? Icon( - Icons.motion_photos_pause_outlined, - color: Colors.grey[200], - ) - : Icon( - Icons.play_circle_outline_rounded, - color: Colors.grey[200], - ), - ); - } - Widget buildMoreInfoButton() { return IconButton( onPressed: () { @@ -175,13 +155,11 @@ class TopControlAppBar extends HookConsumerWidget { foregroundColor: Colors.grey[100], backgroundColor: Colors.transparent, leading: buildBackButton(), - actionsIconTheme: const IconThemeData( - size: iconSize, - ), + actionsIconTheme: const IconThemeData(size: iconSize), shape: const Border(), actions: [ if (asset.isRemote && isOwner) buildFavoriteButton(a), - if (asset.livePhotoVideoId != null) buildLivePhotoButton(), + if (asset.livePhotoVideoId != null) const MotionPhotoButton(), if (asset.isLocal && !asset.isRemote) buildUploadButton(), if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(), if (asset.isRemote && (isOwner || isPartner) && !asset.isTrashed) diff --git a/mobile/lib/widgets/asset_viewer/video_player.dart b/mobile/lib/widgets/asset_viewer/video_player.dart deleted file mode 100644 index ebf158b59a5fb..0000000000000 --- a/mobile/lib/widgets/asset_viewer/video_player.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:chewie/chewie.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/utils/hooks/chewiew_controller_hook.dart'; -import 'package:immich_mobile/widgets/asset_viewer/custom_video_player_controls.dart'; -import 'package:video_player/video_player.dart'; - -class VideoPlayerViewer extends HookConsumerWidget { - final VideoPlayerController controller; - final bool isMotionVideo; - final Widget? placeholder; - final Duration hideControlsTimer; - final bool showControls; - final bool showDownloadingIndicator; - final bool loopVideo; - - const VideoPlayerViewer({ - super.key, - required this.controller, - required this.isMotionVideo, - this.placeholder, - required this.hideControlsTimer, - required this.showControls, - required this.showDownloadingIndicator, - required this.loopVideo, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final chewie = useChewieController( - controller: controller, - controlsSafeAreaMinimum: const EdgeInsets.only( - bottom: 100, - ), - placeholder: SizedBox.expand(child: placeholder), - customControls: CustomVideoPlayerControls( - hideTimerDuration: hideControlsTimer, - ), - showControls: showControls && !isMotionVideo, - hideControlsTimer: hideControlsTimer, - loopVideo: loopVideo, - ); - - return Chewie( - controller: chewie, - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/video_position.dart b/mobile/lib/widgets/asset_viewer/video_position.dart index ef309b9c8561a..b1f70b868685a 100644 --- a/mobile/lib/widgets/asset_viewer/video_position.dart +++ b/mobile/lib/widgets/asset_viewer/video_position.dart @@ -56,10 +56,16 @@ class VideoPosition extends HookConsumerWidget { ref.read(videoPlayerControlsProvider.notifier).play(); } }, - onChanged: (position) { + onChanged: (value) { + final inSeconds = + (duration * (value / 100.0)).inSeconds; + final position = inSeconds.toDouble(); ref .read(videoPlayerControlsProvider.notifier) .position = position; + // This immediately updates the slider position without waiting for the video to update + ref.read(videoPlaybackValueProvider.notifier).position = + Duration(seconds: inSeconds); }, ), ), diff --git a/mobile/lib/widgets/common/immich_image.dart b/mobile/lib/widgets/common/immich_image.dart index 5946dee453ad5..ab0f2584b554c 100644 --- a/mobile/lib/widgets/common/immich_image.dart +++ b/mobile/lib/widgets/common/immich_image.dart @@ -28,12 +28,11 @@ class ImmichImage extends StatelessWidget { // either by using the asset ID or the asset itself /// [asset] is the Asset to request, or else use [assetId] to get a remote /// image provider - /// Use [isThumbnail] and [thumbnailSize] if you'd like to request a thumbnail - /// The size of the square thumbnail to request. Ignored if isThumbnail - /// is not true static ImageProvider imageProvider({ Asset? asset, String? assetId, + double width = 1080, + double height = 1920, }) { if (asset == null && assetId == null) { throw Exception('Must supply either asset or assetId'); @@ -48,6 +47,8 @@ class ImmichImage extends StatelessWidget { if (useLocal(asset)) { return ImmichLocalImageProvider( asset: asset, + width: width, + height: height, ); } else { return ImmichRemoteImageProvider( @@ -87,6 +88,8 @@ class ImmichImage extends StatelessWidget { }, image: ImmichImage.imageProvider( asset: asset, + width: context.width, + height: context.height, ), width: width, height: height, diff --git a/mobile/lib/widgets/memories/memory_card.dart b/mobile/lib/widgets/memories/memory_card.dart index fb7cc882a0d31..4954d0bfccc8d 100644 --- a/mobile/lib/widgets/memories/memory_card.dart +++ b/mobile/lib/widgets/memories/memory_card.dart @@ -2,9 +2,9 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/pages/common/video_viewer.page.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; import 'package:immich_mobile/utils/hooks/blurhash_hook.dart'; import 'package:immich_mobile/widgets/common/immich_image.dart'; @@ -68,18 +68,20 @@ class MemoryCard extends StatelessWidget { } else { return Hero( tag: 'memory-${asset.id}', - child: VideoViewerPage( - key: ValueKey(asset), - asset: asset, - showDownloadingIndicator: false, - placeholder: SizedBox.expand( - child: ImmichImage( + child: SizedBox( + width: context.width, + height: context.height, + child: NativeVideoViewerPage( + key: ValueKey(asset.id), + asset: asset, + showControls: false, + image: ImmichImage( asset, + width: context.width, + height: context.height, fit: fit, ), ), - hideControlsTimer: const Duration(seconds: 2), - showControls: false, ), ); } @@ -137,6 +139,8 @@ class _BlurredBackdrop extends HookWidget { image: DecorationImage( image: ImmichImage.imageProvider( asset: asset, + height: context.height, + width: context.width, ), fit: BoxFit.cover, ), diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 9dc53e42b97b4..9203dcdf825e5 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -214,14 +214,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" - chewie: - dependency: "direct main" - description: - name: chewie - sha256: "2243e41e79e865d426d9dd9c1a9624aa33c4ad11de2d0cd680f826e2cd30e879" - url: "https://pub.dev" - source: hosted - version: "1.8.3" ci: dependency: transitive description: @@ -318,14 +310,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - cupertino_icons: - dependency: transitive - description: - name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.dev" - source: hosted - version: "1.0.8" custom_lint: dependency: "direct dev" description: @@ -378,10 +362,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: db03b2d2a3fa466a4627709e1db58692c3f7f658e36a5942d342d86efedc4091 + sha256: f545ffbadee826f26f2e1a0f0cbd667ae9a6011cc0f77c0f8f00a969655e6e95 url: "https://pub.dev" source: hosted - version: "11.0.0" + version: "11.1.1" device_info_plus_platform_interface: dependency: transitive description: @@ -450,10 +434,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "167bb619cdddaa10ef2907609feb8a79c16dfa479d3afaf960f8e223f754bf12" + sha256: aac85f20436608e01a6ffd1fdd4e746a7f33c93a2c83752e626bdfaea139b877 url: "https://pub.dev" source: hosted - version: "8.1.2" + version: "8.1.3" file_selector_linux: dependency: transitive description: @@ -548,10 +532,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "674173fd3c9eda9d4c8528da2ce0ea69f161577495a9cc835a2a4ecd7eadeb35" + sha256: dd6676d8c2926537eccdf9f72128bbb2a9d0814689527b17f92c248ff192eaf3 url: "https://pub.dev" source: hosted - version: "17.2.4" + version: "17.2.1+2" flutter_local_notifications_linux: dependency: transitive description: @@ -1024,14 +1008,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" - nested: - dependency: transitive + native_video_player: + dependency: "direct main" description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" + path: "." + ref: ac78487 + resolved-ref: ac78487b9a87c9e72cd15b428270a905ac551f29 + url: "https://github.com/immich-app/native_video_player" + source: git + version: "1.3.1" nm: dependency: transitive description: @@ -1067,10 +1052,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "894f37107424311bdae3e476552229476777b8752c5a2a2369c0cb9a2d5442ef" + sha256: da8d9ac8c4b1df253d1a328b7bf01ae77ef132833479ab40763334db13b91cce url: "https://pub.dev" source: hosted - version: "8.0.3" + version: "8.1.1" package_info_plus_platform_interface: dependency: transitive description: @@ -1255,14 +1240,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.2" - provider: - dependency: transitive - description: - name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c - url: "https://pub.dev" - source: hosted - version: "6.1.2" pub_semver: dependency: transitive description: @@ -1339,10 +1316,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: fec12c3c39f01e4df1ec6ad92b6e85503c5ca64ffd6e28d18c9ffe53fcc4cb11 + sha256: "9c9bafd4060728d7cdb2464c341743adbd79d327cb067ec7afb64583540b47c8" url: "https://pub.dev" source: hosted - version: "10.0.3" + version: "10.1.2" share_plus_platform_interface: dependency: transitive description: @@ -1708,46 +1685,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - video_player: - dependency: "direct main" - description: - name: video_player - sha256: "4a8c3492d734f7c39c2588a3206707a05ee80cef52e8c7f3b2078d430c84bc17" - url: "https://pub.dev" - source: hosted - version: "2.9.2" - video_player_android: - dependency: "direct main" - description: - name: video_player_android - sha256: "4de50df9ee786f5891d3281e1e633d7b142ef1acf47392592eb91cba5d355849" - url: "https://pub.dev" - source: hosted - version: "2.6.0" - video_player_avfoundation: - dependency: transitive - description: - name: video_player_avfoundation - sha256: d1e9a824f2b324000dc8fb2dcb2a3285b6c1c7c487521c63306cc5b394f68a7c - url: "https://pub.dev" - source: hosted - version: "2.6.1" - video_player_platform_interface: - dependency: transitive - description: - name: video_player_platform_interface - sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6" - url: "https://pub.dev" - source: hosted - version: "6.2.2" - video_player_web: - dependency: transitive - description: - name: video_player_web - sha256: "6dcdd298136523eaf7dfc31abaf0dfba9aa8a8dbc96670e87e9d42b6f2caf774" - url: "https://pub.dev" - source: hosted - version: "2.3.2" vm_service: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 235c58ce63e84..a037f9b947206 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -25,9 +25,6 @@ dependencies: intl: ^0.19.0 auto_route: ^9.2.0 fluttertoast: ^8.2.4 - video_player: ^2.9.2 - video_player_android: 2.6.0 - chewie: ^1.7.4 socket_io_client: ^2.0.3+1 maplibre_gl: 0.19.0+2 geolocator: ^11.0.0 # used to move to current location in map view @@ -45,7 +42,7 @@ dependencies: path_provider: ^2.1.2 collection: ^1.18.0 http_parser: ^4.0.2 - flutter_web_auth: ^0.6.0 + flutter_web_auth: 0.6.0 easy_image_viewer: ^1.4.0 isar: version: *isar_version @@ -64,6 +61,10 @@ dependencies: async: ^2.11.0 dynamic_color: ^1.7.0 #package to apply system theme background_downloader: ^8.5.5 + native_video_player: + git: + url: https://github.com/immich-app/native_video_player + ref: ac78487 #image editing packages crop_image: ^1.0.13 From 055f1fc72fd1b3cecd00a5110e1f830a71a41945 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 5 Dec 2024 09:11:48 -0600 Subject: [PATCH 08/19] feat(mobile): Auto switching server URLs (#14437) --- mobile/analysis_options.yaml | 2 + .../android/app/src/main/AndroidManifest.xml | 2 + mobile/assets/i18n/en-US.json | 38 ++- mobile/ios/Podfile | 7 + mobile/ios/Podfile.lock | 8 +- mobile/ios/Runner.xcodeproj/project.pbxproj | 6 +- mobile/ios/Runner/AppDelegate.swift | 22 +- mobile/ios/Runner/Info.plist | 6 +- mobile/ios/Runner/Runner.entitlements | 5 +- mobile/ios/Runner/RunnerProfile.entitlements | 2 + mobile/lib/entities/store.entity.dart | 6 + .../extensions/build_context_extensions.dart | 4 + mobile/lib/interfaces/auth.interface.dart | 6 + mobile/lib/interfaces/network.interface.dart | 4 + .../models/auth/auxilary_endpoint.model.dart | 105 +++++++ mobile/lib/pages/common/settings.page.dart | 107 +++++-- .../lib/pages/common/splash_screen.page.dart | 102 ++++--- .../providers/app_life_cycle.provider.dart | 40 ++- mobile/lib/providers/auth.provider.dart | 41 +++ mobile/lib/providers/network.provider.dart | 38 +++ .../lib/providers/server_info.provider.dart | 2 +- mobile/lib/repositories/auth.repository.dart | 39 +++ .../lib/repositories/network.repository.dart | 37 +++ .../repositories/permission.repository.dart | 45 +++ mobile/lib/services/api.service.dart | 4 +- mobile/lib/services/app_settings.service.dart | 1 + mobile/lib/services/auth.service.dart | 93 ++++++ mobile/lib/services/background.service.dart | 26 ++ mobile/lib/services/network.service.dart | 47 ++++ .../networking_settings/endpoint_input.dart | 155 ++++++++++ .../external_network_preference.dart | 189 +++++++++++++ .../local_network_preference.dart | 256 +++++++++++++++++ .../networking_settings.dart | 266 ++++++++++++++++++ mobile/openapi/devtools_options.yaml | 3 + mobile/pubspec.lock | 16 ++ mobile/pubspec.yaml | 1 + mobile/test/service.mocks.dart | 3 + mobile/test/services/auth.service_test.dart | 192 ++++++++++++- 38 files changed, 1823 insertions(+), 103 deletions(-) create mode 100644 mobile/lib/interfaces/network.interface.dart create mode 100644 mobile/lib/models/auth/auxilary_endpoint.model.dart create mode 100644 mobile/lib/providers/network.provider.dart create mode 100644 mobile/lib/repositories/network.repository.dart create mode 100644 mobile/lib/repositories/permission.repository.dart create mode 100644 mobile/lib/services/network.service.dart create mode 100644 mobile/lib/widgets/settings/networking_settings/endpoint_input.dart create mode 100644 mobile/lib/widgets/settings/networking_settings/external_network_preference.dart create mode 100644 mobile/lib/widgets/settings/networking_settings/local_network_preference.dart create mode 100644 mobile/lib/widgets/settings/networking_settings/networking_settings.dart create mode 100644 mobile/openapi/devtools_options.yaml diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 2b4b810f2aeb5..9cb03f6758651 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -104,6 +104,8 @@ custom_lint: - lib/widgets/album/album_thumbnail_listtile.dart - lib/widgets/forms/login/login_form.dart - lib/widgets/search/search_filter/{camera_picker,location_picker,people_picker}.dart + - lib/services/auth.service.dart # on ApiException + - test/services/auth.service_test.dart # on ApiException dart_code_metrics: metrics: diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index 8f239015dd62a..bbc562c103cd5 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -16,6 +16,8 @@ + + diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index d588507a07c19..121e3e498212a 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -1,4 +1,35 @@ { + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", + "current_server_address": "Current server address", + "grant_permission": "Grant permission", + "automatic_endpoint_switching_title": "Automatic URL switching", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", + "cancel": "Cancel", + "save": "Save", + "wifi_name": "WiFi Name", + "enter_wifi_name": "Enter WiFi name", + "your_wifi_name": "Your WiFi name", + "server_endpoint": "Server Endpoint", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "use_current_connection": "use current connection", + "add_endpoint": "Add endpoint", + "validate_endpoint_error": "Please enter a valid URL", + "advanced_settings_tile_subtitle": "Manage advanced settings", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", + "backup_setting_subtitle": "Manage background and foreground upload settings", + "setting_languages_subtitle": "Change the app's language", + "setting_notifications_subtitle": "Manage your notification settings", + "preferences_settings_subtitle": "Manage the app's preferences", + "asset_list_settings_subtitle": "Manage the look of the timeline", "action_common_back": "Back", "action_common_cancel": "Cancel", "action_common_clear": "Clear", @@ -16,7 +47,6 @@ "advanced_settings_proxy_headers_title": "Proxy Headers", "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates", - "advanced_settings_tile_subtitle": "Advanced user's settings", "advanced_settings_tile_title": "Advanced", "advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting", "advanced_settings_troubleshooting_title": "Troubleshooting", @@ -56,7 +86,6 @@ "asset_list_layout_settings_group_by_month": "Month", "asset_list_layout_settings_group_by_month_day": "Month + day", "asset_list_layout_sub_title": "Layout", - "asset_list_settings_subtitle": "Photo grid layout settings", "asset_list_settings_title": "Photo Grid", "asset_restored_successfully": "Asset restored successfully", "assets_deleted_permanently": "{} asset(s) deleted permanently", @@ -65,7 +94,7 @@ "assets_restored_successfully": "{} asset(s) restored successfully", "assets_trashed": "{} asset(s) trashed", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", - "asset_viewer_settings_title": "Asset Viewer", + "asset_viewer_settings_title": "Gallery Viewer", "backup_album_selection_page_albums_device": "Albums on device ({})", "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", @@ -492,7 +521,6 @@ "setting_notifications_notify_seconds": "{} seconds", "setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset", "setting_notifications_single_progress_title": "Show background backup detail progress", - "setting_notifications_subtitle": "Adjust your notification preferences", "setting_notifications_title": "Notifications", "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", "setting_notifications_total_progress_title": "Show background backup total progress", @@ -625,4 +653,4 @@ "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", "viewer_unstack": "Un-Stack" -} \ No newline at end of file +} diff --git a/mobile/ios/Podfile b/mobile/ios/Podfile index f38ac9619ba7e..b048c0bb0c87b 100644 --- a/mobile/ios/Podfile +++ b/mobile/ios/Podfile @@ -102,6 +102,13 @@ post_install do |installer| ## dart: PermissionGroup.criticalAlerts # 'PERMISSION_CRITICAL_ALERTS=1' + + ## The 'PERMISSION_LOCATION' macro enables the `locationWhenInUse` and `locationAlways` permission. If + ## the application only requires `locationWhenInUse`, only specify the `PERMISSION_LOCATION_WHENINUSE` + ## macro. + ## + ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse] + 'PERMISSION_LOCATION=1', ] end diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 2e71937a84776..bc65bd4b7f919 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -67,6 +67,8 @@ PODS: - MapLibre (= 5.14.0-pre3) - native_video_player (1.0.0): - Flutter + - network_info_plus (0.0.1): + - Flutter - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): @@ -115,6 +117,7 @@ DEPENDENCIES: - isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`) - maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`) - native_video_player (from `.symlinks/plugins/native_video_player/ios`) + - network_info_plus (from `.symlinks/plugins/network_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) @@ -169,6 +172,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/maplibre_gl/ios" native_video_player: :path: ".symlinks/plugins/native_video_player/ios" + network_info_plus: + :path: ".symlinks/plugins/network_info_plus/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: @@ -210,6 +215,7 @@ SPEC CHECKSUMS: MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9 native_video_player: d12af78a1a4a8cf09775a5177d5b392def6fd23c + network_info_plus: 6613d9d7cdeb0e6f366ed4dbe4b3c51c52d567a9 package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 @@ -225,6 +231,6 @@ SPEC CHECKSUMS: url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 -PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d +PODFILE CHECKSUM: 2282844f7aed70427ae663932332dad1225156c8 COCOAPODS: 1.15.2 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 45d0b7d0ef877..49ac6c4cffbc9 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -51,6 +51,7 @@ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; F7101BB0391A314774615E89 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + FA9973382CF6DF4B000EF859 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = ""; }; /* End PBXFileReference section */ @@ -126,6 +127,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + FA9973382CF6DF4B000EF859 /* Runner.entitlements */, 65DD438629917FAD0047FFA8 /* BackgroundSync */, FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, @@ -541,6 +543,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 184; @@ -553,7 +556,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.121.0; - PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.debug; + PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.vdebug; PRODUCT_NAME = "Immich-Debug"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -569,6 +572,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 184; diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index 446c82e78f539..8f635bc61b932 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -1,19 +1,18 @@ import BackgroundTasks import Flutter -import UIKit +import network_info_plus import path_provider_ios import permission_handler_apple import photo_manager import shared_preferences_foundation +import UIKit @main @objc class AppDelegate: FlutterAppDelegate { - override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - // Required for flutter_local_notification if #available(iOS 10.0, *) { UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate @@ -33,27 +32,26 @@ import shared_preferences_foundation BackgroundServicePlugin.setPluginRegistrantCallback { registry in if !registry.hasPlugin("org.cocoapods.path-provider-ios") { - FLTPathProviderPlugin.register( - with: registry.registrar(forPlugin: "org.cocoapods.path-provider-ios")!) + FLTPathProviderPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.path-provider-ios")!) } if !registry.hasPlugin("org.cocoapods.photo-manager") { - PhotoManagerPlugin.register( - with: registry.registrar(forPlugin: "org.cocoapods.photo-manager")!) + PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.photo-manager")!) } if !registry.hasPlugin("org.cocoapods.shared-preferences-foundation") { - SharedPreferencesPlugin.register( - with: registry.registrar(forPlugin: "org.cocoapods.shared-preferences-foundation")!) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.shared-preferences-foundation")!) } if !registry.hasPlugin("org.cocoapods.permission-handler-apple") { - PermissionHandlerPlugin.register( - with: registry.registrar(forPlugin: "org.cocoapods.permission-handler-apple")!) + PermissionHandlerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.permission-handler-apple")!) + } + + if !registry.hasPlugin("org.cocoapods.network-info-plus") { + FPPNetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.network-info-plus")!) } } return super.application(application, didFinishLaunchingWithOptions: launchOptions) } - } diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index f4ded26c68788..4389b39114f59 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -82,8 +82,12 @@ NSCameraUsageDescription We need to access the camera to let you take beautiful video using this app + NSLocationAlwaysAndWhenInUseUsageDescription + We require this permission to access the local WiFi name for background upload mechanism + NSLocationUsageDescription + We require this permission to access the local WiFi name NSLocationWhenInUseUsageDescription - Enable location setting to show position of assets on map + We require this permission to access the local WiFi name NSMicrophoneUsageDescription We need to access the microphone to let you take beautiful video using this app NSPhotoLibraryAddUsageDescription diff --git a/mobile/ios/Runner/Runner.entitlements b/mobile/ios/Runner/Runner.entitlements index 0c67376ebacb4..ba21fbdaf2902 100644 --- a/mobile/ios/Runner/Runner.entitlements +++ b/mobile/ios/Runner/Runner.entitlements @@ -1,5 +1,8 @@ - + + com.apple.developer.networking.wifi-info + + diff --git a/mobile/ios/Runner/RunnerProfile.entitlements b/mobile/ios/Runner/RunnerProfile.entitlements index 903def2af5306..75e36a143e4f0 100644 --- a/mobile/ios/Runner/RunnerProfile.entitlements +++ b/mobile/ios/Runner/RunnerProfile.entitlements @@ -4,5 +4,7 @@ aps-environment development + com.apple.developer.networking.wifi-info + diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart index 1dda2b9a12a03..316859b064d1e 100644 --- a/mobile/lib/entities/store.entity.dart +++ b/mobile/lib/entities/store.entity.dart @@ -236,6 +236,12 @@ enum StoreKey { colorfulInterface(130, type: bool), syncAlbums(131, type: bool), + + // Auto endpoint switching + autoEndpointSwitching(132, type: bool), + preferredWifiName(133, type: String), + localEndpoint(134, type: String), + externalEndpointList(135, type: String), ; const StoreKey( diff --git a/mobile/lib/extensions/build_context_extensions.dart b/mobile/lib/extensions/build_context_extensions.dart index d87ab2845f3aa..69a9c3b34706f 100644 --- a/mobile/lib/extensions/build_context_extensions.dart +++ b/mobile/lib/extensions/build_context_extensions.dart @@ -54,4 +54,8 @@ extension ContextHelper on BuildContext { // Managing focus within the widget tree from the current context FocusScopeNode get focusScope => FocusScope.of(this); + + // Show SnackBars from the current context + void showSnackBar(SnackBar snackBar) => + ScaffoldMessenger.of(this).showSnackBar(snackBar); } diff --git a/mobile/lib/interfaces/auth.interface.dart b/mobile/lib/interfaces/auth.interface.dart index e37323b994d3c..57088f45695c7 100644 --- a/mobile/lib/interfaces/auth.interface.dart +++ b/mobile/lib/interfaces/auth.interface.dart @@ -1,5 +1,11 @@ import 'package:immich_mobile/interfaces/database.interface.dart'; +import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; abstract interface class IAuthRepository implements IDatabaseRepository { Future clearLocalData(); + String getAccessToken(); + bool getEndpointSwitchingFeature(); + String? getPreferredWifiName(); + String? getLocalEndpoint(); + List getExternalEndpointList(); } diff --git a/mobile/lib/interfaces/network.interface.dart b/mobile/lib/interfaces/network.interface.dart new file mode 100644 index 0000000000000..098d67a27bc1b --- /dev/null +++ b/mobile/lib/interfaces/network.interface.dart @@ -0,0 +1,4 @@ +abstract interface class INetworkRepository { + Future getWifiName(); + Future getWifiIp(); +} diff --git a/mobile/lib/models/auth/auxilary_endpoint.model.dart b/mobile/lib/models/auth/auxilary_endpoint.model.dart new file mode 100644 index 0000000000000..89aba60913cc6 --- /dev/null +++ b/mobile/lib/models/auth/auxilary_endpoint.model.dart @@ -0,0 +1,105 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; + +class AuxilaryEndpoint { + final String url; + final AuxCheckStatus status; + + AuxilaryEndpoint({ + required this.url, + required this.status, + }); + + AuxilaryEndpoint copyWith({ + String? url, + AuxCheckStatus? status, + }) { + return AuxilaryEndpoint( + url: url ?? this.url, + status: status ?? this.status, + ); + } + + @override + String toString() => 'AuxilaryEndpoint(url: $url, status: $status)'; + + @override + bool operator ==(covariant AuxilaryEndpoint other) { + if (identical(this, other)) return true; + + return other.url == url && other.status == status; + } + + @override + int get hashCode => url.hashCode ^ status.hashCode; + + Map toMap() { + return { + 'url': url, + 'status': status.toMap(), + }; + } + + factory AuxilaryEndpoint.fromMap(Map map) { + return AuxilaryEndpoint( + url: map['url'] as String, + status: AuxCheckStatus.fromMap(map['status'] as Map), + ); + } + + String toJson() => json.encode(toMap()); + + factory AuxilaryEndpoint.fromJson(String source) => + AuxilaryEndpoint.fromMap(json.decode(source) as Map); +} + +class AuxCheckStatus { + final String name; + AuxCheckStatus({ + required this.name, + }); + const AuxCheckStatus._(this.name); + + static const loading = AuxCheckStatus._('loading'); + static const valid = AuxCheckStatus._('valid'); + static const error = AuxCheckStatus._('error'); + static const unknown = AuxCheckStatus._('unknown'); + + @override + bool operator ==(covariant AuxCheckStatus other) { + if (identical(this, other)) return true; + + return other.name == name; + } + + @override + int get hashCode => name.hashCode; + + AuxCheckStatus copyWith({ + String? name, + }) { + return AuxCheckStatus( + name: name ?? this.name, + ); + } + + Map toMap() { + return { + 'name': name, + }; + } + + factory AuxCheckStatus.fromMap(Map map) { + return AuxCheckStatus( + name: map['name'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory AuxCheckStatus.fromJson(String source) => + AuxCheckStatus.fromMap(json.decode(source) as Map); + + @override + String toString() => 'AuxCheckStatus(name: $name)'; +} diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index a6ca239962df3..ba3150c046b6a 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -8,36 +8,69 @@ import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_se import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart'; import 'package:immich_mobile/widgets/settings/language_settings.dart'; +import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart'; import 'package:immich_mobile/widgets/settings/notification_setting.dart'; import 'package:immich_mobile/widgets/settings/preference_settings/preference_setting.dart'; import 'package:immich_mobile/routing/router.dart'; enum SettingSection { + advanced( + 'advanced_settings_tile_title', + Icons.build_outlined, + "advanced_settings_tile_subtitle", + ), + assetViewer( + 'asset_viewer_settings_title', + Icons.image_outlined, + "asset_viewer_settings_subtitle", + ), + backup( + 'backup_controller_page_backup', + Icons.cloud_upload_outlined, + "backup_setting_subtitle", + ), + languages( + 'setting_languages_title', + Icons.language, + "setting_languages_subtitle", + ), + networking( + 'networking_settings', + Icons.wifi, + "networking_subtitle", + ), notifications( 'setting_notifications_title', Icons.notifications_none_rounded, + "setting_notifications_subtitle", + ), + preferences( + 'preferences_settings_title', + Icons.interests_outlined, + "preferences_settings_subtitle", ), - languages('setting_languages_title', Icons.language), - preferences('preferences_settings_title', Icons.interests_outlined), - backup('backup_controller_page_backup', Icons.cloud_upload_outlined), - timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined), - viewer('asset_viewer_settings_title', Icons.image_outlined), - advanced('advanced_settings_tile_title', Icons.build_outlined); + timeline( + 'asset_list_settings_title', + Icons.auto_awesome_mosaic_outlined, + "asset_list_settings_subtitle", + ); final String title; + final String subtitle; final IconData icon; Widget get widget => switch (this) { - SettingSection.notifications => const NotificationSetting(), + SettingSection.advanced => const AdvancedSettings(), + SettingSection.assetViewer => const AssetViewerSettings(), + SettingSection.backup => const BackupSettings(), SettingSection.languages => const LanguageSettings(), + SettingSection.networking => const NetworkingSettings(), + SettingSection.notifications => const NotificationSetting(), SettingSection.preferences => const PreferenceSetting(), - SettingSection.backup => const BackupSettings(), SettingSection.timeline => const AssetListSettings(), - SettingSection.viewer => const AssetViewerSettings(), - SettingSection.advanced => const AdvancedSettings(), }; - const SettingSection(this.title, this.icon); + const SettingSection(this.title, this.icon, this.subtitle); } @RoutePage() @@ -61,22 +94,50 @@ class _MobileLayout extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( + physics: const ClampingScrollPhysics(), + padding: const EdgeInsets.symmetric(vertical: 10.0), children: SettingSection.values .map( - (s) => ListTile( - contentPadding: - const EdgeInsets.symmetric(vertical: 2.0, horizontal: 16.0), - leading: Icon(s.icon), - title: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - s.title, - style: const TextStyle( - fontWeight: FontWeight.bold, + (setting) => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + child: Card( + elevation: 0, + clipBehavior: Clip.antiAlias, + color: context.colorScheme.surfaceContainer, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + margin: const EdgeInsets.symmetric(vertical: 4.0), + child: ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + leading: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(16)), + color: context.isDarkTheme + ? Colors.black26 + : Colors.white.withAlpha(100), + ), + padding: const EdgeInsets.all(16.0), + child: Icon(setting.icon, color: context.primaryColor), ), - ).tr(), + title: Text( + setting.title, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w600, + color: context.primaryColor, + ), + ).tr(), + subtitle: Text( + setting.subtitle, + ).tr(), + onTap: () => + context.pushRoute(SettingsSubRoute(section: setting)), + ), ), - onTap: () => context.pushRoute(SettingsSubRoute(section: s)), ), ) .toList(), diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index d88c6cf3663ff..6a060e19f0dcc 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -1,6 +1,5 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; @@ -10,65 +9,80 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:logging/logging.dart'; @RoutePage() -class SplashScreenPage extends HookConsumerWidget { +class SplashScreenPage extends StatefulHookConsumerWidget { const SplashScreenPage({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + SplashScreenPageState createState() => SplashScreenPageState(); +} + +class SplashScreenPageState extends ConsumerState { + final log = Logger("SplashScreenPage"); + @override + void initState() { + super.initState(); + ref + .read(authProvider.notifier) + .setOpenApiServiceEndpoint() + .then(logConnectionInfo) + .whenComplete(() => resumeSession()); + } + + void logConnectionInfo(String? endpoint) { + if (endpoint == null) { + return; + } + + log.info("Resuming session at $endpoint"); + } + + void resumeSession() async { final serverUrl = Store.tryGet(StoreKey.serverUrl); final endpoint = Store.tryGet(StoreKey.serverEndpoint); final accessToken = Store.tryGet(StoreKey.accessToken); - final log = Logger("SplashScreenPage"); - void performLoggingIn() async { - bool isAuthSuccess = false; + bool isAuthSuccess = false; - if (accessToken != null && serverUrl != null && endpoint != null) { - try { - isAuthSuccess = await ref.read(authProvider.notifier).saveAuthInfo( - accessToken: accessToken, - ); - } catch (error, stackTrace) { - log.severe( - 'Cannot set success login info', - error, - stackTrace, - ); - } - } else { - isAuthSuccess = false; + if (accessToken != null && serverUrl != null && endpoint != null) { + try { + isAuthSuccess = await ref.read(authProvider.notifier).saveAuthInfo( + accessToken: accessToken, + ); + } catch (error, stackTrace) { log.severe( - 'Missing authentication, server, or endpoint info from the local store', + 'Cannot set success login info', + error, + stackTrace, ); } + } else { + isAuthSuccess = false; + log.severe( + 'Missing authentication, server, or endpoint info from the local store', + ); + } - if (!isAuthSuccess) { - log.severe( - 'Unable to login using offline or online methods - Logging out completely', - ); - ref.read(authProvider.notifier).logout(); - context.replaceRoute(const LoginRoute()); - return; - } + if (!isAuthSuccess) { + log.severe( + 'Unable to login using offline or online methods - Logging out completely', + ); + ref.read(authProvider.notifier).logout(); + context.replaceRoute(const LoginRoute()); + return; + } - context.replaceRoute(const TabControllerRoute()); + context.replaceRoute(const TabControllerRoute()); - final hasPermission = - await ref.read(galleryPermissionNotifier.notifier).hasPermission; - if (hasPermission) { - // Resume backup (if enable) then navigate - ref.watch(backupProvider.notifier).resumeBackup(); - } + final hasPermission = + await ref.read(galleryPermissionNotifier.notifier).hasPermission; + if (hasPermission) { + // Resume backup (if enable) then navigate + ref.watch(backupProvider.notifier).resumeBackup(); } + } - useEffect( - () { - performLoggingIn(); - return null; - }, - [], - ); - + @override + Widget build(BuildContext context) { return const Scaffold( body: Center( child: Image( diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 8cacb70eb2352..780e22b818d7e 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/services/background.service.dart'; @@ -35,7 +36,7 @@ class AppLifeCycleNotifier extends StateNotifier { return state; } - void handleAppResume() { + void handleAppResume() async { state = AppLifeCycleEnum.resumed; // no need to resume because app was never really paused @@ -46,32 +47,49 @@ class AppLifeCycleNotifier extends StateNotifier { // Needs to be logged in if (isAuthenticated) { + // switch endpoint if needed + final endpoint = + await _ref.read(authProvider.notifier).setOpenApiServiceEndpoint(); + if (kDebugMode) { + debugPrint("Using server URL: $endpoint"); + } + final permission = _ref.watch(galleryPermissionNotifier); if (permission.isGranted || permission.isLimited) { - _ref.read(backupProvider.notifier).resumeBackup(); - _ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); + await _ref.read(backupProvider.notifier).resumeBackup(); + await _ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); } - _ref.read(serverInfoProvider.notifier).getServerVersion(); + + await _ref.read(serverInfoProvider.notifier).getServerVersion(); + switch (_ref.read(tabProvider)) { case TabEnum.home: - _ref.read(assetProvider.notifier).getAllAsset(); + await _ref.read(assetProvider.notifier).getAllAsset(); + break; case TabEnum.search: - // nothing to do + // nothing to do + break; + case TabEnum.albums: - _ref.read(albumProvider.notifier).refreshRemoteAlbums(); + await _ref.read(albumProvider.notifier).refreshRemoteAlbums(); + break; case TabEnum.library: - // nothing to do + // nothing to do + break; } } _ref.read(websocketProvider.notifier).connect(); - _ref + await _ref .read(notificationPermissionProvider.notifier) .getNotificationPermission(); - _ref.read(galleryPermissionNotifier.notifier).getGalleryPermissionStatus(); - _ref.read(iOSBackgroundSettingsProvider.notifier).refresh(); + await _ref + .read(galleryPermissionNotifier.notifier) + .getGalleryPermissionStatus(); + + await _ref.read(iOSBackgroundSettingsProvider.notifier).refresh(); _ref.invalidate(memoryFutureProvider); } diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index 5efbdab8d331e..a23ffd3d68a2a 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -45,6 +45,17 @@ class AuthNotifier extends StateNotifier { return _authService.validateServerUrl(url); } + /// Validating the url is the alternative connecting server url without + /// saving the infomation to the local database + Future validateAuxilaryServerUrl(String url) async { + try { + final validEndpoint = await _apiService.resolveEndpoint(url); + return await _authService.validateAuxilaryServerUrl(validEndpoint); + } catch (_) { + return false; + } + } + Future login(String email, String password) async { final response = await _authService.login(email, password); await saveAuthInfo(accessToken: response.accessToken); @@ -161,4 +172,34 @@ class AuthNotifier extends StateNotifier { return true; } + + Future saveWifiName(String wifiName) { + return Store.put(StoreKey.preferredWifiName, wifiName); + } + + Future saveLocalEndpoint(String url) { + return Store.put(StoreKey.localEndpoint, url); + } + + String? getSavedWifiName() { + return Store.tryGet(StoreKey.preferredWifiName); + } + + String? getSavedLocalEndpoint() { + return Store.tryGet(StoreKey.localEndpoint); + } + + /// Returns the current server endpoint (with /api) URL from the store + String? getServerEndpoint() { + return Store.tryGet(StoreKey.serverEndpoint); + } + + /// Returns the current server URL (input by the user) from the store + String? getServerUrl() { + return Store.tryGet(StoreKey.serverUrl); + } + + Future setOpenApiServiceEndpoint() { + return _authService.setOpenApiServiceEndpoint(); + } } diff --git a/mobile/lib/providers/network.provider.dart b/mobile/lib/providers/network.provider.dart new file mode 100644 index 0000000000000..5cb2fae4b1645 --- /dev/null +++ b/mobile/lib/providers/network.provider.dart @@ -0,0 +1,38 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/services/network.service.dart'; + +final networkProvider = StateNotifierProvider((ref) { + return NetworkNotifier( + ref.watch(networkServiceProvider), + ); +}); + +class NetworkNotifier extends StateNotifier { + final NetworkService _networkService; + + NetworkNotifier(this._networkService) : super(''); + + Future getWifiName() { + return _networkService.getWifiName(); + } + + Future getWifiReadPermission() { + return _networkService.getLocationWhenInUserPermission(); + } + + Future getWifiReadBackgroundPermission() { + return _networkService.getLocationAlwaysPermission(); + } + + Future requestWifiReadPermission() { + return _networkService.requestLocationWhenInUsePermission(); + } + + Future requestWifiReadBackgroundPermission() { + return _networkService.requestLocationAlwaysPermission(); + } + + Future openSettings() { + return _networkService.openSettings(); + } +} diff --git a/mobile/lib/providers/server_info.provider.dart b/mobile/lib/providers/server_info.provider.dart index 14521b06f64ca..a793acb3f66d4 100644 --- a/mobile/lib/providers/server_info.provider.dart +++ b/mobile/lib/providers/server_info.provider.dart @@ -59,7 +59,7 @@ class ServerInfoNotifier extends StateNotifier { await getServerConfig(); } - getServerVersion() async { + Future getServerVersion() async { try { final serverVersion = await _serverInfoService.getServerVersion(); diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index ababf35c9b5b3..fa504e6ac335d 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -1,10 +1,14 @@ +import 'dart:convert'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/exif_info.entity.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/interfaces/auth.interface.dart'; +import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/repositories/database.repository.dart'; @@ -27,4 +31,39 @@ class AuthRepository extends DatabaseRepository implements IAuthRepository { ]); }); } + + @override + String getAccessToken() { + return Store.get(StoreKey.accessToken); + } + + @override + bool getEndpointSwitchingFeature() { + return Store.tryGet(StoreKey.autoEndpointSwitching) ?? false; + } + + @override + String? getPreferredWifiName() { + return Store.tryGet(StoreKey.preferredWifiName); + } + + @override + String? getLocalEndpoint() { + return Store.tryGet(StoreKey.localEndpoint); + } + + @override + List getExternalEndpointList() { + final jsonString = Store.tryGet(StoreKey.externalEndpointList); + + if (jsonString == null) { + return []; + } + + final List jsonList = jsonDecode(jsonString); + final endpointList = + jsonList.map((e) => AuxilaryEndpoint.fromJson(e)).toList(); + + return endpointList; + } } diff --git a/mobile/lib/repositories/network.repository.dart b/mobile/lib/repositories/network.repository.dart new file mode 100644 index 0000000000000..54f527afb164e --- /dev/null +++ b/mobile/lib/repositories/network.repository.dart @@ -0,0 +1,37 @@ +import 'dart:io'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/interfaces/network.interface.dart'; +import 'package:network_info_plus/network_info_plus.dart'; + +final networkRepositoryProvider = Provider((_) { + final networkInfo = NetworkInfo(); + + return NetworkRepository(networkInfo); +}); + +class NetworkRepository implements INetworkRepository { + final NetworkInfo _networkInfo; + + NetworkRepository(this._networkInfo); + + @override + Future getWifiName() { + if (Platform.isAndroid) { + // remove quote around the return value on Android + // https://github.com/fluttercommunity/plus_plugins/tree/main/packages/network_info_plus/network_info_plus#android + return _networkInfo.getWifiName().then((value) { + if (value != null) { + return value.replaceAll(RegExp(r'"'), ''); + } + return value; + }); + } + return _networkInfo.getWifiName(); + } + + @override + Future getWifiIp() { + return _networkInfo.getWifiIP(); + } +} diff --git a/mobile/lib/repositories/permission.repository.dart b/mobile/lib/repositories/permission.repository.dart new file mode 100644 index 0000000000000..f825c36075729 --- /dev/null +++ b/mobile/lib/repositories/permission.repository.dart @@ -0,0 +1,45 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:permission_handler/permission_handler.dart'; + +final permissionRepositoryProvider = Provider((_) { + return PermissionRepository(); +}); + +class PermissionRepository implements IPermissionRepository { + PermissionRepository(); + + @override + Future hasLocationWhenInUsePermission() { + return Permission.locationWhenInUse.isGranted; + } + + @override + Future requestLocationWhenInUsePermission() async { + final result = await Permission.locationWhenInUse.request(); + return result.isGranted; + } + + @override + Future hasLocationAlwaysPermission() { + return Permission.locationAlways.isGranted; + } + + @override + Future requestLocationAlwaysPermission() async { + final result = await Permission.locationAlways.request(); + return result.isGranted; + } + + @override + Future openSettings() { + return openAppSettings(); + } +} + +abstract interface class IPermissionRepository { + Future hasLocationWhenInUsePermission(); + Future requestLocationWhenInUsePermission(); + Future hasLocationAlwaysPermission(); + Future requestLocationAlwaysPermission(); + Future openSettings(); +} diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index 63cd3f9f8cf74..0f6fe8a100ef8 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -67,7 +67,7 @@ class ApiService implements Authentication { } Future resolveAndSetEndpoint(String serverUrl) async { - final endpoint = await _resolveEndpoint(serverUrl); + final endpoint = await resolveEndpoint(serverUrl); setEndpoint(endpoint); // Save in local database for next startup @@ -82,7 +82,7 @@ class ApiService implements Authentication { /// host - required /// port - optional (default: based on schema) /// path - optional - Future _resolveEndpoint(String serverUrl) async { + Future resolveEndpoint(String serverUrl) async { final url = sanitizeUrl(serverUrl); if (!await _isEndpointAvailable(serverUrl)) { diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index 8f773e1bb33a9..14d800a4ef6e6 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -77,6 +77,7 @@ enum AppSettingsEnum { ), enableHapticFeedback(StoreKey.enableHapticFeedback, null, true), syncAlbums(StoreKey.syncAlbums, null, false), + autoEndpointSwitching(StoreKey.autoEndpointSwitching, null, false), ; const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); diff --git a/mobile/lib/services/auth.service.dart b/mobile/lib/services/auth.service.dart index fa6e282e63bb5..0393470098109 100644 --- a/mobile/lib/services/auth.service.dart +++ b/mobile/lib/services/auth.service.dart @@ -1,19 +1,26 @@ +import 'dart:async'; +import 'dart:io'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/interfaces/auth.interface.dart'; import 'package:immich_mobile/interfaces/auth_api.interface.dart'; +import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/auth.repository.dart'; import 'package:immich_mobile/repositories/auth_api.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/services/network.service.dart'; import 'package:logging/logging.dart'; +import 'package:openapi/api.dart'; final authServiceProvider = Provider( (ref) => AuthService( ref.watch(authApiRepositoryProvider), ref.watch(authRepositoryProvider), ref.watch(apiServiceProvider), + ref.watch(networkServiceProvider), ), ); @@ -21,6 +28,7 @@ class AuthService { final IAuthApiRepository _authApiRepository; final IAuthRepository _authRepository; final ApiService _apiService; + final NetworkService _networkService; final _log = Logger("AuthService"); @@ -28,6 +36,7 @@ class AuthService { this._authApiRepository, this._authRepository, this._apiService, + this._networkService, ); /// Validates the provided server URL by resolving and setting the endpoint. @@ -46,6 +55,28 @@ class AuthService { return validUrl; } + Future validateAuxilaryServerUrl(String url) async { + final httpclient = HttpClient(); + final accessToken = _authRepository.getAccessToken(); + bool isValid = false; + + try { + final uri = Uri.parse('$url/users/me'); + final request = await httpclient.getUrl(uri); + request.headers.add('x-immich-user-token', accessToken); + final response = await request.close(); + if (response.statusCode == 200) { + isValid = true; + } + } catch (error) { + _log.severe("Error validating auxilary endpoint", error); + } finally { + httpclient.close(); + } + + return isValid; + } + Future login(String email, String password) { return _authApiRepository.login(email, password); } @@ -84,6 +115,10 @@ class AuthService { Store.delete(StoreKey.currentUser), Store.delete(StoreKey.accessToken), Store.delete(StoreKey.assetETag), + Store.delete(StoreKey.autoEndpointSwitching), + Store.delete(StoreKey.preferredWifiName), + Store.delete(StoreKey.localEndpoint), + Store.delete(StoreKey.externalEndpointList), ]); } @@ -95,4 +130,62 @@ class AuthService { rethrow; } } + + Future setOpenApiServiceEndpoint() async { + final enable = _authRepository.getEndpointSwitchingFeature(); + if (!enable) { + return null; + } + + final wifiName = await _networkService.getWifiName(); + final savedWifiName = _authRepository.getPreferredWifiName(); + String? endpoint; + + if (wifiName == savedWifiName) { + endpoint = await _setLocalConnection(); + } + + endpoint ??= await _setRemoteConnection(); + + return endpoint; + } + + Future _setLocalConnection() async { + try { + final localEndpoint = _authRepository.getLocalEndpoint(); + if (localEndpoint != null) { + await _apiService.resolveAndSetEndpoint(localEndpoint); + return localEndpoint; + } + } catch (error, stackTrace) { + _log.severe("Cannot set local endpoint", error, stackTrace); + } + + return null; + } + + Future _setRemoteConnection() async { + List endpointList; + + try { + endpointList = _authRepository.getExternalEndpointList(); + } catch (error, stackTrace) { + _log.severe("Cannot get external endpoint", error, stackTrace); + return null; + } + + for (final endpoint in endpointList) { + try { + return await _apiService.resolveAndSetEndpoint(endpoint.url); + } on ApiException catch (error) { + _log.severe("Cannot resolve endpoint", error); + continue; + } catch (_) { + _log.severe("Auxilary server is not valid"); + continue; + } + } + + return null; + } } diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart index 3959e2a6edc2b..27be2c046df79 100644 --- a/mobile/lib/services/background.service.dart +++ b/mobile/lib/services/background.service.dart @@ -6,6 +6,7 @@ import 'dart:ui' show DartPluginRegistrant, IsolateNameServer, PluginUtilities; import 'package:cancellation_token_http/http.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -17,15 +18,20 @@ import 'package:immich_mobile/repositories/album.repository.dart'; import 'package:immich_mobile/repositories/album_api.repository.dart'; import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart'; +import 'package:immich_mobile/repositories/auth.repository.dart'; +import 'package:immich_mobile/repositories/auth_api.repository.dart'; import 'package:immich_mobile/repositories/backup.repository.dart'; import 'package:immich_mobile/repositories/album_media.repository.dart'; import 'package:immich_mobile/repositories/etag.repository.dart'; import 'package:immich_mobile/repositories/exif_info.repository.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart'; +import 'package:immich_mobile/repositories/network.repository.dart'; import 'package:immich_mobile/repositories/partner_api.repository.dart'; +import 'package:immich_mobile/repositories/permission.repository.dart'; import 'package:immich_mobile/repositories/user.repository.dart'; import 'package:immich_mobile/repositories/user_api.repository.dart'; import 'package:immich_mobile/services/album.service.dart'; +import 'package:immich_mobile/services/auth.service.dart'; import 'package:immich_mobile/services/entity.service.dart'; import 'package:immich_mobile/services/hash.service.dart'; import 'package:immich_mobile/services/localization.service.dart'; @@ -36,11 +42,13 @@ import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/services/network.service.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/services/user.service.dart'; import 'package:immich_mobile/utils/backup_progress.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; +import 'package:network_info_plus/network_info_plus.dart'; import 'package:path_provider_ios/path_provider_ios.dart'; import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; @@ -422,6 +430,24 @@ class BackgroundService { assetMediaRepository, ); + AuthApiRepository authApiRepository = AuthApiRepository(apiService); + AuthRepository authRepository = AuthRepository(db); + NetworkRepository networkRepository = NetworkRepository(NetworkInfo()); + PermissionRepository permissionRepository = PermissionRepository(); + NetworkService networkService = + NetworkService(networkRepository, permissionRepository); + AuthService authService = AuthService( + authApiRepository, + authRepository, + apiService, + networkService, + ); + + final endpoint = await authService.setOpenApiServiceEndpoint(); + if (kDebugMode) { + debugPrint("[BG UPLOAD] Using endpoint: $endpoint"); + } + final selectedAlbums = await backupRepository.getAllBySelection(BackupSelection.select); final excludedAlbums = diff --git a/mobile/lib/services/network.service.dart b/mobile/lib/services/network.service.dart new file mode 100644 index 0000000000000..f2d2de325d05c --- /dev/null +++ b/mobile/lib/services/network.service.dart @@ -0,0 +1,47 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/interfaces/network.interface.dart'; +import 'package:immich_mobile/repositories/network.repository.dart'; +import 'package:immich_mobile/repositories/permission.repository.dart'; + +final networkServiceProvider = Provider((ref) { + return NetworkService( + ref.watch(networkRepositoryProvider), + ref.watch(permissionRepositoryProvider), + ); +}); + +class NetworkService { + final INetworkRepository _repository; + final IPermissionRepository _permissionRepository; + + NetworkService(this._repository, this._permissionRepository); + + Future getLocationWhenInUserPermission() { + return _permissionRepository.hasLocationWhenInUsePermission(); + } + + Future requestLocationWhenInUsePermission() { + return _permissionRepository.requestLocationWhenInUsePermission(); + } + + Future getLocationAlwaysPermission() { + return _permissionRepository.hasLocationAlwaysPermission(); + } + + Future requestLocationAlwaysPermission() { + return _permissionRepository.requestLocationAlwaysPermission(); + } + + Future getWifiName() async { + final canRead = await getLocationWhenInUserPermission(); + if (!canRead) { + return null; + } + + return await _repository.getWifiName(); + } + + Future openSettings() { + return _permissionRepository.openSettings(); + } +} diff --git a/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart b/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart new file mode 100644 index 0000000000000..6302f9422ab95 --- /dev/null +++ b/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart @@ -0,0 +1,155 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; +import 'package:immich_mobile/providers/auth.provider.dart'; +import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart'; + +class EndpointInput extends StatefulHookConsumerWidget { + const EndpointInput({ + super.key, + required this.initialValue, + required this.index, + required this.onValidated, + required this.onDismissed, + this.enabled = true, + }); + + final AuxilaryEndpoint initialValue; + final int index; + final Function(String url, int index, AuxCheckStatus status) onValidated; + final Function(int index) onDismissed; + final bool enabled; + + @override + EndpointInputState createState() => EndpointInputState(); +} + +class EndpointInputState extends ConsumerState { + late final TextEditingController controller; + late final FocusNode focusNode; + late AuxCheckStatus auxCheckStatus; + bool isInputValid = false; + + @override + void initState() { + super.initState(); + controller = TextEditingController(text: widget.initialValue.url); + focusNode = FocusNode()..addListener(_onOutFocus); + + setState(() { + auxCheckStatus = widget.initialValue.status; + }); + } + + @override + void dispose() { + focusNode.removeListener(_onOutFocus); + focusNode.dispose(); + controller.dispose(); + super.dispose(); + } + + void _onOutFocus() { + if (!focusNode.hasFocus && isInputValid) { + validateAuxilaryServerUrl(); + } + } + + Future validateAuxilaryServerUrl() async { + final url = controller.text; + setState(() => auxCheckStatus = AuxCheckStatus.loading); + + final isValid = + await ref.read(authProvider.notifier).validateAuxilaryServerUrl(url); + + setState(() { + if (mounted) { + auxCheckStatus = isValid ? AuxCheckStatus.valid : AuxCheckStatus.error; + } + }); + + widget.onValidated(url, widget.index, auxCheckStatus); + } + + String? validateUrl(String? url) { + try { + if (url == null || url.isEmpty || !Uri.parse(url).isAbsolute) { + isInputValid = false; + return 'validate_endpoint_error'.tr(); + } + } catch (_) { + isInputValid = false; + return 'validate_endpoint_error'.tr(); + } + + isInputValid = true; + return null; + } + + @override + Widget build(BuildContext context) { + return Dismissible( + key: ValueKey(widget.index.toString()), + direction: DismissDirection.endToStart, + onDismissed: (_) => widget.onDismissed(widget.index), + background: Container( + color: Colors.red, + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 16), + child: const Icon( + Icons.delete, + color: Colors.white, + ), + ), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + trailing: ReorderableDragStartListener( + enabled: widget.enabled, + index: widget.index, + child: const Icon(Icons.drag_handle_rounded), + ), + leading: NetworkStatusIcon( + key: ValueKey('status_$auxCheckStatus'), + status: auxCheckStatus, + enabled: widget.enabled, + ), + subtitle: TextFormField( + enabled: widget.enabled, + onTapOutside: (_) => focusNode.unfocus(), + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: validateUrl, + keyboardType: TextInputType.url, + style: const TextStyle( + fontFamily: 'Inconsolata', + fontWeight: FontWeight.w600, + fontSize: 14, + ), + decoration: InputDecoration( + hintText: 'http(s)://immich.domain.com', + contentPadding: const EdgeInsets.all(16), + filled: true, + fillColor: context.colorScheme.surfaceContainer, + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + errorBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.red[300]!), + borderRadius: const BorderRadius.all(Radius.circular(16)), + ), + disabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: + context.isDarkTheme ? Colors.grey[900]! : Colors.grey[300]!, + ), + borderRadius: const BorderRadius.all(Radius.circular(16)), + ), + ), + controller: controller, + focusNode: focusNode, + ), + ), + ); + } +} diff --git a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart new file mode 100644 index 0000000000000..13c109fa0e3f1 --- /dev/null +++ b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart @@ -0,0 +1,189 @@ +import 'dart:convert'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; +import 'package:immich_mobile/entities/store.entity.dart' as db_store; +import 'package:immich_mobile/widgets/settings/networking_settings/endpoint_input.dart'; + +class ExternalNetworkPreference extends HookConsumerWidget { + const ExternalNetworkPreference({super.key, required this.enabled}); + + final bool enabled; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final entries = + useState([AuxilaryEndpoint(url: '', status: AuxCheckStatus.unknown)]); + final canSave = useState(false); + + saveEndpointList() { + canSave.value = + entries.value.every((e) => e.status == AuxCheckStatus.valid); + + final endpointList = entries.value + .where((url) => url.status == AuxCheckStatus.valid) + .toList(); + + final jsonString = jsonEncode(endpointList); + + db_store.Store.put( + db_store.StoreKey.externalEndpointList, + jsonString, + ); + } + + updateValidationStatus(String url, int index, AuxCheckStatus status) { + entries.value[index] = + entries.value[index].copyWith(url: url, status: status); + + saveEndpointList(); + } + + handleReorder(int oldIndex, int newIndex) { + if (oldIndex < newIndex) { + newIndex -= 1; + } + + final entry = entries.value.removeAt(oldIndex); + entries.value.insert(newIndex, entry); + entries.value = [...entries.value]; + + saveEndpointList(); + } + + handleDismiss(int index) { + entries.value = [...entries.value..removeAt(index)]; + + saveEndpointList(); + } + + Widget proxyDecorator( + Widget child, + int index, + Animation animation, + ) { + return AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget? child) { + return Material( + color: context.colorScheme.surfaceContainerHighest, + shadowColor: context.colorScheme.primary.withOpacity(0.2), + child: child, + ); + }, + child: child, + ); + } + + useEffect( + () { + final jsonString = + db_store.Store.tryGet(db_store.StoreKey.externalEndpointList); + + if (jsonString == null) { + return null; + } + + final List jsonList = jsonDecode(jsonString); + entries.value = + jsonList.map((e) => AuxilaryEndpoint.fromJson(e)).toList(); + return null; + }, + const [], + ); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(16)), + color: context.colorScheme.surfaceContainerLow, + border: Border.all( + color: context.colorScheme.surfaceContainerHighest, + width: 1, + ), + ), + child: Stack( + children: [ + Positioned( + bottom: -36, + right: -36, + child: Icon( + Icons.dns_rounded, + size: 120, + color: context.primaryColor.withOpacity(0.05), + ), + ), + ListView( + padding: const EdgeInsets.symmetric(vertical: 16.0), + physics: const ClampingScrollPhysics(), + shrinkWrap: true, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 24, + ), + child: Text( + "external_network_sheet_info".tr(), + style: context.textTheme.bodyMedium, + ), + ), + const SizedBox(height: 4), + Divider(color: context.colorScheme.surfaceContainerHighest), + Form( + key: GlobalKey(), + child: ReorderableListView.builder( + buildDefaultDragHandles: false, + proxyDecorator: proxyDecorator, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: entries.value.length, + onReorder: handleReorder, + itemBuilder: (context, index) { + return EndpointInput( + key: Key(index.toString()), + index: index, + initialValue: entries.value[index], + onValidated: updateValidationStatus, + onDismissed: handleDismiss, + enabled: enabled, + ); + }, + ), + ), + const SizedBox(height: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: SizedBox( + height: 48, + child: OutlinedButton.icon( + icon: const Icon(Icons.add), + label: Text('add_endpoint'.tr().toUpperCase()), + onPressed: enabled + ? () { + entries.value = [ + ...entries.value, + AuxilaryEndpoint( + url: '', + status: AuxCheckStatus.unknown, + ), + ]; + } + : null, + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart b/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart new file mode 100644 index 0000000000000..0258cc38474a4 --- /dev/null +++ b/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart @@ -0,0 +1,256 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/auth.provider.dart'; +import 'package:immich_mobile/providers/network.provider.dart'; + +class LocalNetworkPreference extends HookConsumerWidget { + const LocalNetworkPreference({ + super.key, + required this.enabled, + }); + + final bool enabled; + + Future _showEditDialog( + BuildContext context, + String title, + String hintText, + String initialValue, + ) { + final controller = TextEditingController(text: initialValue); + + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(title), + content: TextField( + controller: controller, + autofocus: true, + decoration: InputDecoration( + border: const OutlineInputBorder(), + hintText: hintText, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'cancel'.tr().toUpperCase(), + style: const TextStyle(color: Colors.red), + ), + ), + TextButton( + onPressed: () => Navigator.pop(context, controller.text), + child: Text('save'.tr().toUpperCase()), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final wifiNameText = useState(""); + final localEndpointText = useState(""); + + useEffect( + () { + final wifiName = ref.read(authProvider.notifier).getSavedWifiName(); + final localEndpoint = + ref.read(authProvider.notifier).getSavedLocalEndpoint(); + + if (wifiName != null) { + wifiNameText.value = wifiName; + } + + if (localEndpoint != null) { + localEndpointText.value = localEndpoint; + } + + return null; + }, + [], + ); + + saveWifiName(String wifiName) { + wifiNameText.value = wifiName; + return ref.read(authProvider.notifier).saveWifiName(wifiName); + } + + saveLocalEndpoint(String url) { + localEndpointText.value = url; + return ref.read(authProvider.notifier).saveLocalEndpoint(url); + } + + handleEditWifiName() async { + final wifiName = await _showEditDialog( + context, + "wifi_name".tr(), + "your_wifi_name".tr(), + wifiNameText.value, + ); + + if (wifiName != null) { + await saveWifiName(wifiName); + } + } + + handleEditServerEndpoint() async { + final localEndpoint = await _showEditDialog( + context, + "server_endpoint".tr(), + "http://local-ip:2283/api", + localEndpointText.value, + ); + + if (localEndpoint != null) { + await saveLocalEndpoint(localEndpoint); + } + } + + autofillCurrentNetwork() async { + final wifiName = await ref.read(networkProvider.notifier).getWifiName(); + + if (wifiName == null) { + context.showSnackBar( + SnackBar( + content: Text( + "get_wifiname_error".tr(), + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + color: context.colorScheme.onSecondary, + ), + ), + backgroundColor: context.colorScheme.secondary, + ), + ); + } else { + saveWifiName(wifiName); + } + + final serverEndpoint = + ref.read(authProvider.notifier).getServerEndpoint(); + + if (serverEndpoint != null) { + saveLocalEndpoint(serverEndpoint); + } + } + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Stack( + children: [ + Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(16)), + color: context.colorScheme.surfaceContainerLow, + border: Border.all( + color: context.colorScheme.surfaceContainerHighest, + width: 1, + ), + ), + child: Stack( + children: [ + Positioned( + bottom: -36, + right: -36, + child: Icon( + Icons.home_outlined, + size: 120, + color: context.primaryColor.withOpacity(0.05), + ), + ), + ListView( + padding: const EdgeInsets.symmetric(vertical: 16.0), + physics: const ClampingScrollPhysics(), + shrinkWrap: true, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 24, + ), + child: Text( + "local_network_sheet_info".tr(), + style: context.textTheme.bodyMedium, + ), + ), + const SizedBox(height: 4), + Divider( + color: context.colorScheme.surfaceContainerHighest, + ), + ListTile( + enabled: enabled, + contentPadding: const EdgeInsets.only(left: 24, right: 8), + leading: const Icon(Icons.wifi_rounded), + title: Text("wifi_name".tr()), + subtitle: wifiNameText.value.isEmpty + ? Text("enter_wifi_name".tr()) + : Text( + wifiNameText.value, + style: context.textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.bold, + color: enabled + ? context.primaryColor + : context.colorScheme.onSurface + .withAlpha(100), + fontFamily: 'Inconsolata', + ), + ), + trailing: IconButton( + onPressed: enabled ? handleEditWifiName : null, + icon: const Icon(Icons.edit_rounded), + ), + ), + ListTile( + enabled: enabled, + contentPadding: const EdgeInsets.only(left: 24, right: 8), + leading: const Icon(Icons.lan_rounded), + title: Text("server_endpoint".tr()), + subtitle: localEndpointText.value.isEmpty + ? const Text("http://local-ip:2283/api") + : Text( + localEndpointText.value, + style: context.textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.bold, + color: enabled + ? context.primaryColor + : context.colorScheme.onSurface + .withAlpha(100), + fontFamily: 'Inconsolata', + ), + ), + trailing: IconButton( + onPressed: enabled ? handleEditServerEndpoint : null, + icon: const Icon(Icons.edit_rounded), + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24.0, + ), + child: SizedBox( + height: 48, + child: OutlinedButton.icon( + icon: const Icon(Icons.wifi_find_rounded), + label: + Text('use_current_connection'.tr().toUpperCase()), + onPressed: enabled ? autofillCurrentNetwork : null, + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart new file mode 100644 index 0000000000000..59d05fd4cf525 --- /dev/null +++ b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart @@ -0,0 +1,266 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; +import 'package:immich_mobile/providers/network.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; +import 'package:immich_mobile/widgets/settings/networking_settings/external_network_preference.dart'; +import 'package:immich_mobile/widgets/settings/networking_settings/local_network_preference.dart'; +import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; + +import 'package:immich_mobile/entities/store.entity.dart' as db_store; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; + +class NetworkingSettings extends HookConsumerWidget { + const NetworkingSettings({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final currentEndpoint = + db_store.Store.get(db_store.StoreKey.serverEndpoint); + final featureEnabled = + useAppSettingsState(AppSettingsEnum.autoEndpointSwitching); + + Future checkWifiReadPermission() async { + final [hasLocationInUse, hasLocationAlways] = await Future.wait([ + ref.read(networkProvider.notifier).getWifiReadPermission(), + ref.read(networkProvider.notifier).getWifiReadBackgroundPermission(), + ]); + + bool? isGrantLocationAlwaysPermission; + + if (!hasLocationInUse) { + await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text("location_permission".tr()), + content: Text("location_permission_content".tr()), + actions: [ + TextButton( + onPressed: () async { + final isGrant = await ref + .read(networkProvider.notifier) + .requestWifiReadPermission(); + + Navigator.pop(context, isGrant); + }, + child: Text("grant_permission".tr()), + ), + ], + ); + }, + ); + } + + if (!hasLocationAlways) { + isGrantLocationAlwaysPermission = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text("background_location_permission".tr()), + content: Text("background_location_permission_content".tr()), + actions: [ + TextButton( + onPressed: () async { + final isGrant = await ref + .read(networkProvider.notifier) + .requestWifiReadBackgroundPermission(); + + Navigator.pop(context, isGrant); + }, + child: Text("grant_permission".tr()), + ), + ], + ); + }, + ); + } + + if (isGrantLocationAlwaysPermission != null && + !isGrantLocationAlwaysPermission) { + await ref.read(networkProvider.notifier).openSettings(); + } + } + + useEffect( + () { + if (featureEnabled.value == true) { + checkWifiReadPermission(); + } + return null; + }, + [featureEnabled.value], + ); + + return ListView( + padding: const EdgeInsets.only(bottom: 96), + physics: const ClampingScrollPhysics(), + children: [ + Padding( + padding: const EdgeInsets.only(top: 8, left: 16, bottom: 8), + child: NetworkPreferenceTitle( + title: "current_server_address".tr().toUpperCase(), + icon: currentEndpoint.startsWith('https') + ? Icons.https_outlined + : Icons.http_outlined, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(16)), + side: BorderSide( + color: context.colorScheme.surfaceContainerHighest, + width: 1, + ), + ), + child: ListTile( + leading: + const Icon(Icons.check_circle_rounded, color: Colors.green), + title: Text( + currentEndpoint, + style: TextStyle( + fontSize: 16, + fontFamily: 'Inconsolata', + fontWeight: FontWeight.bold, + color: context.primaryColor, + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 10.0), + child: Divider( + color: context.colorScheme.surfaceContainerHighest, + ), + ), + SettingsSwitchListTile( + enabled: true, + valueNotifier: featureEnabled, + title: "automatic_endpoint_switching_title".tr(), + subtitle: "automatic_endpoint_switching_subtitle".tr(), + ), + Padding( + padding: const EdgeInsets.only(top: 8, left: 16, bottom: 16), + child: NetworkPreferenceTitle( + title: "local_network".tr().toUpperCase(), + icon: Icons.home_outlined, + ), + ), + LocalNetworkPreference( + enabled: featureEnabled.value, + ), + Padding( + padding: const EdgeInsets.only(top: 32, left: 16, bottom: 16), + child: NetworkPreferenceTitle( + title: "external_network".tr().toUpperCase(), + icon: Icons.dns_outlined, + ), + ), + ExternalNetworkPreference( + enabled: featureEnabled.value, + ), + ], + ); + } +} + +class NetworkPreferenceTitle extends StatelessWidget { + const NetworkPreferenceTitle({ + super.key, + required this.icon, + required this.title, + }); + + final IconData icon; + final String title; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Icon( + icon, + color: context.colorScheme.onSurface.withAlpha(150), + ), + const SizedBox(width: 8), + Text( + title, + style: context.textTheme.displaySmall?.copyWith( + color: context.colorScheme.onSurface.withAlpha(200), + fontWeight: FontWeight.w500, + ), + ), + ], + ); + } +} + +class NetworkStatusIcon extends StatelessWidget { + const NetworkStatusIcon({ + super.key, + required this.status, + this.enabled = true, + }) : super(); + + final AuxCheckStatus status; + final bool enabled; + + @override + Widget build(BuildContext context) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: _buildIcon(context), + ); + } + + Widget _buildIcon(BuildContext context) { + switch (status) { + case AuxCheckStatus.loading: + return Padding( + padding: const EdgeInsets.only(left: 4.0), + child: SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + color: context.primaryColor, + strokeWidth: 2, + key: const ValueKey('loading'), + ), + ), + ); + case AuxCheckStatus.valid: + return enabled + ? const Icon( + Icons.check_circle_rounded, + color: Colors.green, + key: ValueKey('success'), + ) + : Icon( + Icons.check_circle_rounded, + color: context.colorScheme.onSurface.withAlpha(100), + key: const ValueKey('success'), + ); + case AuxCheckStatus.error: + return enabled + ? const Icon( + Icons.error_rounded, + color: Colors.red, + key: ValueKey('error'), + ) + : const Icon( + Icons.error_rounded, + color: Colors.grey, + key: ValueKey('error'), + ); + default: + return const Icon(Icons.circle_outlined, key: ValueKey('unknown')); + } + } +} diff --git a/mobile/openapi/devtools_options.yaml b/mobile/openapi/devtools_options.yaml new file mode 100644 index 0000000000000..fa0b357c4f4a2 --- /dev/null +++ b/mobile/openapi/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 9203dcdf825e5..34eb217828102 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1017,6 +1017,22 @@ packages: url: "https://github.com/immich-app/native_video_player" source: git version: "1.3.1" + network_info_plus: + dependency: "direct main" + description: + name: network_info_plus + sha256: bf9e39e523e9951d741868dc33ac386b0bc24301e9b7c8a7d60dbc34879150a8 + url: "https://pub.dev" + source: hosted + version: "6.1.1" + network_info_plus_platform_interface: + dependency: transitive + description: + name: network_info_plus_platform_interface + sha256: b7f35f4a7baef511159e524499f3c15464a49faa5ec10e92ee0bce265e664906 + url: "https://pub.dev" + source: hosted + version: "2.0.1" nm: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index a037f9b947206..e8bee37653e19 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -61,6 +61,7 @@ dependencies: async: ^2.11.0 dynamic_color: ^1.7.0 #package to apply system theme background_downloader: ^8.5.5 + network_info_plus: ^6.1.1 native_video_player: git: url: https://github.com/immich-app/native_video_player diff --git a/mobile/test/service.mocks.dart b/mobile/test/service.mocks.dart index de49a98cc4e5a..507b4f281b4fb 100644 --- a/mobile/test/service.mocks.dart +++ b/mobile/test/service.mocks.dart @@ -1,6 +1,7 @@ import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/entity.service.dart'; import 'package:immich_mobile/services/hash.service.dart'; +import 'package:immich_mobile/services/network.service.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/services/user.service.dart'; import 'package:mocktail/mocktail.dart'; @@ -14,3 +15,5 @@ class MockSyncService extends Mock implements SyncService {} class MockHashService extends Mock implements HashService {} class MockEntityService extends Mock implements EntityService {} + +class MockNetworkService extends Mock implements NetworkService {} diff --git a/mobile/test/services/auth.service_test.dart b/mobile/test/services/auth.service_test.dart index b864babb14174..edbf6495e3e48 100644 --- a/mobile/test/services/auth.service_test.dart +++ b/mobile/test/services/auth.service_test.dart @@ -1,8 +1,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/services/auth.service.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:openapi/api.dart'; import '../repository.mocks.dart'; import '../service.mocks.dart'; import '../test_utils.dart'; @@ -12,12 +14,22 @@ void main() { late MockAuthApiRepository authApiRepository; late MockAuthRepository authRepository; late MockApiService apiService; + late MockNetworkService networkService; setUp(() async { authApiRepository = MockAuthApiRepository(); authRepository = MockAuthRepository(); apiService = MockApiService(); - sut = AuthService(authApiRepository, authRepository, apiService); + networkService = MockNetworkService(); + + sut = AuthService( + authApiRepository, + authRepository, + apiService, + networkService, + ); + + registerFallbackValue(Uri()); }); group('validateServerUrl', () { @@ -115,4 +127,182 @@ void main() { verify(() => authRepository.clearLocalData()).called(1); }); }); + + group('setOpenApiServiceEndpoint', () { + setUp(() { + when(() => networkService.getWifiName()) + .thenAnswer((_) async => 'TestWifi'); + }); + + test('Should return null if auto endpoint switching is disabled', () async { + when(() => authRepository.getEndpointSwitchingFeature()) + .thenReturn((false)); + + final result = await sut.setOpenApiServiceEndpoint(); + + expect(result, isNull); + verify(() => authRepository.getEndpointSwitchingFeature()).called(1); + verifyNever(() => networkService.getWifiName()); + }); + + test('Should set local connection if wifi name matches', () async { + when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true); + when(() => authRepository.getPreferredWifiName()).thenReturn('TestWifi'); + when(() => authRepository.getLocalEndpoint()) + .thenReturn('http://local.endpoint'); + when(() => apiService.resolveAndSetEndpoint('http://local.endpoint')) + .thenAnswer((_) async => 'http://local.endpoint'); + + final result = await sut.setOpenApiServiceEndpoint(); + + expect(result, 'http://local.endpoint'); + verify(() => authRepository.getEndpointSwitchingFeature()).called(1); + verify(() => networkService.getWifiName()).called(1); + verify(() => authRepository.getPreferredWifiName()).called(1); + verify(() => authRepository.getLocalEndpoint()).called(1); + verify(() => apiService.resolveAndSetEndpoint('http://local.endpoint')) + .called(1); + }); + + test('Should set external endpoint if wifi name not matching', () async { + when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true); + when(() => authRepository.getPreferredWifiName()) + .thenReturn('DifferentWifi'); + when(() => authRepository.getExternalEndpointList()).thenReturn([ + AuxilaryEndpoint( + url: 'https://external.endpoint', + status: AuxCheckStatus.valid, + ), + ]); + when( + () => apiService.resolveAndSetEndpoint('https://external.endpoint'), + ).thenAnswer((_) async => 'https://external.endpoint/api'); + + final result = await sut.setOpenApiServiceEndpoint(); + + expect(result, 'https://external.endpoint/api'); + verify(() => authRepository.getEndpointSwitchingFeature()).called(1); + verify(() => networkService.getWifiName()).called(1); + verify(() => authRepository.getPreferredWifiName()).called(1); + verify(() => authRepository.getExternalEndpointList()).called(1); + verify( + () => apiService.resolveAndSetEndpoint('https://external.endpoint'), + ).called(1); + }); + + test('Should set second external endpoint if the first throw any error', + () async { + when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true); + when(() => authRepository.getPreferredWifiName()) + .thenReturn('DifferentWifi'); + when(() => authRepository.getExternalEndpointList()).thenReturn([ + AuxilaryEndpoint( + url: 'https://external.endpoint', + status: AuxCheckStatus.valid, + ), + AuxilaryEndpoint( + url: 'https://external.endpoint2', + status: AuxCheckStatus.valid, + ), + ]); + + when( + () => apiService.resolveAndSetEndpoint('https://external.endpoint'), + ).thenThrow(Exception('Invalid endpoint')); + when( + () => apiService.resolveAndSetEndpoint('https://external.endpoint2'), + ).thenAnswer((_) async => 'https://external.endpoint2/api'); + + final result = await sut.setOpenApiServiceEndpoint(); + + expect(result, 'https://external.endpoint2/api'); + verify(() => authRepository.getEndpointSwitchingFeature()).called(1); + verify(() => networkService.getWifiName()).called(1); + verify(() => authRepository.getPreferredWifiName()).called(1); + verify(() => authRepository.getExternalEndpointList()).called(1); + verify( + () => apiService.resolveAndSetEndpoint('https://external.endpoint2'), + ).called(1); + }); + + test('Should set second external endpoint if the first throw ApiException', + () async { + when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true); + when(() => authRepository.getPreferredWifiName()) + .thenReturn('DifferentWifi'); + when(() => authRepository.getExternalEndpointList()).thenReturn([ + AuxilaryEndpoint( + url: 'https://external.endpoint', + status: AuxCheckStatus.valid, + ), + AuxilaryEndpoint( + url: 'https://external.endpoint2', + status: AuxCheckStatus.valid, + ), + ]); + + when( + () => apiService.resolveAndSetEndpoint('https://external.endpoint'), + ).thenThrow(ApiException(503, 'Invalid endpoint')); + when( + () => apiService.resolveAndSetEndpoint('https://external.endpoint2'), + ).thenAnswer((_) async => 'https://external.endpoint2/api'); + + final result = await sut.setOpenApiServiceEndpoint(); + + expect(result, 'https://external.endpoint2/api'); + verify(() => authRepository.getEndpointSwitchingFeature()).called(1); + verify(() => networkService.getWifiName()).called(1); + verify(() => authRepository.getPreferredWifiName()).called(1); + verify(() => authRepository.getExternalEndpointList()).called(1); + verify( + () => apiService.resolveAndSetEndpoint('https://external.endpoint2'), + ).called(1); + }); + + test('Should handle error when setting local connection', () async { + when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true); + when(() => authRepository.getPreferredWifiName()).thenReturn('TestWifi'); + when(() => authRepository.getLocalEndpoint()) + .thenReturn('http://local.endpoint'); + when(() => apiService.resolveAndSetEndpoint('http://local.endpoint')) + .thenThrow(Exception('Local endpoint error')); + + final result = await sut.setOpenApiServiceEndpoint(); + + expect(result, isNull); + verify(() => authRepository.getEndpointSwitchingFeature()).called(1); + verify(() => networkService.getWifiName()).called(1); + verify(() => authRepository.getPreferredWifiName()).called(1); + verify(() => authRepository.getLocalEndpoint()).called(1); + verify(() => apiService.resolveAndSetEndpoint('http://local.endpoint')) + .called(1); + }); + + test('Should handle error when setting external connection', () async { + when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true); + when(() => authRepository.getPreferredWifiName()) + .thenReturn('DifferentWifi'); + when(() => authRepository.getExternalEndpointList()).thenReturn([ + AuxilaryEndpoint( + url: 'https://external.endpoint', + status: AuxCheckStatus.valid, + ), + ]); + when( + () => apiService.resolveAndSetEndpoint('https://external.endpoint'), + ).thenThrow(Exception('External endpoint error')); + + final result = await sut.setOpenApiServiceEndpoint(); + + expect(result, isNull); + verify(() => authRepository.getEndpointSwitchingFeature()).called(1); + verify(() => networkService.getWifiName()).called(1); + verify(() => authRepository.getPreferredWifiName()).called(1); + verify(() => authRepository.getExternalEndpointList()).called(1); + verify( + () => apiService.resolveAndSetEndpoint('https://external.endpoint'), + ).called(1); + }); + }); } From bb0242ae0a468cc408196b922fbcbbb8b04dfe6d Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Thu, 5 Dec 2024 17:11:02 +0100 Subject: [PATCH 09/19] chore(web): update translations (#14255) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alisher Nabiev Co-authored-by: Armand Maree Co-authored-by: Bezruchenko Simon Co-authored-by: Daniel Co-authored-by: Dean Cvjetanović Co-authored-by: Enoé Mugnaschi Co-authored-by: Enrico Zangrando Co-authored-by: Eugenio Marotta Co-authored-by: Fjuro Co-authored-by: Florian Ostertag Co-authored-by: Hurricane-32 Co-authored-by: Indrek Haav Co-authored-by: Jiri Grönroos Co-authored-by: Jonathan Co-authored-by: João Pedro Grugel Co-authored-by: KecskeTech Co-authored-by: Koen <62koen@users.noreply.hosted.weblate.org> Co-authored-by: Leo Bottaro Co-authored-by: LeonardoCasarotto Co-authored-by: Linerly Co-authored-by: Manar Aldroubi Co-authored-by: Marco Lampis Co-authored-by: Matjaž T Co-authored-by: Max Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Co-authored-by: Mickaël Descamps Co-authored-by: Miki Mrvos Co-authored-by: OskarSidor Co-authored-by: Paweł Co-authored-by: Petri Hämäläinen Co-authored-by: Ramazan S Co-authored-by: Rasulmmdv Co-authored-by: Rookie Nguyễn Co-authored-by: Stan P Co-authored-by: Stijn Co-authored-by: Stsiapan Ranchynski Co-authored-by: Suryo Wibowo Co-authored-by: Sylvain Pichon Co-authored-by: Sylvain Pichon Co-authored-by: Theofilos Nikolaou Co-authored-by: Vegard Fladby Co-authored-by: Viliam Co-authored-by: Vladislav Tkalin Co-authored-by: Xo Co-authored-by: bill85101 Co-authored-by: chamdim Co-authored-by: gallegonovato Co-authored-by: mitakskia Co-authored-by: pyccl Co-authored-by: stelle Co-authored-by: therry47 Co-authored-by: tomechio Co-authored-by: waclaw66 Co-authored-by: Ömer Efe ÇELİK Co-authored-by: Вячеслав Лукьяненко Co-authored-by: Мĕтри Сантăр ывалĕ Упа-Миччи --- i18n/af.json | 58 ++++++++++++++++- i18n/ar.json | 16 ++++- i18n/az.json | 11 +++- i18n/be.json | 59 +++++++++++++++-- i18n/bg.json | 4 +- i18n/ca.json | 5 ++ i18n/cs.json | 23 ++++++- i18n/cv.json | 5 +- i18n/de.json | 32 ++++++--- i18n/el.json | 57 ++++++++++------ i18n/es.json | 23 ++++++- i18n/et.json | 23 ++++++- i18n/fi.json | 12 ++++ i18n/fr.json | 140 ++++++++++++++++++++++++---------------- i18n/he.json | 19 +++++- i18n/hr.json | 47 ++++++++++++-- i18n/hu.json | 12 ++++ i18n/id.json | 12 ++++ i18n/it.json | 20 ++++-- i18n/ms.json | 26 +++++++- i18n/nb_NO.json | 11 +++- i18n/nl.json | 20 ++++-- i18n/pl.json | 7 ++ i18n/pt.json | 12 ++++ i18n/pt_BR.json | 36 +++++++++-- i18n/ru.json | 23 ++++--- i18n/sk.json | 60 +++++++++++++---- i18n/sl.json | 20 ++++-- i18n/sr_Cyrl.json | 13 ++++ i18n/sr_Latn.json | 7 ++ i18n/tr.json | 12 ++++ i18n/uk.json | 20 ++++-- i18n/vi.json | 11 +++- i18n/zh_Hant.json | 12 ++++ i18n/zh_SIMPLIFIED.json | 7 ++ 35 files changed, 718 insertions(+), 157 deletions(-) diff --git a/i18n/af.json b/i18n/af.json index 0967ef424bce6..ede1a745eb321 100644 --- a/i18n/af.json +++ b/i18n/af.json @@ -1 +1,57 @@ -{} +{ + "about": "Verfris", + "account": "Rekening", + "account_settings": "Rekeninginstellings", + "acknowledge": "Erken", + "action": "Aksie", + "actions": "Aksies", + "active": "Aktief", + "activity": "Aktiwiteite", + "activity_changed": "Aktiwiteit is {enabled, select, true {aangeskakel} other {afgeskakel}}", + "add": "Voegby", + "add_a_description": "Voeg 'n beskrywing by", + "add_a_location": "Voeg 'n ligging by", + "add_a_name": "Voeg 'n naam by", + "add_a_title": "Voeg 'n titel by", + "add_exclusion_pattern": "Voeg uitsgluitingspatrone by", + "add_import_path": "Voeg invoerpad by", + "add_location": "Voeg ligging by", + "add_more_users": "Voeg meer gebruikers by", + "add_partner": "Voeg vennoot by", + "add_path": "Voeg pad by", + "add_photos": "Voeg foto's by", + "add_to": "Voeg na...", + "add_to_album": "Voeg na album", + "add_to_shared_album": "Voeg na gedeelde album", + "added_to_archive": "By argief gevoeg", + "added_to_favorites": "By gunstelinge gevoeg", + "added_to_favorites_count": "Het {count, number} by gunstelinge gevoeg", + "admin": { + "add_exclusion_pattern_description": "Voeg uitsluitingspatrone by. Globbing met *, ** en ? word ondersteun. Om alle lêers in enige lêergids genaamd \"Raw\" te ignoreer, gebruik \"**/Raw/**\". Om alle lêers wat op \".tif\" eindig, te ignoreer, gebruik \"**/*.tif\". Om 'n absolute pad te ignoreer, gebruik \"/path/to/ignore/**\".", + "asset_offline_description": "Hierdie eksterne biblioteekbate word nie meer op skyf gevind nie en is na die asblik geskuif. As die lêer binne die biblioteek geskuif is, gaan jou tydlyn na vir die nuwe ooreenstemmende bate. Om hierdie bate te herstel, maak asseblief seker dat die lêerpad hieronder deur Immich verkry kan word en skandeer die biblioteek.", + "authentication_settings": "Verifikasie instellings", + "authentication_settings_description": "Bestuur wagwoord, OAuth en ander verifikasie instellings", + "authentication_settings_disable_all": "Is jy seker jy wil alle aanmeldmetodes deaktiveer? Aanmelding sal heeltemal gedeaktiveer word.", + "authentication_settings_reenable": "Om te heraktiveer, gebruik 'n Server Command.", + "background_task_job": "Agtergrondtake", + "backup_database": "Rugsteun databasis", + "backup_database_enable_description": "Aktiveer databasisrugsteun", + "backup_keep_last_amount": "Aantal vorige rugsteune om te hou", + "backup_settings": "Rugsteun instellings", + "backup_settings_description": "Bestuur databasis rugsteun instellings", + "check_all": "Kies Alles", + "cleared_jobs": "Poste gevee vir: {job}", + "config_set_by_file": "Config word tans deur 'n konfigurasielêer gestel", + "confirm_delete_library": "Is jy seker jy wil {library}-biblioteek uitvee?", + "confirm_delete_library_assets": "Is jy seker jy wil hierdie biblioteek uitvee? Dit sal {count, plural, one {# bevatte base} other {# bevatte bates}} uit Immich uitvee en kan nie ongedaan gemaak word nie. Lêers sal op skyf bly.", + "confirm_email_below": "Om te bevestig, tik \"{email}\" hieronder", + "confirm_reprocess_all_faces": "Is jy seker jy wil alle gesigte herverwerk? Dit sal ook genoemde mense skoonmaak.", + "confirm_user_password_reset": "Is jy seker jy wil {user} se wagwoord terugstel?", + "create_job": "Skep werk", + "cron_expression": "Cron uitdrukking", + "cron_expression_description": "Stel die skanderingsinterval in met die cron-formaat. Vir meer inligting verwys asseblief na bv. Crontab Guru", + "cron_expression_presets": "Cron uitdrukking voorafinstellings", + "disable_login": "Deaktiveer aanmelding", + "duplicate_detection_job_description": "Begin masjienleer op bates om soortgelyke beelde op te spoor. Maak staat op Smart Search" + } +} diff --git a/i18n/ar.json b/i18n/ar.json index 41aa700ebe072..7e1805ca34795 100644 --- a/i18n/ar.json +++ b/i18n/ar.json @@ -1,5 +1,5 @@ { - "about": "حول", + "about": "تحديث", "account": "الحساب", "account_settings": "إعدادات الحساب", "acknowledge": "أُدرك ذلك", @@ -222,6 +222,8 @@ "send_welcome_email": "إرسال بريد ترحيبي", "server_external_domain_settings": "إسم النطاق الخارجي", "server_external_domain_settings_description": "إسم النطاق لروابط المشاركة العامة، بما في ذلك http(s)://", + "server_public_users": "المستخدمون العامون", + "server_public_users_description": "يتم إدراج جميع المستخدمين (الاسم والبريد الإلكتروني) عند إضافة مستخدم إلى الألبومات المشتركة. عند تعطيل هذه الميزة، ستكون قائمة المستخدمين متاحة فقط لمستخدمي الإدارة.", "server_settings": "إعدادات الخادم", "server_settings_description": "إدارة إعدادات الخادم", "server_welcome_message": "الرسالة الترحيبية", @@ -465,6 +467,7 @@ "confirm": "تأكيد", "confirm_admin_password": "تأكيد كلمة مرور المسؤول", "confirm_delete_shared_link": "هل أنت متأكد أنك تريد حذف هذا الرابط المشترك؟", + "confirm_keep_this_delete_others": "سيتم حذف جميع الأصول الأخرى في المجموعة باستثناء هذا الأصل. هل أنت متأكد من أنك تريد المتابعة؟", "confirm_password": "تأكيد كلمة المرور", "contain": "محتواة", "context": "السياق", @@ -514,6 +517,7 @@ "delete_key": "حذف المفتاح", "delete_library": "حذف المكتبة", "delete_link": "حذف الرابط", + "delete_others": "حذف الأخرى", "delete_shared_link": "حذف الرابط المشترك", "delete_tag": "حذف العلامة", "delete_tag_confirmation_prompt": "هل أنت متأكد أنك تريد حذف العلامة {tagName}؟", @@ -604,6 +608,7 @@ "failed_to_create_shared_link": "فشل إنشاء رابط مشترك", "failed_to_edit_shared_link": "فشل تعديل الرابط المشترك", "failed_to_get_people": "فشل في الحصول على الناس", + "failed_to_keep_this_delete_others": "فشل في الاحتفاظ بهذا الأصل وحذف الأصول الأخرى", "failed_to_load_asset": "فشل تحميل المحتوى", "failed_to_load_assets": "فشل تحميل المحتويات", "failed_to_load_people": "فشل تحميل الأشخاص", @@ -787,6 +792,8 @@ "jobs": "الوظائف", "keep": "احتفظ", "keep_all": "احتفظ بالكل", + "keep_this_delete_others": "احتفظ بهذا، واحذف الآخرين", + "kept_this_deleted_others": "تم الاحتفاظ بهذا الأصل وحذف {count, plural, one {# asset} other {# assets}}", "keyboard_shortcuts": "اختصارات لوحة المفاتيح", "language": "اللغة", "language_setting_description": "اختر لغتك المفضلة", @@ -1218,6 +1225,7 @@ "they_will_be_merged_together": "سيتم دمجهم معًا", "third_party_resources": "موارد الطرف الثالث", "time_based_memories": "ذكريات استنادًا للوقت", + "timeline": "الخط الزمني", "timezone": "المنطقة الزمنية", "to_archive": "أرشفة", "to_change_password": "تغيير كلمة المرور", @@ -1227,6 +1235,7 @@ "to_trash": "حذف", "toggle_settings": "الإعدادات", "toggle_theme": "تبديل المظهر الداكن", + "total": "الإجمالي", "total_usage": "الاستخدام الإجمالي", "trash": "المهملات", "trash_all": "نقل الكل إلى سلة المهملات", @@ -1276,6 +1285,8 @@ "user_purchase_settings_description": "إدارة عملية الشراء الخاصة بك", "user_role_set": "قم بتعيين {user} كـ {role}", "user_usage_detail": "تفاصيل استخدام المستخدم", + "user_usage_stats": "إحصائيات استخدام الحساب", + "user_usage_stats_description": "عرض إحصائيات استخدام الحساب", "username": "اسم المستخدم", "users": "المستخدمين", "utilities": "أدوات", @@ -1283,7 +1294,7 @@ "variables": "المتغيرات", "version": "الإصدار", "version_announcement_closing": "صديقك، أليكس", - "version_announcement_message": "مرحباً يا صديقي، هنالك نسخة جديدة من التطبيق. خذ وقتك لزيارة ملاحظات الإصدار والتأكد من أن ملف docker-compose.yml وإعداد .env مُحدّثين لتجنب أي إعدادات خاطئة، خاصةً إذا كنت تستخدم WatchTower أو أي آلية تقوم بتحديث التطبيق تلقائياً.", + "version_announcement_message": "مرحبًا! يتوفر إصدار جديد من Immich. يُرجى تخصيص بعض الوقت لقراءة ملاحظات الإصدار للتأكد من تحديث إعداداتك لمنع أي أخطاء في التكوين، خاصة إذا كنت تستخدم WatchTower أو أي آلية تتولى تحديث مثيل Immich الخاص بك تلقائيًا.", "version_history": "تاريخ الإصدار", "version_history_item": "تم تثبيت {version} في {date}", "video": "فيديو", @@ -1297,6 +1308,7 @@ "view_all_users": "عرض كافة المستخدمين", "view_in_timeline": "عرض في الجدول الزمني", "view_links": "عرض الروابط", + "view_name": "عرض", "view_next_asset": "عرض المحتوى التالي", "view_previous_asset": "عرض المحتوى السابق", "view_stack": "عرض التكديس", diff --git a/i18n/az.json b/i18n/az.json index 5a5e8ac0c96d1..7848462414273 100644 --- a/i18n/az.json +++ b/i18n/az.json @@ -1,8 +1,10 @@ { - "about": "Haqqında", + "about": "Yenilə", "account": "Hesab", "account_settings": "Hesab parametrləri", "acknowledge": "Təsdiq et", + "action": "Əməliyyat", + "actions": "Əməliyyatlar", "active": "Aktiv", "activity": "Fəaliyyət", "add": "Əlavə et", @@ -10,9 +12,12 @@ "add_a_location": "Məkan əlavə et", "add_a_name": "Ad əlavə et", "add_a_title": "Başlıq əlavə et", + "add_exclusion_pattern": "İstisna nümunəsi əlavə et", + "add_import_path": "Import yolunu əlavə et", "add_location": "Məkanı əlavə et", "add_more_users": "Daha çox istifadəçi əlavə et", "add_partner": "Partnyor əlavə et", + "add_path": "Yol əlavə et", "add_photos": "Şəkilləri əlavə et", "add_to": "... əlavə et", "add_to_album": "Albom əlavə et", @@ -26,7 +31,11 @@ "authentication_settings_disable_all": "Bütün giriş etmə metodlarını söndürmək istədiyinizdən əminsinizmi? Giriş etmə funksiyası tamamilə söndürüləcəkdir.", "authentication_settings_reenable": "Yenidən aktiv etmək üçün Server Əmri -ni istifadə edin.", "background_task_job": "Arxa plan tapşırıqları", + "backup_database_enable_description": "Verilənlər bazasının ehtiyat nüsxələrini aktiv et", + "backup_settings": "Ehtiyat Nüsxə Parametrləri", + "backup_settings_description": "Verilənlər bazasının ehtiyat nüsxə parametrlərini idarə et", "check_all": "Hamısını yoxla", + "config_set_by_file": "Konfiqurasiya hal-hazırda konfiqurasiya faylı ilə təyin olunub", "confirm_delete_library": "{library} kitabxanasını silmək istədiyinizdən əminmisiniz?", "confirm_email_below": "Təsdiqləmək üçün aşağıya {email} yazın", "confirm_user_password_reset": "{user} adlı istifadəçinin şifrəsini sıfırlamaq istədiyinizdən əminmisiniz?", diff --git a/i18n/be.json b/i18n/be.json index 8747b4ac8cc6a..ff809e1aaf5c5 100644 --- a/i18n/be.json +++ b/i18n/be.json @@ -1,7 +1,7 @@ { - "about": "Пра праграму", + "about": "Аднавіць", "account": "Уліковы запіс", - "account_settings": "Налады акаўнта", + "account_settings": "Налады ўліковага запісу", "acknowledge": "Пацвердзіць", "action": "Дзеянне", "actions": "Дзеянні", @@ -27,6 +27,57 @@ "added_to_favorites": "Дададзена ў абраныя", "added_to_favorites_count": "Дададзена {count, number} да абранага", "admin": { - "add_exclusion_pattern_description": "Дадайце шаблоны выключэнняў. Падтрымліваецца выкарыстанне сімвалаў * , ** і ?. Каб ігнараваць усе файлы ў любой дырэкторыі з назвай \"Raw\", выкарыстоўвайце \"**/Raw/**\". Каб ігнараваць усе файлы, якія заканчваюцца на \".tif\", выкарыстоўвайце \"**/.tif\". Каб ігнараваць абсолютны шлях, выкарыстоўвайце \"/path/to/ignore/**\"." - } + "add_exclusion_pattern_description": "Дадайце шаблоны выключэнняў. Падтрымліваецца выкарыстанне сімвалаў * , ** і ?. Каб ігнараваць усе файлы ў любой дырэкторыі з назвай \"Raw\", выкарыстоўвайце \"**/Raw/**\". Каб ігнараваць усе файлы, якія заканчваюцца на \".tif\", выкарыстоўвайце \"**/.tif\". Каб ігнараваць абсолютны шлях, выкарыстоўвайце \"/path/to/ignore/**\".", + "authentication_settings": "Налады праверкі сапраўднасці", + "authentication_settings_description": "Кіраванне паролямі, OAuth, і іншыя налады праверкі сапраўднасці", + "authentication_settings_disable_all": "Вы ўпэўнены, што жадаеце адключыць усе спосабы логіну? Логін будзе цалкам адключаны.", + "authentication_settings_reenable": "Каб зноў уключыць, выкарыстайце Каманду сервера.", + "background_task_job": "Фонавыя заданні", + "backup_database": "Рэзервовая копія базы даных", + "backup_database_enable_description": "Уключыць рэзерваванне базы даных", + "backup_settings": "Налады рэзервовага капіявання", + "check_all": "Праверыць усе", + "confirm_delete_library": "Вы ўпэўнены што жадаеце выдаліць {library} бібліятэку?", + "confirm_email_below": "Каб пацвердзіць, увядзіце \"{email}\" ніжэй", + "confirm_user_password_reset": "Вы ўпэўнены ў тым, што жадаеце скінуць пароль {user}?", + "disable_login": "Адключыць уваход", + "force_delete_user_warning": "ПАПЯРЭДЖАННЕ: Гэта дзеянне неадкладна выдаліць карыстальніка і ўсе аб'екты. Гэта дзеянне не можа быць адроблена і файлы немагчыма будзе аднавіць.", + "image_format": "Фармат", + "image_preview_title": "Налады папярэдняга прагляду", + "image_quality": "Якасць", + "image_resolution": "Раздзяляльнасць", + "image_settings": "Налады відарыса", + "image_settings_description": "Кіруйце якасцю і раздзяляльнасцю сгенерыраваных відарысаў" + }, + "timeline": "Хроніка", + "total": "Усяго", + "user": "Карыстальнік", + "user_id": "ID карыстальніка", + "user_usage_stats": "Статыстыка карыстання ўліковага запісу", + "user_usage_stats_description": "Прагледзець статыстыку карыстання ўліковага запісу", + "username": "Імя карыстальніка", + "users": "Карыстальнікі", + "utilities": "Утыліты", + "validate": "Праверыць", + "variables": "Пераменныя", + "version": "Версія", + "video": "Відэа", + "videos": "Відэа", + "view": "Прагляд", + "view_album": "Праглядзець альбом", + "view_all": "Праглядзець усё", + "view_all_users": "Праглядзець усех карыстальнікаў", + "view_in_timeline": "Паглядзець на хроніцы", + "view_links": "Праглядзець спасылкі", + "view_name": "Прагледзець", + "waiting": "Чакаюць", + "warning": "Папярэджанне", + "week": "Тыдзень", + "welcome": "Вітаем", + "welcome_to_immich": "Вітаем у Immich", + "year": "Год", + "years_ago": "{years, plural, one {# год} other {# гадоў}} таму", + "yes": "Так", + "you_dont_have_any_shared_links": "У вас няма абагуленых спасылак", + "zoom_image": "Павелічэнне відарыса" } diff --git a/i18n/bg.json b/i18n/bg.json index d24349c39a4e0..83cfab9c67275 100644 --- a/i18n/bg.json +++ b/i18n/bg.json @@ -1,5 +1,5 @@ { - "about": "За Immich", + "about": "Обновяване", "account": "Акаунт", "account_settings": "Настройки на профила", "acknowledge": "Потвърждавам", @@ -1059,6 +1059,8 @@ "user_purchase_settings_description": "Управлявай покупката си", "user_role_set": "Задай {user} като {role}", "user_usage_detail": "Подробности за използването на потребителя", + "user_usage_stats": "Статистика за използването на акаунта", + "user_usage_stats_description": "Преглед на статистиката за използването на акаунта", "username": "Потребителско име", "users": "Потребители", "utilities": "Инструменти", diff --git a/i18n/ca.json b/i18n/ca.json index 577e1eeed301e..c369f722c1624 100644 --- a/i18n/ca.json +++ b/i18n/ca.json @@ -465,6 +465,7 @@ "confirm": "Confirmar", "confirm_admin_password": "Confirmeu la contrasenya d'administrador", "confirm_delete_shared_link": "Esteu segurs que voleu eliminar aquest enllaç compartit?", + "confirm_keep_this_delete_others": "Excepte aquest element, tots els altres de la pila se suprimiran. Esteu segur que voleu continuar?", "confirm_password": "Confirmació de contrasenya", "contain": "Contingut", "context": "Context", @@ -514,6 +515,7 @@ "delete_key": "Suprimeix la clau", "delete_library": "Suprimeix la Llibreria", "delete_link": "Esborra l'enllaç", + "delete_others": "Suprimeix altres", "delete_shared_link": "Odstranit sdílený odkaz", "delete_tag": "Eliminar etiqueta", "delete_tag_confirmation_prompt": "Estàs segur que vols eliminar l'etiqueta {tagName}?", @@ -604,6 +606,7 @@ "failed_to_create_shared_link": "No s'ha pogut crear l'enllaç compartit", "failed_to_edit_shared_link": "No s'ha pogut editar l'enllaç compartit", "failed_to_get_people": "No s'han pogut aconseguir persones", + "failed_to_keep_this_delete_others": "No s'ha pogut conservar aquest element i suprimir els altres", "failed_to_load_asset": "No s'ha pogut carregar l'element", "failed_to_load_assets": "No s'han pogut carregar els elements", "failed_to_load_people": "No s'han pogut carregar les persones", @@ -787,6 +790,8 @@ "jobs": "Tasques", "keep": "Mantenir", "keep_all": "Mantenir-ho tot", + "keep_this_delete_others": "Conserveu-ho, suprimiu-ne els altres", + "kept_this_deleted_others": "S'ha conservat aquest element i s'han suprimit {count, plural, one {# asset} other {# assets}}", "keyboard_shortcuts": "Dreceres de teclat", "language": "Idioma", "language_setting_description": "Seleccioneu el vostre idioma", diff --git a/i18n/cs.json b/i18n/cs.json index fa49cf64545f7..e6997e228746b 100644 --- a/i18n/cs.json +++ b/i18n/cs.json @@ -23,6 +23,7 @@ "add_to": "Přidat do...", "add_to_album": "Přidat do alba", "add_to_shared_album": "Přidat do sdíleného alba", + "add_url": "Přidat URL", "added_to_archive": "Přidáno do archivu", "added_to_favorites": "Přidáno do oblíbených", "added_to_favorites_count": "Přidáno {count, number} do oblíbených", @@ -130,7 +131,7 @@ "machine_learning_smart_search_description": "Sémantické vyhledávání obrázků pomocí CLIP embeddings", "machine_learning_smart_search_enabled": "Povolit chytré vyhledávání", "machine_learning_smart_search_enabled_description": "Pokud je vypnuto, obrázky nebudou kódovány pro inteligentní vyhledávání.", - "machine_learning_url_description": "URL serveru pro strojové učení", + "machine_learning_url_description": "URL serveru strojového učení. Pokud je zadáno více URL adres, budou jednotlivé servery zkoušeny postupně, dokud jeden z nich neodpoví úspěšně, a to v pořadí od prvního k poslednímu.", "manage_concurrency": "Správa souběžnosti", "manage_log_settings": "Správa nastavení protokolu", "map_dark_style": "Tmavý motiv", @@ -222,6 +223,8 @@ "send_welcome_email": "Odeslat uvítací e-mail", "server_external_domain_settings": "Externí doména", "server_external_domain_settings_description": "Doména pro veřejně sdílené odkazy, včetně http(s)://", + "server_public_users": "Veřejní uživatelé", + "server_public_users_description": "Všichni uživatelé (jméno a e-mail) jsou uvedeni při přidávání uživatele do sdílených alb. Pokud je tato funkce vypnuta, bude seznam uživatelů dostupný pouze uživatelům z řad správců.", "server_settings": "Server", "server_settings_description": "Správa nastavení serveru", "server_welcome_message": "Uvítací zpráva", @@ -247,6 +250,16 @@ "storage_template_user_label": "{label} je štítek úložiště uživatele", "system_settings": "Systémová nastavení", "tag_cleanup_job": "Promazání značek", + "template_email_available_tags": "V šabloně můžete použít následující proměnné: {tags}", + "template_email_if_empty": "Pokud je šablona prázdná, použije se výchozí e-mail.", + "template_email_invite_album": "Šablona pozvánky do alba", + "template_email_preview": "Náhled", + "template_email_settings": "Šablony e-mailů", + "template_email_settings_description": "Správa vlastních šablon e-mailových oznámení", + "template_email_update_album": "Aktualizace šablony alba", + "template_email_welcome": "Šablona uvítacího e-mailu", + "template_settings": "Šablony oznámení", + "template_settings_description": "Správa vlastních šablon oznámení.", "theme_custom_css_settings": "Vlastní CSS", "theme_custom_css_settings_description": "Kaskádové styly umožňují přizpůsobit design aplikace Immich.", "theme_settings": "Motivy", @@ -721,6 +734,7 @@ "external": "Externí", "external_libraries": "Externí knihovny", "face_unassigned": "Nepřiřazena", + "failed_to_load_assets": "Nepodařilo se načíst položky", "favorite": "Oblíbit", "favorite_or_unfavorite_photo": "Oblíbit nebo zrušit oblíbení fotky", "favorites": "Oblíbené", @@ -1020,6 +1034,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {Přeřazena # položka} few {Přeřazeny # položky} other {Přeřazeno # položek}} na novou osobu", "reassing_hint": "Přiřazení vybraných položek existující osobě", "recent": "Nedávné", + "recent-albums": "Nedávná alba", "recent_searches": "Nedávná vyhledávání", "refresh": "Obnovit", "refresh_encoded_videos": "Obnovit kódovaná videa", @@ -1041,6 +1056,7 @@ "remove_from_album": "Odstranit z alba", "remove_from_favorites": "Odstranit z oblíbených", "remove_from_shared_link": "Odstranit ze sdíleného odkazu", + "remove_url": "Odstranit URL", "remove_user": "Odebrat uživatele", "removed_api_key": "Odstraněn API klíč: {name}", "removed_from_archive": "Odstraněno z archivu", @@ -1223,6 +1239,7 @@ "they_will_be_merged_together": "Budou sloučeny dohromady", "third_party_resources": "Zdroje třetích stran", "time_based_memories": "Časové vzpomínky", + "timeline": "Časová osa", "timezone": "Časové pásmo", "to_archive": "Archivovat", "to_change_password": "Změnit heslo", @@ -1232,6 +1249,7 @@ "to_trash": "Vyhodit", "toggle_settings": "Přepnout nastavení", "toggle_theme": "Přepnout tmavý motiv", + "total": "Celkem", "total_usage": "Celkové využití", "trash": "Koš", "trash_all": "Vyhodit vše", @@ -1281,6 +1299,8 @@ "user_purchase_settings_description": "Správa vašeho nákupu", "user_role_set": "Uživatel {user} nastaven jako {role}", "user_usage_detail": "Podrobnosti využití uživatelů", + "user_usage_stats": "Statistiky používání účtu", + "user_usage_stats_description": "Zobrazit statistiky používání účtu", "username": "Uživateleské jméno", "users": "Uživatelé", "utilities": "Nástroje", @@ -1302,6 +1322,7 @@ "view_all_users": "Zobrazit všechny uživatele", "view_in_timeline": "Zobrazit na časové ose", "view_links": "Zobrazit odkazy", + "view_name": "Zobrazit", "view_next_asset": "Zobrazit další položku", "view_previous_asset": "Zobrazit předchozí položku", "view_stack": "Zobrazit seskupení", diff --git a/i18n/cv.json b/i18n/cv.json index 8f0581053e2ca..61dcb12b8d54c 100644 --- a/i18n/cv.json +++ b/i18n/cv.json @@ -23,6 +23,7 @@ "add_to": "Мӗн те пулин хуш...", "add_to_album": "Альбома хуш", "add_to_shared_album": "Пӗрлехи альбома хуш", + "add_url": "URL хушӑр", "added_to_archive": "Архива хушнӑ", "added_to_favorites": "Суйласа илнине хушнӑ", "added_to_favorites_count": "Суйласа илнине {count, number} хушнӑ", @@ -45,5 +46,7 @@ "image_preview_title": "Малтанлӑха пӑхмалли ӗнерлевсем", "image_quality": "Пахалӑх", "image_resolution": "Виҫе" - } + }, + "user_usage_stats": "Шута ҫырни усӑ курмалли статистика", + "user_usage_stats_description": "Шута ҫырни усӑ курмалли статистикӑна пӑхасси" } diff --git a/i18n/de.json b/i18n/de.json index 849aa0337c380..e1eaf573048c3 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -222,6 +222,8 @@ "send_welcome_email": "Begrüssungsmail senden", "server_external_domain_settings": "Externe Domain", "server_external_domain_settings_description": "Domäne für öffentlich freigegebene Links, einschließlich http(s)://", + "server_public_users": "Öffentliche Benutzer", + "server_public_users_description": "Beim hinzufügen eines benutzers zu freigegebenen alben werden alle benutzer (name und e-mail) aufgelistet. Wenn diese option deaktiviert ist, steht die benutzerliste nur administratorbenutzern zur verfügung.", "server_settings": "Servereinstellungen", "server_settings_description": "Servereinstellungen verwalten", "server_welcome_message": "Willkommensnachricht", @@ -406,7 +408,7 @@ "assets_added_to_name_count": "{count, plural, one {# Element} other {# Elemente}} zu {hasName, select, true {{name}} other {neuem Album}} hinzugefügt", "assets_count": "{count, plural, one {# Datei} other {# Dateien}}", "assets_moved_to_trash_count": "{count, plural, one {# Datei} other {# Dateien}} in den Papierkorb verschoben", - "assets_permanently_deleted_count": "{count, plural, one {# Datei} other {# Dateien}} dauerhaft gelöscht", + "assets_permanently_deleted_count": "{count, plural, one {# Datei} other {# Dateien}} endgültig gelöscht", "assets_removed_count": "{count, plural, one {# Datei} other {# Dateien}} entfernt", "assets_restore_confirmation": "Bist du sicher, dass du alle Dateien aus dem Papierkorb wiederherstellen willst? Diese Aktion kann nicht rückgängig gemacht werden! Beachte, dass Offline-Dateien auf diese Weise nicht wiederhergestellt werden können.", "assets_restored_count": "{count, plural, one {# Datei} other {# Dateien}} wiederhergestellt", @@ -422,7 +424,7 @@ "bugs_and_feature_requests": "Fehler & Verbesserungsvorschläge", "build": "Build", "build_image": "Build Abbild", - "bulk_delete_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien gemeinsam}} löschen möchtest? Dabei wird die größte Datei jeder Gruppe behalten und alle anderen Duplikate dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden!", + "bulk_delete_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien gemeinsam}} löschen möchtest? Dabei wird die größte Datei jeder Gruppe behalten und alle anderen Duplikate endgültig gelöscht. Diese Aktion kann nicht rückgängig gemacht werden!", "bulk_keep_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien}} behalten möchtest? Dies wird alle Duplikat-Gruppen auflösen ohne etwas zu löschen.", "bulk_trash_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien gemeinsam}} in den Papierkorb verschieben möchtest? Dies wird die größte Datei jeder Gruppe behalten und alle anderen Duplikate in den Papierkorb verschieben.", "buy": "Immich erwerben", @@ -465,6 +467,7 @@ "confirm": "Bestätigen", "confirm_admin_password": "Administrator Passwort bestätigen", "confirm_delete_shared_link": "Bist du sicher, dass du diesen geteilten Link löschen willst?", + "confirm_keep_this_delete_others": "Alle anderen Dateien im Stapel bis auf diese werden gelöscht. Bist du sicher, dass du fortfahren möchten?", "confirm_password": "Passwort bestätigen", "contain": "Vollständig", "context": "Kontext", @@ -510,10 +513,11 @@ "delete": "Löschen", "delete_album": "Album löschen", "delete_api_key_prompt": "Bist du sicher, dass du diesen API-Schlüssel löschen willst?", - "delete_duplicates_confirmation": "Bist du sicher, dass du diese Duplikate dauerhaft löschen willst?", + "delete_duplicates_confirmation": "Bist du sicher, dass du diese Duplikate endgültig löschen willst?", "delete_key": "Schlüssel löschen", "delete_library": "Bibliothek löschen", "delete_link": "Link löschen", + "delete_others": "Andere löschen", "delete_shared_link": "geteilten Link löschen", "delete_tag": "Tag löschen", "delete_tag_confirmation_prompt": "Bist du sicher, dass der Tag {tagName} gelöscht werden soll?", @@ -572,7 +576,7 @@ "editor_crop_tool_h2_rotation": "Drehung", "email": "E-Mail", "empty_trash": "Papierkorb leeren", - "empty_trash_confirmation": "Bist du sicher, dass du den Papierkorb leeren willst?\nDies entfernt alle Dateien im Papierkorb permanent aus Immich und kann nicht rückgängig gemacht werden!", + "empty_trash_confirmation": "Bist du sicher, dass du den Papierkorb leeren willst?\nDies entfernt alle Dateien im Papierkorb endgültig aus Immich und kann nicht rückgängig gemacht werden!", "enable": "Aktivieren", "enabled": "Aktiviert", "end_date": "Enddatum", @@ -604,6 +608,7 @@ "failed_to_create_shared_link": "Geteilter Link konnte nicht erstellt werden", "failed_to_edit_shared_link": "Geteilter Link konnte nicht bearbeitet werden", "failed_to_get_people": "Personen konnten nicht abgerufen werden", + "failed_to_keep_this_delete_others": "Fehler beim Löschen der anderen Dateien", "failed_to_load_asset": "Fehler beim Laden der Datei", "failed_to_load_assets": "Fehler beim Laden der Dateien", "failed_to_load_people": "Fehler beim Laden von Personen", @@ -787,6 +792,8 @@ "jobs": "Aufgaben", "keep": "Behalten", "keep_all": "Alle behalten", + "keep_this_delete_others": "Dieses behalten, andere löschen", + "kept_this_deleted_others": "Diese Datei behalten und {count, plural, one {# Datei} other {# Dateien}} gelöscht", "keyboard_shortcuts": "Tastenkürzel", "language": "Sprache", "language_setting_description": "Wähle deine bevorzugte Sprache", @@ -940,12 +947,12 @@ "people_feature_description": "Fotos und Videos nach Personen gruppiert durchsuchen", "people_sidebar_description": "Eine Verknüpfung zu Personen in der Seitenleiste anzeigen", "permanent_deletion_warning": "Warnung vor endgültiger Löschung", - "permanent_deletion_warning_setting_description": "Anzeige einer Warnung beim permanenten Löschen von Objekten", - "permanently_delete": "Dauerhaft löschen", - "permanently_delete_assets_count": "{count, plural, one {Datei} other {Dateien}} dauerhaft gelöscht", - "permanently_delete_assets_prompt": "Bist du sicher, dass {count, plural, one {diese Datei} other {diese # Dateien}} dauerhaft gelöscht werden soll? Dadurch {count, plural, one {wird} other {werden}} diese auch aus deinen Alben entfernt.", - "permanently_deleted_asset": "Dauerhaft gelöschtes Objekt", - "permanently_deleted_assets_count": "{count, plural, one {# Datei} other {# Dateien}} dauerhaft gelöscht", + "permanent_deletion_warning_setting_description": "Anzeige einer Warnung beim endgültigen Löschen von Objekten", + "permanently_delete": "Endgültig löschen", + "permanently_delete_assets_count": "{count, plural, one {Datei} other {Dateien}} endgültig löschen", + "permanently_delete_assets_prompt": "Bist du sicher, dass {count, plural, one {diese Datei} other {diese # Dateien}} endgültig gelöscht werden soll? Dadurch {count, plural, one {wird} other {werden}} diese auch aus deinen Alben entfernt.", + "permanently_deleted_asset": "Endgültig gelöschtes Objekt", + "permanently_deleted_assets_count": "{count, plural, one {# Datei} other {# Dateien}} endgültig gelöscht", "person": "Person", "person_hidden": "{name}{hidden, select, true { (verborgen)} other {}}", "photo_shared_all_users": "Es sieht so aus, als hättest du deine Fotos mit allen Benutzern geteilt oder du hast keine Benutzer, mit denen du teilen kannst.", @@ -1218,6 +1225,7 @@ "they_will_be_merged_together": "Sie werden zusammengeführt", "third_party_resources": "Drittanbieter-Quellen", "time_based_memories": "Zeitbasierte Erinnerungen", + "timeline": "Zeitleiste", "timezone": "Zeitzone", "to_archive": "Archivieren", "to_change_password": "Passwort ändern", @@ -1227,6 +1235,7 @@ "to_trash": "In den Papierkorb verschieben", "toggle_settings": "Einstellungen umschalten", "toggle_theme": "Dunkles Theme umschalten", + "total": "Gesamt", "total_usage": "Gesamtnutzung", "trash": "Papierkorb", "trash_all": "Alle löschen", @@ -1276,6 +1285,8 @@ "user_purchase_settings_description": "Kauf verwalten", "user_role_set": "{user} als {role} festlegen", "user_usage_detail": "Nutzungsdetails der Nutzer", + "user_usage_stats": "Statistiken über die Kontonutzung", + "user_usage_stats_description": "Statistiken über die Kontonutzung anzeigen", "username": "Nutzername", "users": "Benutzer", "utilities": "Hilfsmittel", @@ -1297,6 +1308,7 @@ "view_all_users": "Alle Nutzer anzeigen", "view_in_timeline": "In Zeitleiste anzeigen", "view_links": "Links anzeigen", + "view_name": "Ansicht", "view_next_asset": "Nächste Datei anzeigen", "view_previous_asset": "Vorherige Datei anzeigen", "view_stack": "Stapel anzeigen", diff --git a/i18n/el.json b/i18n/el.json index 7357c26b5ea12..cd97a6d953315 100644 --- a/i18n/el.json +++ b/i18n/el.json @@ -5,7 +5,7 @@ "acknowledge": "Έλαβα γνώση", "action": "Ενέργεια", "actions": "Ενέργειες", - "active": "Ενεργές", + "active": "Ενεργά", "activity": "Δραστηριότητα", "activity_changed": "Η δραστηριότητα είναι {enabled, select, true {ενεργοποιημένη} other {απενεργοποιημένη}}", "add": "Προσθήκη", @@ -23,6 +23,7 @@ "add_to": "Προσθήκη σε...", "add_to_album": "Προσθήκη σε άλμπουμ", "add_to_shared_album": "Προσθήκη σε κοινόχρηστο άλμπουμ", + "add_url": "Προσθήκη Συνδέσμου", "added_to_archive": "Προστέθηκε στο αρχείο", "added_to_favorites": "Προστέθηκε στα αγαπημένα", "added_to_favorites_count": "Προστέθηκαν {count, number} στα αγαπημένα", @@ -50,7 +51,7 @@ "create_job": "Δημιουργία εργασίας", "cron_expression": "Σύνταξη Cron", "cron_expression_description": "Ορίστε το διάστημα σάρωσης χρησιμοποιώντας τη μορφή cron. Για περισσότερες πληροφορίες, ανατρέξτε π.χ. στο Crontab Guru", - "cron_expression_presets": "Προεπιλεγμένες εκφράσεις Cron", + "cron_expression_presets": "Προκαθορισμένες εκφράσεις cron", "disable_login": "Απενεργοποίηση σύνδεσης", "duplicate_detection_job_description": "Εκτελέστε μηχανική μάθηση σε στοιχεία για να εντοπίσετε παρόμοιες εικόνες. Βασίζεται στην Έξυπνη Αναζήτηση", "exclusion_pattern_description": "Τα μοτίβα αποκλεισμού σας επιτρέπουν να αγνοείται αρχεία και φακέλους κατά τη σάρωση της βιβλιοθήκης σας. Αυτό είναι χρήσιμο εάν εχετε φακέλους που περιέχουν αρχεία που δεν θέλετε να εισάγετε, όπως αρχεία RAW.", @@ -67,7 +68,7 @@ "image_prefer_embedded_preview": "Προτίμηση ενσωματωμένης προεπισκόπησης", "image_prefer_embedded_preview_setting_description": "Χρήση ενσωματωμένων προεπισκοπίσεων σε RAW εικόνες ως είσοδο για την επεξεργασία εικόνας εφόσον είναι διαθέσιμες. Αυτό μπορεί να δημιουργήσει πιο ακριβή χρώματα για κάποιες εικόνες, αλλά η ποιότητα των προεπισκοπίσεων εξαρτάται από την κάμερα και ενδέχεται να υπάρχουν περισσότερες αλλοιώσεις στην εικόνα λόγω συμπίεσης.", "image_prefer_wide_gamut": "Προτίμηση ευρέος φάσματος", - "image_prefer_wide_gamut_setting_description": "Χρησιμοποιήστε Display P3 για τις μικρογραφίες. Αυτό διατηρεί καλύτερα την ζωντάνια των χρωμάτων σε εικόνες μεγάλου χρωματικού εύρους, αλλά ενδέχεται να εμφανίζονται αλλιώς σε παλαιότερες συσκευές με παλαιότερες εκδόσεις περιηγητών. Οι εικόνες sRGB μένουν ως έχουν για να αποφευχθούν χρωματικές αλλαγές.", + "image_prefer_wide_gamut_setting_description": "Χρήση Display P3 για τις μικρογραφίες. Αυτό διατηρεί καλύτερα την ζωντάνια των χρωμάτων σε εικόνες μεγάλου χρωματικού εύρους, αλλά ενδέχεται να εμφανίζονται αλλιώς σε παλαιότερες συσκευές με παλαιότερες εκδόσεις περιηγητών. Οι εικόνες sRGB μένουν ως έχουν για να αποφευχθούν χρωματικές αλλαγές.", "image_preview_description": "Μεσαίου μεγέθους εικόνες, χωρίς μεταδεδομένα, οι οποίες χρησιμοποιούνται στην προβολή ενός αντικειμένου και για μηχανική μάθηση", "image_preview_quality_description": "Ποιότητα προεπισκόπησης από 1 έως 100. Όσο μεγαλύτερη τιμή τόσο καλύτερη η ποιότητα, αλλά παράγονται μεγαλύτερα αρχεία που ενδέχεται να μειώσουν την ταχύτητα απόκρισης της εφαρμογής. Οι χαμηλές τιμές μπορεί να επηρεάσουν τη ποιότητα της μηχανικής μάθησης.", "image_preview_title": "Ρυθμίσεις Προεπισκόπισης", @@ -82,9 +83,9 @@ "job_concurrency": "Ταυτόχρονη εκτέλεση {job}", "job_created": "Εργασία δημιουργήθηκε", "job_not_concurrency_safe": "Αυτή η εργασία δεν είναι ασφαλής για ταυτόχρονη εκτέλεση.", - "job_settings": "Ρυθμίσεις Εργασιών", - "job_settings_description": "Διαχείριση ταυτόχρονων εργασιών", - "job_status": "Κατάσταση Εργασιών", + "job_settings": "Ρυθμίσεις Εργασίας", + "job_settings_description": "Διαχείριση ταυτόχρονης εκτέλεσης εργασίας", + "job_status": "Κατάσταση Εργασίας", "jobs_delayed": "{jobCount, plural, one {# καθυστέρησε} other {# καθυστέρησαν}}", "jobs_failed": "{jobCount, plural, one {# απέτυχε} other {# απέτυχαν}}", "library_created": "Δημιουργήθηκε η βιβλιοθήκη: {library}", @@ -95,23 +96,23 @@ "library_scanning_enable_description": "Ενεργοποίηση περιοδικής σάρωσης βιβλιοθήκης", "library_settings": "Εξωτερική Βιβλιοθήκη", "library_settings_description": "Διαχείριση ρυθμίσεων εξωτερικής βιβλιοθήκης", - "library_tasks_description": "Εκτέλεση εργασιών βιβλιοθήκης", + "library_tasks_description": "Εκτελούν εργασίες της βιβλιοθήκης", "library_watching_enable_description": "Παρακολούθηση εξωτερικών βιβλιοθηκών για τροποποιήσεις αρχείων", "library_watching_settings": "Παρακολούθηση βιβλιοθήκης (ΠΕΙΡΑΜΑΤΙΚΟ)", "library_watching_settings_description": "Αυτόματη παρακολούθηση για τροποποιημένα αρχεία", - "logging_enable_description": "Ενεργοποίηση καταγραφής", - "logging_level_description": "Το επίπεδο καταγραφής που θα εφαρμοστεί, όταν αυτή είναι ενεργοποιημένη.", - "logging_settings": "Καταγραφή", + "logging_enable_description": "Ενεργοποίηση καταγραφής συμβάντων", + "logging_level_description": "Το επίπεδο καταγραφής συμβάντων που θα εφαρμοστεί, όταν αυτή είναι ενεργοποιημένη.", + "logging_settings": "Καταγραφή Συμβάντων", "machine_learning_clip_model": "Μοντέλο CLIP", - "machine_learning_clip_model_description": "Το όνομα ενός μοντέλου CLIP που καταγράφεται εδώ. Σημειώστε ότι πρέπει να εκτελέσετε ξανά τη εργασία 'Έξυπνη Αναζήτηση' για όλες τις εικόνες μετά την αλλαγή του μοντέλου.", + "machine_learning_clip_model_description": "Το όνομα ενός μοντέλου CLIP που αναφέρεται εδώ. Σημειώστε ότι πρέπει να επανεκτελέσετε την εργασία 'Έξυπνη Αναζήτηση' για όλες τις εικόνες μετά την αλλαγή μοντέλου.", "machine_learning_duplicate_detection": "Εντοπισμός Διπλότυπων", "machine_learning_duplicate_detection_enabled": "Ενεργοποίηση εντοπισμού διπλότυπων", "machine_learning_duplicate_detection_enabled_description": "Εάν απενεργοποιηθεί, απολύτως παρόμοια στοιχεία θα συνεχίσουν να εκκαθαρίζονται από διπλότυπα.", - "machine_learning_duplicate_detection_setting_description": "Χρησιμοποιήστε τα ενσωματωμένα χαρακτηριστικά του CLIP για να βρείτε πιθανά διπλότυπα", - "machine_learning_enabled": "Ενεργοποίηση μηχανικής εκμάθησης", - "machine_learning_enabled_description": "Εάν απενεργοποιηθεί, όλες οι λειτουργίες μηχανικής εκμάθησης θα απενεργοποιηθούν, ανεξάρτητα από τις παρακάτω ρυθμίσεις.", - "machine_learning_facial_recognition": "Αναγνώριση προσώπου", - "machine_learning_facial_recognition_description": "Εντοπισμός, αναγνώριση και ομαδοποίηση προσώπων σε εικόνες", + "machine_learning_duplicate_detection_setting_description": "Χρησιμοποιήστε τις ενσωματώσεις CLIP για να βρείτε πιθανά διπλότυπα", + "machine_learning_enabled": "Ενεργοποίηση μηχανικής μάθησης", + "machine_learning_enabled_description": "Εάν απενεργοποιηθεί, όλες οι λειτουργίες μηχανικής μάθησης θα απενεργοποιηθούν, ανεξάρτητα από τις παρακάτω ρυθμίσεις.", + "machine_learning_facial_recognition": "Αναγνώριση Προσώπου", + "machine_learning_facial_recognition_description": "Εντοπισμός, αναγνώριση και ομαδοποίηση προσώπων που υπάρχουν σε εικόνες", "machine_learning_facial_recognition_model": "Μοντέλο αναγνώρισης προσώπου", "machine_learning_facial_recognition_model_description": "Τα μοντέλα παρατίθενται με φθίνουσα σειρά μεγέθους. Τα μεγαλύτερα μοντέλα είναι πιο αργά και χρησιμοποιούν περισσότερη μνήμη, αλλά παράγουν καλύτερα αποτελέσματα. Σημειώστε ότι πρέπει να εκτελέσετε ξανά την εργασία Ανίχνευση προσώπου για όλες τις εικόνες κατά την αλλαγή ενός μοντέλου.", "machine_learning_facial_recognition_setting": "Ενεργοποίηση αναγνώρισης προσώπου", @@ -130,7 +131,7 @@ "machine_learning_smart_search_description": "Αναζητήστε εικόνες σημασιολογικά χρησιμοποιώντας ενσωματώσεις CLIP", "machine_learning_smart_search_enabled": "Ενεργοποίηση έξυπνης αναζήτησης", "machine_learning_smart_search_enabled_description": "Αν απενεργοποιηθεί, οι εικόνες δεν θα κωδικοποιούνται για έξυπνη αναζήτηση.", - "machine_learning_url_description": "URL του διακομιστή μηχανικής εκμάθησης", + "machine_learning_url_description": "Η διεύθυνση URL του διακομιστή μηχανικής εκμάθησης. Αν παρέχονται περισσότερες από μία διευθύνσεις URL, τότε, κάθε διακομιστής θα προσπαθήσει να συνδεθεί διαδοχικά, από την πρώτη μέχρι την τελευταία, έως ότου απαντήσει επιτυχώς.", "manage_concurrency": "Διαχείριση ταυτόχρονη εκτέλεσης", "manage_log_settings": "Διαχείριση ρυθμίσεων αρχείου καταγραφής", "map_dark_style": "Σκούρο Θέμα", @@ -222,6 +223,8 @@ "send_welcome_email": "Αποστολή email καλωσορίσματος", "server_external_domain_settings": "Εξωτερική διεύθυνση τομέα", "server_external_domain_settings_description": "Διεύθυνση τομέα για δημόσιους κοινούς συνδέσμους, περιλαμβανομένου του http(s)://", + "server_public_users": "Δημόσιοι Χρήστες", + "server_public_users_description": "Όλοι οι χρήστες (όνομα και email) εμφανίζονται κατά την προσθήκη ενός χρήστη σε κοινόχρηστα άλμπουμ. Όταν αυτή η επιλογή είναι απενεργοποιημένη, η λίστα χρηστών θα είναι διαθέσιμη μόνο στους διαχειριστές.", "server_settings": "Ρυθμίσεις Διακομιστή", "server_settings_description": "Διαχείριση ρυθμίσεων διακομιστή", "server_welcome_message": "Μήνυμα καλωσορίσματος", @@ -247,6 +250,16 @@ "storage_template_user_label": "{label} είναι η Ετικέτα Αποθήκευσης του χρήστη", "system_settings": "Ρυθμίσεις Συστήματος", "tag_cleanup_job": "Καθαρισμός ετικετών", + "template_email_available_tags": "Μπορείτε να χρησιμοποιήσετε τις εξής μεταβλητές στο πρότυπό σας: {tags}", + "template_email_if_empty": "Αν το πρότυπο είναι κενό, θα χρησιμοποιηθεί το προεπιλεγμένο email.", + "template_email_invite_album": "Πρότυπο άλμπουμ πρόσκλησης", + "template_email_preview": "Προεπισκόπηση", + "template_email_settings": "Πρότυπα Email", + "template_email_settings_description": "Διαχείριση προσαρμοσμένων προτύπων ειδοποιήσεων email", + "template_email_update_album": "Ενημέρωση πρότυπου Άλμπουμ", + "template_email_welcome": "Πρότυπο email καλωσορίσματος", + "template_settings": "Πρότυπα ειδοποιήσεων", + "template_settings_description": "Διαχείριση προσαρμοσμένων προτύπων για ειδοποιήσεις.", "theme_custom_css_settings": "Προσαρμοσμένο CSS", "theme_custom_css_settings_description": "Τα Cascading Style Sheets(CSS) επιτρέπει την προσαρμογή του σχεδιασμού του Immich.", "theme_settings": "Ρυθμίσεις Θέματος", @@ -527,7 +540,7 @@ "direction": "Κατεύθυνση", "disabled": "Απενεργοποιημένο", "disallow_edits": "Απαγόρευση επεξεργασιών", - "discord": "Διαφωνία", + "discord": "Discord", "discover": "Ανίχνευση", "dismiss_all_errors": "Παράβλεψη όλων των σφαλμάτων", "dismiss_error": "Παράβλεψη σφάλματος", @@ -721,6 +734,7 @@ "external": "Εξωτερικός", "external_libraries": "Εξωτερικές βιβλιοθήκες", "face_unassigned": "Μη ανατεθειμένο", + "failed_to_load_assets": "Αποτυχία φόρτωσης στοιχείων", "favorite": "Αγαπημένο", "favorite_or_unfavorite_photo": "Ορίστε μία φωτογραφία ως αγαπημένη ή αφαιρέστε την από τα αγαπημένα", "favorites": "Αγαπημένα", @@ -1020,6 +1034,7 @@ "reassigned_assets_to_new_person": "Η ανάθεση {count, plural, one {# αρχείου} other {# αρχείων}} σε νέο άτομο", "reassing_hint": "Ανάθεση των επιλεγμένων στοιχείων σε υπάρχον άτομο", "recent": "Πρόσφατα", + "recent-albums": "Πρόσφατα άλμπουμ", "recent_searches": "Πρόσφατες αναζητήσεις", "refresh": "Ανανέωση", "refresh_encoded_videos": "Ανανέωση κωδικοποιημένων βίντεο", @@ -1041,6 +1056,7 @@ "remove_from_album": "Αφαίρεση από το άλμπουμ", "remove_from_favorites": "Αφαίρεση από τα αγαπημένα", "remove_from_shared_link": "Αφαίρεση από τον κοινόχρηστο σύνδεσμο", + "remove_url": "Αφαίρεση Συνδέσμου", "remove_user": "Αφαίρεση χρήστη", "removed_api_key": "Αφαιρέθηκε το API Key: {name}", "removed_from_archive": "Αφαιρέθηκε/καν από το Αρχείο", @@ -1223,6 +1239,7 @@ "they_will_be_merged_together": "Θα συγχωνευθούν μαζί", "third_party_resources": "Πόροι τρίτων", "time_based_memories": "Μνήμες βασισμένες στο χρόνο", + "timeline": "Χρονολόγιο", "timezone": "Ζώνη ώρας", "to_archive": "Αρχειοθέτηση", "to_change_password": "Αλλαγή κωδικού πρόσβασης", @@ -1232,6 +1249,7 @@ "to_trash": "Κάδος απορριμμάτων", "toggle_settings": "Εναλλαγή ρυθμίσεων", "toggle_theme": "Εναλλαγή θέματος", + "total": "Σύνολο", "total_usage": "Συνολική χρήση", "trash": "Κάδος απορριμμάτων", "trash_all": "Διαγραφή Όλων", @@ -1281,6 +1299,8 @@ "user_purchase_settings_description": "Διαχείριση Αγοράς", "user_role_set": "Ορισμός {user} ως {role}", "user_usage_detail": "Λεπτομέρειες χρήσης του χρήστη", + "user_usage_stats": "Στατιστικά χρήσης λογαριασμού", + "user_usage_stats_description": "Προβολή στατιστικών χρήσης λογαριασμού", "username": "Όνομα Χρήστη", "users": "Χρήστες", "utilities": "Βοηθητικά προγράμματα", @@ -1302,6 +1322,7 @@ "view_all_users": "Προβολή όλων των χρηστών", "view_in_timeline": "Προβολή στο χρονοδιάγραμμα", "view_links": "Προβολή συνδέσμων", + "view_name": "Προβολή", "view_next_asset": "Προβολή επόμενου στοιχείου", "view_previous_asset": "Προβολή προηγούμενου στοιχείου", "view_stack": "Προβολή της στοίβας", diff --git a/i18n/es.json b/i18n/es.json index 84da8a6647852..c091b816fe221 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -23,6 +23,7 @@ "add_to": "Agregar a...", "add_to_album": "Agregar a un álbum", "add_to_shared_album": "Agregar a un álbum compartido", + "add_url": "Añadir URL", "added_to_archive": "Archivado", "added_to_favorites": "Agregado a favoritos", "added_to_favorites_count": "Agregado {count, number} a favoritos", @@ -130,7 +131,7 @@ "machine_learning_smart_search_description": "Busque imágenes semánticamente utilizando incrustaciones CLIP", "machine_learning_smart_search_enabled": "Habilitar búsqueda inteligente", "machine_learning_smart_search_enabled_description": "Si está deshabilitado, las imágenes no se codificarán para la búsqueda inteligente.", - "machine_learning_url_description": "URL del servidor de aprendizaje automático", + "machine_learning_url_description": "La URL del servidor de aprendizaje automático. Si se proporciona más de una URL, se intentará acceder a cada servidor de uno en uno hasta que uno responda correctamente, en orden del primero al último.", "manage_concurrency": "Ajustes de concurrencia", "manage_log_settings": "Administrar la configuración de los registros", "map_dark_style": "Estilo oscuro", @@ -222,6 +223,8 @@ "send_welcome_email": "Enviar correo de bienvenida", "server_external_domain_settings": "Dominio externo", "server_external_domain_settings_description": "Dominio para enlaces públicos compartidos, incluidos http(s)://", + "server_public_users": "Usuarios públicos", + "server_public_users_description": "Todos los usuarios (nombre y correo electrónico) aparecen en la lista cuando se añade un usuario a los álbumes compartidos. Si se desactiva, la lista de usuarios sólo estará disponible para los usuarios administradores.", "server_settings": "Configuración del servidor", "server_settings_description": "Administrar la configuración del servidor", "server_welcome_message": "Mensaje de bienvenida", @@ -247,6 +250,16 @@ "storage_template_user_label": "{label} es la etiqueta de almacenamiento del usuario", "system_settings": "Ajustes del Sistema", "tag_cleanup_job": "Limpieza de etiquetas", + "template_email_available_tags": "Puede utilizar las siguientes variables en su plantilla: {tags}", + "template_email_if_empty": "Si la plantilla está vacía, se utilizará el correo electrónico predeterminado.", + "template_email_invite_album": "Plantilla de álbum de invitaciones", + "template_email_preview": "Vista previa", + "template_email_settings": "Modelos de correo electrónico", + "template_email_settings_description": "Gestionar plantillas de notificación por correo electrónico personalizadas", + "template_email_update_album": "Actualizar plantilla del álbum", + "template_email_welcome": "Plantilla de correo electrónico de bienvenida", + "template_settings": "Plantillas de notificación", + "template_settings_description": "Gestione plantillas personalizadas para las notificaciones.", "theme_custom_css_settings": "CSS Personalizado", "theme_custom_css_settings_description": "Las Hojas de Estilo (CSS) permiten personalizar el diseño de Immich.", "theme_settings": "Ajustes Tema", @@ -721,6 +734,7 @@ "external": "Externo", "external_libraries": "Bibliotecas Externas", "face_unassigned": "Sin asignar", + "failed_to_load_assets": "Error al cargar los activos", "favorite": "Favorito", "favorite_or_unfavorite_photo": "Foto favorita o no favorita", "favorites": "Favoritos", @@ -1020,6 +1034,7 @@ "reassigned_assets_to_new_person": "Reasignado {count, plural, one {# elemento} other {# elementos}} a un nuevo usuario", "reassing_hint": "Asignar archivos seleccionados a una persona existente", "recent": "Reciente", + "recent-albums": "Últimos álbumes", "recent_searches": "Búsquedas recientes", "refresh": "Actualizar", "refresh_encoded_videos": "Recargar los vídeos codificados", @@ -1041,6 +1056,7 @@ "remove_from_album": "Eliminar del álbum", "remove_from_favorites": "Quitar de favoritos", "remove_from_shared_link": "Eliminar desde enlace compartido", + "remove_url": "Eliminar URL", "remove_user": "Eliminar usuario", "removed_api_key": "Clave API eliminada: {name}", "removed_from_archive": "Eliminado del archivo", @@ -1223,6 +1239,7 @@ "they_will_be_merged_together": "Se fusionarán entre sí", "third_party_resources": "Recursos de terceros", "time_based_memories": "Recuerdos basados en tiempo", + "timeline": "Cronología", "timezone": "Zona horaria", "to_archive": "Archivar", "to_change_password": "Cambiar contraseña", @@ -1232,6 +1249,7 @@ "to_trash": "Descartar", "toggle_settings": "Alternar ajustes", "toggle_theme": "Alternar tema oscuro", + "total": "Total", "total_usage": "Uso total", "trash": "Papelera", "trash_all": "Descartar todo", @@ -1281,6 +1299,8 @@ "user_purchase_settings_description": "Gestiona tu compra", "user_role_set": "Carbiar {user} a {role}", "user_usage_detail": "Detalle del uso del usuario", + "user_usage_stats": "Estadísticas de uso de la cuenta", + "user_usage_stats_description": "Ver estadísticas de uso de la cuenta", "username": "Nombre de usuario", "users": "Usuarios", "utilities": "Utilidades", @@ -1302,6 +1322,7 @@ "view_all_users": "Mostrar todos los usuarios", "view_in_timeline": "Mostrar en la línea de tiempo", "view_links": "Mostrar enlaces", + "view_name": "Ver", "view_next_asset": "Mostrar siguiente elemento", "view_previous_asset": "Mostrar elemento anterior", "view_stack": "Ver Pila", diff --git a/i18n/et.json b/i18n/et.json index 817c22eb08e49..fc2cc3de9353f 100644 --- a/i18n/et.json +++ b/i18n/et.json @@ -23,6 +23,7 @@ "add_to": "Lisa kohta...", "add_to_album": "Lisa albumisse", "add_to_shared_album": "Lisa jagatud albumisse", + "add_url": "Lisa URL", "added_to_archive": "Lisatud arhiivi", "added_to_favorites": "Lisatud lemmikutesse", "added_to_favorites_count": "{count, number} pilti lisatud lemmikutesse", @@ -130,7 +131,7 @@ "machine_learning_smart_search_description": "Otsi pilte semantiliselt CLIP-manuste abil", "machine_learning_smart_search_enabled": "Luba nutiotsing", "machine_learning_smart_search_enabled_description": "Kui keelatud, siis ei kodeerita pilte nutiotsingu jaoks.", - "machine_learning_url_description": "Masinõppe serveri URL", + "machine_learning_url_description": "Masinõppe serveri URL. Kui ette on antud rohkem kui üks URL, proovitakse neid järjest ükshaaval, kuni üks edukalt vastab.", "manage_concurrency": "Halda samaaegsust", "manage_log_settings": "Halda logi seadeid", "map_dark_style": "Tume stiil", @@ -222,6 +223,8 @@ "send_welcome_email": "Saada tervituskiri", "server_external_domain_settings": "Väline domeen", "server_external_domain_settings_description": "Domeen avalikult jagatud linkide jaoks, k.a. http(s)://", + "server_public_users": "Avalikud kasutajad", + "server_public_users_description": "Kasutaja jagatud albumisse lisamisel kuvatakse kõiki kasutajaid (nime ja e-posti aadressiga). Kui keelatud, kuvatakse kasutajate nimekirja ainult administraatoritele.", "server_settings": "Serveri seaded", "server_settings_description": "Halda serveri seadeid", "server_welcome_message": "Tervitusteade", @@ -247,6 +250,16 @@ "storage_template_user_label": "{label} on kasutaja talletussilt", "system_settings": "Süsteemi seaded", "tag_cleanup_job": "Siltide korrastamine", + "template_email_available_tags": "Saad mallis kasutada järgmisi muutujaid: {tags}", + "template_email_if_empty": "Kui mall on tühi, kasutatakse vaikimisi e-kirja.", + "template_email_invite_album": "Albumisse kutse mall", + "template_email_preview": "Eelvaade", + "template_email_settings": "E-posti mallid", + "template_email_settings_description": "Halda e-posti teavitusmalle", + "template_email_update_album": "Albumi muutmise mall", + "template_email_welcome": "Tervituskirja mall", + "template_settings": "Teavituse mallid", + "template_settings_description": "Teavituste mallide haldamine.", "theme_custom_css_settings": "Kohandatud CSS", "theme_custom_css_settings_description": "Cascading Style Sheets lubab Immich'i kujunduse kohandamist.", "theme_settings": "Teema seaded", @@ -719,6 +732,7 @@ "external": "Väline", "external_libraries": "Välised kogud", "face_unassigned": "Seostamata", + "failed_to_load_assets": "Üksuste laadimine ebaõnnestus", "favorite": "Lemmik", "favorites": "Lemmikud", "feature_photo_updated": "Esiletõstetud foto muudetud", @@ -1011,6 +1025,7 @@ "reassigned_assets_to_existing_person": "{count, plural, one {# üksus} other {# üksust}} seostatud {name, select, null {olemasoleva isikuga} other {isikuga {name}}}", "reassigned_assets_to_new_person": "{count, plural, one {# üksus} other {# üksust}} seostatud uue isikuga", "reassing_hint": "Seosta valitud üksused olemasoleva isikuga", + "recent-albums": "Hiljutised albumid", "recent_searches": "Hiljutised otsingud", "refresh": "Värskenda", "refresh_encoded_videos": "Värskenda kodeeritud videod", @@ -1032,6 +1047,7 @@ "remove_from_album": "Eemalda albumist", "remove_from_favorites": "Eemalda lemmikutest", "remove_from_shared_link": "Eemalda jagatud lingist", + "remove_url": "Eemalda URL", "remove_user": "Eemalda kasutaja", "removed_api_key": "API võti eemaldatud: {name}", "removed_from_archive": "Arhiivist eemaldatud", @@ -1210,13 +1226,16 @@ "they_will_be_merged_together": "Nad ühendatakse kokku", "third_party_resources": "Kolmanda osapoole ressursid", "time_based_memories": "Ajapõhised mälestused", + "timeline": "Ajajoon", "timezone": "Ajavöönd", "to_archive": "Arhiivi", "to_change_password": "Muuda parool", "to_favorite": "Lemmik", + "to_login": "Logi sisse", "to_trash": "Prügikasti", "toggle_settings": "Kuva/peida seaded", "toggle_theme": "Lülita tume teema", + "total": "Kokku", "total_usage": "Kogukasutus", "trash": "Prügikast", "trash_all": "Kõik prügikasti", @@ -1262,6 +1281,8 @@ "user_purchase_settings_description": "Halda oma ostu", "user_role_set": "Määra kasutajale {user} roll {role}", "user_usage_detail": "Kasutajate kasutusandmed", + "user_usage_stats": "Konto kasutuse statistika", + "user_usage_stats_description": "Vaata konto kasutuse statistikat", "username": "Kasutajanimi", "users": "Kasutajad", "utilities": "Tööriistad", diff --git a/i18n/fi.json b/i18n/fi.json index 062cc8615bc54..c2765eb8f4650 100644 --- a/i18n/fi.json +++ b/i18n/fi.json @@ -222,6 +222,8 @@ "send_welcome_email": "Lähetä tervetuloviesti", "server_external_domain_settings": "Ulkoinen osoite", "server_external_domain_settings_description": "Osoite julkisille linkeille, http(s):// mukaan lukien", + "server_public_users": "Julkiset käyttäjät", + "server_public_users_description": "Kaikki käyttäjät (nimi ja sähköpostiosoite) luetellaan, kun käyttäjä lisätään jaettuihin albumeihin. Kun toiminto on poistettu käytöstä, käyttäjäluettelo on vain pääkäyttäjien käytettävissä.", "server_settings": "Palvelimen asetukset", "server_settings_description": "Ylläpidä palvelimen asetuksia", "server_welcome_message": "Tervetuloviesti", @@ -465,6 +467,7 @@ "confirm": "Vahvista", "confirm_admin_password": "Vahvista ylläpitäjän salasana", "confirm_delete_shared_link": "Haluatko varmasti poistaa tämän jaetun linkin?", + "confirm_keep_this_delete_others": "Kuvapinon muut kuvat tätä lukuunottamatta poistetaan. Oletko varma, että haluat jatkaa?", "confirm_password": "Vahvista salasana", "contain": "Mahduta", "context": "Konteksti", @@ -514,6 +517,7 @@ "delete_key": "Poista avain", "delete_library": "Poista kirjasto", "delete_link": "Poista linkki", + "delete_others": "Poista muut", "delete_shared_link": "Poista jaettu linkki", "delete_tag": "Poista tunniste", "delete_tag_confirmation_prompt": "Haluatko varmasti poistaa tunnisteen {tagName}?", @@ -604,6 +608,7 @@ "failed_to_create_shared_link": "Jaetun linkin luonti epäonnistui", "failed_to_edit_shared_link": "Jaetun linkin muokkaus epäonnistui", "failed_to_get_people": "Henkilöiden haku epäonnistui", + "failed_to_keep_this_delete_others": "Muiden kohteiden poisto epäonnistui", "failed_to_load_asset": "Kohteen lataus epäonnistui", "failed_to_load_assets": "Kohteiden lataus epäonnistui", "failed_to_load_people": "Henkilöiden lataus epäonnistui", @@ -787,6 +792,8 @@ "jobs": "Taustatehtävät", "keep": "Säilytä", "keep_all": "Säilytä kaikki", + "keep_this_delete_others": "Säilytä tämä, poista muut", + "kept_this_deleted_others": "Tämä kohde säilytettiin. {count, plural, one {# asset} other {# assets}} poistettiin", "keyboard_shortcuts": "Pikanäppäimet", "language": "Kieli", "language_setting_description": "Valitse suosimasi kieli", @@ -1218,6 +1225,7 @@ "they_will_be_merged_together": "Nämä tullaan yhdistämään", "third_party_resources": "Kolmannen osapuolen resurssit", "time_based_memories": "Aikaan perustuvat muistot", + "timeline": "Aikajana", "timezone": "Aikavyöhyke", "to_archive": "Arkistoi", "to_change_password": "Vaihda salasana", @@ -1227,6 +1235,7 @@ "to_trash": "Roskakoriin", "toggle_settings": "Määritä asetukset", "toggle_theme": "Aseta tumma teema", + "total": "Yhteensä", "total_usage": "Käyttö yhteensä", "trash": "Roskakori", "trash_all": "Vie kaikki roskakoriin", @@ -1276,6 +1285,8 @@ "user_purchase_settings_description": "Hallitse ostostasi", "user_role_set": "Tee käyttäjästä {user} {role}", "user_usage_detail": "Käyttäjän käytön tiedot", + "user_usage_stats": "Tilin käyttötilastot", + "user_usage_stats_description": "Näytä tilin käyttötilastot", "username": "Käyttäjänimi", "users": "Käyttäjät", "utilities": "Apuohjelmat", @@ -1297,6 +1308,7 @@ "view_all_users": "Näytä kaikki käyttäjät", "view_in_timeline": "Näytä aikajanalla", "view_links": "Näytä linkit", + "view_name": "Näkymä", "view_next_asset": "Näytä seuraava", "view_previous_asset": "Näytä edellinen", "view_stack": "Näytä pinona", diff --git a/i18n/fr.json b/i18n/fr.json index e226e38fe9e2e..3f7ac6d521398 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -5,12 +5,12 @@ "acknowledge": "Compris", "action": "Action", "actions": "Actions", - "active": "En cours d'exécution", + "active": "En cours", "activity": "Activité", "activity_changed": "Activité {enabled, select, true {autorisée} other {interdite}}", "add": "Ajouter", "add_a_description": "Ajouter une description", - "add_a_location": "Ajouter un emplacement", + "add_a_location": "Ajouter une localisation", "add_a_name": "Ajouter un nom", "add_a_title": "Ajouter un titre", "add_exclusion_pattern": "Ajouter un schéma d'exclusion", @@ -23,6 +23,7 @@ "add_to": "Ajouter à…", "add_to_album": "Ajouter à l'album", "add_to_shared_album": "Ajouter à l'album partagé", + "add_url": "Ajouter l'URL", "added_to_archive": "Ajouté à l'archive", "added_to_favorites": "Ajouté aux favoris", "added_to_favorites_count": "{count, number} ajouté(s) aux favoris", @@ -30,7 +31,7 @@ "add_exclusion_pattern_description": "Ajouter des schémas d'exclusion. Les caractères génériques *, ** et ? sont pris en charge. Pour ignorer tous les fichiers dans un répertoire nommé « Raw », utilisez « **/Raw/** ». Pour ignorer tous les fichiers se terminant par « .tif », utilisez « **/*.tif ». Pour ignorer un chemin absolu, utilisez « /chemin/à/ignorer/** ».", "asset_offline_description": "Ce média de la bibliothèque externe n'est plus présent sur le disque et a été déplacé vers la corbeille. Si le fichier a été déplacé dans la bibliothèque, vérifiez votre chronologie pour le nouveau média correspondant. Pour restaurer ce média, veuillez vous assurer que le chemin du fichier ci-dessous peut être accédé par Immich et lancez l'analyse de la bibliothèque.", "authentication_settings": "Paramètres d'authentification", - "authentication_settings_description": "Gérer le mot de passe, la délégation d'authentification OAuth et d'autres paramètres d'authentification", + "authentication_settings_description": "Gérer le mot de passe, l'authentification OAuth et d'autres paramètres d'authentification", "authentication_settings_disable_all": "Êtes-vous sûr de vouloir désactiver toutes les méthodes de connexion ? La connexion sera complètement désactivée.", "authentication_settings_reenable": "Pour réactiver, utilisez une Commande Serveur.", "background_task_job": "Tâches de fond", @@ -39,8 +40,8 @@ "backup_keep_last_amount": "Nombre de sauvegardes à conserver", "backup_settings": "Paramètres de la sauvegarde", "backup_settings_description": "Gérer les paramètres de la sauvegarde", - "check_all": "Vérifier tout", - "cleared_jobs": "Tâches supprimées pour : {job}", + "check_all": "Tout cocher", + "cleared_jobs": "Tâches supprimées pour : {job}", "config_set_by_file": "La configuration est actuellement définie par un fichier de configuration", "confirm_delete_library": "Êtes-vous sûr de vouloir supprimer la bibliothèque {library} ?", "confirm_delete_library_assets": "Êtes-vous sûr de vouloir supprimer cette bibliothèque ? Cette opération supprimera d'Immich {count, plural, one {le média} other {les # médias}} qu'elle contient et ne pourra pas être annulée. Les fichiers resteront sur le disque.", @@ -50,14 +51,14 @@ "create_job": "Créer une tâche", "cron_expression": "Expression cron", "cron_expression_description": "Définir l'intervalle d'analyse à l'aide d'une expression cron. Pour plus d'informations, voir Crontab Guru", - "cron_expression_presets": "Préréglages expression cron", + "cron_expression_presets": "Préréglages d'expression cron", "disable_login": "Désactiver la connexion", - "duplicate_detection_job_description": "Exécution de l'apprentissage automatique sur les médias pour détecter les images similaires. S'appuie sur la recherche intelligente", + "duplicate_detection_job_description": "Lancement de l'apprentissage automatique sur les médias pour détecter les images similaires. Se base sur la recherche intelligente", "exclusion_pattern_description": "Les schémas d'exclusion vous permettent d'ignorer des fichiers et des dossiers lors de l'analyse de votre bibliothèque. Cette fonction est utile si des dossiers contiennent des fichiers que vous ne souhaitez pas importer, tels que des fichiers RAW.", "external_library_created_at": "Bibliothèque externe (créée le {date})", "external_library_management": "Gestion de la bibliothèque externe", "face_detection": "Détection des visages", - "face_detection_description": "Détection des visages dans les médias à l'aide de l'apprentissage automatique. Pour les vidéos, seule la miniature est prise en compte. « Rafraichir» (re)traite tous les médias. « Réinitialise» met en file d'attente les médias qui n'ont pas encore été traités. Les visages détectés seront mis en file d'attente pour la reconnaissance faciale une fois la détection des visages terminée, les regroupant en personnes existantes ou nouvelles.", + "face_detection_description": "Détection des visages dans les médias à l'aide de l'apprentissage automatique. Pour les vidéos, seule la miniature est prise en compte. « Actualiser » (re)traite tous les médias. « Réinitialiser » efface en plus toutes les données actuelles de visages. « Manquants » Les visages détectés seront mis en file d'attente pour la reconnaissance faciale. Une fois la détection des visages terminée, les regroupant en personnes existantes ou nouvelles.", "facial_recognition_job_description": "Regrouper les visages détectés en personnes. Cette étape est exécutée une fois la détection des visages terminée. « Rafraichir» (re)regroupe tous les visages. « Manquant» met en file d'attente les visages auxquels aucune personne n'a été attribuée.", "failed_job_command": "La commande {command} a échoué pour la tâche : {job}", "force_delete_user_warning": "ATTENTION : Cette opération entraîne la suppression immédiate de l'utilisateur et de tous ses médias. Cette opération ne peut être annulée et les fichiers ne peuvent être récupérés.", @@ -89,7 +90,7 @@ "jobs_failed": "{jobCount, plural, other {# en échec}}", "library_created": "Bibliothèque créée : {library}", "library_deleted": "Bibliothèque supprimée", - "library_import_path_description": "Spécifier un dossier à importer. Ce dossier, y compris les sous-dossiers, sera analysé à la recherche d'images et de vidéos.", + "library_import_path_description": "Spécifier un dossier à importer. Ce dossier, y compris ses sous-dossiers, sera analysé à la recherche d'images et de vidéos.", "library_scanning": "Analyse périodique", "library_scanning_description": "Configurer l'analyse périodique de la bibliothèque", "library_scanning_enable_description": "Activer l'analyse périodique de la bibliothèque", @@ -130,7 +131,7 @@ "machine_learning_smart_search_description": "Rechercher des images de manière sémantique en utilisant les intégrations CLIP", "machine_learning_smart_search_enabled": "Activer la recherche intelligente", "machine_learning_smart_search_enabled_description": "Si cette option est désactivée, les images ne seront pas encodées pour la recherche intelligente.", - "machine_learning_url_description": "URL du serveur d'apprentissage automatique", + "machine_learning_url_description": "L’URL du serveur d'apprentissage automatique. Si plusieurs URL sont fournies, chaque serveur sera essayé un par un jusqu’à ce que l’un d’eux réponde avec succès, dans l’ordre de la première à la dernière.", "manage_concurrency": "Gérer du multitâche", "manage_log_settings": "Gérer les paramètres de journalisation", "map_dark_style": "Thème sombre", @@ -189,7 +190,7 @@ "oauth_mobile_redirect_uri_override_description": "Activer quand le fournisseur d'OAuth ne permet pas un URI mobile, comme '{callback} '", "oauth_profile_signing_algorithm": "Algorithme de signature de profil", "oauth_profile_signing_algorithm_description": "Algorithme utilisé pour signer le profil utilisateur.", - "oauth_scope": "Portée", + "oauth_scope": "Périmètre", "oauth_settings": "OAuth", "oauth_settings_description": "Gérer les paramètres de connexion OAuth", "oauth_settings_more_details": "Pour plus de détails sur cette fonctionnalité, consultez ce lien.", @@ -222,6 +223,8 @@ "send_welcome_email": "Envoyer un courriel de bienvenue", "server_external_domain_settings": "Domaine externe", "server_external_domain_settings_description": "Nom de domaine pour les liens partagés publics, y compris http(s)://", + "server_public_users": "Utilisateurs publics", + "server_public_users_description": "Tous les utilisateurs (nom et courriel) sont listés lors de l'ajout d'un utilisateur à des albums partagés. Quand cela est désactivé, la liste des utilisateurs est uniquement disponible pour les comptes administrateurs.", "server_settings": "Paramètres du serveur", "server_settings_description": "Gérer les paramètres du serveur", "server_welcome_message": "Message de bienvenue", @@ -247,11 +250,21 @@ "storage_template_user_label": "{label} est l'étiquette de stockage de l'utilisateur", "system_settings": "Paramètres du système", "tag_cleanup_job": "Nettoyage des étiquettes", + "template_email_available_tags": "Vous pouvez utiliser les variables suivantes dans votre modèle : {tags}", + "template_email_if_empty": "Si le modèle est vide, l’e-mail par défaut sera utilisé.", + "template_email_invite_album": "Modèle d'invitation à un album", + "template_email_preview": "Prévisualiser", + "template_email_settings": "Modèles de courriel", + "template_email_settings_description": "Gérer les modèles de notifications par courriel personnalisés", + "template_email_update_album": "Mettre à jour le modèle d’album", + "template_email_welcome": "Modèle de courriel de bienvenue", + "template_settings": "Modèles de notifications", + "template_settings_description": "Gérer les modèles personnalisés pour les notifications.", "theme_custom_css_settings": "CSS personnalisé", "theme_custom_css_settings_description": "Les feuilles de style en cascade (CSS) permettent de personnaliser l'apparence d'Immich.", "theme_settings": "Paramètres du thème", "theme_settings_description": "Gérer la personnalisation de l'interface web d'Immich", - "these_files_matched_by_checksum": "Ces fichiers correspondent par leur somme de contrôle", + "these_files_matched_by_checksum": "Ces fichiers sont identiques d'après leur somme de contrôle", "thumbnail_generation_job": "Génération des miniatures", "thumbnail_generation_job_description": "Génération des miniatures pour chaque média ainsi que pour les visages détectés", "transcoding_acceleration_api": "API d'accélération", @@ -262,7 +275,7 @@ "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Codecs audio acceptés", "transcoding_accepted_audio_codecs_description": "Sélectionnez les codecs audio qui n'ont pas besoin d'être transcodés. Utilisé uniquement pour certaines politiques de transcodage.", - "transcoding_accepted_containers": "Containers acceptés", + "transcoding_accepted_containers": "Conteneurs acceptés", "transcoding_accepted_containers_description": "Sélectionnez les formats de conteneurs qui n'ont pas besoin d'être remuxés en MP4. Utilisé uniquement pour certaines politiques de transcodage.", "transcoding_accepted_video_codecs": "Codecs vidéo acceptés", "transcoding_accepted_video_codecs_description": "Sélectionnez les codecs vidéo qui n'ont pas besoin d'être transcodés. Utilisé uniquement pour certaines politiques de transcodage.", @@ -299,7 +312,7 @@ "transcoding_settings_description": "Gérer les informations de résolution et d'encodage des fichiers vidéo", "transcoding_target_resolution": "Résolution cible", "transcoding_target_resolution_description": "Des résolutions plus élevées peuvent préserver plus de détails, mais prennent plus de temps à encoder, ont de plus grandes tailles de fichiers, et peuvent réduire la réactivité de l'application.", - "transcoding_temporal_aq": "AQ temporelle", + "transcoding_temporal_aq": "Quantification adaptative temporelle (temporal AQ)", "transcoding_temporal_aq_description": "S'applique uniquement à NVENC. Améliore la qualité des scènes riches en détails et à faible mouvement. Peut ne pas être compatible avec les anciens appareils.", "transcoding_threads": "Processus", "transcoding_threads_description": "Une valeur plus élevée entraîne un encodage plus rapide, mais laisse moins de place au serveur pour traiter d'autres tâches pendant son activité. Cette valeur ne doit pas être supérieure au nombre de cœurs de CPU. Une valeur égale à 0 maximise l'utilisation.", @@ -392,7 +405,7 @@ "asset_adding_to_album": "Ajout à l'album...", "asset_description_updated": "La description du média a été mise à jour", "asset_filename_is_offline": "Le média {filename} est hors ligne", - "asset_has_unassigned_faces": "Le média a des visages non assignés", + "asset_has_unassigned_faces": "Le média a des visages non attribués", "asset_hashing": "Hachage...", "asset_offline": "Média hors ligne", "asset_offline_description": "Ce média externe n'est plus accessible sur le disque. Veuillez contacter votre administrateur Immich pour obtenir de l'aide.", @@ -465,6 +478,7 @@ "confirm": "Confirmer", "confirm_admin_password": "Confirmer le mot de passe Admin", "confirm_delete_shared_link": "Voulez-vous vraiment supprimer ce lien partagé ?", + "confirm_keep_this_delete_others": "Tous les autres médias dans la pile seront supprimés sauf celui-ci. Êtes-vous sûr de vouloir continuer ?", "confirm_password": "Confirmer le mot de passe", "contain": "Contenu", "context": "Contexte", @@ -514,6 +528,7 @@ "delete_key": "Supprimer la clé", "delete_library": "Supprimer la bibliothèque", "delete_link": "Supprimer le lien", + "delete_others": "Supprimer les autres", "delete_shared_link": "Supprimer le lien partagé", "delete_tag": "Supprimer l'étiquette", "delete_tag_confirmation_prompt": "Êtes-vous sûr de vouloir supprimer l'étiquette {tagName} ?", @@ -532,12 +547,12 @@ "display_options": "Afficher les options", "display_order": "Ordre d'affichage", "display_original_photos": "Afficher les photos originales", - "display_original_photos_setting_description": "Préférer afficher la photo originale lors de la visualisation d'un média plutôt que sa miniature lorsque cela est possible. Cela peut entraîner des vitesses d'affichage plus lentes.", + "display_original_photos_setting_description": "Afficher de préférence la photo originale lors de la visualisation d'un média plutôt que sa miniature lorsque cela est possible. Cela peut entraîner des vitesses d'affichage plus lentes.", "do_not_show_again": "Ne plus afficher ce message", "documentation": "Documentation", "done": "Terminé", "download": "Télécharger", - "download_include_embedded_motion_videos": "Vidéos embarquées", + "download_include_embedded_motion_videos": "Vidéos intégrées", "download_include_embedded_motion_videos_description": "Inclure des vidéos intégrées dans les photos de mouvement comme un fichier séparé", "download_settings": "Télécharger", "download_settings_description": "Gérer les paramètres de téléchargement des médias", @@ -562,7 +577,7 @@ "edit_name": "Modifier le nom", "edit_people": "Modifier les personnes", "edit_tag": "Modifier l'étiquette", - "edit_title": "Modifier le title", + "edit_title": "Modifier le titre", "edit_user": "Modifier l'utilisateur", "edited": "Modifié", "editor": "Editeur", @@ -585,15 +600,15 @@ "cant_apply_changes": "Impossible d'appliquer les changements", "cant_change_activity": "Impossible {enabled, select, true {d'interdire} other {d'autoriser}} l'activité", "cant_change_asset_favorite": "Impossible de changer le favori du média", - "cant_change_metadata_assets_count": "Impossible de modifier les métadonnées de {count, plural, one {# média} other {# médias}}", - "cant_get_faces": "Impossible d'obtenir de visages", + "cant_change_metadata_assets_count": "Impossible de modifier les métadonnées {count, plural, one {d'un média} other {de # médias}}", + "cant_get_faces": "Impossible d'obtenir des visages", "cant_get_number_of_comments": "Impossible d'obtenir le nombre de commentaires", "cant_search_people": "Impossible de rechercher des personnes", "cant_search_places": "Impossible de rechercher des lieux", "cleared_jobs": "Tâches supprimées pour : {job}", "error_adding_assets_to_album": "Erreur lors de l'ajout des médias à l'album", "error_adding_users_to_album": "Erreur lors de l'ajout d'utilisateurs à l'album", - "error_deleting_shared_user": "Erreur lors de la suppression l'utilisateur partagé", + "error_deleting_shared_user": "Erreur lors de la suppression de l'utilisateur partagé", "error_downloading": "Erreur lors du téléchargement de {filename}", "error_hiding_buy_button": "Impossible de masquer le bouton d'achat", "error_removing_assets_from_album": "Erreur lors de la suppression des médias de l'album, vérifier la console pour plus de détails", @@ -604,6 +619,7 @@ "failed_to_create_shared_link": "Impossible de créer le lien partagé", "failed_to_edit_shared_link": "Impossible de modifier le lien partagé", "failed_to_get_people": "Impossible d'obtenir les personnes", + "failed_to_keep_this_delete_others": "Impossible de conserver ce média et de supprimer les autres médias", "failed_to_load_asset": "Impossible de charger le média", "failed_to_load_assets": "Impossible de charger les médias", "failed_to_load_people": "Impossible de charger les personnes", @@ -637,21 +653,21 @@ "unable_to_copy_to_clipboard": "Impossible de copier dans le presse-papiers, assurez-vous que vous accédez à la page via https", "unable_to_create_admin_account": "Impossible de créer le compte administrateur", "unable_to_create_api_key": "Impossible de créer une nouvelle clé API", - "unable_to_create_library": "Création de bibliothèque impossible", - "unable_to_create_user": "Création de l'utilisateur impossible", - "unable_to_delete_album": "Suppression de l'album impossible", - "unable_to_delete_asset": "Suppression du média impossible", + "unable_to_create_library": "Impossible de créer la bibliothèque", + "unable_to_create_user": "Impossible de créer l'utilisateur", + "unable_to_delete_album": "Impossible de supprimer l'album", + "unable_to_delete_asset": "Impossible de supprimer le média", "unable_to_delete_assets": "Erreur lors de la suppression des médias", - "unable_to_delete_exclusion_pattern": "Suppression du modèle d'exclusion impossible", - "unable_to_delete_import_path": "Suppression du chemin d'importation impossible", - "unable_to_delete_shared_link": "Suppression du lien de partage impossible", - "unable_to_delete_user": "Suppression de l'utilisateur impossible", + "unable_to_delete_exclusion_pattern": "Impossible de supprimer le modèle d'exclusion", + "unable_to_delete_import_path": "Impossible de supprimer le chemin d'importation", + "unable_to_delete_shared_link": "Impossible de supprimer le lien de partage", + "unable_to_delete_user": "Impossible de supprimer l'utilisateur", "unable_to_download_files": "Impossible de télécharger les fichiers", - "unable_to_edit_exclusion_pattern": "Modification du modèle d'exclusion impossible", - "unable_to_edit_import_path": "Modification du chemin d'importation impossible", + "unable_to_edit_exclusion_pattern": "Impossible de modifier le modèle d'exclusion", + "unable_to_edit_import_path": "Impossible de modifier le chemin d'importation", "unable_to_empty_trash": "Impossible de vider la corbeille", "unable_to_enter_fullscreen": "Mode plein écran indisponible", - "unable_to_exit_fullscreen": "Sortie du mode plein écran impossible", + "unable_to_exit_fullscreen": "Impossible de sortir du mode plein écran", "unable_to_get_comments_number": "Impossible d'obtenir le nombre de commentaires", "unable_to_get_shared_link": "Échec de la récupération du lien partagé", "unable_to_hide_person": "Impossible de cacher la personne", @@ -665,8 +681,8 @@ "unable_to_log_out_device": "Impossible de déconnecter l'appareil", "unable_to_login_with_oauth": "Impossible de se connecter avec OAuth", "unable_to_play_video": "Impossible de jouer la vidéo", - "unable_to_reassign_assets_existing_person": "Incapable de réaffecter des médias à {name, select, null {une personne existante} other {{name}}}", - "unable_to_reassign_assets_new_person": "Impossible de réaffecter les médias à une nouvelle personne", + "unable_to_reassign_assets_existing_person": "Impossible de réattribuer les médias à {name, select, null {une personne existante} other {{name}}}", + "unable_to_reassign_assets_new_person": "Impossible de réattribuer les médias à une nouvelle personne", "unable_to_refresh_user": "Impossible d'actualiser l'utilisateur", "unable_to_remove_album_users": "Impossible de supprimer les utilisateurs de l'album", "unable_to_remove_api_key": "Impossible de supprimer la clé API", @@ -685,7 +701,7 @@ "unable_to_save_api_key": "Impossible de sauvegarder la clé API", "unable_to_save_date_of_birth": "Impossible de sauvegarder la date de naissance", "unable_to_save_name": "Impossible de sauvegarder le nom", - "unable_to_save_profile": "Impossible de sauvegarder le profile", + "unable_to_save_profile": "Impossible de sauvegarder le profil", "unable_to_save_settings": "Impossible d'enregistrer les préférences", "unable_to_scan_libraries": "Impossible de scanner les bibliothèques", "unable_to_scan_library": "Impossible de scanner la bibliothèque", @@ -716,8 +732,9 @@ "export_as_json": "Exporter en JSON", "extension": "Extension", "external": "Externe", - "external_libraries": "Bibliothèques ext.", + "external_libraries": "Bibliothèques externes", "face_unassigned": "Non attribué", + "failed_to_load_assets": "Échec du chargement des ressources", "favorite": "Favori", "favorite_or_unfavorite_photo": "Ajouter ou supprimer des favoris", "favorites": "Favoris", @@ -787,6 +804,8 @@ "jobs": "Tâches", "keep": "Conserver", "keep_all": "Les conserver tous", + "keep_this_delete_others": "Conserver celui-ci, supprimer les autres", + "kept_this_deleted_others": "Ce média a été conservé, et {count, plural, one {un autre a été supprimé} other {# autres ont été supprimés}}", "keyboard_shortcuts": "Raccourcis clavier", "language": "Langue", "language_setting_description": "Sélectionnez votre langue préférée", @@ -836,7 +855,7 @@ "media_type": "Type de média", "memories": "Souvenirs", "memories_setting_description": "Gérer ce que vous voyez dans vos souvenirs", - "memory": "Mémoire", + "memory": "Souvenir", "memory_lane_title": "Fil de souvenirs {title}", "menu": "Menu", "merge": "Fusionner", @@ -942,7 +961,7 @@ "permanent_deletion_warning": "Avertissement avant suppression définitive", "permanent_deletion_warning_setting_description": "Afficher un avertissement avant la suppression définitive d'un média", "permanently_delete": "Supprimer définitivement", - "permanently_delete_assets_count": "Suppression définitive de {count, plural, one {média} other {médias}}", + "permanently_delete_assets_count": "Suppression définitive {count, plural, one {du média} other {des médias}}", "permanently_delete_assets_prompt": "Êtes-vous sûr de vouloir supprimer définitivement {count, plural, one {ce média ?} other {ces # médias ?}} Cela {count, plural, one {le} other {les}} supprimera aussi de {count, plural, one {son (ses)} other {leur(s)}} album(s).", "permanently_deleted_asset": "Média supprimé définitivement", "permanently_deleted_assets_count": "{count, plural, one {# média définitivement supprimé} other {# médias définitivement supprimés}}", @@ -973,7 +992,7 @@ "public_album": "Album public", "public_share": "Partage public", "purchase_account_info": "Contributeur", - "purchase_activated_subtitle": "Merci d'avoir apporté votre soutien à Immich et les logiciels open source", + "purchase_activated_subtitle": "Merci d'avoir apporté votre soutien à Immich et aux logiciels open source", "purchase_activated_time": "Activé le {date, date}", "purchase_activated_title": "Votre clé a été activée avec succès", "purchase_button_activate": "Activer", @@ -983,7 +1002,7 @@ "purchase_button_reminder": "Me le rappeler dans 30 jours", "purchase_button_remove_key": "Supprimer la clé", "purchase_button_select": "Sélectionner", - "purchase_failed_activation": "Erreur à l'activation. Veuillez vérifier votre e-mail pour obtenir la clé du produit correcte !", + "purchase_failed_activation": "Erreur à l'activation. Veuillez vérifier votre courriel pour obtenir la clé du produit correcte !", "purchase_individual_description_1": "Pour un utilisateur", "purchase_individual_description_2": "Statut de contributeur", "purchase_individual_title": "Utilisateur", @@ -992,14 +1011,14 @@ "purchase_lifetime_description": "Achat à vie", "purchase_option_title": "OPTIONS D'ACHAT", "purchase_panel_info_1": "Développer Immich nécessite du temps et de l'énergie, et nous avons des ingénieurs qui travaillent à plein temps pour en faire le meilleur produit possible. Notre mission est de générer, pour les logiciels open source et les pratiques de travail éthique, une source de revenus suffisante pour les développeurs et de créer un écosystème respectueux de la vie privée grâce a des alternatives crédibles aux services cloud peu scrupuleux.", - "purchase_panel_info_2": "Étant donné que nous nous engageons à ne pas ajouter de murs de paiement, cet achat ne vous donnera pas de fonctionnalités supplémentaires dans Immich. Nous comptons sur des utilisateurs comme vous pour soutenir le développement continu d'Immich.", + "purchase_panel_info_2": "Étant donné que nous nous engageons à ne pas ajouter de fonctionnalités payantes, cet achat ne vous donnera pas de fonctionnalités supplémentaires dans Immich. Nous comptons sur des utilisateurs comme vous pour soutenir le développement continu d'Immich.", "purchase_panel_title": "Soutenir le projet", "purchase_per_server": "Par serveur", "purchase_per_user": "Par utilisateur", "purchase_remove_product_key": "Supprimer la clé du produit", "purchase_remove_product_key_prompt": "Êtes-vous sûr de vouloir supprimer la clé du produit ?", "purchase_remove_server_product_key": "Supprimer la clé du produit pour le Serveur", - "purchase_remove_server_product_key_prompt": "Êtes-vous sûr de vouloir supprimer la clé du produit pour le serveur ?", + "purchase_remove_server_product_key_prompt": "Êtes-vous sûr de vouloir supprimer la clé du produit pour le Serveur ?", "purchase_server_description_1": "Pour l'ensemble du serveur", "purchase_server_description_2": "Statut de contributeur", "purchase_server_title": "Serveur", @@ -1010,23 +1029,24 @@ "rating_description": "Afficher l'évaluation EXIF dans le panneau d'information", "reaction_options": "Options de réaction", "read_changelog": "Lire les changements", - "reassign": "Réaffecter", - "reassigned_assets_to_existing_person": "{count, plural, one {# média réaffecté} other {# médias réaffectés}} à {name, select, null {une personne existante} other {{name}}}", - "reassigned_assets_to_new_person": "{count, plural, one {# média réassigné} other {# médias réassignés}} à une nouvelle personne", + "reassign": "Réattribuer", + "reassigned_assets_to_existing_person": "{count, plural, one {# média réattribué} other {# médias réattribués}} à {name, select, null {une personne existante} other {{name}}}", + "reassigned_assets_to_new_person": "{count, plural, one {# média réattribué} other {# médias réattribués}} à une nouvelle personne", "reassing_hint": "Attribuer ces médias à une personne existante", "recent": "Récent", + "recent-albums": "Albums récents", "recent_searches": "Recherches récentes", "refresh": "Actualiser", "refresh_encoded_videos": "Actualiser les vidéos encodées", - "refresh_faces": "Mettre à jour les visages", + "refresh_faces": "Actualiser les visages", "refresh_metadata": "Actualiser les métadonnées", "refresh_thumbnails": "Actualiser les vignettes", "refreshed": "Actualisé", "refreshes_every_file": "Actualise tous les fichiers (existants et nouveaux)", "refreshing_encoded_video": "Actualisation de la vidéo encodée", - "refreshing_faces": "Actualiser les visages", + "refreshing_faces": "Actualisation des visages", "refreshing_metadata": "Actualisation des métadonnées", - "regenerating_thumbnails": "Régénération des vignettes", + "regenerating_thumbnails": "Regénération des vignettes", "remove": "Supprimer", "remove_assets_album_confirmation": "Êtes-vous sûr de vouloir supprimer {count, plural, one {# média} other {# médias}} de l'album ?", "remove_assets_shared_link_confirmation": "Êtes-vous sûr de vouloir supprimer {count, plural, one {# média} other {# médias}} de ce lien partagé ?", @@ -1036,6 +1056,7 @@ "remove_from_album": "Supprimer de l'album", "remove_from_favorites": "Supprimer des favoris", "remove_from_shared_link": "Supprimer des liens partagés", + "remove_url": "Supprimer l'URL", "remove_user": "Supprimer l'utilisateur", "removed_api_key": "Clé API supprimée : {name}", "removed_from_archive": "Supprimé de l'archive", @@ -1116,7 +1137,7 @@ "send_welcome_email": "Envoyer un courriel de bienvenue", "server_offline": "Serveur hors ligne", "server_online": "Serveur en ligne", - "server_stats": "Statistiques Serveur", + "server_stats": "Statistiques du serveur", "server_version": "Version du serveur", "set": "Définir", "set_as_album_cover": "Définir comme couverture d'album", @@ -1138,14 +1159,14 @@ "shared_with_partner": "Partagé avec {partner}", "sharing": "Partage", "sharing_enter_password": "Veuillez saisir le mot de passe pour visualiser cette page.", - "sharing_sidebar_description": "Afficher un lien vers Partage dans la barre latérale", + "sharing_sidebar_description": "Afficher un lien vers Partager dans la barre latérale", "shift_to_permanent_delete": "appuyez sur ⇧ pour supprimer définitivement le média", "show_album_options": "Afficher les options de l'album", "show_albums": "Montrer les albums", "show_all_people": "Montrer toutes les personnes", "show_and_hide_people": "Afficher / Masquer les personnes", "show_file_location": "Afficher l'emplacement du fichier", - "show_gallery": "Afficher la gallerie", + "show_gallery": "Afficher la galerie", "show_hidden_people": "Afficher les personnes masquées", "show_in_timeline": "Afficher dans la vue chronologique", "show_in_timeline_setting_description": "Afficher les photos et vidéos de cet utilisateur dans votre vue chronologique", @@ -1197,19 +1218,19 @@ "storage_usage": "{used} sur {available} utilisé", "submit": "Soumettre", "suggestions": "Suggestions", - "sunrise_on_the_beach": "Aurore sur la plage", + "sunrise_on_the_beach": "Lever de soleil sur la plage", "support": "Support", "support_and_feedback": "Support & Retours", "support_third_party_description": "Votre installation d'Immich est packagée via une application tierce. Si vous rencontrez des anomalies, elles peuvent venir de ce packaging tiers, merci de créer les anomalies avec ces tiers en premier lieu en utilisant les liens ci-dessous.", "swap_merge_direction": "Inverser la direction de fusion", "sync": "Synchroniser", - "tag": "Tag", - "tag_assets": "Taguer les médias", + "tag": "Étiquette", + "tag_assets": "Étiqueter les médias", "tag_created": "Étiquette créée : {tag}", "tag_feature_description": "Parcourir les photos et vidéos groupées par thèmes logiques", "tag_not_found_question": "Vous ne trouvez pas une étiquette ? Créer une nouvelle étiquette.", "tag_updated": "Étiquette mise à jour : {tag}", - "tagged_assets": "Tag ajouté à {count, plural, one {# média} other {# médias}}", + "tagged_assets": "Étiquette ajoutée à {count, plural, one {# média} other {# médias}}", "tags": "Étiquettes", "template": "Modèle", "theme": "Thème", @@ -1218,6 +1239,7 @@ "they_will_be_merged_together": "Elles seront fusionnées ensemble", "third_party_resources": "Ressources tierces", "time_based_memories": "Souvenirs basés sur la date", + "timeline": "Vue chronologique", "timezone": "Fuseau horaire", "to_archive": "Archiver", "to_change_password": "Modifier le mot de passe", @@ -1227,11 +1249,12 @@ "to_trash": "Corbeille", "toggle_settings": "Inverser les paramètres", "toggle_theme": "Inverser le thème sombre", + "total": "Total", "total_usage": "Utilisation globale", "trash": "Corbeille", "trash_all": "Tout supprimer", "trash_count": "Corbeille {count, number}", - "trash_delete_asset": "Corbeille/Suppression d'un média", + "trash_delete_asset": "Mettre à la corbeille/Supprimer un média", "trash_no_results_message": "Les photos et vidéos supprimées s'afficheront ici.", "trashed_items_will_be_permanently_deleted_after": "Les éléments dans la corbeille seront supprimés définitivement après {days, plural, one {# jour} other {# jours}}.", "type": "Type", @@ -1276,6 +1299,8 @@ "user_purchase_settings_description": "Gérer votre achat", "user_role_set": "Définir {user} comme {role}", "user_usage_detail": "Détail de l'utilisation des utilisateurs", + "user_usage_stats": "Statistiques d'utilisation du compte", + "user_usage_stats_description": "Voir les statistiques d'utilisation du compte", "username": "Nom d'utilisateur", "users": "Utilisateurs", "utilities": "Utilitaires", @@ -1297,6 +1322,7 @@ "view_all_users": "Voir tous les utilisateurs", "view_in_timeline": "Voir dans la vue chronologique", "view_links": "Voir les liens", + "view_name": "Vue", "view_next_asset": "Voir le média suivant", "view_previous_asset": "Voir le média précédent", "view_stack": "Afficher la pile", @@ -1305,7 +1331,7 @@ "warning": "Attention", "week": "Semaine", "welcome": "Bienvenue", - "welcome_to_immich": "Bienvenue sur immich", + "welcome_to_immich": "Bienvenue sur Immich", "year": "Année", "years_ago": "Il y a {years, plural, one {# an} other {# ans}}", "yes": "Oui", diff --git a/i18n/he.json b/i18n/he.json index 4c676315cd76c..c2f382e49cebf 100644 --- a/i18n/he.json +++ b/i18n/he.json @@ -1,5 +1,5 @@ { - "about": "אודות", + "about": "רענן", "account": "חשבון", "account_settings": "הגדרות חשבון", "acknowledge": "הבנתי", @@ -23,6 +23,7 @@ "add_to": "הוסף ל..", "add_to_album": "הוסף לאלבום", "add_to_shared_album": "הוסף לאלבום משותף", + "add_url": "הוספת קישור", "added_to_archive": "נוסף לארכיון", "added_to_favorites": "נוסף למועדפים", "added_to_favorites_count": "{count, number} נוספו למועדפים", @@ -130,7 +131,7 @@ "machine_learning_smart_search_description": "חפש תמונות באופן סמנטי באמצעות הטמעות של CLIP", "machine_learning_smart_search_enabled": "אפשר חיפוש חכם", "machine_learning_smart_search_enabled_description": "אם מושבת, תמונות לא יקודדו לחיפוש חכם.", - "machine_learning_url_description": "כתובת האתר של שרת למידת המכונה", + "machine_learning_url_description": "כתובת האתר של שרת למידת המכונה. אם ניתן יותר מכתוובת אחת, כל שרת ינסה בתורו עד אשר יענה בחיוב, בסדר התחלתי.", "manage_concurrency": "נהל בו-זמניות", "manage_log_settings": "נהל הגדרות רישום ביומן", "map_dark_style": "עיצוב כהה", @@ -222,6 +223,8 @@ "send_welcome_email": "שלח דוא\"ל ברוכים הבאים", "server_external_domain_settings": "דומיין חיצוני", "server_external_domain_settings_description": "דומיין עבור קישורים משותפים ציבוריים, כולל http(s)://", + "server_public_users": "משתמשים ציבוריים", + "server_public_users_description": "כל המשתמשים (שם ודוא\"ל) מופיעים בעת הוספת משתמש לאלבומים משותפים. כאשר התכונה מושבתת, רשימת המשתמשים תהיה זמינה רק למשתמשים בעלי הרשאות מנהל.", "server_settings": "הגדרות שרת", "server_settings_description": "נהל הגדרות שרת", "server_welcome_message": "הודעת פתיחה", @@ -421,7 +424,7 @@ "blurred_background": "רקע מטושטש", "bugs_and_feature_requests": "באגים & בקשות לתכונות", "build": "Build", - "build_image": "Build Image", + "build_image": "בניית גרסה", "bulk_delete_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך למחוק בכמות גדולה {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה ישמור על הנכס הכי גדול של כל קבוצה וימחק לצמיתות את כל שאר הכפילויות. את/ה לא יכול/ה לבטל את הפעולה הזו!", "bulk_keep_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך להשאיר {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה יפתור את כל הקבוצות הכפולות מבלי למחוק דבר.", "bulk_trash_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך להעביר לאשפה בכמות גדולה {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה ישמור על הנכס הגדול ביותר של כל קבוצה ויעביר לאשפה את כל שאר הכפילויות.", @@ -465,6 +468,7 @@ "confirm": "אישור", "confirm_admin_password": "אשר סיסמת מנהל", "confirm_delete_shared_link": "האם את/ה בטוח/ה שברצונך למחוק את הקישור המשותף הזה?", + "confirm_keep_this_delete_others": "כל שאר הנכסים בערימה יימחקו למעט נכס זה. האם את/ה בטוח/ה שברצונך להמשיך?", "confirm_password": "אשר סיסמה", "contain": "מכיל", "context": "הקשר", @@ -514,6 +518,7 @@ "delete_key": "מחק מפתח", "delete_library": "מחק ספרייה", "delete_link": "מחק קישור", + "delete_others": "מחק אחרים", "delete_shared_link": "מחק קישור משותף", "delete_tag": "מחק תג", "delete_tag_confirmation_prompt": "האם את/ה בטוח/ה שברצונך למחוק תג {tagName}?", @@ -604,6 +609,7 @@ "failed_to_create_shared_link": "יצירת קישור משותף נכשלה", "failed_to_edit_shared_link": "עריכת קישור משותף נכשלה", "failed_to_get_people": "קבלת אנשים נכשלה", + "failed_to_keep_this_delete_others": "נכשל לשמור את הנכס הזה ולמחוק את הנכסים האחרים", "failed_to_load_asset": "טעינת נכס נכשלה", "failed_to_load_assets": "טעינת נכסים נכשלה", "failed_to_load_people": "נכשל באחזור אנשים", @@ -787,6 +793,8 @@ "jobs": "משימות", "keep": "שמור", "keep_all": "שמור הכל", + "keep_this_delete_others": "שמור על זה, מחק אחרים", + "kept_this_deleted_others": "נכס זה נשמר ונמחקו {count, plural, one {נכס #} other {# נכסים}}", "keyboard_shortcuts": "קיצורי מקלדת", "language": "שפה", "language_setting_description": "בחר/י את השפה המועדפת עליך", @@ -1218,6 +1226,7 @@ "they_will_be_merged_together": "הם יתמזגו יחד", "third_party_resources": "משאבי צד שלישי", "time_based_memories": "זכרונות מבוססי זמן", + "timeline": "ציר זמן", "timezone": "אזור זמן", "to_archive": "העבר לארכיון", "to_change_password": "שנה סיסמה", @@ -1227,6 +1236,7 @@ "to_trash": "אשפה", "toggle_settings": "החלף מצב הגדרות", "toggle_theme": "החלף ערכת נושא כהה", + "total": "סה\"כ", "total_usage": "שימוש כולל", "trash": "אשפה", "trash_all": "העבר הכל לאשפה", @@ -1276,6 +1286,8 @@ "user_purchase_settings_description": "נהל את הרכישה שלך", "user_role_set": "הגדר את {user} בתור {role}", "user_usage_detail": "פרטי השימוש של המשתמש", + "user_usage_stats": "סטטיסטיקות שימוש בחשבון", + "user_usage_stats_description": "הצג סטטיסטיקות שימוש בחשבון", "username": "שם משתמש", "users": "משתמשים", "utilities": "כלים", @@ -1297,6 +1309,7 @@ "view_all_users": "הצג את כל המשתמשים", "view_in_timeline": "ראה בציר הזמן", "view_links": "הצג קישורים", + "view_name": "לצפות", "view_next_asset": "הצג את הנכס הבא", "view_previous_asset": "הצג את הנכס הקודם", "view_stack": "הצג ערימה", diff --git a/i18n/hr.json b/i18n/hr.json index 3cf4c7d55495f..d4273b8741665 100644 --- a/i18n/hr.json +++ b/i18n/hr.json @@ -34,6 +34,11 @@ "authentication_settings_disable_all": "Jeste li sigurni da želite onemogućenit sve načine prijave? Prijava će biti potpuno onemogućena.", "authentication_settings_reenable": "Za ponovno uključivanje upotrijebite naredbu poslužitelja.", "background_task_job": "Pozadinski zadaci", + "backup_database": "Sigurnosna kopija baze podataka", + "backup_database_enable_description": "Omogućite sigurnosne kopije baze podataka", + "backup_keep_last_amount": "Količina prethodnih sigurnosnih kopija za čuvanje", + "backup_settings": "Postavke sigurnosne kopije", + "backup_settings_description": "Upravljanje postavkama sigurnosne kopije baze podataka", "check_all": "Provjeri sve", "cleared_jobs": "Izbrisani poslovi za: {job}", "config_set_by_file": "Konfiguracija je trenutno postavljena konfiguracijskom datotekom", @@ -43,6 +48,9 @@ "confirm_reprocess_all_faces": "Jeste li sigurni da želite ponovno obraditi sva lica? Ovo će također obrisati imenovane osobe.", "confirm_user_password_reset": "Jeste li sigurni da želite poništiti lozinku korisnika {user}?", "create_job": "Izradi zadatak", + "cron_expression": "Cron izraz (expression)", + "cron_expression_description": "Postavite interval skeniranja koristeći cron format. Za više informacija pogledajte npr. Crontab Guru", + "cron_expression_presets": "Cron unaprijed postavljene postavke izraza", "disable_login": "Onemogući prijavu", "duplicate_detection_job_description": "Pokrenite strojno učenje na materijalima kako biste otkrili slične slike. Oslanja se na Pametno Pretraživanje", "exclusion_pattern_description": "Uzorci izuzimanja omogućuju vam da zanemarite datoteke i mape prilikom skeniranja svoje biblioteke. Ovo je korisno ako imate mape koje sadrže datoteke koje ne želite uvesti, kao što su RAW datoteke.", @@ -62,9 +70,15 @@ "image_prefer_wide_gamut_setting_description": "Koristite Display P3 za sličice. Ovo bolje čuva živost slika sa širokim prostorima boja, ali slike mogu izgledati drugačije na starim uređajima sa starom verzijom preglednika. sRGB slike čuvaju se kao sRGB kako bi se izbjegle promjene boja.", "image_preview_description": "Slika srednje veličine s ogoljenim metapodacima, koristi se prilikom pregledavanja jednog sredstva i za strojno učenje", "image_preview_quality_description": "Kvaliteta pregleda od 1-100. Više je bolje, ali proizvodi veće datoteke i može smanjiti odziv aplikacije. Postavljanje niske vrijednosti može utjecati na kvalitetu strojnog učenja.", + "image_preview_title": "Postavke pregleda", "image_quality": "Kvaliteta", + "image_resolution": "Rezolucija", + "image_resolution_description": "Veće razlučivosti mogu sačuvati više detalja, ali trebaju dulje za kodiranje, imaju veće veličine datoteka i mogu smanjiti odziv aplikacije.", "image_settings": "Postavke slike", "image_settings_description": "Upravljajte kvalitetom i rezolucijom generiranih slika", + "image_thumbnail_description": "Mala minijatura s ogoljenim metapodacima, koristi se pri gledanju grupa fotografija poput glavne vremenske trake", + "image_thumbnail_quality_description": "Kvaliteta sličica od 1-100. Više je bolje, ali proizvodi veće datoteke i može smanjiti odziv aplikacije.", + "image_thumbnail_title": "Postavke sličica", "job_concurrency": "{job} istovremenost", "job_created": "Zadatak je kreiran", "job_not_concurrency_safe": "Ovaj posao nije siguran za istovremenost.", @@ -203,10 +217,13 @@ "require_password_change_on_login": "Zahtijevajte od korisnika promjenu lozinke pri prvoj prijavi", "reset_settings_to_default": "Vrati postavke na zadane", "reset_settings_to_recent_saved": "Resetirajte postavke na nedavno spremljene postavke", + "scanning_library": "Skeniranje biblioteke", "search_jobs": "Traži zadatke…", "send_welcome_email": "Pošaljite email dobrodošlice", "server_external_domain_settings": "Vanjska domena", "server_external_domain_settings_description": "Domena za javno dijeljene linkove, uključujući http(s)://", + "server_public_users": "Javni korisnici", + "server_public_users_description": "Svi korisnici (ime i e-pošta) navedeni su prilikom dodavanja korisnika u dijeljene albume. Kada je onemogućeno, popis korisnika bit će dostupan samo korisnicima administratora.", "server_settings": "Postavke servera", "server_settings_description": "Upravljanje postavkama servera", "server_welcome_message": "Poruka dobrodošlice", @@ -404,6 +421,7 @@ "birthdate_saved": "Datum rođenja uspješno spremljen", "birthdate_set_description": "Datum rođenja se koristi za izračunavanje godina ove osobe u trenutku fotografije.", "blurred_background": "Zamućena pozadina", + "bugs_and_feature_requests": "Bugovi i zahtjevi za značajke", "build": "Sagradi (Build)", "build_image": "Sagradi (Build) Image", "bulk_delete_duplicates_confirmation": "Jeste li sigurni da želite skupno izbrisati {count, plural, one {# duplicate asset} other {# duplicate asset}}? Ovo će zadržati najveće sredstvo svake grupe i trajno izbrisati sve druge duplikate. Ne možete poništiti ovu radnju!", @@ -449,6 +467,7 @@ "confirm": "Potvrdi", "confirm_admin_password": "Potvrdite lozinku administratora", "confirm_delete_shared_link": "Jeste li sigurni da želite izbrisati ovu zajedničku vezu?", + "confirm_keep_this_delete_others": "Sva druga sredstva u nizu bit će izbrisana osim ovog sredstva. Jeste li sigurni da želite nastaviti?", "confirm_password": "Potvrdite lozinku", "contain": "Sadrži", "context": "Kontekst", @@ -498,11 +517,13 @@ "delete_key": "Ključ za brisanje", "delete_library": "Izbriši knjižnicu", "delete_link": "Izbriši poveznicu", + "delete_others": "Izbriši druge", "delete_shared_link": "Izbriši dijeljenu poveznicu", "delete_tag": "Izbriši oznaku", "delete_tag_confirmation_prompt": "Jeste li sigurni da želite izbrisati oznaku {tagName}?", "delete_user": "Izbriši korisnika", "deleted_shared_link": "Izbrisana dijeljena poveznica", + "deletes_missing_assets": "Briše sredstva koja nedostaju s diska", "description": "Opis", "details": "Detalji", "direction": "Smjer", @@ -587,6 +608,7 @@ "failed_to_create_shared_link": "Stvaranje dijeljene veze nije uspjelo", "failed_to_edit_shared_link": "Nije uspjelo uređivanje dijeljene poveznice", "failed_to_get_people": "Dohvaćanje ljudi nije uspjelo", + "failed_to_keep_this_delete_others": "Zadržavanje ovog sredstva i brisanje ostalih sredstava nije uspjelo", "failed_to_load_asset": "Učitavanje sredstva nije uspjelo", "failed_to_load_assets": "Učitavanje sredstava nije uspjelo", "failed_to_load_people": "Učitavanje ljudi nije uspjelo", @@ -654,6 +676,7 @@ "unable_to_remove_album_users": "Nije moguće ukloniti korisnike iz albuma", "unable_to_remove_api_key": "Nije moguće ukloniti API ključ", "unable_to_remove_assets_from_shared_link": "Nije moguće ukloniti sredstva iz dijeljene poveznice", + "unable_to_remove_deleted_assets": "Nije moguće ukloniti izvanmrežne datoteke", "unable_to_remove_library": "Nije moguće ukloniti biblioteku", "unable_to_remove_partner": "Nije moguće ukloniti partnera", "unable_to_remove_reaction": "Nije moguće ukloniti reakciju", @@ -769,6 +792,8 @@ "jobs": "Poslovi", "keep": "Zadrži", "keep_all": "Zadrži Sve", + "keep_this_delete_others": "Zadrži ovo, izbriši ostale", + "kept_this_deleted_others": "Zadržana je ova datoteka i izbrisano {count, plural, one {# datoteka} other {# datoteka}}", "keyboard_shortcuts": "Prečaci tipkovnice", "language": "Jezik", "language_setting_description": "Odaberite željeni jezik", @@ -801,6 +826,7 @@ "look": "Izgled", "loop_videos": "Ponavljajte videozapise", "loop_videos_description": "Omogućite automatsko ponavljanje videozapisa u pregledniku detalja.", + "main_branch_warning": "Koristite razvojnu verziju; strogo preporučamo korištenje izdane verzije!", "make": "Proizvođač", "manage_shared_links": "Upravljanje dijeljenim vezama", "manage_sharing_with_partners": "Upravljajte dijeljenjem s partnerima", @@ -870,6 +896,7 @@ "notifications": "Obavijesti", "notifications_setting_description": "Upravljanje obavijestima", "oauth": "OAuth", + "official_immich_resources": "Službeni Immich resursi", "offline": "Izvan mreže", "offline_paths": "Izvanmrežne putanje", "offline_paths_description": "Ovi rezultati mogu biti posljedica ručnog brisanja datoteka koje nisu dio vanjske biblioteke.", @@ -998,11 +1025,13 @@ "recent_searches": "Nedavne pretrage", "refresh": "Osvježi", "refresh_encoded_videos": "Osvježite kodirane videozapise", + "refresh_faces": "Osvježite lica", "refresh_metadata": "Osvježi metapodatke", "refresh_thumbnails": "Osvježi sličice", "refreshed": "Osvježeno", "refreshes_every_file": "Osvježava svaku datoteku", "refreshing_encoded_video": "Osvježavanje kodiranog videa", + "refreshing_faces": "Osvježavanje lica", "refreshing_metadata": "Osvježavanje metapodataka", "regenerating_thumbnails": "Obnavljanje sličica", "remove": "Ukloni", @@ -1010,7 +1039,7 @@ "remove_assets_shared_link_confirmation": "Jeste li sigurni da želite ukloniti {count, plural, one {# datoteku} other {# datoteke}} iz ove dijeljene veze?", "remove_assets_title": "Ukloniti datoteke?", "remove_custom_date_range": "Ukloni prilagođeni datumski raspon", - "remove_deleted_assets": "", + "remove_deleted_assets": "Ukloni izbrisana sredstva", "remove_from_album": "Ukloni iz albuma", "remove_from_favorites": "Ukloni iz favorita", "remove_from_shared_link": "Ukloni iz dijeljene poveznice", @@ -1068,13 +1097,15 @@ "search_people": "Traži ljude", "search_places": "Traži mjesta", "search_settings": "Postavke pretraživanja", - "search_state": "", + "search_state": "Država pretraživanja...", + "search_tags": "Traži oznake...", "search_timezone": "Pretraži vremenske zone", - "search_type": "", - "search_your_photos": "", - "searching_locales": "", - "second": "", - "select_album_cover": "", + "search_type": "Vrsta pretraživanja", + "search_your_photos": "Pretražite svoje fotografije", + "searching_locales": "Traženje lokaliteta...", + "second": "Drugi", + "see_all_people": "Vidi sve ljude", + "select_album_cover": "Odaberite omot albuma", "select_all": "Odaberi sve", "select_all_duplicates": "Odaberi sve duplikate", "select_avatar_color": "", @@ -1194,6 +1225,8 @@ "user": "", "user_id": "", "user_usage_detail": "", + "user_usage_stats": "Statistika korištenja računa", + "user_usage_stats_description": "Pregledajte statistiku korištenja računa", "username": "", "users": "", "utilities": "", diff --git a/i18n/hu.json b/i18n/hu.json index 9461923b5f2d5..c0582b0e31ba8 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -222,6 +222,8 @@ "send_welcome_email": "Üdvözlő email küldése", "server_external_domain_settings": "Külső domain", "server_external_domain_settings_description": "Nyilvánosan megosztott linkek domainje (http(s)://-sel)", + "server_public_users": "Nyilvános felhasználók", + "server_public_users_description": "Az összes felhasználó (név és email) ki van írva, amikor egy felhasználót adsz hozzá egy megosztott albumhoz. Amikor le van tiltva, a felhasználólista csak adminok számára lesz elérhető.", "server_settings": "Szerver Beállítások", "server_settings_description": "Szerver beállítások kezelése", "server_welcome_message": "Üdvözlő üzenet", @@ -465,6 +467,7 @@ "confirm": "Jóváhagy", "confirm_admin_password": "Admin Jelszó Újból", "confirm_delete_shared_link": "Biztosan törölni szeretnéd ezt a megosztott linket?", + "confirm_keep_this_delete_others": "Minden más elem a készletben törlésre kerül, kivéve ezt az elemet. Biztosan folytatni szeretnéd?", "confirm_password": "Jelszó megerősítése", "contain": "Belül", "context": "Kontextus", @@ -514,6 +517,7 @@ "delete_key": "Kulcs törlése", "delete_library": "Képtár Törlése", "delete_link": "Link törlése", + "delete_others": "Többi törlése", "delete_shared_link": "Megosztott link törlése", "delete_tag": "Címke törlése", "delete_tag_confirmation_prompt": "Biztosan törölni szeretnéd a(z) {tagName} címkét?", @@ -604,6 +608,7 @@ "failed_to_create_shared_link": "Megosztott link készítése sikertelen", "failed_to_edit_shared_link": "Megosztott link módosítása sikertelen", "failed_to_get_people": "Személyek lekérdezése sikertelen", + "failed_to_keep_this_delete_others": "Nem sikerült megtartani ezt az elemet, és a többi elemet törölni", "failed_to_load_asset": "Elem betöltése sikertelen", "failed_to_load_assets": "Elemek betöltése sikertelen", "failed_to_load_people": "Személyek betöltése sikertelen", @@ -787,6 +792,8 @@ "jobs": "Feladatok", "keep": "Megtart", "keep_all": "Összeset Megtart", + "keep_this_delete_others": "Ennek a meghagyása, a többi törlése", + "kept_this_deleted_others": "Ennek az elemnek és a törölteknek meghagyása {count, plural, one {# asset} other {# assets}}", "keyboard_shortcuts": "Billentyűparancsok", "language": "Nyelv", "language_setting_description": "Válaszd ki preferált nyelvet", @@ -1218,6 +1225,7 @@ "they_will_be_merged_together": "Egyesítve lesznek", "third_party_resources": "Harmadik Féltől Származó Források", "time_based_memories": "Emlékek idő alapján", + "timeline": "Idővonal", "timezone": "Időzóna", "to_archive": "Archiválás", "to_change_password": "Jelszó megváltoztatása", @@ -1227,6 +1235,7 @@ "to_trash": "Lomtárba helyezés", "toggle_settings": "Beállítások átállítása", "toggle_theme": "Sötét téma átváltása", + "total": "Összesen", "total_usage": "Összesen használatban", "trash": "Lomtár", "trash_all": "Mindet lomtárba", @@ -1276,6 +1285,8 @@ "user_purchase_settings_description": "Vásárlás kezelése", "user_role_set": "{user} felhasználónak {role} jogkör biztosítása", "user_usage_detail": "Felhasználó használati adatai", + "user_usage_stats": "Fiók használati statisztikái", + "user_usage_stats_description": "Fiók használati statisztikáinak megtekintése", "username": "Felhasználónév", "users": "Felhasználók", "utilities": "Segédeszközök", @@ -1297,6 +1308,7 @@ "view_all_users": "Minden Felhasználó Megtekintése", "view_in_timeline": "Megtekintés az idővonalon", "view_links": "Linkek megtekintése", + "view_name": "Megtekintés", "view_next_asset": "Következő elem megtekintése", "view_previous_asset": "Előző elem megtekintése", "view_stack": "Csoport Megtekintése", diff --git a/i18n/id.json b/i18n/id.json index a3defe3842e50..8ae53e04649be 100644 --- a/i18n/id.json +++ b/i18n/id.json @@ -222,6 +222,8 @@ "send_welcome_email": "Kirim surel selamat datang", "server_external_domain_settings": "Domain eksternal", "server_external_domain_settings_description": "Domain untuk tautan terbagi publik, termasuk http(s)://", + "server_public_users": "Pengguna Publik", + "server_public_users_description": "Semua pengguna (nama dan email) didaftarkan ketika menambahkan pengguna ke album terbagi. Ketika dinonaktifkan, daftar pengguna hanya akan tersedia kepada pengguna admin.", "server_settings": "Pengaturan Server", "server_settings_description": "Kelola pengaturan server", "server_welcome_message": "Pesan selamat datang", @@ -465,6 +467,7 @@ "confirm": "Konfirmasi", "confirm_admin_password": "Konfirmasi Kata Sandi Admin", "confirm_delete_shared_link": "Apakah Anda yakin ingin menghapus tautan terbagi ini?", + "confirm_keep_this_delete_others": "Semua aset lain di dalam stack akan dihapus kecuali aset ini. Anda yakin untuk melanjutkan?", "confirm_password": "Konfirmasi kata sandi", "contain": "Berisi", "context": "Konteks", @@ -514,6 +517,7 @@ "delete_key": "Hapus kunci", "delete_library": "Hapus Pustaka", "delete_link": "Hapus tautan", + "delete_others": "Hapus lainnya", "delete_shared_link": "Hapus tautan terbagi", "delete_tag": "Hapus tag", "delete_tag_confirmation_prompt": "Apakah Anda yakin ingin menghapus label tag {tagName}?", @@ -604,6 +608,7 @@ "failed_to_create_shared_link": "Gagal membuat tautan terbagi", "failed_to_edit_shared_link": "Gagal menyunting tautan terbagi", "failed_to_get_people": "Gagal mendapatkan orang", + "failed_to_keep_this_delete_others": "Gagal mempertahankan aset ini dan hapus aset-aset lainnya", "failed_to_load_asset": "Gagal membuka aset", "failed_to_load_assets": "Gagal membuka aset-aset", "failed_to_load_people": "Gagal mengunggah orang", @@ -787,6 +792,8 @@ "jobs": "Tugas", "keep": "Simpan", "keep_all": "Simpan Semua", + "keep_this_delete_others": "Pertahankan ini, hapus lainnya", + "kept_this_deleted_others": "Aset ini dipertahankan dan {count, plural, one {# asset} other {# assets}} dihapus", "keyboard_shortcuts": "Pintasan papan ketik", "language": "Bahasa", "language_setting_description": "Pilih bahasa Anda yang disukai", @@ -1218,6 +1225,7 @@ "they_will_be_merged_together": "Mereka akan digabungkan bersama", "third_party_resources": "Sumber Daya Pihak Ketiga", "time_based_memories": "Kenangan berbasis waktu", + "timeline": "Lini masa", "timezone": "Zona waktu", "to_archive": "Arsipkan", "to_change_password": "Ubah kata sandi", @@ -1227,6 +1235,7 @@ "to_trash": "Sampah", "toggle_settings": "Saklar pengaturan", "toggle_theme": "Beralih tema gelap", + "total": "Jumlah", "total_usage": "Jumlah penggunaan", "trash": "Sampah", "trash_all": "Buang Semua", @@ -1276,6 +1285,8 @@ "user_purchase_settings_description": "Atur pembelian kamu", "user_role_set": "Tetapkan {user} sebagai {role}", "user_usage_detail": "Detail penggunaan pengguna", + "user_usage_stats": "Statistik penggunaan akun", + "user_usage_stats_description": "Tampilkan statistik penggunaan akun", "username": "Nama pengguna", "users": "Pengguna", "utilities": "Peralatan", @@ -1297,6 +1308,7 @@ "view_all_users": "Tampilkan semua pengguna", "view_in_timeline": "Lihat di timeline", "view_links": "Tampilkan tautan", + "view_name": "Tampilkan", "view_next_asset": "Tampilkan aset berikutnya", "view_previous_asset": "Tampilkan aset sebelumnya", "view_stack": "Tampilkan Tumpukan", diff --git a/i18n/it.json b/i18n/it.json index f3e85802fbf76..ebde6072b469a 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -23,6 +23,7 @@ "add_to": "Aggiungi a...", "add_to_album": "Aggiungi all'album", "add_to_shared_album": "Aggiungi all'album condiviso", + "add_url": "Aggiungi URL", "added_to_archive": "Aggiunto all'archivio", "added_to_favorites": "Aggiunto ai preferiti", "added_to_favorites_count": "Aggiunti {count, number} ai preferiti", @@ -222,6 +223,8 @@ "send_welcome_email": "Invia email di benvenuto", "server_external_domain_settings": "Dominio esterno", "server_external_domain_settings_description": "Dominio per link condivisi pubblicamente, incluso http(s)://", + "server_public_users": "Utenti Pubblici", + "server_public_users_description": "Tutti gli utenti (nome ed e-mail) sono elencati quando si aggiunge un utente agli album condivisi. Quando disabilitato, l'elenco degli utenti sarà disponibile solo per gli utenti amministratori.", "server_settings": "Impostazioni Server", "server_settings_description": "Gestisci le impostazioni del server", "server_welcome_message": "Messaggio di benvenuto", @@ -239,7 +242,7 @@ "storage_template_migration_description": "Applica il {template} attuale agli asset caricati in precedenza", "storage_template_migration_info": "Le modifiche al modello di archiviazione verranno applicate solo agli asset nuovi. Per applicare le modifiche retroattivamente esegui {job}.", "storage_template_migration_job": "Processo Migrazione Modello di Archiviazione", - "storage_template_more_details": "Per più informazioni riguardo a questa funzionalità, consulta il Modello Archiviazione e le sue conseguenze", + "storage_template_more_details": "Per maggiori informazioni riguardo a questa funzionalità, consulta il Modello Archiviazione e le sue conseguenze", "storage_template_onboarding_description": "Quando attivata, questa funzionalità organizzerà automaticamente i file utilizzando il modello di archiviazione definito dall'utente. Per ragioni di stabilità, questa funzionalità è disabilitata per impostazione predefinita. Per più informazioni, consulta la documentazione.", "storage_template_path_length": "Limite approssimativo lunghezza percorso: {length, number}/{limit, number}", "storage_template_settings": "Modello Archiviazione", @@ -247,6 +250,9 @@ "storage_template_user_label": "{label} è l'etichetta di archiviazione dell'utente", "system_settings": "Impostazioni di sistema", "tag_cleanup_job": "Pulisci Tag", + "template_email_preview": "Anteprima", + "template_email_settings": "Template Email", + "template_settings": "Templates Notifiche", "theme_custom_css_settings": "CSS Personalizzato", "theme_custom_css_settings_description": "I Cascading Style Sheets (CSS) permettono di personalizzare l'interfaccia di Immich.", "theme_settings": "Impostazioni Tema", @@ -465,6 +471,7 @@ "confirm": "Conferma", "confirm_admin_password": "Conferma password amministratore", "confirm_delete_shared_link": "Sei sicuro di voler eliminare questo link condiviso?", + "confirm_keep_this_delete_others": "Tutti gli altri asset nello stack saranno eliminati, eccetto questo asset. Vuoi continuare?", "confirm_password": "Conferma password", "contain": "Adatta", "context": "Contesto", @@ -514,6 +521,7 @@ "delete_key": "Elimina chiave", "delete_library": "Elimina Libreria", "delete_link": "Elimina link", + "delete_others": "Elimina altri", "delete_shared_link": "Elimina link condiviso", "delete_tag": "Elimina tag", "delete_tag_confirmation_prompt": "Sei sicuro di voler cancellare il tag {tagName}?", @@ -604,6 +612,7 @@ "failed_to_create_shared_link": "Creazione del link condivisibile non riuscita", "failed_to_edit_shared_link": "Errore durante la modifica del link condivisibile", "failed_to_get_people": "Impossibile ottenere le persone", + "failed_to_keep_this_delete_others": "Impossibile conservare questa risorsa ed eliminare le altre risorse", "failed_to_load_asset": "Errore durante il caricamento della risorsa", "failed_to_load_assets": "Errore durante il caricamento delle risorse", "failed_to_load_people": "Caricamento delle persone non riuscito", @@ -787,6 +796,7 @@ "jobs": "Processi", "keep": "Mantieni", "keep_all": "Tieni tutto", + "keep_this_delete_others": "Tieni questo, elimina gli altri", "keyboard_shortcuts": "Scorciatoie da tastiera", "language": "Lingua", "language_setting_description": "Seleziona la tua lingua predefinita", @@ -944,18 +954,18 @@ "permanently_delete": "Elimina definitivamente", "permanently_delete_assets_count": "Cancella definitivamente {count, plural, one {l'asset} other {gli assets}}", "permanently_delete_assets_prompt": "Sei sicuro di voler cancellare definitivamente {count, plural, one {questo asset?} other {# assets?}} Questa operazione {count, plural, one {lo cancellerà dal suo} other {li cancellerà dai loro}} album.", - "permanently_deleted_asset": "Elimina asset definitivamente", + "permanently_deleted_asset": "Asset eliminato definitivamente", "permanently_deleted_assets_count": "Cancellati {count, plural, one {# asset} other {# assets}} definitivamente", "person": "Persona", "person_hidden": "{name}{hidden, select, true { (nascosto)} other {}}", - "photo_shared_all_users": "Sembra che tu abbia condiviso le foto con tutti gli utenti (oppure non hai utenti con cui condividerle).", + "photo_shared_all_users": "Sembra che tu abbia condiviso le foto con tutti gli utenti, oppure che non ci siano utenti con i quali condividerle.", "photos": "Foto", "photos_and_videos": "Foto & Video", "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Foto}}", "photos_from_previous_years": "Foto degli anni scorsi", "pick_a_location": "Scegli una posizione", "place": "Posizione", - "places": "Location", + "places": "Luoghi", "play": "Avvia", "play_memories": "Avvia ricordi", "play_motion_photo": "Avvia Foto in movimento", @@ -1276,6 +1286,8 @@ "user_purchase_settings_description": "Gestisci il tuo acquisto", "user_role_set": "Imposta {user} come {role}", "user_usage_detail": "Dettagli utilizzo utente", + "user_usage_stats": "Statistiche d'uso", + "user_usage_stats_description": "Consulta le statistiche d'uso dell'account", "username": "Nome utente", "users": "Utenti", "utilities": "Utilità", diff --git a/i18n/ms.json b/i18n/ms.json index 9494d485b58c5..0474caa542957 100644 --- a/i18n/ms.json +++ b/i18n/ms.json @@ -1,5 +1,5 @@ { - "about": "Tentang", + "about": "Kemaskini", "account": "Akaun", "account_settings": "Tetapan Akaun", "acknowledge": "Akui", @@ -34,6 +34,11 @@ "authentication_settings_disable_all": "Adakah anda pasti mahu melumpuhkan semua kaedah log masuk? Log masuk akan dilumpuhkan sepenuhnya.", "authentication_settings_reenable": "Untuk menghidupkan semula, guna Arahan Pelayan.", "background_task_job": "Tugas Latar Belakang", + "backup_database": "Sandar pangkalan data", + "backup_database_enable_description": "Aktifkan sandaran pangkalan data", + "backup_keep_last_amount": "Jumlah sandaran sebelumnya yang hendak disimpan", + "backup_settings": "Tetapan Sandaran", + "backup_settings_description": "Urus tetapan sandaran pangkalan data", "check_all": "Tanda Semua", "cleared_jobs": "Kerja telah dibersihkan untuk: {job}", "config_set_by_file": "Konfigurasi kini ditetapkan oleh fail konfigurasi", @@ -43,6 +48,8 @@ "confirm_reprocess_all_faces": "Adakah anda pasti mahu memproses semula semua wajah? Ini juga akan membersihkan orang bernama.", "confirm_user_password_reset": "Adakah anda pasti mahu menetapkan semula kata laluan {user}?", "create_job": "Cipta tugas", + "cron_expression": "Ungkapan cron", + "cron_expression_presets": "Pratetap-pratetap ungkapan Cron", "disable_login": "Lumpuhkan fungsi log masuk", "duplicate_detection_job_description": "Jalankan pembelajaran mesin pada aset untuk mengesan imej yang serupa. Bergantung pada Carian Pintar", "exclusion_pattern_description": "Corak pengecualian membolehkan anda mengabaikan fail dan folder semasa mengimbas pustaka anda. Ini berguna jika anda mempunyai folder yang mengandungi fail yang anda tidak mahu import, seperti fail RAW.", @@ -114,6 +121,19 @@ "machine_learning_max_recognition_distance_description": "Jarak maksimum antara dua muka untuk dianggap sebagai orang yang sama, antara 0-2. Menurunkan ini boleh menghalang pelabelan dua orang sebagai orang yang sama, manakala menaikkannya boleh menghalang pelabelan orang yang sama sebagai dua orang yang berbeza. Ambil perhatian bahawa adalah lebih mudah untuk menggabungkan dua orang daripada membelah satu orang kepada dua, jadi silap pada bahagian ambang yang lebih rendah apabila boleh.", "machine_learning_min_detection_score": "Skor pengesanan minimum", "machine_learning_min_detection_score_description": "Skor keyakinan minimum untuk wajah dikesan dari 0-1. Nilai yang lebih rendah akan mengesan lebih banyak muka tetapi mungkin menghasilkan positif palsu.", - "machine_learning_min_recognized_faces": "Minimum mengenali wajah" - } + "machine_learning_min_recognized_faces": "Minimum mengenali wajah", + "machine_learning_min_recognized_faces_description": "Bilangan minima wajah yang dikenali untuk seseorang dicipta. Peningkatan ini menjadikan Pengecaman Wajah lebih tepat atas kos meningkatkan peluang wajah tidak diberikan kepada seseorang.", + "machine_learning_settings": "Tetapan Pembelajaran Mesin", + "map_dark_style": "Tema gelap", + "map_enable_description": "Aktifkan ciri peta", + "map_gps_settings": "Tetapan Peta & GPS", + "map_light_style": "Tema terang", + "map_settings": "Peta", + "map_settings_description": "Urus tetapan peta", + "notification_email_from_address": "Dari alamat", + "notification_email_from_address_description": "Alamat e-mel penghantar, sebagai contoh: \"Immich Photo Server \"", + "notification_settings": "Tetapan Pemberitahuan" + }, + "user_usage_stats": "Statistik penggunaan akaun", + "user_usage_stats_description": "Papar statistik penggunaan akaun" } diff --git a/i18n/nb_NO.json b/i18n/nb_NO.json index 3f74b3a7d664a..958d08e62f899 100644 --- a/i18n/nb_NO.json +++ b/i18n/nb_NO.json @@ -1,5 +1,5 @@ { - "about": "Om", + "about": "Oppdater", "account": "Konto", "account_settings": "Konto Innstillinger", "acknowledge": "Bekreft", @@ -33,6 +33,11 @@ "authentication_settings_disable_all": "Er du sikker på at du ønsker å deaktivere alle innloggingsmetoder? Innlogging vil bli fullstendig deaktivert.", "authentication_settings_reenable": "For å aktivere på nytt, bruk en Server Command.", "background_task_job": "Bakgrunnsjobber", + "backup_database": "Backupdatabase", + "backup_database_enable_description": "Aktiver databasebackup", + "backup_keep_last_amount": "Antall backuper å beholde", + "backup_settings": "Backupinnstillinger", + "backup_settings_description": "Håndter innstillinger for databasebackup", "check_all": "Merk Alle", "cleared_jobs": "Ryddet opp jobber for: {job}", "config_set_by_file": "Konfigurasjonen er for øyeblikket satt av en konfigurasjonsfil", @@ -41,6 +46,7 @@ "confirm_email_below": "For å bekrefte, skriv inn \"{email}\" nedenfor", "confirm_reprocess_all_faces": "Er du sikker på at du vil behandle alle ansikter på nytt? Dette vil også fjerne navngitte personer.", "confirm_user_password_reset": "Er du sikker på at du vil tilbakestille passordet til {user}?", + "create_job": "Lag jobb", "disable_login": "Deaktiver innlogging", "duplicate_detection_job_description": "Kjør maskinlæring på filer for å oppdage lignende bilder. Krever bruk av Smart Search", "exclusion_pattern_description": "Ekskluderingsmønstre lar deg ignorere filer og mapper når du skanner biblioteket ditt. Dette er nyttig hvis du har mapper som inneholder filer du ikke vil importere, for eksempel RAW-filer.", @@ -52,12 +58,15 @@ "failed_job_command": "Kommandoen {command} feilet for jobben: {job}", "force_delete_user_warning": "ADVARSEL: Dette vil umiddelbart fjerne brukeren og alle eiendeler. Dette kan ikke angres, og filene kan ikke gjenopprettes.", "forcing_refresh_library_files": "Tvinger oppdatering av alle bibliotekfiler", + "image_format": "Format", "image_format_description": "WebP gir mindre filer enn JPEG, men er tregere å lage.", "image_prefer_embedded_preview": "Foretrekk innebygd forhåndsvisning", "image_prefer_embedded_preview_setting_description": "Bruk innebygd forhåndsvisning i RAW-bilder som inndata til bildebehandling når tilgjengelig. Dette kan gi mer nøyaktige farger for noen bilder, men kvaliteten er avhengig av kamera og bildet kan ha komprimeringsartefakter.", "image_prefer_wide_gamut": "Foretrekk bredt fargespekter", "image_prefer_wide_gamut_setting_description": "Bruk Display P3 for miniatyrbilder. Dette bevarer glød bedre i bilder med bredt fargerom, men det kan hende bilder ser annerledes ut på gamle enheter med en gammel nettleserversjon. sRBG bilder beholdes som sRGB for å unngå fargeforskyvninger.", + "image_preview_title": "Forhåndsvisningsinnstillinger", "image_quality": "Kvalitet", + "image_resolution": "Oppløsning", "image_settings": "Bildeinnstilliinger", "image_settings_description": "Administrer kvalitet og oppløsning på genererte bilder", "job_concurrency": "{job} samtidighet", diff --git a/i18n/nl.json b/i18n/nl.json index 3420c5d10585f..9a16efe6e9a29 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -222,11 +222,13 @@ "send_welcome_email": "Stuur een welkomstmail", "server_external_domain_settings": "Extern domein", "server_external_domain_settings_description": "Domein voor openbaar gedeelde links, inclusief http(s)://", + "server_public_users": "Openbare gebruikerslijst", + "server_public_users_description": "Alle gebruikers (met naam en e-mailadres) worden weergegeven wanneer een gebruiker wordt toegevoegd aan gedeelde albums. Wanneer uitgeschakeld, is de gebruikerslijst alleen beschikbaar voor beheerders.", "server_settings": "Serverinstellingen", "server_settings_description": "Beheer serverinstellingen", "server_welcome_message": "Welkomstbericht", "server_welcome_message_description": "Een bericht dat op de inlogpagina wordt weergegeven.", - "sidecar_job": "Sidecar metadata", + "sidecar_job": "Sidecar metagegevens", "sidecar_job_description": "Zoek of synchroniseer sidecar metadata van het bestandssysteem", "slideshow_duration_description": "Aantal seconden dat iedere afbeelding wordt getoond", "smart_search_job_description": "Voer machine learning uit op assets om te gebruiken voor slim zoeken", @@ -302,7 +304,7 @@ "transcoding_preferred_hardware_device_description": "Geldt alleen voor VAAPI en QSV. Stelt de dri node in die wordt gebruikt voor hardwaretranscodering.", "transcoding_preset_preset": "Preset (-preset)", "transcoding_preset_preset_description": "Compressiesnelheid. Langzamere presets produceren kleinere bestanden en verhogen de kwaliteit bij het targeten van een bepaalde bitrate. VP9 negeert snelheden boven 'faster'.", - "transcoding_reference_frames": "Reference frames", + "transcoding_reference_frames": "Referentie frames", "transcoding_reference_frames_description": "Het aantal frames om naar te verwijzen bij het comprimeren van een bepaald frame. Hogere waarden verbeteren de compressie-efficiëntie, maar vertragen de codering. Bij 0 wordt deze waarde automatisch ingesteld.", "transcoding_required_description": "Alleen video's die geen geaccepteerd formaat hebben", "transcoding_settings": "Instellingen voor videotranscodering", @@ -317,7 +319,7 @@ "transcoding_tone_mapping_description": "Probeert het uiterlijk van HDR-video's te behouden wanneer ze worden geconverteerd naar SDR. Elk algoritme maakt verschillende afwegingen voor kleur, detail en helderheid. Hable behoudt detail, Mobius behoudt kleur en Reinhard behoudt helderheid.", "transcoding_transcode_policy": "Transcodeerbeleid", "transcoding_transcode_policy_description": "Beleid voor wanneer een video getranscodeerd moet worden. HDR-video's worden altijd getranscodeerd (behalve als transcodering is uitgeschakeld).", - "transcoding_two_pass_encoding": "Two-pass encoding", + "transcoding_two_pass_encoding": "Two-pass encodering", "transcoding_two_pass_encoding_setting_description": "Transcodeer in twee passes om beter gecodeerde video's te produceren. Wanneer de maximale bitrate is ingeschakeld (vereist om te werken met H.264 en HEVC), gebruikt deze modus een bitraterange op basis van de maximale bitrate en negeert CRF. Voor VP9 kan CRF worden gebruikt als de maximale bitrate is uitgeschakeld.", "transcoding_video_codec": "Video codec", "transcoding_video_codec_description": "VP9 heeft een hoge efficiëntie en webcompatibiliteit, maar duurt langer om te transcoderen. HEVC presteert vergelijkbaar, maar heeft een lagere webcompatibiliteit. H.264 is breed compatibel en snel om te transcoderen, maar produceert veel grotere bestanden. AV1 is de meest efficiënte codec, maar mist ondersteuning op oudere apparaten.", @@ -475,6 +477,7 @@ "confirm": "Bevestigen", "confirm_admin_password": "Bevestig beheerder wachtwoord", "confirm_delete_shared_link": "Weet je zeker dat je deze gedeelde link wilt verwijderen?", + "confirm_keep_this_delete_others": "Alle andere assets in de stack worden verwijderd, behalve deze. Weet je zeker dat je wilt doorgaan?", "confirm_password": "Bevestig wachtwoord", "contain": "Bevat", "context": "Context", @@ -524,6 +527,7 @@ "delete_key": "Verwijder sleutel", "delete_library": "Verwijder bibliotheek", "delete_link": "Verwijder link", + "delete_others": "Andere verwijderen", "delete_shared_link": "Verwijder gedeelde link", "delete_tag": "Tag verwijderen", "delete_tag_confirmation_prompt": "Weet je zeker dat je de tag {tagName} wilt verwijderen?", @@ -614,6 +618,7 @@ "failed_to_create_shared_link": "Fout bij maken van gedeelde link", "failed_to_edit_shared_link": "Fout bij bewerken van gedeelde link", "failed_to_get_people": "Fout bij ophalen van mensen", + "failed_to_keep_this_delete_others": "Het is niet gelukt om dit asset te behouden en de andere assets te verwijderen", "failed_to_load_asset": "Kan asset niet laden", "failed_to_load_assets": "Kan assets niet laden", "failed_to_load_people": "Kan mensen niet laden", @@ -662,7 +667,7 @@ "unable_to_empty_trash": "Kan prullenbak niet legen", "unable_to_enter_fullscreen": "Kan volledig scherm niet openen", "unable_to_exit_fullscreen": "Kan volledig scherm niet afsluiten", - "unable_to_get_comments_number": "Kan het aantal opmerkingen niet ophalen", + "unable_to_get_comments_number": "Niet mogelijk om het aantal opmerkingen op te halen", "unable_to_get_shared_link": "Kan gedeelde link niet ophalen", "unable_to_hide_person": "Kan persoon niet verbergen", "unable_to_link_motion_video": "Kan bewegende video niet verbinden", @@ -797,6 +802,8 @@ "jobs": "Taken", "keep": "Behouden", "keep_all": "Behoud alle", + "keep_this_delete_others": "Deze behouden, andere verwijderen", + "kept_this_deleted_others": "Deze asset behouden en {count, plural, one {# andere asset} other {# andere assets}} verwijderd", "keyboard_shortcuts": "Sneltoetsen", "language": "Taal", "language_setting_description": "Selecteer je voorkeurstaal", @@ -1228,6 +1235,7 @@ "they_will_be_merged_together": "Zij zullen worden samengevoegd", "third_party_resources": "Bronnen van derden", "time_based_memories": "Tijdgebaseerde herinneringen", + "timeline": "Tijdlijn", "timezone": "Tijdzone", "to_archive": "Archiveren", "to_change_password": "Wijzig wachtwoord", @@ -1237,6 +1245,7 @@ "to_trash": "Prullenbak", "toggle_settings": "Zichtbaarheid instellingen wisselen", "toggle_theme": "Donker thema toepassen", + "total": "Totaal", "total_usage": "Totaal gebruik", "trash": "Prullenbak", "trash_all": "Verplaats alle naar prullenbak", @@ -1286,6 +1295,8 @@ "user_purchase_settings_description": "Beheer je aankoop", "user_role_set": "{user} instellen als {role}", "user_usage_detail": "Gedetailleerd gebruik van gebruikers", + "user_usage_stats": "Statistieken van accountgebruik", + "user_usage_stats_description": "Bekijk statistieken van accountgebruik", "username": "Gebruikersnaam", "users": "Gebruikers", "utilities": "Gereedschap", @@ -1307,6 +1318,7 @@ "view_all_users": "Bekijk alle gebruikers", "view_in_timeline": "Bekijk in tijdlijn", "view_links": "Links bekijken", + "view_name": "Bekijken", "view_next_asset": "Bekijk volgende asset", "view_previous_asset": "Bekijk vorige asset", "view_stack": "Bekijk stapel", diff --git a/i18n/pl.json b/i18n/pl.json index 7e966c1cb56ec..8e2e52e03f5ec 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -222,6 +222,8 @@ "send_welcome_email": "Wyślij powitalny e-mail", "server_external_domain_settings": "Domena zewnętrzna", "server_external_domain_settings_description": "Domena dla publicznie udostępnionych linków, wraz z http(s)://", + "server_public_users": "Użytkownicy publiczni", + "server_public_users_description": "Wszyscy użytkownicy (nazwa i adres e-mail) są wymienieni podczas dodawania użytkownika do udostępnionych albumów. Po wyłączeniu lista użytkowników będzie dostępna tylko dla administratorów.", "server_settings": "Ustawienia Serwera", "server_settings_description": "Zarządzaj ustawieniami serwera", "server_welcome_message": "Wiadomość powitalna", @@ -1223,6 +1225,7 @@ "they_will_be_merged_together": "Zostaną one ze sobą połączone", "third_party_resources": "Zasoby stron trzecich", "time_based_memories": "Wspomnienia oparte na czasie", + "timeline": "Oś czasu", "timezone": "Strefa czasowa", "to_archive": "Archiwum", "to_change_password": "Zmień hasło", @@ -1232,6 +1235,7 @@ "to_trash": "Kosz", "toggle_settings": "Przełącz ustawienia", "toggle_theme": "Przełącz ciemny motyw", + "total": "Całkowity", "total_usage": "Całkowite wykorzystanie", "trash": "Kosz", "trash_all": "Usuń wszystko", @@ -1281,6 +1285,8 @@ "user_purchase_settings_description": "Zarządzaj swoim zakupem", "user_role_set": "Ustaw {user} jako {role}", "user_usage_detail": "Szczegóły używania przez użytkownika", + "user_usage_stats": "Statystyki użytkowania konta", + "user_usage_stats_description": "Wyświetl statystyki użytkowania konta", "username": "Nazwa użytkownika", "users": "Użytkownicy", "utilities": "Narzędzia", @@ -1302,6 +1308,7 @@ "view_all_users": "Pokaż wszystkich użytkowników", "view_in_timeline": "Pokaż na osi czasu", "view_links": "Pokaż łącza", + "view_name": "Widok", "view_next_asset": "Wyświetl następny zasób", "view_previous_asset": "Wyświetl poprzedni zasób", "view_stack": "Zobacz Ułożenie", diff --git a/i18n/pt.json b/i18n/pt.json index 2677d0ffe37a5..3d9198644d8c2 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -222,6 +222,8 @@ "send_welcome_email": "Enviar e-mail de boas-vindas", "server_external_domain_settings": "Domínio externo", "server_external_domain_settings_description": "Domínio para links públicos partilhados, incluindo http(s)://", + "server_public_users": "Utilizadores Públicos", + "server_public_users_description": "Todos os utilizadores (nome e e-mail) serão listados quando adicionar um utilizador a álbuns partilhados. Quando desativado, a lista de utilizadores só será visível a administradores.", "server_settings": "Definições do Servidor", "server_settings_description": "Gerir definições do servidor", "server_welcome_message": "Mensagem de boas-vindas", @@ -465,6 +467,7 @@ "confirm": "Confirmar", "confirm_admin_password": "Confirmar palavra-passe de administrador", "confirm_delete_shared_link": "Tem a certeza de que deseja eliminar este link partilhado?", + "confirm_keep_this_delete_others": "Todos os outros ficheiros na pilha serão eliminados, exceto este ficheiro. Tem a certeza de que deseja continuar?", "confirm_password": "Confirmar a palavra-passe", "contain": "Ajustar", "context": "Contexto", @@ -514,6 +517,7 @@ "delete_key": "Eliminar chave", "delete_library": "Eliminar Biblioteca", "delete_link": "Eliminar link", + "delete_others": "Excluir outros", "delete_shared_link": "Eliminar link de partilha", "delete_tag": "Eliminar etiqueta", "delete_tag_confirmation_prompt": "Tem a certeza de que pretende eliminar a etiqueta {tagName} ?", @@ -604,6 +608,7 @@ "failed_to_create_shared_link": "Não foi possível criar o link partilhado", "failed_to_edit_shared_link": "Não foi possível editar o link partilhado", "failed_to_get_people": "Não foi possível obter pessoas", + "failed_to_keep_this_delete_others": "Ocorreu um erro ao manter este ficheiro e eliminar os outros", "failed_to_load_asset": "Não foi possível ler o ficheiro", "failed_to_load_assets": "Não foi possível ler ficheiros", "failed_to_load_people": "Não foi possível carregar pessoas", @@ -787,6 +792,8 @@ "jobs": "Tarefas", "keep": "Manter", "keep_all": "Manter Todos", + "keep_this_delete_others": "Manter este ficheiro, eliminar os outros", + "kept_this_deleted_others": "Foi mantido ficheiro e {count, plural, one {eliminado # outro} other {eliminados # outros}}", "keyboard_shortcuts": "Atalhos do teclado", "language": "Idioma", "language_setting_description": "Selecione o seu Idioma preferido", @@ -1218,6 +1225,7 @@ "they_will_be_merged_together": "Eles serão unidos", "third_party_resources": "Recursos de terceiros", "time_based_memories": "Memórias baseadas no tempo", + "timeline": "Linha de tempo", "timezone": "Fuso horário", "to_archive": "Arquivar", "to_change_password": "Alterar palavra-passe", @@ -1227,6 +1235,7 @@ "to_trash": "Reciclagem", "toggle_settings": "Alternar configurações", "toggle_theme": "Ativar modo escuro", + "total": "Total", "total_usage": "Total utilizado", "trash": "Reciclagem", "trash_all": "Mover todos para a reciclagem", @@ -1276,6 +1285,8 @@ "user_purchase_settings_description": "Gerir a sua compra", "user_role_set": "Definir {user} como {role}", "user_usage_detail": "Detalhes de utilização do utilizador", + "user_usage_stats": "Estatísticas de utilização de conta", + "user_usage_stats_description": "Ver estatísticas de utilização de conta", "username": "Nome de utilizador", "users": "Utilizadores", "utilities": "Ferramentas", @@ -1297,6 +1308,7 @@ "view_all_users": "Ver todos os utilizadores", "view_in_timeline": "Ver na linha do tempo", "view_links": "Ver links", + "view_name": "Ver", "view_next_asset": "Ver próximo ficheiro", "view_previous_asset": "Ver ficheiro anterior", "view_stack": "Ver pilha", diff --git a/i18n/pt_BR.json b/i18n/pt_BR.json index ae129e603d980..4c4608cf7220d 100644 --- a/i18n/pt_BR.json +++ b/i18n/pt_BR.json @@ -23,6 +23,7 @@ "add_to": "Adicionar a...", "add_to_album": "Adicionar ao álbum", "add_to_shared_album": "Adicionar ao álbum compartilhado", + "add_url": "Adicionar URL", "added_to_archive": "Adicionado ao arquivo", "added_to_favorites": "Adicionado aos favoritos", "added_to_favorites_count": "{count, plural, one {{count, number} adicionado aos favoritos} other {{count, number} adicionados aos favoritos}}", @@ -44,7 +45,7 @@ "config_set_by_file": "A configuração está atualmente definida por um arquivo de configuração", "confirm_delete_library": "Você tem certeza que deseja excluir a biblioteca {library} ?", "confirm_delete_library_assets": "Você tem certeza que deseja excluir esta biblioteca? Isso excluirá {count, plural, one {# arquivo contido do Immich e não poderá ser desfeito. O arquivo permanecerá no disco} other {todos os # arquivos contidos do Immich e não poderá ser desfeito. Os arquivos permanecerão no disco}}.", - "confirm_email_below": "Para confirmar, digite o {email} abaixo", + "confirm_email_below": "Para confirmar, digite \"{email}\" abaixo", "confirm_reprocess_all_faces": "Tem certeza de que deseja reprocessar todos os rostos? Isso também limpará as pessoas nomeadas.", "confirm_user_password_reset": "Tem certeza de que deseja redefinir a senha de {user}?", "create_job": "Criar tarefa", @@ -130,7 +131,7 @@ "machine_learning_smart_search_description": "Buscar imagens semanticamente usando embeddings CLIP", "machine_learning_smart_search_enabled": "Habilitar a Pesquisa Inteligente", "machine_learning_smart_search_enabled_description": "Se desativado, as imagens não serão codificadas para pesquisa inteligente.", - "machine_learning_url_description": "URL do servidor de inteligência artificial", + "machine_learning_url_description": "A URL do servidor de inteligência artificial. Se mais de uma URL for configurada, o servidor irá tentar uma de cada vez até que uma delas responda com sucesso, em ordem sequencial igual a configurada.", "manage_concurrency": "Gerenciar simultaneidade", "manage_log_settings": "Gerenciar configurações de registro", "map_dark_style": "Tema Escuro", @@ -159,7 +160,7 @@ "note_apply_storage_label_previous_assets": "Observação: Para aplicar o rótulo de armazenamento a arquivos carregados anteriormente, execute o", "note_cannot_be_changed_later": "NOTA: Isto não pode ser alterado posteriormente!", "note_unlimited_quota": "Observação: insira 0 para cota ilimitada", - "notification_email_from_address": "A partir do endereço", + "notification_email_from_address": "E-mail de origem", "notification_email_from_address_description": "Endereço de e-mail do remetente, por exemplo: \"Immich Photo Server \"", "notification_email_host_description": "Host do servidor de e-mail (por exemplo, smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignorar erros de certificado", @@ -170,8 +171,8 @@ "notification_email_setting_description": "Configurações para envio de notificações por e-mail", "notification_email_test_email": "Enviar e-mail de teste", "notification_email_test_email_failed": "Falha ao enviar e-mail de teste. Verifique seus valores", - "notification_email_test_email_sent": "Um email de teste foi enviado para {email}. Por favor, verifique sua caixa de entrada.", - "notification_email_username_description": "Nome de usuário a ser usado ao autenticar com o servidor de e-mail", + "notification_email_test_email_sent": "Um e-mail de teste foi enviado para {email}. Por favor, verifique sua caixa de entrada.", + "notification_email_username_description": "Nome de usuário que será usado para autenticar com o servidor de e-mail", "notification_enable_email_notifications": "Habilitar notificações por e-mail", "notification_settings": "Configurações de notificação", "notification_settings_description": "Gerenciar configurações de notificação, incluindo e-mail", @@ -222,6 +223,8 @@ "send_welcome_email": "Enviar e-mail de boas-vindas", "server_external_domain_settings": "Domínio externo", "server_external_domain_settings_description": "Domínio para links públicos compartilhados, incluindo http(s)://", + "server_public_users": "Usuários públicos", + "server_public_users_description": "Todos os usuários (nome e e-mail) serão exibidos na lista de adicionar usuários em álbuns compartilhados. Quando desativado, essa lista de usuários só será visível aos administradores.", "server_settings": "Configurações do servidor", "server_settings_description": "Gerenciar configurações do servidor", "server_welcome_message": "Mensagem de boas-vindas", @@ -247,6 +250,16 @@ "storage_template_user_label": "{label} é o Rótulo de Armazenamento do usuário", "system_settings": "Configurações do Sistema", "tag_cleanup_job": "Limpeza de tags", + "template_email_available_tags": "Você pode usar as seguintes variáveis no modelo: {tags}", + "template_email_if_empty": "Se o modelo estiver em branco, o modelo de e-mail padrão será usado.", + "template_email_invite_album": "Modelo do e-mail de convite para álbum", + "template_email_preview": "Pré visualização", + "template_email_settings": "Modelos de e-mail", + "template_email_settings_description": "Gerenciar modelos personalizados de e-mail de notificação", + "template_email_update_album": "Modelo do e-mail de atualização do álbum", + "template_email_welcome": "Modelo do e-mail de boas vindas", + "template_settings": "Modelos de notificação", + "template_settings_description": "Gerenciar modelos personalizados para notificações.", "theme_custom_css_settings": "CSS customizado", "theme_custom_css_settings_description": "Folhas de estilo em cascata permitem que o design do Immich seja personalizado.", "theme_settings": "Configurações de tema", @@ -465,6 +478,7 @@ "confirm": "Confirmar", "confirm_admin_password": "Confirmar senha de administrador", "confirm_delete_shared_link": "Tem certeza de que deseja excluir este link compartilhado?", + "confirm_keep_this_delete_others": "Todos os outros arquivos da pilha serão excluídos, exceto este arquivo. Tem certeza de que deseja continuar?", "confirm_password": "Confirme a senha", "contain": "Caber", "context": "Contexto", @@ -514,6 +528,7 @@ "delete_key": "Excluir chave", "delete_library": "Excluir biblioteca", "delete_link": "Excluir link", + "delete_others": "Excluir restante", "delete_shared_link": "Excluir link de compartilhamento", "delete_tag": "Remover tag", "delete_tag_confirmation_prompt": "Tem certeza que deseja excluir a tag {tagName} ?", @@ -604,6 +619,7 @@ "failed_to_create_shared_link": "Falha ao criar o link compartilhado", "failed_to_edit_shared_link": "Falha ao editar o link compartilhado", "failed_to_get_people": "Falha na obtenção de pessoas", + "failed_to_keep_this_delete_others": "Falha ao manter este arquivo e excluir os outros", "failed_to_load_asset": "Não foi possível carregar o ativo", "failed_to_load_assets": "Não foi possível carregar os ativos", "failed_to_load_people": "Falha ao carregar pessoas", @@ -718,6 +734,7 @@ "external": "Externo", "external_libraries": "Bibliotecas externas", "face_unassigned": "Sem nome", + "failed_to_load_assets": "Falha ao carregar arquivos", "favorite": "Favorito", "favorite_or_unfavorite_photo": "Marque ou desmarque a foto como favorita", "favorites": "Favoritos", @@ -787,6 +804,8 @@ "jobs": "Tarefas", "keep": "Manter", "keep_all": "Manter Todos", + "keep_this_delete_others": "Manter este, excluir o resto", + "kept_this_deleted_others": "Este foi mantido e {count, plural, one {# arquivo foi excluído} other {# arquivos foram excluídos}}", "keyboard_shortcuts": "Atalhos do teclado", "language": "Idioma", "language_setting_description": "Selecione seu Idioma preferido", @@ -1015,6 +1034,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {# arquivo reatribuído} other {# arquivos reatribuídos}} a uma nova pessoa", "reassing_hint": "Atribuir arquivos selecionados a uma pessoa existente", "recent": "Recente", + "recent-albums": "Álbuns recentes", "recent_searches": "Pesquisas recentes", "refresh": "Atualizar", "refresh_encoded_videos": "Atualizar vídeos codificados", @@ -1036,6 +1056,7 @@ "remove_from_album": "Remover do álbum", "remove_from_favorites": "Remover dos favoritos", "remove_from_shared_link": "Remover do link compartilhado", + "remove_url": "Remover URL", "remove_user": "Remover usuário", "removed_api_key": "Removido a Chave de API: {name}", "removed_from_archive": "Removido do arquivo", @@ -1218,6 +1239,7 @@ "they_will_be_merged_together": "Eles serão mesclados", "third_party_resources": "Recursos de terceiros", "time_based_memories": "Memórias baseada no tempo", + "timeline": "Linha do tempo", "timezone": "Fuso horário", "to_archive": "Arquivar", "to_change_password": "Alterar senha", @@ -1227,6 +1249,7 @@ "to_trash": "Mover para a lixeira", "toggle_settings": "Alternar configurações", "toggle_theme": "Alternar tema escuro", + "total": "Total", "total_usage": "Utilização total", "trash": "Lixeira", "trash_all": "Mover todos para o lixo", @@ -1276,6 +1299,8 @@ "user_purchase_settings_description": "Gerenciar sua compra", "user_role_set": "Definir {user} como {role}", "user_usage_detail": "Detalhes de uso do usuário", + "user_usage_stats": "Estatísticas de utilização de conta", + "user_usage_stats_description": "Ver estatísticas de utilização de conta", "username": "Nome do usuário", "users": "Usuários", "utilities": "Utilitários", @@ -1297,6 +1322,7 @@ "view_all_users": "Ver todos usuários", "view_in_timeline": "Ver na linha do tempo", "view_links": "Ver links", + "view_name": "Ver", "view_next_asset": "Ver próximo arquivo", "view_previous_asset": "Ver arquivo anterior", "view_stack": "Exibir Pilha", diff --git a/i18n/ru.json b/i18n/ru.json index d2326a275c1ea..909cda6738792 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -222,6 +222,8 @@ "send_welcome_email": "Отправить приветственное письмо", "server_external_domain_settings": "Внешний домен", "server_external_domain_settings_description": "Домен для публичных ссылок, включая http(s)://", + "server_public_users": "Публичные пользователи", + "server_public_users_description": "Отображать всех пользователей (имена и email) для добавления в общие альбомы. Когда отключено, список пользователей будет доступен только администраторам.", "server_settings": "Настройки сервера", "server_settings_description": "Управление настройками сервера", "server_welcome_message": "Приветственное сообщение", @@ -465,7 +467,7 @@ "confirm": "Подтвердить", "confirm_admin_password": "Подтвердите пароль Администратора", "confirm_delete_shared_link": "Вы уверены, что хотите удалить эту публичную ссылку?", - "confirm_keep_this_delete_others": "Все остальные объекты в стеке будут удалены, кроме этого объекта. Вы уверены, что хотите продолжить?", + "confirm_keep_this_delete_others": "Все остальные объекты в серии будут удалены, кроме этого объекта. Вы уверены, что хотите продолжить?", "confirm_password": "Подтвердите пароль", "contain": "Вместить", "context": "Контекст", @@ -1183,11 +1185,11 @@ "sort_recent": "Недавние фото", "sort_title": "Заголовок", "source": "Исходный код", - "stack": "В стопку", - "stack_duplicates": "Стек дубликатов", - "stack_select_one_photo": "Выберите одну главную фотографию для стека", - "stack_selected_photos": "Сложить выбранные фотографии в стопку", - "stacked_assets_count": "{count, plural, one {# объект добавлен} few {# объекта добавлено} other {# объектов добавлено}} в стек", + "stack": "Превратить в серию", + "stack_duplicates": "Превратить дубликаты в серию", + "stack_select_one_photo": "Выберите главную фотографию для серии", + "stack_selected_photos": "Объединить выбранные объекты в серию", + "stacked_assets_count": "{count, plural, one {# объект добавлен} few {# объекта добавлено} other {# объектов добавлено}} в серию", "stacktrace": "Трассировка стека", "start": "Старт", "start_date": "Дата начала", @@ -1223,6 +1225,7 @@ "they_will_be_merged_together": "Они будут объединены вместе", "third_party_resources": "Сторонние ресурсы", "time_based_memories": "Воспоминания, основанные на времени", + "timeline": "Временная шкала", "timezone": "Часовой пояс", "to_archive": "В архив", "to_change_password": "Изменить пароль", @@ -1232,6 +1235,7 @@ "to_trash": "Корзина", "toggle_settings": "Переключение настроек", "toggle_theme": "Переключение темы", + "total": "Всего", "total_usage": "Общее использование", "trash": "Корзина", "trash_all": "Удалить всё", @@ -1256,8 +1260,8 @@ "unsaved_change": "Не сохраненное изменение", "unselect_all": "Снять всё", "unselect_all_duplicates": "Отменить выбор всех дубликатов", - "unstack": "Разобрать стек", - "unstacked_assets_count": "{count, plural, one {# объект извлечен} few {# объекта извлечено} other {# объектов извлечено}} из стека", + "unstack": "Разгруппировать серию", + "unstacked_assets_count": "{count, plural, one {# объект извлечен} few {# объекта извлечено} other {# объектов извлечено}} из серии", "untracked_files": "НЕОТСЛЕЖИВАЕМЫЕ ФАЙЛЫ", "untracked_files_decription": "Приложение не отслеживает эти файлы. Они могут быть результатом неудачных перемещений, прерванных загрузок или пропущены из-за ошибки", "up_next": "Следующее", @@ -1281,6 +1285,8 @@ "user_purchase_settings_description": "Управление покупкой", "user_role_set": "Установить {user} в качестве {role}", "user_usage_detail": "Подробная информация об использовании пользователем", + "user_usage_stats": "Статистика использования аккаунта", + "user_usage_stats_description": "Посмотреть статистику использования аккаунта", "username": "Имя пользователя", "users": "Пользователи", "utilities": "Утилиты", @@ -1302,6 +1308,7 @@ "view_all_users": "Показать всех пользователей", "view_in_timeline": "Показать на временной шкале", "view_links": "Показать ссылки", + "view_name": "Посмотреть", "view_next_asset": "Показать следующий объект", "view_previous_asset": "Показать предыдущий объект", "view_stack": "Показать стек", diff --git a/i18n/sk.json b/i18n/sk.json index 22202359bc3b3..fd58d50308f86 100644 --- a/i18n/sk.json +++ b/i18n/sk.json @@ -2,7 +2,7 @@ "about": "Obnoviť", "account": "Účet", "account_settings": "Nastavenia účtu", - "acknowledge": "Potvrdiť", + "acknowledge": "Rozumiem", "action": "Akcia", "actions": "Akcie", "active": "Aktívny", @@ -222,6 +222,8 @@ "send_welcome_email": "Odoslať uvítací e-mail", "server_external_domain_settings": "Externá doména", "server_external_domain_settings_description": "Verejná doména pre zdieľané odkazy, vrátane http(s)://", + "server_public_users": "Verejní užívatelia", + "server_public_users_description": "Všetci užívatelia (meno a email) sú uvedení pri pridávaní užívateľa do zdieľaných albumov. Ak je táto funkcia vypnutá, zoznam užívateľov bude dostupný iba správcom.", "server_settings": "Nastavenia servera", "server_settings_description": "Spravovať nastavenia servera", "server_welcome_message": "Uvítacia správa", @@ -233,7 +235,7 @@ "storage_template_date_time_description": "Časová pečiatka vytvorenia médií sa používa pre informácie o dátume a čase", "storage_template_date_time_sample": "Čas vzorky {date}", "storage_template_enable_description": "Povoliť nástroj šablóny úložiska", - "storage_template_hash_verification_enabled": "Hash overenie povolené", + "storage_template_hash_verification_enabled": "Overenie hash povolené", "storage_template_hash_verification_enabled_description": "Povolí overenie hash, nezakazujte to, pokiaľ si nie ste istí dôsledkami", "storage_template_migration": "Migrácia šablóny úložiska", "storage_template_migration_description": "Použite aktuálnu {template} na predtým nahrané médiá", @@ -392,35 +394,58 @@ "asset_adding_to_album": "Pridáva sa do albumu...", "asset_description_updated": "Popis média bol aktualizovaný", "asset_filename_is_offline": "Médium {filename} je offline", - "asset_offline": "", + "asset_has_unassigned_faces": "Položka má nepriradené tváre", + "asset_hashing": "Hašovanie...", + "asset_offline": "Médium je offline", + "asset_offline_description": "Toto externý obsah sa už nenachádza na disku. Požiadajte o pomoc svojho správcu Immich.", "asset_skipped": "Preskočené", "asset_skipped_in_trash": "V koši", "asset_uploaded": "Nahrané", "asset_uploading": "Nahráva sa...", "assets": "Položky", + "assets_added_count": "{count, plural, one {Pridaná # položka} few {Pridané # položky} other {Pridaných # položek}}", + "assets_added_to_album_count": "Do albumu {count, plural, one {bola pridaná # položka} few {boli pridané # položky} other {bolo pridaných # položiek}}", + "assets_added_to_name_count": "{count, plural, one {Pridaná # položka} few {Pridané # položky} other {Pridaných # položiek}} do {hasName, select, true {alba {name}} other {nového albumu}}", + "assets_count": "{count, plural, one {# položka} few {# položky} other {# položiek}}", + "assets_moved_to_trash_count": "Do koša {count, plural, one {bola presunutá # položka} few {boli presunuté # položky} other {bolo presunutých # položiek}}", + "assets_permanently_deleted_count": "Trvalo {count, plural, one {vymazaná # položka} few {vymazané # položky} other {vymazaných # položiek}}", + "assets_removed_count": "{count, plural, one {Odstránená # položka} few {Odstránené # položky} other {Odstránených # položiek}}", + "assets_restore_confirmation": "Naozaj chcete obnoviť všetky vyhodené položky? Túto akciu nie je možné vrátiť späť! Upozorňujeme, že týmto spôsobom nie je možné obnoviť žiadne offline položky.", + "assets_restored_count": "{count, plural, one {Obnovená # položka} few {Obnovené # položky} other {Obnovených # položiek}}", + "assets_trashed_count": "{count, plural, one {Odstránená # položka} few {Odstránené # položky} other {Odstránených # položiek}}", + "assets_were_part_of_album_count": "{count, plural, one {Položka bola} other {Položky boli}} súčasťou albumu", "authorized_devices": "Autorizované zariadenia", "back": "Späť", - "backward": "", + "back_close_deselect": "Späť, zavrieť alebo zrušiť výber", + "backward": "Spätne", "birthdate_saved": "Dátum narodenia bol úspešne uložený", - "blurred_background": "", + "birthdate_set_description": "Dátum narodenia sa používa na výpočet veku tejto osoby v čase fotografie.", + "blurred_background": "Rozmazané pozadie", + "bugs_and_feature_requests": "Chyby a požiadavky na funkcie", + "build": "Budovať", + "build_image": "Vytvoriť obrázok", + "bulk_delete_duplicates_confirmation": "Naozaj chcete hromadne odstrániť {count, plural, one {# duplikátnu položku} few {# duplikáte položky} other {# duplikátnych položiek}}? Týmto sa zachová najväčšia položka z každej skupiny a všetky ostatné duplikáty sa natrvalo odstránia. Túto akciu nie je možné vrátiť späť!", "buy": "Kúpiť Immich", "camera": "Fotoaparát", "camera_brand": "Výrobca fotoaparátu", "camera_model": "Model fotoaparátu", "cancel": "Zrušiť", "cancel_search": "Zrušiť vyhľadávanie", - "cannot_merge_people": "", + "cannot_merge_people": "Nie je možné zlúčiť ľudí", + "cannot_undo_this_action": "Túto akciu nemôžete vrátiť späť!", "cannot_update_the_description": "Popis nie je možné aktualizovať", "change_date": "Upraviť dátum", "change_expiration_time": "Zmeniť čas vypršania", "change_location": "Upraviť lokáciu", "change_name": "Upraviť meno", - "change_name_successfully": "", + "change_name_successfully": "Meno bolo zmenené", "change_password": "Zmeniť Heslo", - "change_your_password": "", - "changed_visibility_successfully": "", + "change_password_description": "Buď sa do systému prihlasujete prvýkrát, alebo bola podaná žiadosť o zmenu hesla. Nižšie zadajte nové heslo.", + "change_your_password": "Zmeňte si heslo", + "changed_visibility_successfully": "Viditeľnosť bola úspešne zmenená", "check_all": "Skontrolovať Všetko", "check_logs": "Skontrolovať logy", + "choose_matching_people_to_merge": "Vyberte rovnakých ľudí na zlúčenie", "city": "Mesto", "clear": "VYMAZAŤ", "clear_all": "Vymazať všetko", @@ -429,14 +454,18 @@ "clear_value": "Vymazať hodnotu", "clockwise": "V smere hodinových ručičiek", "close": "Zatvoriť", - "collapse_all": "", - "color_theme": "", + "collapse": "Zbaliť", + "collapse_all": "Zbaliť všetko", + "color": "Farba", + "color_theme": "Farba témy", "comment_deleted": "Komentár bol odstránený", "comment_options": "Možnosti komentára", + "comments_and_likes": "Komentáre a páči sa mi to", "comments_are_disabled": "Komentáre sú vypnuté", "confirm": "Potvrdiť", "confirm_admin_password": "Potvrdiť Administrátorské Heslo", "confirm_delete_shared_link": "Ste si istý, že chcete odstrániť tento zdieľaný odkaz?", + "confirm_keep_this_delete_others": "Všetky ostatné položky v zásobníku budú odstránené okrem tejto položky. Naozaj chcete pokračovať?", "confirm_password": "Potvrdiť heslo", "contain": "", "context": "Kontext", @@ -444,20 +473,21 @@ "copied_image_to_clipboard": "Obrázok skopírovaný do schránky.", "copied_to_clipboard": "Skopírované do schránky!", "copy_error": "Chyba pri kopírovaní", - "copy_file_path": "", + "copy_file_path": "Kopírovať cestu odkazu", "copy_image": "Skopírovať obrázok", "copy_link": "Skopírovať odkaz", "copy_link_to_clipboard": "Skopírovať do schránky", "copy_password": "Skopírovať heslo", "copy_to_clipboard": "Skopírovať do schránky", "country": "Štát", - "cover": "", - "covers": "", + "cover": "Titulka", + "covers": "Dlaždice", "create": "Vytvoriť", "create_album": "Vytvoriť album", "create_library": "Vytvoriť knižnicu", "create_link": "Vytvoriť odkaz", "create_link_to_share": "Vytvoriť odkaz na zdieľanie", + "create_link_to_share_description": "Umožniť každému kto má odkaz zobraziť vybrané fotografie", "create_new_person": "Vytvoriť novú osobu", "create_new_user": "Vytvorenie nového používateľa", "create_tag": "Vytvoriť značku", @@ -949,6 +979,8 @@ "user_id": "Používateľské ID", "user_role_set": "Nastav {user} ako {role}", "user_usage_detail": "", + "user_usage_stats": "Štatistiky využitia účtu", + "user_usage_stats_description": "Zobraziť štatistiky využitia účtu", "username": "Používateľské meno", "users": "Používatelia", "utilities": "Nástroje", diff --git a/i18n/sl.json b/i18n/sl.json index a61adbba93cbb..3c3c67e1313ba 100644 --- a/i18n/sl.json +++ b/i18n/sl.json @@ -202,7 +202,7 @@ "oauth_storage_quota_default_description": "Kvota v GiB, ki se uporabi, ko ni predložen noben zahtevek (vnesite 0 za neomejeno kvoto).", "offline_paths": "Poti brez povezave", "offline_paths_description": "Ti rezultati so morda posledica ročnega brisanja datotek, ki niso del zunanje knjižnice.", - "password_enable_description": "Prijava se z e-pošto in geslom", + "password_enable_description": "Prijava z e-pošto in geslom", "password_settings": "Prijava z geslom", "password_settings_description": "Upravljajte nastavitve prijave z geslom", "paths_validated_successfully": "Vse poti so bile uspešno potrjene", @@ -222,6 +222,8 @@ "send_welcome_email": "Pošlji pozdravno e-pošto", "server_external_domain_settings": "Zunanja domena", "server_external_domain_settings_description": "Domena za javne skupne povezave, vključno s http(s)://", + "server_public_users": "Javni uporabniki", + "server_public_users_description": "Vsi uporabniki (ime in e-pošta) so navedeni pri dodajanju uporabnika v albume v skupni rabi. Ko je onemogočen, bo seznam uporabnikov na voljo samo skrbniškim uporabnikom.", "server_settings": "Nastavitve strežnika", "server_settings_description": "Upravljanje nastavitev strežnika", "server_welcome_message": "Pozdravno sporočilo", @@ -400,7 +402,7 @@ "asset_skipped_in_trash": "V smetnjak", "asset_uploaded": "Naloženo", "asset_uploading": "Nalaganje ...", - "assets": "sredstva", + "assets": "Sredstva", "assets_added_count": "Dodano{count, plural, one {# sredstvo} other {# sredstev}}", "assets_added_to_album_count": "Dodano{count, plural, one {# sredstvo} other {# sredstev}} v album", "assets_added_to_name_count": "Dodano {count, plural, one {# sredstvo} other {# sredstev}} v {hasName, select, true {{name}} other {new album}}", @@ -465,6 +467,7 @@ "confirm": "Potrdi", "confirm_admin_password": "Potrdite skrbniško geslo", "confirm_delete_shared_link": "Ali ste prepričani, da želite izbrisati to skupno povezavo?", + "confirm_keep_this_delete_others": "Vsa druga sredstva v skladu bodo izbrisana, razen tega sredstva. Ste prepričani, da želite nadaljevati?", "confirm_password": "Potrdi geslo", "contain": "Vsebuje", "context": "Kontekst", @@ -514,6 +517,7 @@ "delete_key": "Izbriši ključ", "delete_library": "Izbriši knjižnico", "delete_link": "Izbriši povezavo", + "delete_others": "Izbriši ostale", "delete_shared_link": "Izbriši povezavo skupne rabe", "delete_tag": "Izbriši oznako", "delete_tag_confirmation_prompt": "Ali ste prepričani, da želite izbrisati oznako {tagName}?", @@ -604,6 +608,7 @@ "failed_to_create_shared_link": "Povezave v skupni rabi ni bilo mogoče ustvariti", "failed_to_edit_shared_link": "Povezave v skupni rabi ni bilo mogoče urediti", "failed_to_get_people": "Oseb ni bilo mogoče pridobiti", + "failed_to_keep_this_delete_others": "Tega sredstva ni bilo mogoče obdržati in izbrisati ostalih sredstev", "failed_to_load_asset": "Sredstva ni bilo mogoče naložiti", "failed_to_load_assets": "Sredstev ni bilo mogoče naložiti", "failed_to_load_people": "Oseb ni bilo mogoče naložiti", @@ -784,9 +789,11 @@ "invite_people": "Povabi ljudi", "invite_to_album": "Povabi v album", "items_count": "{count, plural, one {# predmet} other {# predmetov}}", - "jobs": "Dela", + "jobs": "Opravila", "keep": "Obdrži", "keep_all": "Obdrži vse", + "keep_this_delete_others": "Obdrži to, izbriši ostalo", + "kept_this_deleted_others": "Obdrži to sredstvo in izbriši {count, plural, one {# sredstvo} other {# sredstev}}", "keyboard_shortcuts": "Bližnjice na tipkovnici", "language": "Jezik", "language_setting_description": "Izberite želeni jezik", @@ -1218,6 +1225,7 @@ "they_will_be_merged_together": "Združeni bodo skupaj", "third_party_resources": "Viri tretjih oseb", "time_based_memories": "Časovni spomini", + "timeline": "Časovnica", "timezone": "Časovni pas", "to_archive": "Arhiv", "to_change_password": "Spremeni geslo", @@ -1227,6 +1235,7 @@ "to_trash": "Smetnjak", "toggle_settings": "Preklopi na nastavitve", "toggle_theme": "Preklopi na temno temo", + "total": "Skupno", "total_usage": "Skupna poraba", "trash": "Smetnjak", "trash_all": "Vse v smetnjak", @@ -1276,6 +1285,8 @@ "user_purchase_settings_description": "Upravljajte svoj nakup", "user_role_set": "Nastavi {user} kot {role}", "user_usage_detail": "Podrobnosti o uporabi uporabnika", + "user_usage_stats": "Statistika uporabe računa", + "user_usage_stats_description": "Oglejte si statistiko uporabe računa", "username": "Uporabniško ime", "users": "Uporabniki", "utilities": "Pripomočki", @@ -1297,6 +1308,7 @@ "view_all_users": "Ogled vseh uporabnikov", "view_in_timeline": "Ogled na časovnici", "view_links": "Ogled povezav", + "view_name": "Pogled", "view_next_asset": "Ogled naslednjega sredstva", "view_previous_asset": "Ogled prejšnjega sredstva", "view_stack": "Ogled sklada", @@ -1307,7 +1319,7 @@ "welcome": "Dobrodošli", "welcome_to_immich": "Dobrodošli v Immich", "year": "Leto", - "years_ago": "{years, plural, one {# leto} other {# let}} ago", + "years_ago": "{years, plural, one {# leto} other {# let}} nazaj", "yes": "Da", "you_dont_have_any_shared_links": "Nimate nobenih skupnih povezav", "zoom_image": "Povečava slike" diff --git a/i18n/sr_Cyrl.json b/i18n/sr_Cyrl.json index 7fa6494079986..6f60209ad4aae 100644 --- a/i18n/sr_Cyrl.json +++ b/i18n/sr_Cyrl.json @@ -23,6 +23,7 @@ "add_to": "Додај у...", "add_to_album": "Додај у албум", "add_to_shared_album": "Додај у дељен албум", + "add_url": "Додајте URL", "added_to_archive": "Додато у архиву", "added_to_favorites": "Додато у фаворите", "added_to_favorites_count": "Додато {count, number} у фаворите", @@ -222,6 +223,8 @@ "send_welcome_email": "Пошаљите е-пошту добродошлице", "server_external_domain_settings": "Екстерни домаин", "server_external_domain_settings_description": "Домаин за јавне дељене везе, укључујући http(s)://", + "server_public_users": "Јавни корисници", + "server_public_users_description": "Сви корисници (име и адреса е-поште) су наведени приликом додавања корисника у дељене албуме. Када је онемогућена, листа корисника ће бити доступна само администраторима.", "server_settings": "Подешавања сервера", "server_settings_description": "Управљајте подешавањима сервера", "server_welcome_message": "Порука добродошлице", @@ -247,6 +250,11 @@ "storage_template_user_label": "{label} је ознака за складиштење корисника", "system_settings": "Подешавања система", "tag_cleanup_job": "Чишћење ознака (tags)", + "template_email_invite_album": "Шаблон албума позива", + "template_email_preview": "Преглед", + "template_email_settings": "Шаблони е-поште", + "template_email_settings_description": "Управљајте прилагођеним шаблонима обавештења путем е-поште", + "template_email_welcome": "Шаблон е-поште добродошлице", "theme_custom_css_settings": "Прилагођени CSS", "theme_custom_css_settings_description": "Каскадни листови стилова (CSS) омогућавају прилагођавање дизајна Immich-a.", "theme_settings": "Подешавање тема", @@ -1223,6 +1231,7 @@ "they_will_be_merged_together": "Они ће бити спојени заједно", "third_party_resources": "Ресурси трећих страна", "time_based_memories": "Сећања заснована на времену", + "timeline": "Временска линија", "timezone": "Временска зона", "to_archive": "Архивирај", "to_change_password": "Промени лозинку", @@ -1232,6 +1241,7 @@ "to_trash": "Смеће", "toggle_settings": "Намести подешавања", "toggle_theme": "Намести тамну тему", + "total": "Укупно", "total_usage": "Укупна употреба", "trash": "Отпад", "trash_all": "Баци све у отпад", @@ -1281,6 +1291,8 @@ "user_purchase_settings_description": "Управљајте куповином", "user_role_set": "Постави {user} као {role}", "user_usage_detail": "Детаљи коришћења корисника", + "user_usage_stats": "Статистика коришћења налога", + "user_usage_stats_description": "Погледајте статистику коришћења налога", "username": "Корисничко име", "users": "Корисници", "utilities": "Алати", @@ -1302,6 +1314,7 @@ "view_all_users": "Прикажи све кориснике", "view_in_timeline": "Прикажи у временској линији", "view_links": "Прикажи везе", + "view_name": "Погледати", "view_next_asset": "Погледајте следећу датотеку", "view_previous_asset": "Погледај претходну датотеку", "view_stack": "Прикажи гомилу", diff --git a/i18n/sr_Latn.json b/i18n/sr_Latn.json index ea435c41e9b79..569c4efd509c1 100644 --- a/i18n/sr_Latn.json +++ b/i18n/sr_Latn.json @@ -222,6 +222,8 @@ "send_welcome_email": "Pošaljite e-poštu dobrodošlice", "server_external_domain_settings": "Eksterni domain", "server_external_domain_settings_description": "Domain za javne deljene veze, uključujući http(s)://", + "server_public_users": "Javni korisnici", + "server_public_users_description": "Svi korisnici (ime i adresa e-pošte) su navedeni prilikom dodavanja korisnika u deljene albume. Kada je onemogućena, lista korisnika će biti dostupna samo administratorima.", "server_settings": "Podešavanja servera", "server_settings_description": "Upravljajte podešavanjima servera", "server_welcome_message": "Poruka dobrodošlice", @@ -1223,6 +1225,7 @@ "they_will_be_merged_together": "Oni će biti spojeni zajedno", "third_party_resources": "Resursi trećih strana", "time_based_memories": "Sećanja zasnovana na vremenu", + "timeline": "Vremenska linija", "timezone": "Vremenska zona", "to_archive": "Arhiviraj", "to_change_password": "Promeni lozinku", @@ -1232,6 +1235,7 @@ "to_trash": "Smeće", "toggle_settings": "Namesti podešavanja", "toggle_theme": "Namesti tamnu temu", + "total": "Ukupno", "total_usage": "Ukupna upotreba", "trash": "Otpad", "trash_all": "Baci sve u otpad", @@ -1281,6 +1285,8 @@ "user_purchase_settings_description": "Upravljajte kupovinom", "user_role_set": "Postavi {user} kao {role}", "user_usage_detail": "Detalji korišćenja korisnika", + "user_usage_stats": "Statistika korišćenja naloga", + "user_usage_stats_description": "Pogledajte statistiku korišćenja naloga", "username": "Korisničko ime", "users": "Korisnici", "utilities": "Alati", @@ -1302,6 +1308,7 @@ "view_all_users": "Prikaži sve korisnike", "view_in_timeline": "Prikaži u vremenskoj liniji", "view_links": "Prikaži veze", + "view_name": "Pogledati", "view_next_asset": "Pogledajte sledeću datoteku", "view_previous_asset": "Pogledaj prethodnu datoteku", "view_stack": "Prikaži gomilu", diff --git a/i18n/tr.json b/i18n/tr.json index 5233d80d386d4..e9e16f6b60809 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -222,6 +222,8 @@ "send_welcome_email": "Hoş geldin e-postası gönder", "server_external_domain_settings": "Dış domain", "server_external_domain_settings_description": "Paylaşılan fotoğraflar için domain, http(s):// dahil", + "server_public_users": "Harici Kullanıcılar", + "server_public_users_description": "Paylaşılan albümlere bir kullanıcı eklenirken tüm kullanıcılar (ad ve e-posta) listelenir. Devre dışı bırakıldığında, kullanıcı listesi yalnızca yönetici kullanıcılar tarafından kullanılabilir.", "server_settings": "Sunucu ayarları", "server_settings_description": "Sunucu ayarlarını yönet", "server_welcome_message": "Hoş geldin mesajı", @@ -465,6 +467,7 @@ "confirm": "Onayla", "confirm_admin_password": "Yönetici Şifresini Onayla", "confirm_delete_shared_link": "Bu paylaşılan bağlantıyı silmek istediğinizden emin misiniz?", + "confirm_keep_this_delete_others": "Yığındaki diğer tüm öğeler bu varlık haricinde silinecektir. Devam etmek istediğinizden emin misiniz?", "confirm_password": "Şifreyi onayla", "contain": "İçermek", "context": "Bağlam", @@ -514,6 +517,7 @@ "delete_key": "Anahtarı sil", "delete_library": "Kütüphaneyi sil", "delete_link": "Bağlantıyı sil", + "delete_others": "Diğerlerini sil", "delete_shared_link": "Paylaşılmış linki sil", "delete_tag": "Etiketi sil", "delete_tag_confirmation_prompt": "{tagName} etiketini silmek istediğinizden emin misiniz?", @@ -604,6 +608,7 @@ "failed_to_create_shared_link": "Paylaşılan bağlantı oluşturulamadı", "failed_to_edit_shared_link": "Paylaşılan bağlantı düzenlenemedi", "failed_to_get_people": "Kişiler alınamadı", + "failed_to_keep_this_delete_others": "Bu öğenin tutulması ve diğer öğenin silinmesi başarısız oldu", "failed_to_load_asset": "Varlık yüklenemedi", "failed_to_load_assets": "Varlıklar yüklenemedi", "failed_to_load_people": "Kişiler yüklenemedi", @@ -787,6 +792,8 @@ "jobs": "Görevler", "keep": "Koru", "keep_all": "Hepsini koru", + "keep_this_delete_others": "Bunu sakla, diğerlerini sil", + "kept_this_deleted_others": "Bu varlık tutuldu ve {count, plural, one {# varlık} other {# varlık}} silindi", "keyboard_shortcuts": "Klavye kısayolları", "language": "Dil", "language_setting_description": "Tercih ettiğiniz dili seçiniz", @@ -1218,6 +1225,7 @@ "they_will_be_merged_together": "Birlikte birleştirilecekler", "third_party_resources": "Üçüncü taraf kaynaklar", "time_based_memories": "Zaman bazlı anılar", + "timeline": "Zaman Çizelgesi", "timezone": "Zaman dilimi", "to_archive": "Arşivle", "to_change_password": "Şifreyi değiştir", @@ -1227,6 +1235,7 @@ "to_trash": "Çöpe taşı", "toggle_settings": "Ayarları değiştir", "toggle_theme": "Tema değiştir", + "total": "Toplam", "total_usage": "Toplam kullanım", "trash": "Çöp", "trash_all": "Hepsini sil", @@ -1276,6 +1285,8 @@ "user_purchase_settings_description": "Satın alma işlemlerini yönet", "user_role_set": "{user}, {role} olarak ayarlandı", "user_usage_detail": "Kullanıcı kullanım detayı", + "user_usage_stats": "Hesap kullanım istatistikleri", + "user_usage_stats_description": "hesap kullanım istatistiklerini göster", "username": "Kullanıcı adı", "users": "Kullanıcılar", "utilities": "Yardımcılar", @@ -1297,6 +1308,7 @@ "view_all_users": "Tüm kullanıcıları görüntüle", "view_in_timeline": "Zaman çizelgesinde görüntüle", "view_links": "Bağlantıları göster", + "view_name": "Göster", "view_next_asset": "Sonraki dosyayı görüntüle", "view_previous_asset": "Önceki dosyayı görüntüle", "view_stack": "Yığını görüntüle", diff --git a/i18n/uk.json b/i18n/uk.json index 57ac86afe54f9..1ab2812119721 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -222,6 +222,8 @@ "send_welcome_email": "Надіслати лист з вітанням", "server_external_domain_settings": "Зовнішній домен", "server_external_domain_settings_description": "Домен для публічних загальнодоступних посилань, включаючи http(s)://", + "server_public_users": "Публічні користувачі", + "server_public_users_description": "Усі користувачі (ім'я та електронна пошта) відображаються під час додавання користувача до спільних альбомів. Якщо вимкнено, список користувачів буде доступний лише адміністраторам.", "server_settings": "Налаштування сервера", "server_settings_description": "Керування налаштуваннями сервера", "server_welcome_message": "Вітальне повідомлення", @@ -419,7 +421,7 @@ "birthdate_saved": "Дата народження успішно збережена", "birthdate_set_description": "Дата народження використовується для обчислення віку цієї особи на момент фотографії.", "blurred_background": "Розмитий фон", - "bugs_and_feature_requests": "Помилки та запити на функції", + "bugs_and_feature_requests": "Помилки та Запити", "build": "Збірка", "build_image": "Створити зображення", "bulk_delete_duplicates_confirmation": "Ви впевнені, що хочете масово видалити {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}}? Це дія залишить найбільший ресурс у кожній групі і остаточно видалить всі інші дублікати. Цю дію неможливо скасувати!", @@ -465,6 +467,7 @@ "confirm": "Підтвердіть", "confirm_admin_password": "Підтвердити пароль адміністратора", "confirm_delete_shared_link": "Ви впевнені, що хочете видалити це спільне посилання?", + "confirm_keep_this_delete_others": "Усі інші ресурси в стеку буде видалено, окрім цього ресурсу. Ви впевнені, що хочете продовжити?", "confirm_password": "Підтвердити пароль", "contain": "Містити", "context": "Контекст", @@ -514,6 +517,7 @@ "delete_key": "Видалити ключ", "delete_library": "Видалити бібліотеку", "delete_link": "Видалити посилання", + "delete_others": "Видалити інші", "delete_shared_link": "Видалити спільне посилання", "delete_tag": "Видалити тег", "delete_tag_confirmation_prompt": "Ви впевнені, що хочете видалити тег {tagName}?", @@ -604,6 +608,7 @@ "failed_to_create_shared_link": "Не вдалося створити спільне посилання", "failed_to_edit_shared_link": "Не вдалося відредагувати спільне посилання", "failed_to_get_people": "Не вдалося отримати інформацію про людей", + "failed_to_keep_this_delete_others": "Не вдалося зберегти цей ресурс і видалити інші ресурси", "failed_to_load_asset": "Не вдалося завантажити ресурс", "failed_to_load_assets": "Не вдалося завантажити ресурси", "failed_to_load_people": "Не вдалося завантажити людей", @@ -741,8 +746,8 @@ "go_to_search": "Перейти до пошуку", "group_albums_by": "Групувати альбоми за...", "group_no": "Без групування", - "group_owner": "Групування за власником", - "group_year": "Групувати за роками", + "group_owner": "За власником", + "group_year": "За роком", "has_quota": "Квота", "hi_user": "Привіт {name} ({email})", "hide_all_people": "Сховати всіх", @@ -787,6 +792,8 @@ "jobs": "Завдання", "keep": "Залишити", "keep_all": "Зберегти все", + "keep_this_delete_others": "Залишити цей ресурс, видалити інші", + "kept_this_deleted_others": "Збережено цей ресурс і видалено {count, plural, one {# ресурс} few {# ресурси} many {# ресурсів} other {# ресурсу}}", "keyboard_shortcuts": "Сполучення клавіш", "language": "Мова", "language_setting_description": "Виберіть мову, якій ви надаєте перевагу", @@ -1177,7 +1184,7 @@ "sort_oldest": "Старі фото", "sort_recent": "Нещодавні", "sort_title": "Заголовок", - "source": "Джерело", + "source": "Вихідний код", "stack": "У стопку", "stack_duplicates": "Групувати дублікати", "stack_select_one_photo": "Вибрати одне основне фото для групи", @@ -1218,6 +1225,7 @@ "they_will_be_merged_together": "Вони будуть об'єднані разом", "third_party_resources": "Ресурси третіх сторін", "time_based_memories": "Спогади, що базуються на часі", + "timeline": "Хронологія", "timezone": "Часовий пояс", "to_archive": "Архів", "to_change_password": "Змінити пароль", @@ -1227,6 +1235,7 @@ "to_trash": "Смітник", "toggle_settings": "Перемикання налаштувань", "toggle_theme": "Перемикання теми", + "total": "Усього", "total_usage": "Загальне використання", "trash": "Кошик", "trash_all": "Видалити все", @@ -1276,6 +1285,8 @@ "user_purchase_settings_description": "Керувати вашою покупкою", "user_role_set": "Призначити {user} на роль {role}", "user_usage_detail": "Деталі використання користувача", + "user_usage_stats": "Статистика використання акаунта", + "user_usage_stats_description": "Переглянути статистику використання акаунта", "username": "Ім'я користувача", "users": "Користувачі", "utilities": "Утиліти", @@ -1297,6 +1308,7 @@ "view_all_users": "Переглянути всіх користувачів", "view_in_timeline": "Переглянути в хронології", "view_links": "Переглянути посилання", + "view_name": "Переглянути", "view_next_asset": "Переглянути наступний ресурс", "view_previous_asset": "Переглянути попередній ресурс", "view_stack": "Перегляд стеку", diff --git a/i18n/vi.json b/i18n/vi.json index 31f87d8cc6488..9c30ea29356e8 100644 --- a/i18n/vi.json +++ b/i18n/vi.json @@ -48,6 +48,9 @@ "confirm_reprocess_all_faces": "Bạn có chắc chắn muốn xử lý lại tất cả các khuôn mặt? Thao tác này sẽ xoá tên người đã được gán.", "confirm_user_password_reset": "Bạn có chắc chắn muốn đặt lại mật khẩu của {user}?", "create_job": "Tạo tác vụ", + "cron_expression": "Biểu thức Cron", + "cron_expression_description": "Thiết lập khoảng thời gian để quét bằng biểu thức cron. Tham khảo Crontab Guru để biết thêm thông tin.", + "cron_expression_presets": "Mẫu biểu thức Cron", "disable_login": "Vô hiệu hoá đăng nhập", "duplicate_detection_job_description": "Sử dụng Học máy để phát hiện các hình ảnh giống nhau. Dựa vào Tìm kiếm Thông Minh", "exclusion_pattern_description": "Quy tắc loại trừ cho bạn bỏ qua các tập tin và thư mục khi quét thư viện của bạn. Điều này hữu ích nếu bạn có các thư mục chứa tập tin bạn không muốn nhập, chẳng hạn như các tập tin RAW.", @@ -462,6 +465,7 @@ "confirm": "Xác nhận", "confirm_admin_password": "Xác nhận mật khẩu quản trị viên", "confirm_delete_shared_link": "Bạn có chắc chắn muốn xóa liên kết chia sẻ này không?", + "confirm_keep_this_delete_others": "Các hình còn lại trong stack này sẽ bị xoá ngoại trừ hình này. Bạn có chắc chắn tiếp tục không?", "confirm_password": "Xác nhận mật khẩu", "contain": "Chứa", "context": "Ngữ cảnh", @@ -511,6 +515,7 @@ "delete_key": "Xóa khóa", "delete_library": "Xóa Thư viện", "delete_link": "Xóa liên kết", + "delete_others": "Xoá các hình còn lại", "delete_shared_link": "Xóa liên kết chia sẻ", "delete_tag": "Xóa thẻ", "delete_tag_confirmation_prompt": "Bạn có chắc chắn muốn xóa thẻ {tagName} không?", @@ -601,6 +606,7 @@ "failed_to_create_shared_link": "Không thể tạo liên kết chia sẻ", "failed_to_edit_shared_link": "Không thể chỉnh sửa liên kết chia sẻ", "failed_to_get_people": "Không thể tải người", + "failed_to_keep_this_delete_others": "Có lỗi trong quá trình xoá các hình", "failed_to_load_asset": "Không thể tải ảnh", "failed_to_load_assets": "Không thể tải các ảnh", "failed_to_load_people": "Không thể tải người", @@ -784,6 +790,7 @@ "jobs": "Tác vụ", "keep": "Giữ", "keep_all": "Giữ tất cả", + "keep_this_delete_others": "Giữ tấm này và xoá tất cả còn lại", "keyboard_shortcuts": "Phím tắt", "language": "Ngôn ngữ", "language_setting_description": "Chọn ngôn ngữ ưa thích của bạn", @@ -1280,7 +1287,7 @@ "variables": "Các tham số", "version": "Phiên bản", "version_announcement_closing": "Bạn của bạn, Alex", - "version_announcement_message": "Chào bạn, có một phiên bản mới của ứng dụng. Vui lòng dành thời gian để xem ghi chú phát hành và đảm bảo rằng cấu hình docker-compose.yml.env của bạn được cập nhật để tránh bất kỳ cấu hình sai nào, đặc biệt nếu bạn sử dụng WatchTower hoặc bất kỳ cơ chế nào tự động cập nhật ứng dụng của bạn.", + "version_announcement_message": "Chào bạn! Một phiên bản mới của Immich đã phát hành. Vui lòng dành thời gian để xem danh sách thay đổi để đảm bảo cấu hình của bạn được cập nhật để tránh lỗi cấu hình sai, đặc biệt nếu bạn sử dụng WatchTower hoặc bất kỳ cơ chế tự động cập nhật Immich của bạn.", "version_history": "Lịch sử phiên bản", "version_history_item": "Đã cài đặt {version} vào {date}", "video": "Video", @@ -1302,7 +1309,7 @@ "warning": "Cảnh báo", "week": "Tuần", "welcome": "Chào mừng", - "welcome_to_immich": "Chào mừng đến với immich", + "welcome_to_immich": "Chào mừng đến với Immich", "year": "Năm", "years_ago": "{years, plural, one {# năm} other {# năm}} trước", "yes": "Có", diff --git a/i18n/zh_Hant.json b/i18n/zh_Hant.json index 792033cbfca36..f3555148ddd82 100644 --- a/i18n/zh_Hant.json +++ b/i18n/zh_Hant.json @@ -222,6 +222,8 @@ "send_welcome_email": "傳送歡迎電子郵件", "server_external_domain_settings": "外部網域", "server_external_domain_settings_description": "公開網址,,包含 http(s)://", + "server_public_users": "訪客使用者", + "server_public_users_description": "將使用者新增至共用相簿時,會列出所有使用者(姓名、email)。關閉時,使用者列表僅對管理者生效。", "server_settings": "伺服器", "server_settings_description": "管理伺服器設定", "server_welcome_message": "歡迎訊息", @@ -465,6 +467,7 @@ "confirm": "確認", "confirm_admin_password": "確認管理者密碼", "confirm_delete_shared_link": "確定刪除連結嗎?", + "confirm_keep_this_delete_others": "所有的其他堆疊項目將被刪除。確定繼續嗎?", "confirm_password": "確認密碼", "contain": "包含", "context": "情境", @@ -514,6 +517,7 @@ "delete_key": "刪除密鑰", "delete_library": "刪除圖庫", "delete_link": "刪除鏈結", + "delete_others": "刪除其他", "delete_shared_link": "刪除共享鏈結", "delete_tag": "刪除標記", "delete_tag_confirmation_prompt": "確定要刪除「{tagName}」(標記)嗎?", @@ -604,6 +608,7 @@ "failed_to_create_shared_link": "建立共享連結失敗", "failed_to_edit_shared_link": "編輯共享連結失敗", "failed_to_get_people": "無法獲取人物", + "failed_to_keep_this_delete_others": "無法保留此項目並刪除其他項目", "failed_to_load_asset": "檔案載入失敗", "failed_to_load_assets": "檔案載入失敗", "failed_to_load_people": "無法載入人物", @@ -787,6 +792,8 @@ "jobs": "作業", "keep": "保留", "keep_all": "全部保留", + "keep_this_delete_others": "保留這個,刪除其他", + "kept_this_deleted_others": "保留這個項目並刪除{count, plural, one {# asset} other {# assets}}", "keyboard_shortcuts": "鍵盤快捷鍵", "language": "語言", "language_setting_description": "選擇您的首選語言", @@ -1218,6 +1225,7 @@ "they_will_be_merged_together": "它們將會被合併在一起", "third_party_resources": "第三方資源", "time_based_memories": "依時間回憶", + "timeline": "時間軸", "timezone": "時區", "to_archive": "封存", "to_change_password": "更改密碼", @@ -1227,6 +1235,7 @@ "to_trash": "垃圾桶", "toggle_settings": "切換設定", "toggle_theme": "切換深色主題", + "total": "統計", "total_usage": "總用量", "trash": "垃圾桶", "trash_all": "全部丟掉", @@ -1276,6 +1285,8 @@ "user_purchase_settings_description": "管理你的購買", "user_role_set": "設 {user} 爲{role}", "user_usage_detail": "使用者用量詳情", + "user_usage_stats": "帳號使用量統計", + "user_usage_stats_description": "查看帳號使用量", "username": "使用者名稱", "users": "使用者", "utilities": "工具", @@ -1297,6 +1308,7 @@ "view_all_users": "查看所有使用者", "view_in_timeline": "在時間軸中查看", "view_links": "檢視鏈結", + "view_name": "查看", "view_next_asset": "查看下一項", "view_previous_asset": "查看上一項", "view_stack": "查看堆疊", diff --git a/i18n/zh_SIMPLIFIED.json b/i18n/zh_SIMPLIFIED.json index 1e7872c2d660f..7e82df1680bdb 100644 --- a/i18n/zh_SIMPLIFIED.json +++ b/i18n/zh_SIMPLIFIED.json @@ -222,6 +222,8 @@ "send_welcome_email": "发送欢迎邮件", "server_external_domain_settings": "外部域名", "server_external_domain_settings_description": "共享链接域名,包括 http(s)://", + "server_public_users": "公共用户", + "server_public_users_description": "将用户添加到共享相册时,会列出所有用户(姓名和电子邮件)。禁用后,用户列表将仅对管理员用户可用。", "server_settings": "服务器设置", "server_settings_description": "管理服务器设置", "server_welcome_message": "欢迎消息", @@ -1223,6 +1225,7 @@ "they_will_be_merged_together": "项目将会合并到一起", "third_party_resources": "第三方资源", "time_based_memories": "基于时间的回忆", + "timeline": "时间线", "timezone": "时区", "to_archive": "归档", "to_change_password": "修改密码", @@ -1232,6 +1235,7 @@ "to_trash": "放入回收站", "toggle_settings": "切换设置", "toggle_theme": "切换深色主题", + "total": "总计", "total_usage": "总用量", "trash": "回收站", "trash_all": "全部删除", @@ -1281,6 +1285,8 @@ "user_purchase_settings_description": "管理购买订单", "user_role_set": "设置“{user}”为“{role}”", "user_usage_detail": "用户用量详情", + "user_usage_stats": "帐户使用统计", + "user_usage_stats_description": "查看帐户使用统计信息", "username": "用户名", "users": "用户", "utilities": "实用工具", @@ -1302,6 +1308,7 @@ "view_all_users": "查看全部用户", "view_in_timeline": "在时间轴中查看", "view_links": "查看链接", + "view_name": "查看", "view_next_asset": "查看下一项", "view_previous_asset": "查看上一项", "view_stack": "查看堆叠项目", From 5e662e4a937bddaddfe930b4f3b8078c8a82d2cf Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 5 Dec 2024 10:26:48 -0600 Subject: [PATCH 10/19] chore(mobile): Translations update (#14493) chore(mobile): translation update --- mobile/assets/i18n/ar-JO.json | 33 +++++- mobile/assets/i18n/cs-CZ.json | 33 +++++- mobile/assets/i18n/da-DK.json | 35 +++++- mobile/assets/i18n/de-DE.json | 33 +++++- mobile/assets/i18n/el-GR.json | 33 +++++- mobile/assets/i18n/en-US.json | 70 ++++++------ mobile/assets/i18n/es-ES.json | 33 +++++- mobile/assets/i18n/es-MX.json | 33 +++++- mobile/assets/i18n/es-PE.json | 33 +++++- mobile/assets/i18n/es-US.json | 33 +++++- mobile/assets/i18n/fi-FI.json | 33 +++++- mobile/assets/i18n/fr-CA.json | 33 +++++- mobile/assets/i18n/fr-FR.json | 33 +++++- mobile/assets/i18n/he-IL.json | 33 +++++- mobile/assets/i18n/hi-IN.json | 195 ++++++++++++++++++-------------- mobile/assets/i18n/hu-HU.json | 33 +++++- mobile/assets/i18n/it-IT.json | 33 +++++- mobile/assets/i18n/ja-JP.json | 33 +++++- mobile/assets/i18n/ko-KR.json | 33 +++++- mobile/assets/i18n/lt-LT.json | 33 +++++- mobile/assets/i18n/lv-LV.json | 33 +++++- mobile/assets/i18n/mn-MN.json | 33 +++++- mobile/assets/i18n/nb-NO.json | 33 +++++- mobile/assets/i18n/nl-NL.json | 33 +++++- mobile/assets/i18n/pl-PL.json | 33 +++++- mobile/assets/i18n/pt-PT.json | 33 +++++- mobile/assets/i18n/ro-RO.json | 33 +++++- mobile/assets/i18n/ru-RU.json | 33 +++++- mobile/assets/i18n/sk-SK.json | 33 +++++- mobile/assets/i18n/sl-SI.json | 33 +++++- mobile/assets/i18n/sr-Cyrl.json | 33 +++++- mobile/assets/i18n/sr-Latn.json | 33 +++++- mobile/assets/i18n/sv-FI.json | 33 +++++- mobile/assets/i18n/sv-SE.json | 33 +++++- mobile/assets/i18n/th-TH.json | 33 +++++- mobile/assets/i18n/uk-UA.json | 33 +++++- mobile/assets/i18n/vi-VN.json | 39 ++++++- mobile/assets/i18n/zh-CN.json | 33 +++++- mobile/assets/i18n/zh-Hans.json | 33 +++++- mobile/assets/i18n/zh-TW.json | 33 +++++- 40 files changed, 1368 insertions(+), 159 deletions(-) diff --git a/mobile/assets/i18n/ar-JO.json b/mobile/assets/i18n/ar-JO.json index cbf05ca49cfcf..accb707690022 100644 --- a/mobile/assets/i18n/ar-JO.json +++ b/mobile/assets/i18n/ar-JO.json @@ -7,6 +7,7 @@ "action_common_select": "Select", "action_common_update": "تحديث", "add_a_name": "Add a name", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "تمت الاضافة{album}", "add_to_album_bottom_sheet_already_exists": "موجودة مسبقا {album}", "advanced_settings_log_level_title": "Log level: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} asset(s) restored successfully", "assets_trashed": "{} asset(s) trashed", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "عارض الأصول", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Albums on device ({})", "backup_album_selection_page_albums_tap": "انقر للتضمين، وانقر نقرًا مزدوجًا للاستثناء", "backup_album_selection_page_assets_scatter": "يمكن أن تنتشر الأصول عبر ألبومات متعددة. وبالتالي، يمكن تضمين الألبومات أو استبعادها أثناء عملية النسخ الاحتياطي.", @@ -131,6 +137,7 @@ "backup_manual_success": "نجاح", "backup_manual_title": "حالة التحميل", "backup_options_page_title": "خيارات النسخ الاحتياطي", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", "cache_settings_clear_cache_button": "مسح ذاكرة التخزين المؤقت", "cache_settings_clear_cache_button_title": "يقوم بمسح ذاكرة التخزين المؤقت للتطبيق.سيؤثر هذا بشكل كبير على أداء التطبيق حتى إعادة بناء ذاكرة التخزين المؤقت.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "التحكم في سلوك التخزين المحلي", "cache_settings_tile_title": "التخزين المحلي", "cache_settings_title": "إعدادات التخزين المؤقت", + "cancel": "Cancel", "change_password_form_confirm_password": "تأكيد كلمة المرور", "change_password_form_description": "مرحبًا ،هذه هي المرة الأولى التي تقوم فيها بالتسجيل في النظام أو تم تقديم طلب لتغيير كلمة المرور الخاصة بك.الرجاء إدخال كلمة المرور الجديدة أدناه", "change_password_form_new_password": "كلمة المرور الجديدة", "change_password_form_password_mismatch": "كلمة المرور غير مطابقة", "change_password_form_reenter_new_password": "أعد إدخال كلمة مرور جديدة", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", @@ -199,6 +210,7 @@ "crop": "Crop", "curated_location_page_title": "أماكن", "curated_object_page_title": "أشياء", + "current_server_address": "Current server address", "daily_title_text_date": "E ، MMM DD", "daily_title_text_date_year": "E ، MMM DD ، yyyy", "date_format": "E ، Lll D ، Y • H: MM A", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "وحدة زمنية", "edit_image_title": "Edit", "edit_location_dialog_title": "موقع", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Error: {}", "exif_bottom_sheet_description": "اضف وصفا...", "exif_bottom_sheet_details": "تفاصيل", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "تمكين شبكة الصور التجريبية", "experimental_settings_subtitle": "استخدام على مسؤوليتك الخاصة!", "experimental_settings_title": "تجريبي", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favorites", "favorites_page_no_favorites": "لم يتم العثور على الأصول المفضلة", "favorites_page_title": "المفضلة", "filename_search": "File name or extension", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "تمكين ردود الفعل اللمسية", "haptic_feedback_title": "ردود فعل لمسية", "header_settings_add_header_tip": "Add Header", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "أقدم صورة", "library_page_sort_most_recent_photo": "أحدث الصور", "library_page_sort_title": "عنوان الألبوم", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "اختر على الخريطة", "location_picker_latitude": "خط العرض", "location_picker_latitude_error": "أدخل خط عرض صالح", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "لا يمكن تعديل تاريخ الأصول (المواد) للقراءة فقط، سوف يتخطى", "multiselect_grid_edit_gps_err_read_only": "لا يمكن تعديل موقع الأصول (المواد) للقراءة فقط، سوف يتخطى", "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "لا توجد أصول لعرضها", "no_name": "No name", "notification_permission_dialog_cancel": "يلغي", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "إذن محدود. للسماح بالنسخ الاحتياطي للتطبيق وإدارة مجموعة المعرض بالكامل، امنح أذونات الصور والفيديو في الإعدادات.", "permission_onboarding_request": "يتطلب التطبيق إذنًا لعرض الصور ومقاطع الفيديو الخاصة بك", "places": "Places", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "التفضيلات", "profile_drawer_app_logs": "السجلات", "profile_drawer_client_out_of_date_major": "تطبيق الهاتف المحمول قديم.يرجى التحديث إلى أحدث إصدار رئيسي.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "نفايات", "recently_added": "Recently added", "recently_added_page_title": "أضيف مؤخرا", + "save": "Save", "save_to_gallery": "Save to gallery", "scaffold_body_error_occurred": "حدث خطأ", "search_albums": "Search albums", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "اقتراحات", "select_user_for_sharing_page_err_album": "فشل في إنشاء ألبوم", "select_user_for_sharing_page_share_suggestions": "اقتراحات", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "نسخة التطبيق", "server_info_box_latest_release": "احدث اصدار", "server_info_box_server_url": "عنوان URL الخادم", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "تحميل صورة معاينة", "setting_image_viewer_title": "الصور", "setting_languages_apply": "تغيير الإعدادات", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "اللغات", "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", "setting_notifications_notify_hours": "{} hours", @@ -612,6 +639,8 @@ "upload_dialog_info": "هل تريد النسخ الاحتياطي للأصول (الأصول) المحددة إلى الخادم؟", "upload_dialog_ok": "رفع", "upload_dialog_title": "تحميل الأصول", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "يُقرّ", "version_announcement_overlay_release_notes": "ملاحظات الإصدار", "version_announcement_overlay_text_1": "مرحبًا يا صديقي ، هناك إصدار جديد", @@ -621,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "حذف من الكومه أو المجموعة", "viewer_stack_use_as_main_asset": "استخدم كأصل رئيسي", - "viewer_unstack": "فك الكومه" + "viewer_unstack": "فك الكومه", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/cs-CZ.json b/mobile/assets/i18n/cs-CZ.json index 6e3462b26f7f3..296c2ed5ce771 100644 --- a/mobile/assets/i18n/cs-CZ.json +++ b/mobile/assets/i18n/cs-CZ.json @@ -7,6 +7,7 @@ "action_common_select": "Vybrat", "action_common_update": "Aktualizovat", "add_a_name": "Přidat název", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Přidáno do {album}", "add_to_album_bottom_sheet_already_exists": "Je již v {album}", "advanced_settings_log_level_title": "Úroveň protokolování: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} položek úspěšně obnoveno", "assets_trashed": "{} položek vyhozeno do koše", "assets_trashed_from_server": "{} položek vyhozeno do koše na Immich serveru", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Prohlížeč", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Alba v zařízení ({})", "backup_album_selection_page_albums_tap": "Klepnutím na položku ji zahrnete, opětovným klepnutím ji vyloučíte", "backup_album_selection_page_assets_scatter": "Položky mohou být roztroušeny ve více albech. To umožňuje zahrnout nebo vyloučit alba během procesu zálohování.", @@ -131,6 +137,7 @@ "backup_manual_success": "Úspěch", "backup_manual_title": "Stav nahrávání", "backup_options_page_title": "Nastavení záloh", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Náhledy stránek knihovny (položek {})", "cache_settings_clear_cache_button": "Vymazat vyrovnávací paměť", "cache_settings_clear_cache_button_title": "Vymaže vyrovnávací paměť aplikace. To výrazně ovlivní výkon aplikace, dokud se vyrovnávací paměť neobnoví.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Ovládání chování místního úložiště", "cache_settings_tile_title": "Místní úložiště", "cache_settings_title": "Nastavení vyrovnávací paměti", + "cancel": "Cancel", "change_password_form_confirm_password": "Potvrďte heslo", "change_password_form_description": "Dobrý den, {name}\n\nje to buď poprvé, co se přihlašujete do systému, nebo byl vytvořen požadavek na změnu hesla. Níže zadejte nové heslo.", "change_password_form_new_password": "Nové heslo", "change_password_form_password_mismatch": "Hesla se neshodují", "change_password_form_reenter_new_password": "Znovu zadejte nové heslo", + "check_corrupt_asset_backup": "Kontrola poškozených záloh položek", + "check_corrupt_asset_backup_button": "Provést kontrolu", + "check_corrupt_asset_backup_description": "Tuto kontrolu provádějte pouze přes Wi-Fi a po zálohování všech prostředků. Takto operace může trvat několik minut.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Zadejte heslo", "client_cert_import": "Importovat", @@ -199,6 +210,7 @@ "crop": "Oříznout", "curated_location_page_title": "Místa", "curated_object_page_title": "Věci", + "current_server_address": "Current server address", "daily_title_text_date": "EEEE, d. MMMM", "daily_title_text_date_year": "EEEE, d. MMMM y", "date_format": "EEEE, d. MMMM y • H:mm", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Časové pásmo", "edit_image_title": "Upravit", "edit_location_dialog_title": "Poloha", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Chyba: {}", "exif_bottom_sheet_description": "Přidat popis...", "exif_bottom_sheet_details": "PODROBNOSTI", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Povolení experimentální mřížky fotografií", "experimental_settings_subtitle": "Používejte na vlastní riziko!", "experimental_settings_title": "Experimentální", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Oblíbené", "favorites_page_no_favorites": "Nebyla nalezena žádná oblíbená média", "favorites_page_title": "Oblíbené", "filename_search": "Název nebo přípona souboru", "filter": "Filtr", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Povolit dotykovou zpětnou vazbu", "haptic_feedback_title": "Dotyková zpětná vazba", "header_settings_add_header_tip": "Přidat hlavičku", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Nejstarší fotografie", "library_page_sort_most_recent_photo": "Nejnovější fotografie", "library_page_sort_title": "Podle názvu alba", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Vyberte na mapě", "location_picker_latitude": "Zeměpisná šířka", "location_picker_latitude_error": "Zadejte platnou zeměpisnou šířku", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Nelze upravit datum položek pouze pro čtení, přeskakuji", "multiselect_grid_edit_gps_err_read_only": "Nelze upravit polohu položek pouze pro čtení, přeskakuji", "my_albums": "Moje alba", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "Žádné položky k zobrazení", "no_name": "Bez jména", "notification_permission_dialog_cancel": "Zrušit", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Přístup omezen. Chcete-li používat Immich k zálohování a správě celé vaší kolekce galerií, povolte v nastavení přístup k fotkám a videím.", "permission_onboarding_request": "Immich potřebuje přístup k zobrazení vašich fotek a videí.", "places": "Místa", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Předvolby", "profile_drawer_app_logs": "Logy", "profile_drawer_client_out_of_date_major": "Mobilní aplikace je zastaralá. Aktualizujte ji na nejnovější hlavní verzi.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Vyhodit", "recently_added": "Nedávno přidané", "recently_added_page_title": "Nedávno přidané", + "save": "Save", "save_to_gallery": "Uložit do galerie", "scaffold_body_error_occurred": "Došlo k chybě", "search_albums": "Vyhledávejte alba", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Návrhy", "select_user_for_sharing_page_err_album": "Nepodařilo se vytvořit album", "select_user_for_sharing_page_share_suggestions": "Návrhy", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Verze aplikace", "server_info_box_latest_release": "Nejnovější verze", "server_info_box_server_url": "URL serveru", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Načíst náhled obrázku", "setting_image_viewer_title": "Obrázky", "setting_languages_apply": "Použít", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Jazyk", "setting_notifications_notify_failures_grace_period": "Oznámení o selhání zálohování na pozadí: {}", "setting_notifications_notify_hours": "{} hodin", @@ -612,6 +639,8 @@ "upload_dialog_info": "Chcete zálohovat vybrané položky na server?", "upload_dialog_ok": "Nahrát", "upload_dialog_title": "Nahrát položku", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Potvrdit", "version_announcement_overlay_release_notes": "poznámky k vydání", "version_announcement_overlay_text_1": "Ahoj, k dispozici je nová verze", @@ -621,5 +650,7 @@ "videos": "Videa", "viewer_remove_from_stack": "Odstranit ze zásobníku", "viewer_stack_use_as_main_asset": "Použít jako hlavní položku", - "viewer_unstack": "Rozbalit zásobník" + "viewer_unstack": "Rozbalit zásobník", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/da-DK.json b/mobile/assets/i18n/da-DK.json index e264187d5f37a..9b6c42762888d 100644 --- a/mobile/assets/i18n/da-DK.json +++ b/mobile/assets/i18n/da-DK.json @@ -7,6 +7,7 @@ "action_common_select": "Select", "action_common_update": "Opdater", "add_a_name": "Tilføj navn", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Tilføjet til {album}", "add_to_album_bottom_sheet_already_exists": "Allerede i {album}", "advanced_settings_log_level_title": "Logniveau: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} element(er) blev gendannet succesfuldt", "assets_trashed": "{} element(er) blev smidt i papirkurven", "assets_trashed_from_server": "{} element(er) blev smidt i serverens papirkurv", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Billedviser", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Albummer på enhed ({})", "backup_album_selection_page_albums_tap": "Tryk en gang for at inkludere, tryk to gange for at ekskludere", "backup_album_selection_page_assets_scatter": "Elementer kan være spredt på tværs af flere albummer. Albummer kan således inkluderes eller udelukkes under sikkerhedskopieringsprocessen.", @@ -131,6 +137,7 @@ "backup_manual_success": "Succes", "backup_manual_title": "Uploadstatus", "backup_options_page_title": "Backupindstillinger", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Biblioteksminiaturebilleder ({} elementer)", "cache_settings_clear_cache_button": "Fjern cache", "cache_settings_clear_cache_button_title": "Fjern appens cache. Dette vil i stor grad påvirke appens ydeevne indtil cachen er genopbygget.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Kontroller den lokale lagerplads", "cache_settings_tile_title": "Lokal lagerplads", "cache_settings_title": "Cache-indstillinger", + "cancel": "Cancel", "change_password_form_confirm_password": "Bekræft kodeord", "change_password_form_description": "Hej {name},\n\nDette er enten første gang du logger ind eller også er der lavet en anmodning om at ændre dit kodeord. Indtast venligst et nyt kodeord nedenfor.", "change_password_form_new_password": "Nyt kodeord", "change_password_form_password_mismatch": "Kodeord er ikke ens", "change_password_form_reenter_new_password": "Gentag nyt kodeord", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", @@ -199,6 +210,7 @@ "crop": "Beskær", "curated_location_page_title": "Steder", "curated_object_page_title": "Ting", + "current_server_address": "Current server address", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "date_format": "E d. LLL y • hh:mm", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Tidszone", "edit_image_title": "Rediger", "edit_location_dialog_title": "Placering", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Fejl: {}", "exif_bottom_sheet_description": "Tilføj beskrivelse...", "exif_bottom_sheet_details": "DETALJER", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Aktiver eksperimentelt fotogitter", "experimental_settings_subtitle": "Brug på eget ansvar!", "experimental_settings_title": "Eksperimentelle", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favoritter", "favorites_page_no_favorites": "Ingen favoritter blev fundet", "favorites_page_title": "Favoritter", "filename_search": "File name or extension", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Slå haptisk feedback til", "haptic_feedback_title": "Haptisk feedback", "header_settings_add_header_tip": "Add Header", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Ældste billede", "library_page_sort_most_recent_photo": "Seneste billede", "library_page_sort_title": "Albumtitel", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Vælg på kort", "location_picker_latitude": "Breddegrad", "location_picker_latitude_error": "Indtast en gyldig breddegrad", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Kan ikke redigere datoen på kun læselige elementer. Springer over", "multiselect_grid_edit_gps_err_read_only": "Kan ikke redigere lokation af kun læselige elementer. Springer over", "my_albums": "Mine albummer", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "Ingen elementer at vise", "no_name": "Intet navn", "notification_permission_dialog_cancel": "Annuller", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Tilladelse begrænset. For at lade Immich lave sikkerhedskopi og styre hele dit galleri, skal der gives tilladelse til billeder og videoer i indstillinger.", "permission_onboarding_request": "Immich kræver tilliadelse til at se dine billeder og videoer.", "places": "Placeringer", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Præferencer", "profile_drawer_app_logs": "Log", "profile_drawer_client_out_of_date_major": "Mobilapp er forældet. Opdater venligst til den nyeste større version", @@ -412,9 +436,10 @@ "profile_drawer_trash": "Papirkurv", "recently_added": "Senest tilføjet", "recently_added_page_title": "Nyligt tilføjet", + "save": "Save", "save_to_gallery": "Gem til galleri", "scaffold_body_error_occurred": "Der opstod en fejl", - "search_albums": "Søb albummer", + "search_albums": "Søg i albummer", "search_bar_hint": "Søg i dine billeder", "search_filter_apply": "Tilføj filter", "search_filter_camera": "Kamera", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Anbefalinger", "select_user_for_sharing_page_err_album": "Fejlede i at oprette et nyt album", "select_user_for_sharing_page_share_suggestions": "Anbefalinger", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Applikationsversion", "server_info_box_latest_release": "Seneste version", "server_info_box_server_url": "Server URL", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Indlæs forhåndsvisning af billedet", "setting_image_viewer_title": "Images", "setting_languages_apply": "Anvend", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Sprog", "setting_notifications_notify_failures_grace_period": "Giv besked om fejl med sikkerhedskopiering i baggrunden: {}", "setting_notifications_notify_hours": "{} timer", @@ -612,6 +639,8 @@ "upload_dialog_info": "Vil du sikkerhedskopiere de(t) valgte element(er) til serveren?", "upload_dialog_ok": "Upload", "upload_dialog_title": "Upload element", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Accepter", "version_announcement_overlay_release_notes": "udgivelsesnoterne", "version_announcement_overlay_text_1": "Hej ven, der er en ny version af", @@ -621,5 +650,7 @@ "videos": "Videoer", "viewer_remove_from_stack": "Fjern fra stak", "viewer_stack_use_as_main_asset": "Brug som hovedelement", - "viewer_unstack": "Fjern fra stak" + "viewer_unstack": "Fjern fra stak", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/de-DE.json b/mobile/assets/i18n/de-DE.json index d84487973982f..60b2ea31be00d 100644 --- a/mobile/assets/i18n/de-DE.json +++ b/mobile/assets/i18n/de-DE.json @@ -7,6 +7,7 @@ "action_common_select": "Auswählen ", "action_common_update": "Aktualisieren", "add_a_name": "Einen Namen hinzufügen", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Zu {album} hinzugefügt", "add_to_album_bottom_sheet_already_exists": "Bereits in {album}", "advanced_settings_log_level_title": "Log-Level: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} Datei/en erfolgreich wiederhergestellt", "assets_trashed": "{} Datei/en gelöscht", "assets_trashed_from_server": "{} Datei/en vom Immich-Server gelöscht", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Fotoanzeige", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Alben auf dem Gerät ({})", "backup_album_selection_page_albums_tap": "Einmalig das Album antippen um es zu sichern, doppelt antippen um es nicht mehr zu sichern.", "backup_album_selection_page_assets_scatter": "Elemente (Fotos / Videos) können sich über mehrere Alben verteilen. Daher können diese vor der Sicherung eingeschlossen oder ausgeschlossen werden.", @@ -131,6 +137,7 @@ "backup_manual_success": "Erfolgreich", "backup_manual_title": "Sicherungsstatus", "backup_options_page_title": "Sicherungsoptionen", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Vorschaubilder der Bibliothek ({} Elemente)", "cache_settings_clear_cache_button": "Zwischenspeicher löschen", "cache_settings_clear_cache_button_title": "Löscht den Zwischenspeicher der App. Dies wird die Leistungsfähigkeit der App deutlich einschränken, bis der Zwischenspeicher wieder aufgebaut wurde.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Lokalen Speicher verwalten", "cache_settings_tile_title": "Lokaler Speicher", "cache_settings_title": "Zwischenspeicher Einstellungen", + "cancel": "Cancel", "change_password_form_confirm_password": "Passwort bestätigen", "change_password_form_description": "Hallo {name}\n\nDas ist entweder das erste Mal dass du dich einloggst oder es wurde eine Anfrage zur Änderung deines Passwortes gestellt. Bitte gib das neue Passwort ein.", "change_password_form_new_password": "Neues Passwort", "change_password_form_password_mismatch": "Passwörter stimmen nicht überein", "change_password_form_reenter_new_password": "Passwort erneut eingeben", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Passwort eingeben", "client_cert_import": "Importieren", @@ -199,6 +210,7 @@ "crop": "Zuschneiden", "curated_location_page_title": "Orte", "curated_object_page_title": "Dinge", + "current_server_address": "Current server address", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "date_format": "E d. LLL y • hh:mm", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Zeitzone", "edit_image_title": "Bearbeiten", "edit_location_dialog_title": "Ort bearbeiten", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Fehler: {}", "exif_bottom_sheet_description": "Beschreibung hinzufügen...", "exif_bottom_sheet_details": "DETAILS", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Experimentelles Fotogitter aktivieren", "experimental_settings_subtitle": "Benutzung auf eigene Gefahr!", "experimental_settings_title": "Experimentell", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favoriten", "favorites_page_no_favorites": "Keine favorisierten Inhalte gefunden", "favorites_page_title": "Favoriten", "filename_search": "Dateiname oder Dateityp", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Haptisches Feedback aktivieren", "haptic_feedback_title": "Haptisches Feedback", "header_settings_add_header_tip": "Header hinzufügen", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Ältestes Foto", "library_page_sort_most_recent_photo": "Neuestes Foto", "library_page_sort_title": "Titel des Albums", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Auf der Karte auswählen", "location_picker_latitude": "Breitengrad", "location_picker_latitude_error": "Gültigen Breitengrad eingeben", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Das Datum und die Uhrzeit von schreibgeschützten Inhalten kann nicht verändert werden, überspringen...", "multiselect_grid_edit_gps_err_read_only": "Der Aufnahmeort von schreibgeschützten Inhalten kann nicht verändert werden, überspringen...", "my_albums": "Meine Alben", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "Keine Vorschau vorhanden", "no_name": "Kein Name", "notification_permission_dialog_cancel": "Abbrechen", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Berechtigungen unzureichend. Um Immich das Sichern von ganzen Sammlungen zu ermöglichen, muss der Zugriff auf alle Fotos und Videos in den Einstellungen erlaubt werden.", "permission_onboarding_request": "Immich benötigt Berechtigung um auf deine Fotos und Videos zuzugreifen.", "places": "Orte", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Voreinstellungen", "profile_drawer_app_logs": "Logs", "profile_drawer_client_out_of_date_major": "Mobile-App ist veraltet. Bitte aktualisiere auf die neueste Major-Version.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Papierkorb", "recently_added": "Kürzlich hinzugefügt", "recently_added_page_title": "Zuletzt hinzugefügt", + "save": "Save", "save_to_gallery": "In Galerie speichern", "scaffold_body_error_occurred": "Ein Fehler ist aufgetreten", "search_albums": "nach Album suchen", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Vorschläge", "select_user_for_sharing_page_err_album": "Album konnte nicht erstellt werden", "select_user_for_sharing_page_share_suggestions": "Empfehlungen", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "App-Version", "server_info_box_latest_release": "Neueste Version", "server_info_box_server_url": "Server-URL", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Vorschaubild laden", "setting_image_viewer_title": "Bilder", "setting_languages_apply": "Anwenden", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Sprachen", "setting_notifications_notify_failures_grace_period": "Benachrichtigung bei Fehler/n in der Hintergrundsicherung: {}", "setting_notifications_notify_hours": "{} Stunden", @@ -612,6 +639,8 @@ "upload_dialog_info": "Willst du die ausgewählten Elemente auf dem Server sichern?", "upload_dialog_ok": "Hochladen", "upload_dialog_title": "Element hochladen", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Ich habe verstanden", "version_announcement_overlay_release_notes": "Änderungsprotokoll", "version_announcement_overlay_text_1": "Hallo mein Freund! Es gibt eine neue Version von", @@ -621,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "Aus Stapel entfernen", "viewer_stack_use_as_main_asset": "An Stapelanfang", - "viewer_unstack": "Stapel aufheben" + "viewer_unstack": "Stapel aufheben", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/el-GR.json b/mobile/assets/i18n/el-GR.json index d99f6d6ff09fd..f4b2facb24f2b 100644 --- a/mobile/assets/i18n/el-GR.json +++ b/mobile/assets/i18n/el-GR.json @@ -7,6 +7,7 @@ "action_common_select": "Επιλογή", "action_common_update": "Ενημέρωση", "add_a_name": "Πρόσθεση ονόματος", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Προστέθηκε στο {album}", "add_to_album_bottom_sheet_already_exists": "Ήδη στο {album}", "advanced_settings_log_level_title": "Επίπεδο καταγραφής: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} στοιχεία αποκαταστάθηκαν με επιτυχία", "assets_trashed": "{} στοιχεία μεταφέρθηκαν στον κάδο απορριμμάτων", "assets_trashed_from_server": "{} στοιχεία μεταφέρθηκαν στον κάδο απορριμμάτων από τον διακομιστή Immich", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Προβολή Στοιχείων", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Άλμπουμ στη συσκευή ({})", "backup_album_selection_page_albums_tap": "Πάτημα για συμπερίληψη, διπλό πάτημα για εξαίρεση", "backup_album_selection_page_assets_scatter": "Τα στοιχεία μπορεί να διασκορπιστούν σε πολλά άλμπουμ. Έτσι, τα άλμπουμ μπορούν να περιληφθούν ή να εξαιρεθούν κατά τη διαδικασία δημιουργίας αντιγράφων ασφαλείας.", @@ -131,6 +137,7 @@ "backup_manual_success": "Επιτυχία", "backup_manual_title": "Κατάσταση μεταφόρτωσης", "backup_options_page_title": "Επιλογές αντιγράφων ασφαλείας", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Μικρογραφίες σελίδας βιβλιοθήκης ({} στοιχεία)", "cache_settings_clear_cache_button": "Εκκαθάριση προσωρινής μνήμης", "cache_settings_clear_cache_button_title": "Καθαρίζει τη προσωρινή μνήμη της εφαρμογής. Αυτό θα επηρεάσει σημαντικά την απόδοση της εφαρμογής μέχρι να αναδημιουργηθεί η προσωρινή μνήμη.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Χειριστείτε τη συμπεριφορά της τοπικής αποθήκευσης", "cache_settings_tile_title": "Τοπική Αποθήκευση", "cache_settings_title": "Ρυθμίσεις Προσωρινής Μνήμης", + "cancel": "Cancel", "change_password_form_confirm_password": "Επιβεβαίωση Κωδικού", "change_password_form_description": "Γεια σας {name},\n\nΕίτε είναι η πρώτη φορά που συνδέεστε στο σύστημα είτε έχει γίνει αίτηση για αλλαγή του κωδικού σας. Παρακαλώ εισάγετε τον νέο κωδικό.", "change_password_form_new_password": "Νέος Κωδικός", "change_password_form_password_mismatch": "Οι κωδικοί δεν ταιριάζουν", "change_password_form_reenter_new_password": "Επανεισαγωγή Νέου Κωδικού", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "ΟΚ", "client_cert_enter_password": "Εισαγάγετε κωδικό πρόσβασης", "client_cert_import": "Εισαγωγή", @@ -199,6 +210,7 @@ "crop": "Αποκοπή", "curated_location_page_title": "Τοποθεσίες", "curated_object_page_title": "Πράγματα", + "current_server_address": "Current server address", "daily_title_text_date": "Ε, MMM dd", "daily_title_text_date_year": "Ε, MMM dd, yyyy", "date_format": "Ε, LLL d, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Ζώνη ώρας", "edit_image_title": "Επεξεργασία", "edit_location_dialog_title": "Τοποθεσία", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Σφάλμα: {}", "exif_bottom_sheet_description": "Προσθήκη Περιγραφής...", "exif_bottom_sheet_details": "ΛΕΠΤΟΜΕΡΕΙΕΣ", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Ενεργοποίηση πειραματικού πλέγματος φωτογραφιών", "experimental_settings_subtitle": "Χρησιμοποιείτε με δική σας ευθύνη!", "experimental_settings_title": "Πειραματικό", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Αγαπημένα", "favorites_page_no_favorites": "Δεν βρέθηκαν αγαπημένα στοιχεία", "favorites_page_title": "Αγαπημένα", "filename_search": "Όνομα αρχείου ή επέκταση", "filter": "Φίλτρο", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Ενεργοποίηση απτικής ανάδρασης", "haptic_feedback_title": "Απτική Ανάδραση", "header_settings_add_header_tip": "Προσθήκη Κεφαλίδας", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Πιο παλιά φωτογραφία", "library_page_sort_most_recent_photo": "Πιο πρόσφατη φωτογραφία", "library_page_sort_title": "Τίτλος άλμπουμ", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Επιλέξτε στο χάρτη", "location_picker_latitude": "Γεωγραφικό πλάτος", "location_picker_latitude_error": "Εισαγάγετε ένα έγκυρο γεωγραφικό πλάτος", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Δεν είναι δυνατή η επεξεργασία της ημερομηνίας των στοιχείων μόνο για ανάγνωση, παραλείπεται", "multiselect_grid_edit_gps_err_read_only": "Δεν είναι δυνατή η επεξεργασία της τοποθεσίας των στοιχείων μόνο για ανάγνωση, παραλείπεται", "my_albums": "Τα άλμπουμ μου", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "Δεν υπάρχουν στοιχεία προς εμφάνιση", "no_name": "Κανένα όνομα", "notification_permission_dialog_cancel": "Ακύρωση", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Περιορισμένη άδεια. Για να επιτρέψετε στο Immich να δημιουργεί αντίγραφα ασφαλείας και να διαχειρίζεται ολόκληρη τη συλλογή σας, παραχωρήστε άδειες φωτογραφιών και βίντεο στις Ρυθμίσεις.", "permission_onboarding_request": "Το Immich απαιτεί άδεια πρόσβασεις στις φωτογραφίες και τα βίντεό σας.", "places": "Μέρη", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Προτιμήσεις", "profile_drawer_app_logs": "Καταγραφές", "profile_drawer_client_out_of_date_major": "Παρακαλώ ενημερώστε την εφαρμογή στην πιο πρόσφατη κύρια έκδοση.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Σκουπίδια", "recently_added": "Προστέθηκαν πρόσφατα", "recently_added_page_title": "Προστέθηκαν Πρόσφατα", + "save": "Save", "save_to_gallery": "Αποθήκευση στη συλλογή", "scaffold_body_error_occurred": "Παρουσιάστηκε σφάλμα", "search_albums": "Αναζήτηση άλμπουμ", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Προτάσεις", "select_user_for_sharing_page_err_album": "Αποτυχία δημιουργίας άλπουμ", "select_user_for_sharing_page_share_suggestions": "Προτάσεις", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Έκδοση εφαρμογής", "server_info_box_latest_release": "Τελευταία Έκδοση", "server_info_box_server_url": "URL διακομιστή", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Φόρτωση εικόνας προεπισκόπησης", "setting_image_viewer_title": "Εικόνες", "setting_languages_apply": "Εφαρμογή", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Γλώσσες", "setting_notifications_notify_failures_grace_period": "Ειδοποίηση αποτυχιών δημιουργίας αντιγράφων ασφαλείας στο παρασκήνιο: {}", "setting_notifications_notify_hours": "{} ώρες", @@ -612,6 +639,8 @@ "upload_dialog_info": "Θέλετε να αντιγράψετε (κάνετε backup) τα επιλεγμένo(α) στοιχείο(α) στο διακομιστή;", "upload_dialog_ok": "Ανέβασμα", "upload_dialog_title": "Ανέβασμα στοιχείου", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Κατάλαβα", "version_announcement_overlay_release_notes": "σημειώσεις έκδοσης", "version_announcement_overlay_text_1": "Γειά σας, υπάρχει μια νέα έκδοση του", @@ -621,5 +650,7 @@ "videos": "Βίντεο", "viewer_remove_from_stack": "Κατάργηση από τη Στοίβα", "viewer_stack_use_as_main_asset": "Χρήση ως Κύριο Στοιχείο", - "viewer_unstack": "Αποστοίβαξε" + "viewer_unstack": "Αποστοίβαξε", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 121e3e498212a..6fb2ed4ff58ba 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -1,35 +1,4 @@ { - "location_permission": "Location permission", - "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", - "background_location_permission": "Background location permission", - "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", - "current_server_address": "Current server address", - "grant_permission": "Grant permission", - "automatic_endpoint_switching_title": "Automatic URL switching", - "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", - "local_network": "Local network", - "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", - "external_network": "External network", - "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", - "networking_settings": "Networking", - "networking_subtitle": "Manage the server endpoint settings", - "cancel": "Cancel", - "save": "Save", - "wifi_name": "WiFi Name", - "enter_wifi_name": "Enter WiFi name", - "your_wifi_name": "Your WiFi name", - "server_endpoint": "Server Endpoint", - "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", - "use_current_connection": "use current connection", - "add_endpoint": "Add endpoint", - "validate_endpoint_error": "Please enter a valid URL", - "advanced_settings_tile_subtitle": "Manage advanced settings", - "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", - "backup_setting_subtitle": "Manage background and foreground upload settings", - "setting_languages_subtitle": "Change the app's language", - "setting_notifications_subtitle": "Manage your notification settings", - "preferences_settings_subtitle": "Manage the app's preferences", - "asset_list_settings_subtitle": "Manage the look of the timeline", "action_common_back": "Back", "action_common_cancel": "Cancel", "action_common_clear": "Clear", @@ -38,6 +7,7 @@ "action_common_select": "Select", "action_common_update": "Update", "add_a_name": "Add a name", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Added to {album}", "add_to_album_bottom_sheet_already_exists": "Already in {album}", "advanced_settings_log_level_title": "Log level: {}", @@ -47,6 +17,7 @@ "advanced_settings_proxy_headers_title": "Proxy Headers", "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates", + "advanced_settings_tile_subtitle": "Advanced user's settings", "advanced_settings_tile_title": "Advanced", "advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting", "advanced_settings_troubleshooting_title": "Troubleshooting", @@ -86,6 +57,7 @@ "asset_list_layout_settings_group_by_month": "Month", "asset_list_layout_settings_group_by_month_day": "Month + day", "asset_list_layout_sub_title": "Layout", + "asset_list_settings_subtitle": "Photo grid layout settings", "asset_list_settings_title": "Photo Grid", "asset_restored_successfully": "Asset restored successfully", "assets_deleted_permanently": "{} asset(s) deleted permanently", @@ -94,7 +66,12 @@ "assets_restored_successfully": "{} asset(s) restored successfully", "assets_trashed": "{} asset(s) trashed", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", - "asset_viewer_settings_title": "Gallery Viewer", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", + "asset_viewer_settings_title": "Asset Viewer", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Albums on device ({})", "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", @@ -160,6 +137,7 @@ "backup_manual_success": "Success", "backup_manual_title": "Upload status", "backup_options_page_title": "Backup options", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", "cache_settings_clear_cache_button": "Clear cache", "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", @@ -178,14 +156,15 @@ "cache_settings_tile_subtitle": "Control the local storage behaviour", "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Caching Settings", + "cancel": "Cancel", "change_password_form_confirm_password": "Confirm Password", "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", "check_corrupt_asset_backup": "Check for corrupt asset backups", - "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", @@ -231,6 +210,7 @@ "crop": "Crop", "curated_location_page_title": "Places", "curated_object_page_title": "Things", + "current_server_address": "Current server address", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -267,6 +247,7 @@ "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", "edit_location_dialog_title": "Location", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Error: {}", "exif_bottom_sheet_description": "Add Description...", "exif_bottom_sheet_details": "DETAILS", @@ -278,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Enable experimental photo grid", "experimental_settings_subtitle": "Use at your own risk!", "experimental_settings_title": "Experimental", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favorites", "favorites_page_no_favorites": "No favorite assets found", "favorites_page_title": "Favorites", "filename_search": "File name or extension", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Enable haptic feedback", "haptic_feedback_title": "Haptic Feedback", "header_settings_add_header_tip": "Add Header", @@ -328,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Oldest photo", "library_page_sort_most_recent_photo": "Most recent photo", "library_page_sort_title": "Album title", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Choose on map", "location_picker_latitude": "Latitude", "location_picker_latitude_error": "Enter a valid latitude", @@ -397,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "No assets to show", "no_name": "No name", "notification_permission_dialog_cancel": "Cancel", @@ -430,6 +421,7 @@ "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.", "permission_onboarding_request": "Immich requires permission to view your photos and videos.", "places": "Places", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Preferences", "profile_drawer_app_logs": "Logs", "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", @@ -444,6 +436,7 @@ "profile_drawer_trash": "Trash", "recently_added": "Recently added", "recently_added_page_title": "Recently Added", + "save": "Save", "save_to_gallery": "Save to gallery", "scaffold_body_error_occurred": "Error occurred", "search_albums": "Search albums", @@ -501,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Suggestions", "select_user_for_sharing_page_err_album": "Failed to create album", "select_user_for_sharing_page_share_suggestions": "Suggestions", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "App Version", "server_info_box_latest_release": "Latest Version", "server_info_box_server_url": "Server URL", @@ -512,6 +506,7 @@ "setting_image_viewer_preview_title": "Load preview image", "setting_image_viewer_title": "Images", "setting_languages_apply": "Apply", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Languages", "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", "setting_notifications_notify_hours": "{} hours", @@ -521,6 +516,7 @@ "setting_notifications_notify_seconds": "{} seconds", "setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset", "setting_notifications_single_progress_title": "Show background backup detail progress", + "setting_notifications_subtitle": "Adjust your notification preferences", "setting_notifications_title": "Notifications", "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", "setting_notifications_total_progress_title": "Show background backup total progress", @@ -643,6 +639,8 @@ "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", "upload_dialog_ok": "Upload", "upload_dialog_title": "Upload Asset", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Acknowledge", "version_announcement_overlay_release_notes": "release notes", "version_announcement_overlay_text_1": "Hi friend, there is a new release of", @@ -652,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" -} + "viewer_unstack": "Un-Stack", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" +} \ No newline at end of file diff --git a/mobile/assets/i18n/es-ES.json b/mobile/assets/i18n/es-ES.json index 5f7f8a12b17df..43f03f3ace288 100644 --- a/mobile/assets/i18n/es-ES.json +++ b/mobile/assets/i18n/es-ES.json @@ -7,6 +7,7 @@ "action_common_select": "Seleccionar", "action_common_update": "Actualizar", "add_a_name": "Añadir nombre", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Agregado a {album}", "add_to_album_bottom_sheet_already_exists": "Ya se encuentra en {album}", "advanced_settings_log_level_title": "Nivel de registro: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} elemento(s) restaurado(s) exitosamente", "assets_trashed": "{} elemento(s) eliminado(s)", "assets_trashed_from_server": "{} elemento(s) movido a la papelera en Immich", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Visor de Archivos", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})", "backup_album_selection_page_albums_tap": "Toque para incluir, doble toque para excluir", "backup_album_selection_page_assets_scatter": "Los elementos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.", @@ -131,6 +137,7 @@ "backup_manual_success": "Éxito", "backup_manual_title": "Estado de la subida", "backup_options_page_title": "Opciones de Copia de Seguridad", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Miniaturas de la página de la biblioteca ({} elementos)", "cache_settings_clear_cache_button": "Borrar caché", "cache_settings_clear_cache_button_title": "Borra la caché de la aplicación. Esto afectará significativamente el rendimiento de la aplicación hasta que se reconstruya la caché.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Controla el comportamiento del almacenamiento local", "cache_settings_tile_title": "Almacenamiento local", "cache_settings_title": "Configuración de la caché", + "cancel": "Cancel", "change_password_form_confirm_password": "Confirmar Contraseña", "change_password_form_description": "Hola {name},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.", "change_password_form_new_password": "Nueva Contraseña", "change_password_form_password_mismatch": "Las contraseñas no coinciden", "change_password_form_reenter_new_password": "Vuelve a ingresar la nueva contraseña", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Introduzca contraseña", "client_cert_import": "Importar", @@ -199,6 +210,7 @@ "crop": "Recortar", "curated_location_page_title": "Lugares", "curated_object_page_title": "Objetos", + "current_server_address": "Current server address", "daily_title_text_date": "E dd, MMM", "daily_title_text_date_year": "E dd de MMM, yyyy", "date_format": "E d, LLL y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Zona horaria", "edit_image_title": "Editar", "edit_location_dialog_title": "Ubicación", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Error: {}", "exif_bottom_sheet_description": "Agregar Descripción...", "exif_bottom_sheet_details": "DETALLES", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental", "experimental_settings_subtitle": "Úsalo bajo tu responsabilidad", "experimental_settings_title": "Experimental", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favoritos", "favorites_page_no_favorites": "No se encontraron elementos marcados como favoritos", "favorites_page_title": "Favoritos", "filename_search": "Nombre o extensión", "filter": "Filtrar", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Activar respuesta háptica", "haptic_feedback_title": "Respuesta Háptica", "header_settings_add_header_tip": "Añadir cabecera", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Foto más antigua", "library_page_sort_most_recent_photo": "Foto más reciente", "library_page_sort_title": "Título del álbum", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Elegir en el mapa", "location_picker_latitude": "Latitud", "location_picker_latitude_error": "Introduce una latitud válida", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "No se puede cambiar la fecha del archivo(s) de solo lectura, omitiendo", "multiselect_grid_edit_gps_err_read_only": "No se puede cambiar la localización de archivos de solo lectura. Saltando.", "my_albums": "Mis álbumes", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "No hay elementos a mostrar", "no_name": "Sin nombre", "notification_permission_dialog_cancel": "Cancelar", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Permiso limitado. Para permitir que Immich haga copia de seguridad y gestione toda tu colección de galería, concede permisos de fotos y videos en Configuración.", "permission_onboarding_request": "Immich requiere permiso para ver tus fotos y videos.", "places": "Lugares", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Preferencias", "profile_drawer_app_logs": "Registros", "profile_drawer_client_out_of_date_major": "La app está desactualizada. Por favor actualiza a la última versión principal.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Papelera", "recently_added": "Añadidos recientemente", "recently_added_page_title": "Recién Agregadas", + "save": "Save", "save_to_gallery": "Guardado en la galería", "scaffold_body_error_occurred": "Ha ocurrido un error", "search_albums": "Buscar álbum", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Sugerencias", "select_user_for_sharing_page_err_album": "Fallo al crear el álbum", "select_user_for_sharing_page_share_suggestions": "Sugerencias", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Versión de la Aplicación", "server_info_box_latest_release": "Ultima versión", "server_info_box_server_url": "URL del servidor", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Cargar imagen de previsualización", "setting_image_viewer_title": "Imágenes", "setting_languages_apply": "Aplicar", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Idiomas", "setting_notifications_notify_failures_grace_period": "Notificar fallos de copia de seguridad en segundo plano: {}", "setting_notifications_notify_hours": "{} horas", @@ -612,6 +639,8 @@ "upload_dialog_info": "Quieres hacer una copia de seguridad al servidor de los elementos seleccionados?", "upload_dialog_ok": "Subir", "upload_dialog_title": "Subir elementos", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Aceptar", "version_announcement_overlay_release_notes": "notas de versión", "version_announcement_overlay_text_1": "Hola amigo, hay una nueva versión de", @@ -621,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "Quitar de la pila", "viewer_stack_use_as_main_asset": "Usar como elemento principal", - "viewer_unstack": "Desapilar" + "viewer_unstack": "Desapilar", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/es-MX.json b/mobile/assets/i18n/es-MX.json index 8c07c6a3623ed..ea4e794677d3d 100644 --- a/mobile/assets/i18n/es-MX.json +++ b/mobile/assets/i18n/es-MX.json @@ -7,6 +7,7 @@ "action_common_select": "Select", "action_common_update": "Update", "add_a_name": "Add a name", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Agregado a {album}", "add_to_album_bottom_sheet_already_exists": "Ya se encuentra en {album}", "advanced_settings_log_level_title": "Log level: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} asset(s) restored successfully", "assets_trashed": "{} asset(s) trashed", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Asset Viewer", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})", "backup_album_selection_page_albums_tap": "Pulsar para incluir, pulsar dos veces para excluir", "backup_album_selection_page_assets_scatter": "Los archivos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.", @@ -131,6 +137,7 @@ "backup_manual_success": "Éxito", "backup_manual_title": "Estado de la subida", "backup_options_page_title": "Backup options", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Miniaturas de la página de la biblioteca ({} archivos)", "cache_settings_clear_cache_button": "Borrar caché", "cache_settings_clear_cache_button_title": "Borra la caché de la aplicación. Esto afectará significativamente el rendimiento de la aplicación hasta que se reconstruya la caché.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Controla el comportamiento del almacenamiento local", "cache_settings_tile_title": "Almacenamiento local", "cache_settings_title": "Configuración de la caché", + "cancel": "Cancel", "change_password_form_confirm_password": "Confirmar Contraseña", "change_password_form_description": "Hola {name},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.", "change_password_form_new_password": "Nueva Contraseña", "change_password_form_password_mismatch": "Las contraseñas no coinciden", "change_password_form_reenter_new_password": "Vuelve a ingresar la nueva contraseña", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", @@ -199,6 +210,7 @@ "crop": "Crop", "curated_location_page_title": "Lugares", "curated_object_page_title": "Objetos", + "current_server_address": "Current server address", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd de MMM de yyyy", "date_format": "E d, LLL y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", "edit_location_dialog_title": "Location", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Error: {}", "exif_bottom_sheet_description": "Agregar Descripción...", "exif_bottom_sheet_details": "DETALLES", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental", "experimental_settings_subtitle": "Úsalo bajo tu responsabilidad", "experimental_settings_title": "Experimental", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favorites", "favorites_page_no_favorites": "No se encontraron recursos marcados como favoritos", "favorites_page_title": "Favoritos", "filename_search": "File name or extension", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Enable haptic feedback", "haptic_feedback_title": "Haptic Feedback", "header_settings_add_header_tip": "Add Header", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Oldest photo", "library_page_sort_most_recent_photo": "Foto más reciente", "library_page_sort_title": "Título del álbum", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Choose on map", "location_picker_latitude": "Latitude", "location_picker_latitude_error": "Enter a valid latitude", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "No assets to show", "no_name": "No name", "notification_permission_dialog_cancel": "Cancelar", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Permiso limitado. Para permitir que Immich haga copia de seguridad y gestione toda tu colección de galería, concede permisos de fotos y videos en Configuración.", "permission_onboarding_request": "Immich requiere permiso para ver tus fotos y videos.", "places": "Places", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Preferences", "profile_drawer_app_logs": "Registros", "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Papelera", "recently_added": "Recently added", "recently_added_page_title": "Recién Agregadas", + "save": "Save", "save_to_gallery": "Save to gallery", "scaffold_body_error_occurred": "Error occurred", "search_albums": "Search albums", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Sugerencias", "select_user_for_sharing_page_err_album": "Error al crear álbum", "select_user_for_sharing_page_share_suggestions": "Sugerencias", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Versión de la Aplicación", "server_info_box_latest_release": "Ultima versión", "server_info_box_server_url": "URL del servidor", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Cargar imagen de previsualización", "setting_image_viewer_title": "Images", "setting_languages_apply": "Apply", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Languages", "setting_notifications_notify_failures_grace_period": "Notificar fallos de copia de seguridad en segundo plano: {}", "setting_notifications_notify_hours": "{} horas", @@ -612,6 +639,8 @@ "upload_dialog_info": "Quieres hacer una copia de seguridad al servidor de los elementos seleccionados?", "upload_dialog_ok": "Subir", "upload_dialog_title": "Subir elementos", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Aceptar", "version_announcement_overlay_release_notes": "notas de la versión", "version_announcement_overlay_text_1": "Hola, amigo, hay una nueva versión de", @@ -621,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "Quitar de la pila", "viewer_stack_use_as_main_asset": "Usar como elemento principal", - "viewer_unstack": "Desapilar" + "viewer_unstack": "Desapilar", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/es-PE.json b/mobile/assets/i18n/es-PE.json index 23eaa437ff0ed..a88d95837d89a 100644 --- a/mobile/assets/i18n/es-PE.json +++ b/mobile/assets/i18n/es-PE.json @@ -7,6 +7,7 @@ "action_common_select": "Select", "action_common_update": "Update", "add_a_name": "Add a name", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Agregado a {album}", "add_to_album_bottom_sheet_already_exists": "Ya se encuentra en {album}", "advanced_settings_log_level_title": "Log level: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} asset(s) restored successfully", "assets_trashed": "{} asset(s) trashed", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Asset Viewer", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})", "backup_album_selection_page_albums_tap": "Pulsar para incluir, pulsar dos veces para excluir", "backup_album_selection_page_assets_scatter": "Los archivos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.", @@ -131,6 +137,7 @@ "backup_manual_success": "Éxito", "backup_manual_title": "Estado de la subida", "backup_options_page_title": "Backup options", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Miniaturas de la página de la biblioteca ({} archivos)", "cache_settings_clear_cache_button": "Borrar caché", "cache_settings_clear_cache_button_title": "Borra la caché de la aplicación. Esto afectará significativamente el rendimiento de la aplicación hasta que se reconstruya la caché.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Controla el comportamiento del almacenamiento local", "cache_settings_tile_title": "Almacenamiento local", "cache_settings_title": "Configuración de la caché", + "cancel": "Cancel", "change_password_form_confirm_password": "Confirmar Contraseña", "change_password_form_description": "Hola {name},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.", "change_password_form_new_password": "Nueva Contraseña", "change_password_form_password_mismatch": "Las contraseñas no coinciden", "change_password_form_reenter_new_password": "Vuelve a ingresar la nueva contraseña", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", @@ -199,6 +210,7 @@ "crop": "Crop", "curated_location_page_title": "Lugares", "curated_object_page_title": "Objetos", + "current_server_address": "Current server address", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd de MMM de yyyy", "date_format": "E d, LLL y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", "edit_location_dialog_title": "Location", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Error: {}", "exif_bottom_sheet_description": "Agregar Descripción...", "exif_bottom_sheet_details": "DETALLES", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental", "experimental_settings_subtitle": "Úsalo bajo tu responsabilidad", "experimental_settings_title": "Experimental", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favorites", "favorites_page_no_favorites": "No se encontraron recursos marcados como favoritos", "favorites_page_title": "Favoritos", "filename_search": "File name or extension", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Enable haptic feedback", "haptic_feedback_title": "Haptic Feedback", "header_settings_add_header_tip": "Add Header", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Oldest photo", "library_page_sort_most_recent_photo": "Foto más reciente", "library_page_sort_title": "Título del álbum", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Choose on map", "location_picker_latitude": "Latitude", "location_picker_latitude_error": "Enter a valid latitude", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "No assets to show", "no_name": "No name", "notification_permission_dialog_cancel": "Cancelar", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Permiso limitado. Para permitir que Immich haga copia de seguridad y gestione toda tu colección de galería, concede permisos de fotos y videos en Configuración.", "permission_onboarding_request": "Immich requiere permiso para ver tus fotos y videos.", "places": "Places", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Preferences", "profile_drawer_app_logs": "Registros", "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Papelera", "recently_added": "Recently added", "recently_added_page_title": "Recién Agregadas", + "save": "Save", "save_to_gallery": "Save to gallery", "scaffold_body_error_occurred": "Error occurred", "search_albums": "Search albums", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Sugerencias", "select_user_for_sharing_page_err_album": "Error al crear álbum", "select_user_for_sharing_page_share_suggestions": "Sugerencias", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Versión de la Aplicación", "server_info_box_latest_release": "Ultima versión", "server_info_box_server_url": "URL del servidor", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Cargar imagen de previsualización", "setting_image_viewer_title": "Images", "setting_languages_apply": "Apply", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Languages", "setting_notifications_notify_failures_grace_period": "Notificar fallos de copia de seguridad en segundo plano: {}", "setting_notifications_notify_hours": "{} horas", @@ -612,6 +639,8 @@ "upload_dialog_info": "Quieres hacer una copia de seguridad al servidor de los elementos seleccionados?", "upload_dialog_ok": "Subir", "upload_dialog_title": "Subir elementos", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Aceptar", "version_announcement_overlay_release_notes": "notas de la versión", "version_announcement_overlay_text_1": "Hola, amigo, hay una nueva versión de", @@ -621,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "Quitar de la pila", "viewer_stack_use_as_main_asset": "Usar como elemento principal", - "viewer_unstack": "Desapilar" + "viewer_unstack": "Desapilar", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/es-US.json b/mobile/assets/i18n/es-US.json index 61c84d0054cdc..3e4ed7bde294d 100644 --- a/mobile/assets/i18n/es-US.json +++ b/mobile/assets/i18n/es-US.json @@ -7,6 +7,7 @@ "action_common_select": "Select", "action_common_update": "Update", "add_a_name": "Add a name", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Agregado a {album}", "add_to_album_bottom_sheet_already_exists": "Ya se encuentra en {album}", "advanced_settings_log_level_title": "Log level: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} asset(s) restored successfully", "assets_trashed": "{} asset(s) trashed", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Asset Viewer", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})", "backup_album_selection_page_albums_tap": "Pulsar para incluir, pulsar dos veces para excluir", "backup_album_selection_page_assets_scatter": "Los archivos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.", @@ -131,6 +137,7 @@ "backup_manual_success": "Exitoso", "backup_manual_title": "Estado de subida", "backup_options_page_title": "Backup options", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Miniaturas de la página de la biblioteca ({} recursos)", "cache_settings_clear_cache_button": "Borrar caché", "cache_settings_clear_cache_button_title": "Borra la caché de la aplicación. Esto afectará significativamente el rendimiento de la aplicación hasta que se reconstruya la caché.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Controla el comportamiento del almacenamiento local", "cache_settings_tile_title": "Almacenamiento local", "cache_settings_title": "Configuración de la caché", + "cancel": "Cancel", "change_password_form_confirm_password": "Confirmar Contraseña", "change_password_form_description": "Hola {name},\n\nÉsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.", "change_password_form_new_password": "Nueva Contraseña", "change_password_form_password_mismatch": "Las contraseñas no coinciden", "change_password_form_reenter_new_password": "Vuelve a ingresar la nueva contraseña", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", @@ -199,6 +210,7 @@ "crop": "Crop", "curated_location_page_title": "Lugares", "curated_object_page_title": "Objetos", + "current_server_address": "Current server address", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd de MMM, yyyy", "date_format": "E d, LLL y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", "edit_location_dialog_title": "Location", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Error: {}", "exif_bottom_sheet_description": "Agregar Descripción...", "exif_bottom_sheet_details": "DETALLES", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental", "experimental_settings_subtitle": "¡Úsalo bajo tu propio riesgo!", "experimental_settings_title": "Experimental", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favorites", "favorites_page_no_favorites": "No se encontraron recursos marcados como favoritos", "favorites_page_title": "Favoritos", "filename_search": "File name or extension", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Enable haptic feedback", "haptic_feedback_title": "Haptic Feedback", "header_settings_add_header_tip": "Add Header", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Oldest photo", "library_page_sort_most_recent_photo": "Foto más reciente", "library_page_sort_title": "Título del álbum", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Choose on map", "location_picker_latitude": "Latitude", "location_picker_latitude_error": "Enter a valid latitude", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "No assets to show", "no_name": "No name", "notification_permission_dialog_cancel": "Cancelar", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Permiso limitado. Para permitir que Immich haga copia de seguridad y gestione toda tu colección de galería, concede permisos de fotos y videos en Configuración.", "permission_onboarding_request": "Immich requiere permiso para ver tus fotos y videos.", "places": "Places", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Preferences", "profile_drawer_app_logs": "Registros", "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Papelera", "recently_added": "Recently added", "recently_added_page_title": "Recién Agregados", + "save": "Save", "save_to_gallery": "Save to gallery", "scaffold_body_error_occurred": "Error occurred", "search_albums": "Search albums", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Sugerencias", "select_user_for_sharing_page_err_album": "Error al crear álbum", "select_user_for_sharing_page_share_suggestions": "Sugerencias", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Versión de la Aplicación", "server_info_box_latest_release": "Ultima versión", "server_info_box_server_url": "URL del Servidor", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Cargar imagen de previsualización", "setting_image_viewer_title": "Images", "setting_languages_apply": "Apply", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Languages", "setting_notifications_notify_failures_grace_period": "Notificar fallos de copia de seguridad en segundo plano: {}", "setting_notifications_notify_hours": "{} horas", @@ -612,6 +639,8 @@ "upload_dialog_info": "¿Quieres respaldar los recursos seleccionados en el servidor?", "upload_dialog_ok": "Subir", "upload_dialog_title": "Subir recurso", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Aceptar", "version_announcement_overlay_release_notes": "notas de la versión", "version_announcement_overlay_text_1": "Hola, amigo, hay una nueva versión de", @@ -621,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "Eliminar de la pila", "viewer_stack_use_as_main_asset": "Utilizar como recurso principal", - "viewer_unstack": "Desapilar" + "viewer_unstack": "Desapilar", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/fi-FI.json b/mobile/assets/i18n/fi-FI.json index 4f10b4c78b550..63521db3a3744 100644 --- a/mobile/assets/i18n/fi-FI.json +++ b/mobile/assets/i18n/fi-FI.json @@ -7,6 +7,7 @@ "action_common_select": "Select", "action_common_update": "Päivitä", "add_a_name": "Add a name", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Lisätty albumiin {album}", "add_to_album_bottom_sheet_already_exists": "Kohde on jo albumissa {album}", "advanced_settings_log_level_title": "Lokitaso: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} asset(s) restored successfully", "assets_trashed": "{} asset(s) trashed", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Katselin", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Laitteen albumit ({})", "backup_album_selection_page_albums_tap": "Napauta sisällyttääksesi, kaksoisnapauta jättääksesi pois", "backup_album_selection_page_assets_scatter": "Kohteet voivat olla hajaantuneina useisiin albumeihin. Albumeita voidaan sisällyttää varmuuskopiointiin tai jättää siitä pois.", @@ -131,6 +137,7 @@ "backup_manual_success": "Onnistui", "backup_manual_title": "Lähetyksen tila", "backup_options_page_title": "Varmuuskopioinnin asetukset", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Kirjastosivun esikatselukuvat ({} kohdetta)", "cache_settings_clear_cache_button": "Tyhjennä välimuisti", "cache_settings_clear_cache_button_title": "Tyhjennä sovelluksen välimuisti. Tämä vaikuttaa merkittävästi sovelluksen suorituskykyyn, kunnes välimuisti on rakennettu uudelleen.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Hallitse paikallista tallenustilaa", "cache_settings_tile_title": "Paikallinen tallennustila", "cache_settings_title": "Välimuistin asetukset", + "cancel": "Cancel", "change_password_form_confirm_password": "Vahvista salasana", "change_password_form_description": "Hei {name},\n\nTämä on joko ensimmäinen kirjautumisesi järjestelmään tai salasanan vaihtaminen vaihtaminen on pakotettu. Ole hyvä ja syötä uusi salasana alle.", "change_password_form_new_password": "Uusi salasana", "change_password_form_password_mismatch": "Salasanat eivät täsmää", "change_password_form_reenter_new_password": "Uusi salasana uudelleen", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", @@ -199,6 +210,7 @@ "crop": "Crop", "curated_location_page_title": "Paikat", "curated_object_page_title": "Asiat", + "current_server_address": "Current server address", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Aikavyöhyke", "edit_image_title": "Edit", "edit_location_dialog_title": "Sijainti", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Error: {}", "exif_bottom_sheet_description": "Lisää kuvaus…", "exif_bottom_sheet_details": "TIEDOT", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Ota käyttöön kokeellinen kuvaruudukko", "experimental_settings_subtitle": "Käyttö omalla vastuulla!", "experimental_settings_title": "Kokeellinen", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favorites", "favorites_page_no_favorites": "Suosikkikohteita ei löytynyt", "favorites_page_title": "Suosikit", "filename_search": "File name or extension", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Ota haptinen palaute käyttöön", "haptic_feedback_title": "Haptinen palaute", "header_settings_add_header_tip": "Add Header", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Vanhin kuva", "library_page_sort_most_recent_photo": "Viimeisin kuva", "library_page_sort_title": "Albumin otsikko", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Valitse kartalta", "location_picker_latitude": "Leveysaste", "location_picker_latitude_error": "Lisää kelvollinen leveysaste", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Vain luku -tilassa olevien kohteiden päivämäärää ei voitu muokata, ohitetaan", "multiselect_grid_edit_gps_err_read_only": "Vain luku-tilassa olevien kohteiden sijantitietoja ei voitu muokata, ohitetaan", "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "Ei näytettäviä kohteita", "no_name": "No name", "notification_permission_dialog_cancel": "Peruuta", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Rajoitettu käyttöoikeus. Salliaksesi Immichin varmuuskopioida ja hallita koko kuvakirjastoasi, myönnä oikeus kuviin ja videoihin asetuksista.", "permission_onboarding_request": "Immich vaatii käyttöoikeuden kuvien ja videoiden käyttämiseen.", "places": "Places", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Asetukset", "profile_drawer_app_logs": "Lokit", "profile_drawer_client_out_of_date_major": "Sovelluksen mobiiliversio on vanhentunut. Päivitä viimeisimpään merkittävään versioon.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Roskakori", "recently_added": "Recently added", "recently_added_page_title": "Viimeksi lisätyt", + "save": "Save", "save_to_gallery": "Save to gallery", "scaffold_body_error_occurred": "Tapahtui virhe", "search_albums": "Search albums", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Ehdotukset", "select_user_for_sharing_page_err_album": "Albumin luonti epäonnistui", "select_user_for_sharing_page_share_suggestions": "Ehdotukset", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Sovelluksen versio", "server_info_box_latest_release": "Viimeisin versio", "server_info_box_server_url": "Palvelimen URL-osoite", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Lataa esikatselukuva", "setting_image_viewer_title": "Kuvat", "setting_languages_apply": "Käytä", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Kieli", "setting_notifications_notify_failures_grace_period": "Ilmoita taustavarmuuskopioinnin epäonnistumisista: {}", "setting_notifications_notify_hours": "{} tunnin välein", @@ -612,6 +639,8 @@ "upload_dialog_info": "Haluatko varmuuskopioida valitut kohteet palvelimelle?", "upload_dialog_ok": "Lähetä", "upload_dialog_title": "Lähetä kohde", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Tiedostan", "version_announcement_overlay_release_notes": "julkaisutiedoissa", "version_announcement_overlay_text_1": "Hei, kaveri! Uusi palvelinversio on saatavilla sovelluksesta", @@ -621,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "Poista pinosta", "viewer_stack_use_as_main_asset": "Käytä pääkohteena", - "viewer_unstack": "Pura pino" + "viewer_unstack": "Pura pino", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/fr-CA.json b/mobile/assets/i18n/fr-CA.json index 9e51cc7cbf5f6..cf2e5dd963746 100644 --- a/mobile/assets/i18n/fr-CA.json +++ b/mobile/assets/i18n/fr-CA.json @@ -7,6 +7,7 @@ "action_common_select": "Select", "action_common_update": "Update", "add_a_name": "Add a name", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Ajouté à {album}", "add_to_album_bottom_sheet_already_exists": "Déjà dans {album}", "advanced_settings_log_level_title": "Log level: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} asset(s) restored successfully", "assets_trashed": "{} asset(s) trashed", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Asset Viewer", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Albums sur l'appareil ({})", "backup_album_selection_page_albums_tap": "Tapez pour inclure, tapez deux fois pour exclure", "backup_album_selection_page_assets_scatter": "Les éléments peuvent être répartis sur plusieurs albums. De ce fait, les albums peuvent être inclus ou exclus pendant le processus de sauvegarde.", @@ -131,6 +137,7 @@ "backup_manual_success": "Succès ", "backup_manual_title": "Statut du téléchargement ", "backup_options_page_title": "Backup options", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "vignettes de la page bibliothèque ({} éléments)", "cache_settings_clear_cache_button": "Effacer le cache", "cache_settings_clear_cache_button_title": "Efface le cache de l'application. Cela aura un impact significatif sur les performances de l'application jusqu'à ce que le cache soit reconstruit.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Contrôler le comportement du stockage local", "cache_settings_tile_title": "Stockage local", "cache_settings_title": "Paramètres de mise en cache", + "cancel": "Cancel", "change_password_form_confirm_password": "Confirmez le mot de passe", "change_password_form_description": "Bonjour {name},\n\nC'est la première fois que vous vous connectez au système ou vous avez demandé de changer votre mot de passe. Veuillez saisir le nouveau mot de passe ci-dessous.", "change_password_form_new_password": "Nouveau mot de passe", "change_password_form_password_mismatch": "Les mots de passe ne correspondent pas", "change_password_form_reenter_new_password": "Saisissez à nouveau le nouveau mot de passe", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", @@ -199,6 +210,7 @@ "crop": "Crop", "curated_location_page_title": "Places", "curated_object_page_title": "Objets", + "current_server_address": "Current server address", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", "edit_location_dialog_title": "Location", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Error: {}", "exif_bottom_sheet_description": "Ajouter une description...", "exif_bottom_sheet_details": "DÉTAILS", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Activer la grille de photos expérimentale", "experimental_settings_subtitle": "Utilisez à vos dépends!", "experimental_settings_title": "Expérimental", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favorites", "favorites_page_no_favorites": "Aucun élément favori n'a été trouvé", "favorites_page_title": "Favoris", "filename_search": "File name or extension", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Enable haptic feedback", "haptic_feedback_title": "Haptic Feedback", "header_settings_add_header_tip": "Add Header", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Oldest photo", "library_page_sort_most_recent_photo": "Photo la plus récente", "library_page_sort_title": "Titre de l'album", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Choose on map", "location_picker_latitude": "Latitude", "location_picker_latitude_error": "Enter a valid latitude", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "No assets to show", "no_name": "No name", "notification_permission_dialog_cancel": "Annuler", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Permission limitée. Pour permettre à Immich de sauvegarder et de gérer l'ensemble de votre bibliothèque, accordez l'autorisation pour les photos et vidéos dans les Paramètres.", "permission_onboarding_request": "Immich demande l'autorisation de visionner vos photos et vidéo", "places": "Places", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Preferences", "profile_drawer_app_logs": "Journaux", "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Corbeille", "recently_added": "Recently added", "recently_added_page_title": "Récemment ajouté", + "save": "Save", "save_to_gallery": "Save to gallery", "scaffold_body_error_occurred": "Error occurred", "search_albums": "Search albums", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Suggestions", "select_user_for_sharing_page_err_album": "Échec de la création de l'album", "select_user_for_sharing_page_share_suggestions": "Suggestions", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Version de l'application", "server_info_box_latest_release": "Dernière version", "server_info_box_server_url": "URL du serveur", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Charger l'image d'aperçu", "setting_image_viewer_title": "Images", "setting_languages_apply": "Apply", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Languages", "setting_notifications_notify_failures_grace_period": "Notifier les échecs de la sauvegarde en arrière-plan: {}", "setting_notifications_notify_hours": "{} heures", @@ -612,6 +639,8 @@ "upload_dialog_info": "Voulez-vous sauvegarder la sélection vers le serveur?", "upload_dialog_ok": "Télécharger ", "upload_dialog_title": "Télécharger cet élément ", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Confirmer", "version_announcement_overlay_release_notes": "notes de mise à jour", "version_announcement_overlay_text_1": "Bonjour, une nouvelle version de", @@ -621,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "Retirer de la pile", "viewer_stack_use_as_main_asset": "Utiliser comme élément principal", - "viewer_unstack": "Désempiler" + "viewer_unstack": "Désempiler", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/fr-FR.json b/mobile/assets/i18n/fr-FR.json index 593dddaeba5d1..1a5646bc2a13e 100644 --- a/mobile/assets/i18n/fr-FR.json +++ b/mobile/assets/i18n/fr-FR.json @@ -7,6 +7,7 @@ "action_common_select": "Sélectionner", "action_common_update": "Mise à jour", "add_a_name": "Ajouter un nom", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Ajouté à {album}", "add_to_album_bottom_sheet_already_exists": "Déjà dans {album}", "advanced_settings_log_level_title": "Niveau de log : {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "Élément restauré avec succès", "assets_trashed": "{} élément(s) déplacé(s) vers la corbeille", "assets_trashed_from_server": "{} élément(s) déplacé(s) vers la corbeille du serveur Immich", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Visualisateur d'éléments", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Albums sur l'appareil ({})", "backup_album_selection_page_albums_tap": "Tapez pour inclure, tapez deux fois pour exclure", "backup_album_selection_page_assets_scatter": "Les éléments peuvent être répartis sur plusieurs albums. De ce fait, les albums peuvent être inclus ou exclus pendant le processus de sauvegarde.", @@ -131,6 +137,7 @@ "backup_manual_success": "Succès ", "backup_manual_title": "Statut du téléchargement ", "backup_options_page_title": "Options de sauvegarde", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Miniatures de la page bibliothèque ({} éléments)", "cache_settings_clear_cache_button": "Effacer le cache", "cache_settings_clear_cache_button_title": "Efface le cache de l'application. Cela aura un impact significatif sur les performances de l'application jusqu'à ce que le cache soit reconstruit.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Contrôler le comportement du stockage local", "cache_settings_tile_title": "Stockage local", "cache_settings_title": "Paramètres de mise en cache", + "cancel": "Cancel", "change_password_form_confirm_password": "Confirmez le mot de passe", "change_password_form_description": "Bonjour {name},\n\nC'est la première fois que vous vous connectez au système ou vous avez demandé à changer votre mot de passe. Veuillez saisir le nouveau mot de passe ci-dessous.", "change_password_form_new_password": "Nouveau mot de passe", "change_password_form_password_mismatch": "Les mots de passe ne correspondent pas", "change_password_form_reenter_new_password": "Saisissez à nouveau le nouveau mot de passe", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "Ok", "client_cert_enter_password": "Entrer mot de passe", "client_cert_import": "Imorted", @@ -199,6 +210,7 @@ "crop": "Recadrer", "curated_location_page_title": "Lieux", "curated_object_page_title": "Objets", + "current_server_address": "Current server address", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Fuseau horaire", "edit_image_title": "Modifier", "edit_location_dialog_title": "Localisation", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Erreur : {}", "exif_bottom_sheet_description": "Ajouter une description…", "exif_bottom_sheet_details": "DÉTAILS", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Activer la grille de photos expérimentale", "experimental_settings_subtitle": "Utilisez à vos dépends !", "experimental_settings_title": "Expérimental", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favoris", "favorites_page_no_favorites": "Aucun élément favori n'a été trouvé", "favorites_page_title": "Favoris", "filename_search": "Nom de fichier ou extension", "filter": "Filtres", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Activer le retour haptique", "haptic_feedback_title": "Retour haptique", "header_settings_add_header_tip": "Ajouter un en-tête", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Photo la plus ancienne", "library_page_sort_most_recent_photo": "Photo la plus récente", "library_page_sort_title": "Titre de l'album", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Sélectionner sur la carte", "location_picker_latitude": "Latitude", "location_picker_latitude_error": "Saisir une latitude correcte", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Impossible de modifier la date d'un élément d'actif en lecture seule.", "multiselect_grid_edit_gps_err_read_only": "Impossible de modifier l'emplacement d'un élément en lecture seule.", "my_albums": "Mes albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "Aucun élément à afficher", "no_name": "Sans nom", "notification_permission_dialog_cancel": "Annuler", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Permission limitée. Pour permettre à Immich de sauvegarder et de gérer l'ensemble de votre bibliothèque, accordez l'autorisation pour les photos et vidéos dans les Paramètres.", "permission_onboarding_request": "Immich demande l'autorisation de visionner vos photos et vidéo", "places": "Lieux", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Préférences", "profile_drawer_app_logs": "Journaux", "profile_drawer_client_out_of_date_major": "L'application mobile est obsolète. Veuillez effectuer la mise à jour vers la dernière version majeure.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Corbeille", "recently_added": "Récemment ajouté", "recently_added_page_title": "Récemment ajouté", + "save": "Save", "save_to_gallery": "Enregistrer", "scaffold_body_error_occurred": "Une erreur s'est produite", "search_albums": "Rechercher des albums", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Suggestions", "select_user_for_sharing_page_err_album": "Échec de la création de l'album", "select_user_for_sharing_page_share_suggestions": "Suggestions", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Version de l'application", "server_info_box_latest_release": "Dernière version", "server_info_box_server_url": "URL du serveur", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Charger l'image d'aperçu", "setting_image_viewer_title": "Images", "setting_languages_apply": "Appliquer", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Langues", "setting_notifications_notify_failures_grace_period": "Notifier les échecs de la sauvegarde en arrière-plan : {}", "setting_notifications_notify_hours": "{} heures", @@ -612,6 +639,8 @@ "upload_dialog_info": "Voulez-vous sauvegarder la sélection vers le serveur ?", "upload_dialog_ok": "Télécharger ", "upload_dialog_title": "Télécharger cet élément ", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Confirmer", "version_announcement_overlay_release_notes": "notes de mise à jour", "version_announcement_overlay_text_1": "Bonjour, une nouvelle version de", @@ -621,5 +650,7 @@ "videos": "Vidéos", "viewer_remove_from_stack": "Retirer de la pile", "viewer_stack_use_as_main_asset": "Utiliser comme élément principal", - "viewer_unstack": "Désempiler" + "viewer_unstack": "Désempiler", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/he-IL.json b/mobile/assets/i18n/he-IL.json index c0bfb7367b887..5e17cded9cd0e 100644 --- a/mobile/assets/i18n/he-IL.json +++ b/mobile/assets/i18n/he-IL.json @@ -7,6 +7,7 @@ "action_common_select": "בחר", "action_common_update": "עדכון", "add_a_name": "הוסף שם", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "נוסף ל {album}", "add_to_album_bottom_sheet_already_exists": "כבר ב {album}", "advanced_settings_log_level_title": "רמת תיעוד אירועים: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} נכס(ים) שוחזרו בהצלחה", "assets_trashed": "{} נכס(ים) הועברו לאשפה", "assets_trashed_from_server": "{} נכס(ים) הועברו לאשפה משרת ה-Immich", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "מציג הנכסים", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "אלבומים במכשיר ({})", "backup_album_selection_page_albums_tap": "הקש כדי לכלול, הקש פעמיים כדי להחריג", "backup_album_selection_page_assets_scatter": "נכסים יכולים להתפזר על פני אלבומים מרובים. לפיכך, ניתן לכלול או להחריג אלבומים במהלך תהליך הגיבוי", @@ -131,6 +137,7 @@ "backup_manual_success": "הצלחה", "backup_manual_title": "מצב העלאה", "backup_options_page_title": "אפשרויות גיבוי", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "תמונות ממוזערות של דף ספרייה ({} נכסים)", "cache_settings_clear_cache_button": "ניקוי מטמון", "cache_settings_clear_cache_button_title": "מנקה את המטמון של היישום. זה ישפיע באופן משמעותי על הביצועים של היישום עד שהמטמון נבנה מחדש", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "שלוט בהתנהגות האחסון המקומי", "cache_settings_tile_title": "אחסון מקומי", "cache_settings_title": "הגדרות שמירת מטמון", + "cancel": "Cancel", "change_password_form_confirm_password": "אשר סיסמה", "change_password_form_description": "הי {name},\n\nזאת או הפעם הראשונה שאת/ה מתחבר/ת למערכת או שנעשתה בקשה לשינוי הסיסמה שלך. נא להזין את הסיסמה החדשה למטה.", "change_password_form_new_password": "סיסמה חדשה", "change_password_form_password_mismatch": "סיסמאות לא תואמות", "change_password_form_reenter_new_password": "הכנס שוב סיסמה חדשה", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "בסדר", "client_cert_enter_password": "הזן סיסמה", "client_cert_import": "ייבוא", @@ -199,6 +210,7 @@ "crop": "חתוך", "curated_location_page_title": "מקומות", "curated_object_page_title": "דברים", + "current_server_address": "Current server address", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "אזור זמן", "edit_image_title": "ערוך", "edit_location_dialog_title": "מיקום", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "שגיאה: {}", "exif_bottom_sheet_description": "הוסף תיאור...", "exif_bottom_sheet_details": "פרטים", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "אפשר רשת תמונות ניסיונית", "experimental_settings_subtitle": "השימוש הוא על אחריותך בלבד!", "experimental_settings_title": "נסיוני", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "מועדפים", "favorites_page_no_favorites": "לא נמצאו נכסים מועדפים", "favorites_page_title": "מועדפים", "filename_search": "שם קובץ או סיומת", "filter": "סנן", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "אפשר משוב ברטט", "haptic_feedback_title": "משוב ברטט", "header_settings_add_header_tip": "הוסף כותרת", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "תמונה הכי ישנה", "library_page_sort_most_recent_photo": "תמונה אחרונה ביותר", "library_page_sort_title": "כותרת אלבום", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "בחר על מפה", "location_picker_latitude": "קו רוחב", "location_picker_latitude_error": "הזן קו רוחב חוקי", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "לא ניתן לערוך תאריך של נכס(ים) לקריאה בלבד, מדלג", "multiselect_grid_edit_gps_err_read_only": "לא ניתן לערוך מיקום של נכס(ים) לקריאה בלבד, מדלג", "my_albums": "האלבומים שלי", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "אין נכסים להציג", "no_name": "ללא שם", "notification_permission_dialog_cancel": "ביטול", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "הרשאה מוגבלת. כדי לתת ליישום לגבות ולנהל את כל אוסף הגלריה שלך, הענק הרשאה לתמונות וסרטונים בהגדרות", "permission_onboarding_request": "היישום דורש הרשאה כדי לראות את התמונות והסרטונים שלך", "places": "מקומות", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "העדפות", "profile_drawer_app_logs": "יומן", "profile_drawer_client_out_of_date_major": "האפליקציה לנייד היא מיושנת. נא לעדכן לגרסה הראשית האחרונה", @@ -412,6 +436,7 @@ "profile_drawer_trash": "אשפה", "recently_added": "נוסף לאחרונה", "recently_added_page_title": "נוסף לאחרונה", + "save": "Save", "save_to_gallery": "שמור לגלריה", "scaffold_body_error_occurred": "אירעה שגיאה", "search_albums": "חפש/י אלבומים", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "הצעות", "select_user_for_sharing_page_err_album": "יצירת אלבום נכשלה", "select_user_for_sharing_page_share_suggestions": "הצעות", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "גרסת יישום", "server_info_box_latest_release": "גרסה עדכנית ביותר", "server_info_box_server_url": "כתובת שרת", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "טען תמונת תצוגה מקדימה", "setting_image_viewer_title": "תמונות", "setting_languages_apply": "החל", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "שפות", "setting_notifications_notify_failures_grace_period": "הודע על כשלים בגיבוי ברקע: {}", "setting_notifications_notify_hours": "{} שעות", @@ -612,6 +639,8 @@ "upload_dialog_info": "האם ברצונך לגבות את הנכס(ים) שנבחרו לשרת?", "upload_dialog_ok": "העלאה", "upload_dialog_title": "העלאת נכס", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "אשר", "version_announcement_overlay_release_notes": "הערות פרסום", "version_announcement_overlay_text_1": "הי חבר/ה, יש מהדורה חדשה של", @@ -621,5 +650,7 @@ "videos": "סרטונים", "viewer_remove_from_stack": "הסר מערימה", "viewer_stack_use_as_main_asset": "השתמש כנכס ראשי", - "viewer_unstack": "ביטול ערימה" + "viewer_unstack": "ביטול ערימה", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/hi-IN.json b/mobile/assets/i18n/hi-IN.json index 104dae2ebd95e..109192649c153 100644 --- a/mobile/assets/i18n/hi-IN.json +++ b/mobile/assets/i18n/hi-IN.json @@ -3,10 +3,11 @@ "action_common_cancel": "Cancel", "action_common_clear": "Clear", "action_common_confirm": "Confirm", - "action_common_save": "Save", - "action_common_select": "Select", + "action_common_save": "सहेजें", + "action_common_select": "चुनें", "action_common_update": "Update", - "add_a_name": "Add a name", + "add_a_name": "नाम जोड़ें", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Added to {album}", "add_to_album_bottom_sheet_already_exists": "Already in {album}", "advanced_settings_log_level_title": "Log level: {}", @@ -22,7 +23,7 @@ "advanced_settings_troubleshooting_title": "Troubleshooting", "album_info_card_backup_album_excluded": "EXCLUDED", "album_info_card_backup_album_included": "INCLUDED", - "albums": "Albums", + "albums": "एल्बम", "album_thumbnail_card_item": "1 item", "album_thumbnail_card_items": "{} items", "album_thumbnail_card_shared": " · Shared", @@ -38,13 +39,13 @@ "album_viewer_appbar_share_remove": "Remove from album", "album_viewer_appbar_share_to": "साझा करें", "album_viewer_page_share_add_users": "Add users", - "all": "All", + "all": "सभी", "all_people_page_title": "People", "all_videos_page_title": "Videos", "app_bar_signout_dialog_content": "क्या आप सुनिश्चित हैं कि आप लॉग आउट करना चाहते हैं?", "app_bar_signout_dialog_ok": "हाँ", "app_bar_signout_dialog_title": "लॉग आउट", - "archived": "Archived", + "archived": "संग्रहित", "archive_page_no_archived_assets": "No archived assets found", "archive_page_title": "Archive ({})", "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", @@ -58,14 +59,19 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Photo grid layout settings", "asset_list_settings_title": "Photo Grid", - "asset_restored_successfully": "Asset restored successfully", - "assets_deleted_permanently": "{} asset(s) deleted permanently", - "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", - "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", - "assets_restored_successfully": "{} asset(s) restored successfully", - "assets_trashed": "{} asset(s) trashed", - "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "asset_restored_successfully": "संपत्ति(याँ) सफलतापूर्वक पुनर्स्थापित की गईं", + "assets_deleted_permanently": "{} संपत्ति(याँ) स्थायी रूप से हटा दी गईं", + "assets_deleted_permanently_from_server": "{} संपत्ति(याँ) इमिच सर्वर से स्थायी रूप से हटा दी गईं", + "assets_removed_permanently_from_device": "{} संपत्ति(याँ) आपके डिवाइस से स्थायी रूप से हटा दी गईं", + "assets_restored_successfully": "{} संपत्ति(याँ) सफलतापूर्वक पुनर्स्थापित की गईं", + "assets_trashed": "{} संपत्ति(याँ) कचरे में डाली गईं", + "assets_trashed_from_server": "{} संपत्ति(याँ) इमिच सर्वर से कचरे में डाली गईं", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Asset Viewer", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Albums on device ({})", "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", @@ -131,6 +137,7 @@ "backup_manual_success": "Success", "backup_manual_title": "Upload status", "backup_options_page_title": "Backup options", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", "cache_settings_clear_cache_button": "Clear cache", "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "स्थानीय संग्रहण के व्यवहार को नियंत्रित करें", "cache_settings_tile_title": "स्थानीय संग्रहण", "cache_settings_title": "Caching Settings", + "cancel": "Cancel", "change_password_form_confirm_password": "Confirm Password", "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", @@ -168,7 +179,7 @@ "common_create_new_album": "Create new album", "common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.", "common_shared": "Shared", - "contextual_search": "Sunrise on the beach", + "contextual_search": "समुद्र तट पर सूर्योदय", "control_bottom_app_bar_add_to_album": "Add to album", "control_bottom_app_bar_album_info": "{} items", "control_bottom_app_bar_album_info_shared": "{} items · Shared", @@ -177,8 +188,8 @@ "control_bottom_app_bar_delete": "Delete", "control_bottom_app_bar_delete_from_immich": "Delete from Immich", "control_bottom_app_bar_delete_from_local": "Delete from device", - "control_bottom_app_bar_download": "Download", - "control_bottom_app_bar_edit": "Edit", + "control_bottom_app_bar_download": "डाउनलोड", + "control_bottom_app_bar_edit": "संपादित करें", "control_bottom_app_bar_edit_location": "Edit Location", "control_bottom_app_bar_edit_time": "Edit Date & Time", "control_bottom_app_bar_favorite": "Favorite", @@ -189,16 +200,17 @@ "control_bottom_app_bar_unarchive": "Unarchive", "control_bottom_app_bar_unfavorite": "Unfavorite", "control_bottom_app_bar_upload": "Upload", - "create_album": "Create album", + "create_album": "एल्बम बनाएँ", "create_album_page_untitled": "Untitled", - "create_new": "CREATE NEW", + "create_new": "नया बनाएं", "create_shared_album_page_create": "Create", "create_shared_album_page_share": "Share", "create_shared_album_page_share_add_assets": "ADD ASSETS", "create_shared_album_page_share_select_photos": "Select Photos", - "crop": "Crop", + "crop": "छाँटें", "curated_location_page_title": "Places", "curated_object_page_title": "Things", + "current_server_address": "Current server address", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -216,26 +228,27 @@ "delete_shared_link_dialog_title": "साझा किए गए लिंक को हटाएं", "description_input_hint_text": "Add description...", "description_input_submit_error": "Error updating description, check the log for more details", - "download_canceled": "Download canceled", - "download_complete": "Download complete", - "download_enqueue": "Download enqueued", - "download_error": "Download Error", - "download_failed": "Download failed", - "download_filename": "file: {}", - "download_finished": "Download finished", - "downloading": "Downloading...", - "downloading_media": "Downloading media", - "download_notfound": "Download not found", - "download_paused": "Download paused", - "download_started": "Download started", - "download_sucess": "Download success", - "download_sucess_android": "The media has been downloaded to DCIM/Immich", - "download_waiting_to_retry": "Waiting to retry", + "download_canceled": "डाउनलोड रद्द कर दिया गया", + "download_complete": "डाउनलोड पूरा", + "download_enqueue": "डाउनलोड कतार में है", + "download_error": "डाउनलोड त्रुटि", + "download_failed": "डाउनलोड विफल", + "download_filename": "फ़ाइल: {}", + "download_finished": "डाउनलोड समाप्त", + "downloading": "डाउनलोड हो रहा है...", + "downloading_media": "मीडिया डाउनलोड हो रहा है", + "download_notfound": "डाउनलोड नहीं मिला", + "download_paused": "डाउनलोड स्थगित", + "download_started": "डाउनलोड प्रारंभ हुआ", + "download_sucess": "डाउनलोड सफल", + "download_sucess_android": "मीडिया DCIM/Immich में डाउनलोड हो गया है", + "download_waiting_to_retry": "पुनः प्रयास करने का इंतजार कर रहा है", "edit_date_time_dialog_date_time": "Date and Time", "edit_date_time_dialog_timezone": "Timezone", - "edit_image_title": "Edit", + "edit_image_title": "संपादित करें", "edit_location_dialog_title": "Location", - "error_saving_image": "Error: {}", + "enter_wifi_name": "Enter WiFi name", + "error_saving_image": "त्रुटि: {}", "exif_bottom_sheet_description": "Add Description...", "exif_bottom_sheet_details": "DETAILS", "exif_bottom_sheet_location": "LOCATION", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Enable experimental photo grid", "experimental_settings_subtitle": "Use at your own risk!", "experimental_settings_title": "Experimental", - "favorites": "Favorites", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", + "favorites": "पसंदीदा", "favorites_page_no_favorites": "No favorite assets found", "favorites_page_title": "Favorites", - "filename_search": "File name or extension", - "filter": "Filter", + "filename_search": "फ़ाइल नाम या एक्सटेंशन", + "filter": "फ़िल्टर", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Enable haptic feedback", "haptic_feedback_title": "Haptic Feedback", "header_settings_add_header_tip": "Add Header", @@ -274,16 +291,16 @@ "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", "home_page_share_err_local": "लोकल एसेट्स को लिंक के जरिए शेयर नहीं कर सकते, स्किप कर रहे हैं", "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", - "ignore_icloud_photos": "Ignore iCloud photos", - "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server", - "image_saved_successfully": "Image saved", + "ignore_icloud_photos": "आइक्लाउड फ़ोटो को अनदेखा करें", + "ignore_icloud_photos_description": "आइक्लाउड पर स्टोर की गई फ़ोटोज़ इमिच सर्वर पर अपलोड नहीं की जाएंगी", + "image_saved_successfully": "इमेज सहेज दी गई", "image_viewer_page_state_provider_download_error": "Download Error", "image_viewer_page_state_provider_download_started": "Download Started", "image_viewer_page_state_provider_download_success": "Download Success", "image_viewer_page_state_provider_share_error": "Share Error", - "invalid_date": "Invalid date", - "invalid_date_format": "Invalid date format", - "library": "Library", + "invalid_date": "अमान्य तारीख़", + "invalid_date_format": "अमान्य तारीख़ प्रारूप", + "library": "गैलरी", "library_page_albums": "Albums", "library_page_archive": "Archive", "library_page_device_albums": "Albums on Device", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Oldest photo", "library_page_sort_most_recent_photo": "Most recent photo", "library_page_sort_title": "Album title", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Choose on map", "location_picker_latitude": "Latitude", "location_picker_latitude_error": "Enter a valid latitude", @@ -364,16 +385,18 @@ "motion_photos_page_title": "Motion Photos", "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", - "my_albums": "My albums", + "my_albums": "मेरे एल्बम", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "No assets to show", - "no_name": "No name", + "no_name": "कोई नाम नहीं", "notification_permission_dialog_cancel": "Cancel", "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", "notification_permission_dialog_settings": "Settings", "notification_permission_list_tile_content": "Grant permission to enable notifications.", "notification_permission_list_tile_enable_button": "Enable Notifications", "notification_permission_list_tile_title": "Notification Permission", - "on_this_device": "On this device", + "on_this_device": "इस डिवाइस पर", "partner_list_user_photos": "{user}'s photos", "partner_list_view_all": "View all", "partner_page_add_partner": "Add partner", @@ -385,8 +408,8 @@ "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", "partner_page_stop_sharing_title": "Stop sharing your photos?", "partner_page_title": "Partner", - "partners": "Partners", - "people": "People", + "partners": "साझेदार", + "people": "लोग", "permission_onboarding_back": "वापस", "permission_onboarding_continue_anyway": "Continue anyway", "permission_onboarding_get_started": "Get started", @@ -397,7 +420,8 @@ "permission_onboarding_permission_granted": "Permission granted! You are all set.", "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.", "permission_onboarding_request": "Immich requires permission to view your photos and videos.", - "places": "Places", + "places": "स्थान", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Preferences", "profile_drawer_app_logs": "Logs", "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", @@ -410,37 +434,38 @@ "profile_drawer_settings": "Settings", "profile_drawer_sign_out": "Sign Out", "profile_drawer_trash": "Trash", - "recently_added": "Recently added", + "recently_added": "हाल ही में जोड़ा गया", "recently_added_page_title": "Recently Added", - "save_to_gallery": "Save to gallery", + "save": "Save", + "save_to_gallery": "गैलरी में सहेजें", "scaffold_body_error_occurred": "Error occurred", - "search_albums": "Search albums", + "search_albums": "एल्बम खोजें", "search_bar_hint": "Search your photos", "search_filter_apply": "Apply filter", - "search_filter_camera": "Camera", + "search_filter_camera": "कैमरा", "search_filter_camera_make": "Make", "search_filter_camera_model": "Model", - "search_filter_camera_title": "Select camera type", - "search_filter_date": "Date", - "search_filter_date_interval": "{start} to {end}", - "search_filter_date_title": "Select a date range", + "search_filter_camera_title": "कैमरा प्रकार चुनें", + "search_filter_date": "तारीख़", + "search_filter_date_interval": "{start} से {end} तक", + "search_filter_date_title": "तारीख़ की सीमा चुनें", "search_filter_display_option_archive": "Archive", "search_filter_display_option_favorite": "Favorite", "search_filter_display_option_not_in_album": "Not in album", - "search_filter_display_options": "Display Options", - "search_filter_display_options_title": "Display options", - "search_filter_location": "Location", + "search_filter_display_options": "प्रदर्शन विकल्प", + "search_filter_display_options_title": "प्रदर्शन विकल्प", + "search_filter_location": "स्थान", "search_filter_location_city": "City", "search_filter_location_country": "Country", "search_filter_location_state": "State", - "search_filter_location_title": "Select location", - "search_filter_media_type": "Media Type", + "search_filter_location_title": "स्थान चुनें", + "search_filter_media_type": "मीडिया प्रकार", "search_filter_media_type_all": "All", "search_filter_media_type_image": "Image", - "search_filter_media_type_title": "Select media type", + "search_filter_media_type_title": "मीडिया प्रकार चुनें", "search_filter_media_type_video": "Video", - "search_filter_people": "People", - "search_filter_people_title": "Select people", + "search_filter_people": "लोग", + "search_filter_people_title": "लोगों का चयन करें", "search_page_categories": "Categories", "search_page_favorites": "Favorites", "search_page_motion_photos": "Motion Photos", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Suggestions", "select_user_for_sharing_page_err_album": "Failed to create album", "select_user_for_sharing_page_share_suggestions": "Suggestions", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "App Version", "server_info_box_latest_release": "लेटेस्ट वर्ज़न", "server_info_box_server_url": "सर्वर URL", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Load preview image", "setting_image_viewer_title": "Images", "setting_languages_apply": "Apply", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Languages", "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", "setting_notifications_notify_hours": "{} hours", @@ -560,9 +587,9 @@ "shared_link_info_chip_upload": "Upload", "shared_link_manage_links": "साझा किए गए लिंक का प्रबंधन करें", "shared_link_public_album": "Public album", - "shared_links": "Shared links", + "shared_links": "साझा किए गए लिंक", "share_done": "Done", - "shared_with_me": "Shared with me", + "shared_with_me": "मेरे साथ साझा किया गया", "share_invite": "Invite to album", "sharing_page_album": "Shared albums", "sharing_page_description": "Create shared albums to share photos and videos with people in your network.", @@ -570,32 +597,32 @@ "sharing_silver_appbar_create_shared_album": "New shared album", "sharing_silver_appbar_shared_links": "Shared links", "sharing_silver_appbar_share_partner": "Share with partner", - "sync": "Sync", - "sync_albums": "Sync albums", - "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", - "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", + "sync": "सिंक करें", + "sync_albums": "एल्बम्स सिंक करें", + "sync_albums_manual_subtitle": "चुने हुए बैकअप एल्बम्स में सभी अपलोड की गई वीडियो और फ़ोटो सिंक करें", + "sync_upload_album_setting_subtitle": "अपनी फ़ोटो और वीडियो बनाएँ और उन्हें इमिच पर चुने हुए एल्बम्स में अपलोड करें", "tab_controller_nav_library": "Library", "tab_controller_nav_photos": "Photos", "tab_controller_nav_search": "Search", "tab_controller_nav_sharing": "Sharing", "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles", "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})", - "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", - "theme_setting_colorful_interface_title": "Colorful interface", + "theme_setting_colorful_interface_subtitle": "प्राथमिक रंग को पृष्ठभूमि सतहों पर लागू करें", + "theme_setting_colorful_interface_title": "रंगीन इंटरफ़ेस", "theme_setting_dark_mode_switch": "Dark mode", "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer", "theme_setting_image_viewer_quality_title": "Image viewer quality", - "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", - "theme_setting_primary_color_title": "Primary color", - "theme_setting_system_primary_color_title": "Use system color", + "theme_setting_primary_color_subtitle": "प्राथमिक क्रियाओं और उच्चारणों के लिए एक रंग चुनें", + "theme_setting_primary_color_title": "प्राथमिक रंग", + "theme_setting_system_primary_color_title": "सिस्टम रंग का उपयोग करें", "theme_setting_system_theme_switch": "Automatic (Follow system setting)", "theme_setting_theme_subtitle": "Choose the app's theme setting", "theme_setting_theme_title": "Theme", "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", "theme_setting_three_stage_loading_title": "Enable three-stage loading", "translated_text_options": "Options", - "trash": "Trash", - "trash_emptied": "Emptied trash", + "trash": "कचरा", + "trash_emptied": "कचरा खाली कर दिया", "trash_page_delete": "Delete", "trash_page_delete_all": "Delete All", "trash_page_empty_trash_btn": "कूड़ेदान खाली करें", @@ -612,14 +639,18 @@ "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", "upload_dialog_ok": "Upload", "upload_dialog_title": "Upload Asset", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Acknowledge", "version_announcement_overlay_release_notes": "release notes", "version_announcement_overlay_text_1": "Hi friend, there is a new release of", "version_announcement_overlay_text_2": "please take your time to visit the ", "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", - "videos": "Videos", + "videos": "वीडियो", "viewer_remove_from_stack": "स्टैक से हटाएं", "viewer_stack_use_as_main_asset": "मुख्य संपत्ति के रूप में उपयोग करें", - "viewer_unstack": "स्टैक रद्द करें" + "viewer_unstack": "स्टैक रद्द करें", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/hu-HU.json b/mobile/assets/i18n/hu-HU.json index a19263e2bcb30..b71cc21321b19 100644 --- a/mobile/assets/i18n/hu-HU.json +++ b/mobile/assets/i18n/hu-HU.json @@ -7,6 +7,7 @@ "action_common_select": "Kiválaszt", "action_common_update": "Frissít", "add_a_name": "Név hozzáadása", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Hozzáadva a(z) \"{album}\" albumhoz", "add_to_album_bottom_sheet_already_exists": "Már benne van a(z) \"{album}\" albumban", "advanced_settings_log_level_title": "Naplózás szintje: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} elem sikeresen helyreállítva", "assets_trashed": "{} elem lomtárba helyezve", "assets_trashed_from_server": "{} elem lomtárba helyezve az Immich szerveren", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Elem Megjelenítő", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Ezen az eszközön lévő albumok ({})", "backup_album_selection_page_albums_tap": "Koppints a hozzáadáshoz, duplán koppints az eltávolításhoz", "backup_album_selection_page_assets_scatter": "Egy elem több albumban is lehet. Ezért a mentéshez albumokat lehet hozzáadni vagy azokat a mentésből kihagyni.", @@ -131,6 +137,7 @@ "backup_manual_success": "Sikeres", "backup_manual_title": "Feltöltés állapota", "backup_options_page_title": "Biztonági mentés beállításai", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Képtár oldalankénti bélyegképei ({} elem)", "cache_settings_clear_cache_button": "Gyorsítótár kiürítése", "cache_settings_clear_cache_button_title": "Kiüríti az alkalmazás gyorsítótárát. Ez jelentősen kihat az alkalmazás teljesítményére, amíg a gyorsítótár újra nem épül.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Helyi tárhely viselkedésének beállítása", "cache_settings_tile_title": "Helyi Tárhely", "cache_settings_title": "Gyorsítótár Beállítások", + "cancel": "Cancel", "change_password_form_confirm_password": "Jelszó Megerősítése", "change_password_form_description": "Szia {name}!\n\nMost jelentkezel be először a rendszerbe vagy más okból szükséges a jelszavad meváltoztatása. Kérjük, add meg új jelszavad.", "change_password_form_new_password": "Új Jelszó", "change_password_form_password_mismatch": "A beírt jelszavak nem egyeznek", "change_password_form_reenter_new_password": "Jelszó (Még Egyszer)", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Jelszó Megadása", "client_cert_import": "Importálás", @@ -199,6 +210,7 @@ "crop": "Kivágás", "curated_location_page_title": "Helyek", "curated_object_page_title": "Dolgok", + "current_server_address": "Current server address", "daily_title_text_date": "MMM dd (E)", "daily_title_text_date_year": "yyyy MMM dd (E)", "date_format": "y LLL d (E) • HH:mm", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Időzóna", "edit_image_title": "Szerkesztés", "edit_location_dialog_title": "Hely", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Hiba: {}", "exif_bottom_sheet_description": "Leírás Hozzáadása...", "exif_bottom_sheet_details": "RÉSZLETEK", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Kisérleti képrács engedélyezése", "experimental_settings_subtitle": "Csak saját felelősségre használd!", "experimental_settings_title": "Kísérleti", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Kedvencek", "favorites_page_no_favorites": "Nem található kedvencnek jelölt elem", "favorites_page_title": "Kedvencek", "filename_search": "Fájlnév vagy kiterjesztés", "filter": "Szűrő", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Rezgéses visszajelzés engedélyezése", "haptic_feedback_title": "Rezgéses Visszajelzés", "header_settings_add_header_tip": "Fejléc Hozzáadása", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Legrégebbi fotó", "library_page_sort_most_recent_photo": "Legújabb fotó", "library_page_sort_title": "Album címe", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Válassz a térképen", "location_picker_latitude": "Szélességi kör", "location_picker_latitude_error": "Érvényes szélességi kört írj be", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Csak-olvasható elem(ek) dátuma nem módosítható, ezért kihagyjuk", "multiselect_grid_edit_gps_err_read_only": "Csak-olvasható elem(ek) helye nem módosítható, ezért kihagyjuk", "my_albums": "Saját albumaim", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "Nincs megjeleníthető elem", "no_name": "Névtelen", "notification_permission_dialog_cancel": "Mégsem", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Korlátozott hozzáférés. Ha szeretnéd, hogy az Immich a teljes galéria gyűjteményedet mentse és kezelje, akkor a Beállításokban engedélyezd a fotó és videó jogosultságokat.", "permission_onboarding_request": "Engedélyezni kell, hogy az Immich hozzáférjen a képeidhez és videóidhoz", "places": "Helyek", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Beállítások", "profile_drawer_app_logs": "Naplók", "profile_drawer_client_out_of_date_major": "A mobilalkalmazás elavult. Kérjük, frissítsd a legfrisebb főverzióra.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Lomtár", "recently_added": "Nemrég hozzáadott", "recently_added_page_title": "Nemrég Hozzáadott", + "save": "Save", "save_to_gallery": "Mentés a galériába", "scaffold_body_error_occurred": "Hiba történt", "search_albums": "Albumok keresése", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Javaslatok", "select_user_for_sharing_page_err_album": "Az album létrehozása sikertelen", "select_user_for_sharing_page_share_suggestions": "Javaslatok", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Alkalmazás Verzió", "server_info_box_latest_release": "Legfrissebb Verzió", "server_info_box_server_url": "Szerver Címe", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Előnézet betöltése", "setting_image_viewer_title": "Képek", "setting_languages_apply": "Alkalmaz", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Nyelvek", "setting_notifications_notify_failures_grace_period": "Értesítés a háttérben történő mentés hibáiról: {}", "setting_notifications_notify_hours": "{} óra", @@ -612,6 +639,8 @@ "upload_dialog_info": "Szeretnél mentést készíteni a kiválasztott elem(ek)ről a szerverre?", "upload_dialog_ok": "Feltöltés", "upload_dialog_title": "Elem Feltöltése", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Megértettem", "version_announcement_overlay_release_notes": "kiadási megjegyzések áttekintésére", "version_announcement_overlay_text_1": "Szia barátom, ennek az alkalmazásnak van egy új verziója: ", @@ -621,5 +650,7 @@ "videos": "Videók", "viewer_remove_from_stack": "Eltávolít a Csoportból", "viewer_stack_use_as_main_asset": "Fő Elemnek Beállít", - "viewer_unstack": "Csoport Megszűntetése" + "viewer_unstack": "Csoport Megszűntetése", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/it-IT.json b/mobile/assets/i18n/it-IT.json index 3d5c2805f0820..524b313a903cb 100644 --- a/mobile/assets/i18n/it-IT.json +++ b/mobile/assets/i18n/it-IT.json @@ -7,6 +7,7 @@ "action_common_select": "Select", "action_common_update": "Aggiorna", "add_a_name": "Add a name", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Aggiunto in {album}", "add_to_album_bottom_sheet_already_exists": "Già presente in {album}", "advanced_settings_log_level_title": "Livello log: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} asset(s) restored successfully", "assets_trashed": "{} asset(s) trashed", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Visualizzazione risorse", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Album sul dispositivo ({})", "backup_album_selection_page_albums_tap": "Tap per includere, doppio tap per escludere.", "backup_album_selection_page_assets_scatter": "Visto che le risorse possono trovarsi in più album, questi possono essere inclusi o esclusi dal backup.", @@ -131,6 +137,7 @@ "backup_manual_success": "Successo", "backup_manual_title": "Stato del caricamento", "backup_options_page_title": "Opzioni di Backup", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Anteprime pagine librerie ({} risorse)", "cache_settings_clear_cache_button": "Pulisci cache", "cache_settings_clear_cache_button_title": "Pulisce la cache dell'app. Questo impatterà significativamente le prestazioni dell''app fino a quando la cache non sarà rigenerata.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Controlla il comportamento dello storage locale", "cache_settings_tile_title": "Archiviazione locale", "cache_settings_title": "Impostazioni della Cache", + "cancel": "Cancel", "change_password_form_confirm_password": "Conferma Password", "change_password_form_description": "Ciao {name},\n\nQuesto è la prima volta che accedi al sistema oppure è stato fatto una richiesta di cambiare la password. Per favore inserisca la nuova password qui sotto", "change_password_form_new_password": "Nuova Password", "change_password_form_password_mismatch": "Le password non coincidono", "change_password_form_reenter_new_password": "Inserisci ancora la nuova password ", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", @@ -199,6 +210,7 @@ "crop": "Crop", "curated_location_page_title": "Location", "curated_object_page_title": "Oggetti", + "current_server_address": "Current server address", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "date_format": "E, d LLL, y • hh:mm", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Fuso orario", "edit_image_title": "Edit", "edit_location_dialog_title": "Posizione", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Error: {}", "exif_bottom_sheet_description": "Aggiungi una descrizione...", "exif_bottom_sheet_details": "DETTAGLI", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Attiva griglia foto sperimentale", "experimental_settings_subtitle": "Usalo a tuo rischio!", "experimental_settings_title": "Sperimentale", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favorites", "favorites_page_no_favorites": "Nessun preferito", "favorites_page_title": "Preferiti", "filename_search": "File name or extension", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Abilita feedback aptico", "haptic_feedback_title": "Feedback aptico", "header_settings_add_header_tip": "Add Header", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Foto più vecchia", "library_page_sort_most_recent_photo": "Più recente", "library_page_sort_title": "Titolo album", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Scegli una mappa", "location_picker_latitude": "Latitudine", "location_picker_latitude_error": "Inserisci una latitudine valida", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Non puoi modificare la data di risorse in sola lettura, azione ignorata", "multiselect_grid_edit_gps_err_read_only": "Non puoi modificare la posizione di risorse in sola lettura, azione ignorata", "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "Nessuna risorsa da mostrare", "no_name": "No name", "notification_permission_dialog_cancel": "Annulla", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Permessi limitati. Per consentire a Immich di gestire e fare i backup di tutta la galleria, concedi i permessi Foto e Video dalle Impostazioni.", "permission_onboarding_request": "Immich richiede i permessi per vedere le tue foto e video", "places": "Places", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Preferenze", "profile_drawer_app_logs": "Logs", "profile_drawer_client_out_of_date_major": "L'applicazione non è aggiornata. Per favore aggiorna all'ultima versione principale.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Cestino", "recently_added": "Recently added", "recently_added_page_title": "Aggiunti di recente", + "save": "Save", "save_to_gallery": "Save to gallery", "scaffold_body_error_occurred": "Si è verificato un errore.", "search_albums": "Search albums", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Suggerimenti ", "select_user_for_sharing_page_err_album": "Impossibile nel creare l'album ", "select_user_for_sharing_page_share_suggestions": "Suggerimenti", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Versione App", "server_info_box_latest_release": "Ultima Versione", "server_info_box_server_url": "Server URL", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Carica immagine di anteprima", "setting_image_viewer_title": "Images", "setting_languages_apply": "Applica", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Lingue", "setting_notifications_notify_failures_grace_period": "Notifica caricamenti falliti in background: {}", "setting_notifications_notify_hours": "{} ore", @@ -612,6 +639,8 @@ "upload_dialog_info": "Vuoi fare il backup sul server delle risorse selezionate?", "upload_dialog_ok": "Carica", "upload_dialog_title": "Carica file", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Presa visione", "version_announcement_overlay_release_notes": "note di rilascio", "version_announcement_overlay_text_1": "Ciao, c'è una nuova versione di", @@ -621,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "Rimuovi dalla pila", "viewer_stack_use_as_main_asset": "Usa come risorsa principale", - "viewer_unstack": "Rimuovi dal gruppo" + "viewer_unstack": "Rimuovi dal gruppo", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/ja-JP.json b/mobile/assets/i18n/ja-JP.json index bcc1df654866f..7843526d2f6cc 100644 --- a/mobile/assets/i18n/ja-JP.json +++ b/mobile/assets/i18n/ja-JP.json @@ -7,6 +7,7 @@ "action_common_select": "選択", "action_common_update": "更新", "add_a_name": "名前を追加", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "{album}に追加", "add_to_album_bottom_sheet_already_exists": "{album}に追加済み", "advanced_settings_log_level_title": "ログレベル: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{}項目を復元しました", "assets_trashed": "{}項目をゴミ箱に移動しました", "assets_trashed_from_server": "サーバー上の{}項目をゴミ箱に移動しました", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "アセットビューアー", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "端末上のアルバム数: {} ", "backup_album_selection_page_albums_tap": "タップで選択、ダブルタップで除外", "backup_album_selection_page_assets_scatter": "アルバムを選択・除外してバックアップする写真を選ぶ (同じ写真が複数のアルバムに登録されていることがあるため)", @@ -131,6 +137,7 @@ "backup_manual_success": "成功", "backup_manual_title": "アップロード状況", "backup_options_page_title": "バックアップオプション", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "ライブラリのサムネイル ({}枚)", "cache_settings_clear_cache_button": "キャッシュをクリア", "cache_settings_clear_cache_button_title": "キャッシュを削除 (キャッシュが再生成されるまで、アプリのパフォーマンスが著しく低下します)", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "ローカルストレージの挙動を確認する", "cache_settings_tile_title": "ローカルストレージ", "cache_settings_title": "キャッシュの設定", + "cancel": "Cancel", "change_password_form_confirm_password": "確定", "change_password_form_description": "{name}さん こんにちは\n\nサーバーにアクセスするのが初めてか、パスワードリセットのリクエストがされました。新しいパスワードを入力してください", "change_password_form_new_password": "新しいパスワード", "change_password_form_password_mismatch": "パスワードが一致しません", "change_password_form_reenter_new_password": "再度パスワードを入力してください", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "了解", "client_cert_enter_password": "パスワードを入力", "client_cert_import": "インポート", @@ -199,6 +210,7 @@ "crop": "クロップ", "curated_location_page_title": "撮影場所", "curated_object_page_title": "被写体", + "current_server_address": "Current server address", "daily_title_text_date": "MM DD, EE", "daily_title_text_date_year": "yyyy MM DD, EE", "date_format": "MM DD, EE • hh:mm", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "タイムゾーン", "edit_image_title": "編集", "edit_location_dialog_title": "位置情報", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "エラー: {}", "exif_bottom_sheet_description": "説明を追加", "exif_bottom_sheet_details": "詳細", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "試験的なグリッドを有効化", "experimental_settings_subtitle": "試験的機能につき自己責任で!", "experimental_settings_title": "試験的機能", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "お気に入り", "favorites_page_no_favorites": "お気に入り登録された写真またはビデオがありません", "favorites_page_title": "お気に入り", "filename_search": "ファイル名、又は拡張子", "filter": "フィルター", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "ハプティックフィードバック", "haptic_feedback_title": "ハプティックフィードバックを有効にする", "header_settings_add_header_tip": "ヘッダを追加", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "一番古い項目", "library_page_sort_most_recent_photo": "最近の項目", "library_page_sort_title": "アルバム名", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "マップを選択", "location_picker_latitude": "緯度", "location_picker_latitude_error": "有効な緯度を入力してください", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "読み取り専用の項目の日付を変更できません", "multiselect_grid_edit_gps_err_read_only": "読み取り専用の項目の位置情報を変更できません", "my_albums": "自分のアルバム", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "表示する項目がありません", "no_name": "名前がありません", "notification_permission_dialog_cancel": "キャンセル", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "写真へのアクセスが制限されています。Immichが写真のバックアップと管理を行うには、システム設定から写真と動画のアクセス権限を変更してください。", "permission_onboarding_request": "Immichは写真へのアクセス許可が必要です", "places": "場所", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "設定", "profile_drawer_app_logs": "ログ", "profile_drawer_client_out_of_date_major": "アプリが更新されてません。最新のバージョンに更新してください", @@ -412,6 +436,7 @@ "profile_drawer_trash": "ゴミ箱", "recently_added": "最近追加された項目", "recently_added_page_title": "最近", + "save": "Save", "save_to_gallery": "ギャラリーに保存", "scaffold_body_error_occurred": "エラーが発生しました", "search_albums": "アルバムを探す", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "ユーザーリスト", "select_user_for_sharing_page_err_album": "アルバム作成に失敗", "select_user_for_sharing_page_share_suggestions": "ユーザ一覧", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "アプリのバージョン", "server_info_box_latest_release": "最新バージョン", "server_info_box_server_url": " サーバーのURL", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "プレビューを読み込む", "setting_image_viewer_title": "画像", "setting_languages_apply": "適用する", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "言語", "setting_notifications_notify_failures_grace_period": "バックアップ失敗の通知: {}", "setting_notifications_notify_hours": "{}時間後", @@ -612,6 +639,8 @@ "upload_dialog_info": "選択した項目のバックアップをしますか?", "upload_dialog_ok": "アップロード", "upload_dialog_title": "アップロード", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "了解", "version_announcement_overlay_release_notes": "更新情報", "version_announcement_overlay_text_1": "新しい", @@ -621,5 +650,7 @@ "videos": "動画", "viewer_remove_from_stack": "スタックから外す", "viewer_stack_use_as_main_asset": "メインの画像として使用する", - "viewer_unstack": "スタックを解除" + "viewer_unstack": "スタックを解除", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/ko-KR.json b/mobile/assets/i18n/ko-KR.json index 02eace03b08b8..7ecb3da2fa043 100644 --- a/mobile/assets/i18n/ko-KR.json +++ b/mobile/assets/i18n/ko-KR.json @@ -7,6 +7,7 @@ "action_common_select": "선택", "action_common_update": "업데이트", "add_a_name": "이름 추가", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "{album}에 추가되었습니다.", "add_to_album_bottom_sheet_already_exists": "{album}에 이미 존재하는 항목입니다.", "advanced_settings_log_level_title": "로그 레벨: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "항목 {}개를 복원했습니다.", "assets_trashed": "휴지통으로 항목 {}개가 이동되었습니다.", "assets_trashed_from_server": "휴지통으로 Immich 항목 {}개가 이동되었습니다.", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "보기 옵션", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "기기의 앨범 ({})", "backup_album_selection_page_albums_tap": "한 번 눌러 선택, 두 번 눌러 제외하세요.", "backup_album_selection_page_assets_scatter": "각 항목은 여러 앨범에 포함될 수 있으며, 백업 진행 중에도 대상 앨범을 포함하거나 제외할 수 있습니다.", @@ -131,6 +137,7 @@ "backup_manual_success": "성공", "backup_manual_title": "업로드 상태", "backup_options_page_title": "백업 옵션", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "라이브러리 섬네일 ({})", "cache_settings_clear_cache_button": "캐시 지우기", "cache_settings_clear_cache_button_title": "앱 캐시를 지웁니다. 이 작업은 캐시가 다시 생성될 때까지 앱 성능에 상당한 영향을 미칠 수 있습니다.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "로컬 스토리지 동작 제어", "cache_settings_tile_title": "로컬 스토리지", "cache_settings_title": "캐시 설정", + "cancel": "Cancel", "change_password_form_confirm_password": "현재 비밀번호 입력", "change_password_form_description": "안녕하세요 {name}님,\n\n첫 로그인이거나, 비밀번호가 초기화되어 비밀번호를 설정해야 합니다. 아래에 새 비밀번호를 입력해주세요.", "change_password_form_new_password": "새 비밀번호 입력", "change_password_form_password_mismatch": "비밀번호가 일치하지 않습니다.", "change_password_form_reenter_new_password": "새 비밀번호 확인", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "확인", "client_cert_enter_password": "비밀번호 입력", "client_cert_import": "가져오기", @@ -199,6 +210,7 @@ "crop": "자르기", "curated_location_page_title": "장소", "curated_object_page_title": "사물", + "current_server_address": "Current server address", "daily_title_text_date": "M월 d일 EEEE", "daily_title_text_date_year": "yyyy년 M월 d일 EEEE", "date_format": "yyyy년 M월 d일 EEEE • a h:mm", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "시간대", "edit_image_title": "편집", "edit_location_dialog_title": "위치", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "오류: {}", "exif_bottom_sheet_description": "설명 추가...", "exif_bottom_sheet_details": "상세 정보", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "새 사진 배열 사용 (실험적)", "experimental_settings_subtitle": "본인 책임 하에 사용하세요!", "experimental_settings_title": "실험적", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "즐겨찾기", "favorites_page_no_favorites": "즐겨찾기된 항목 없음", "favorites_page_title": "즐겨찾기", "filename_search": "파일 이름 또는 확장자", "filter": "필터", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "햅틱 피드백 활성화", "haptic_feedback_title": "햅틱 피드백", "header_settings_add_header_tip": "헤더 추가", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "오래된 순", "library_page_sort_most_recent_photo": "최신순", "library_page_sort_title": "앨범 제목", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "지도에서 선택", "location_picker_latitude": "위도", "location_picker_latitude_error": "유효한 위도를 입력하세요.", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "읽기 전용 항목의 날짜는 변경할 수 없습니다. 건너뜁니다.", "multiselect_grid_edit_gps_err_read_only": "읽기 전용 항목의 위치는 변경할 수 없습니다. 건너뜁니다.", "my_albums": "내 앨범", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "표시할 항목 없음", "no_name": "이름 없음", "notification_permission_dialog_cancel": "취소", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "권한이 없습니다. Immich가 전체 갤러리 컬렉션을 백업하고 관리할 수 있도록 하려면 설정에서 사진 및 동영상 권한을 부여하세요.", "permission_onboarding_request": "사진 및 동영상 권한이 필요합니다.", "places": "장소", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "설정", "profile_drawer_app_logs": "로그", "profile_drawer_client_out_of_date_major": "모바일 앱이 최신 버전이 아닙니다. 최신 버전으로 업데이트하세요.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "휴지통", "recently_added": "최근 추가", "recently_added_page_title": "최근 추가", + "save": "Save", "save_to_gallery": "갤러리에 저장", "scaffold_body_error_occurred": "문제가 발생했습니다.", "search_albums": "앨범 검색", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "추천", "select_user_for_sharing_page_err_album": "앨범을 생성하지 못했습니다.", "select_user_for_sharing_page_share_suggestions": "제안", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "앱 버전", "server_info_box_latest_release": "최신 버전", "server_info_box_server_url": "서버 URL", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "미리 보기 이미지 불러오기", "setting_image_viewer_title": "이미지", "setting_languages_apply": "적용", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "언어", "setting_notifications_notify_failures_grace_period": "백그라운드 백업 실패 알림: {}", "setting_notifications_notify_hours": "{}시간 후", @@ -612,6 +639,8 @@ "upload_dialog_info": "선택한 항목을 서버에 백업하시겠습니까?", "upload_dialog_ok": "업로드", "upload_dialog_title": "항목 업로드", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "확인", "version_announcement_overlay_release_notes": "릴리스 노트", "version_announcement_overlay_text_1": "안녕하세요,", @@ -621,5 +650,7 @@ "videos": "동영상", "viewer_remove_from_stack": "스택에서 제거", "viewer_stack_use_as_main_asset": "대표 사진으로 설정", - "viewer_unstack": "스택 해제" + "viewer_unstack": "스택 해제", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/lt-LT.json b/mobile/assets/i18n/lt-LT.json index 0075f65de0557..6fb2ed4ff58ba 100644 --- a/mobile/assets/i18n/lt-LT.json +++ b/mobile/assets/i18n/lt-LT.json @@ -7,6 +7,7 @@ "action_common_select": "Select", "action_common_update": "Update", "add_a_name": "Add a name", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Added to {album}", "add_to_album_bottom_sheet_already_exists": "Already in {album}", "advanced_settings_log_level_title": "Log level: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} asset(s) restored successfully", "assets_trashed": "{} asset(s) trashed", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Asset Viewer", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Albums on device ({})", "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", @@ -131,6 +137,7 @@ "backup_manual_success": "Success", "backup_manual_title": "Upload status", "backup_options_page_title": "Backup options", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", "cache_settings_clear_cache_button": "Clear cache", "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Control the local storage behaviour", "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Caching Settings", + "cancel": "Cancel", "change_password_form_confirm_password": "Confirm Password", "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", @@ -199,6 +210,7 @@ "crop": "Crop", "curated_location_page_title": "Places", "curated_object_page_title": "Things", + "current_server_address": "Current server address", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", "edit_location_dialog_title": "Location", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Error: {}", "exif_bottom_sheet_description": "Add Description...", "exif_bottom_sheet_details": "DETAILS", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Enable experimental photo grid", "experimental_settings_subtitle": "Use at your own risk!", "experimental_settings_title": "Experimental", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favorites", "favorites_page_no_favorites": "No favorite assets found", "favorites_page_title": "Favorites", "filename_search": "File name or extension", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Enable haptic feedback", "haptic_feedback_title": "Haptic Feedback", "header_settings_add_header_tip": "Add Header", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Oldest photo", "library_page_sort_most_recent_photo": "Most recent photo", "library_page_sort_title": "Album title", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Choose on map", "location_picker_latitude": "Latitude", "location_picker_latitude_error": "Enter a valid latitude", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "No assets to show", "no_name": "No name", "notification_permission_dialog_cancel": "Cancel", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.", "permission_onboarding_request": "Immich requires permission to view your photos and videos.", "places": "Places", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Preferences", "profile_drawer_app_logs": "Logs", "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Trash", "recently_added": "Recently added", "recently_added_page_title": "Recently Added", + "save": "Save", "save_to_gallery": "Save to gallery", "scaffold_body_error_occurred": "Error occurred", "search_albums": "Search albums", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Suggestions", "select_user_for_sharing_page_err_album": "Failed to create album", "select_user_for_sharing_page_share_suggestions": "Suggestions", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "App Version", "server_info_box_latest_release": "Latest Version", "server_info_box_server_url": "Server URL", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Load preview image", "setting_image_viewer_title": "Images", "setting_languages_apply": "Apply", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Languages", "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", "setting_notifications_notify_hours": "{} hours", @@ -612,6 +639,8 @@ "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", "upload_dialog_ok": "Upload", "upload_dialog_title": "Upload Asset", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Acknowledge", "version_announcement_overlay_release_notes": "release notes", "version_announcement_overlay_text_1": "Hi friend, there is a new release of", @@ -621,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" + "viewer_unstack": "Un-Stack", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/lv-LV.json b/mobile/assets/i18n/lv-LV.json index b49e2f5af75c3..8b9d79327852d 100644 --- a/mobile/assets/i18n/lv-LV.json +++ b/mobile/assets/i18n/lv-LV.json @@ -7,6 +7,7 @@ "action_common_select": "Select", "action_common_update": "Atjaunināt", "add_a_name": "Add a name", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Pievienots {album}", "add_to_album_bottom_sheet_already_exists": "Jau pievienots {album}", "advanced_settings_log_level_title": "Žurnalēšanas līmenis: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} asset(s) restored successfully", "assets_trashed": "{} asset(s) trashed", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Aktīvu Skatītājs", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Albumi ierīcē ({})", "backup_album_selection_page_albums_tap": "Pieskarieties, lai iekļautu, veiciet dubultskārienu, lai izslēgtu", "backup_album_selection_page_assets_scatter": "Aktīvi var būt izmētāti pa vairākiem albumiem. Tādējādi dublēšanas procesā albumus var iekļaut vai neiekļaut.", @@ -131,6 +137,7 @@ "backup_manual_success": "Veiksmīgi", "backup_manual_title": "Augšupielādes statuss", "backup_options_page_title": "Dublēšanas iestatījumi", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Bibliotēkas lapu sīktēli ({} aktīvi)", "cache_settings_clear_cache_button": "Iztīrīt kešatmiņu", "cache_settings_clear_cache_button_title": "Iztīra aplikācijas kešatmiņu. Tas būtiski ietekmēs lietotnes veiktspēju, līdz kešatmiņa būs pārbūvēta.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Kontrolēt lokālās krātuves uzvedību", "cache_settings_tile_title": "Lokālā Krātuve", "cache_settings_title": "Kešdarbes iestatījumi", + "cancel": "Cancel", "change_password_form_confirm_password": "Apstiprināt Paroli", "change_password_form_description": "Sveiki {name},\n\nŠī ir pirmā reize, kad pierakstāties sistēmā, vai arī ir iesniegts pieprasījums mainīt paroli. Lūdzu, zemāk ievadiet jauno paroli.", "change_password_form_new_password": "Jauna Parole", "change_password_form_password_mismatch": "Paroles nesakrīt", "change_password_form_reenter_new_password": "Atkārtoti ievadīt jaunu paroli", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", @@ -199,6 +210,7 @@ "crop": "Crop", "curated_location_page_title": "Vietas", "curated_object_page_title": "Lietas", + "current_server_address": "Current server address", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, gggg", "date_format": "E, LLL d, g • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Laika zona", "edit_image_title": "Edit", "edit_location_dialog_title": "Atrašanās vieta", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Error: {}", "exif_bottom_sheet_description": "Pievienot Aprakstu...", "exif_bottom_sheet_details": "INFORMĀCIJA", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Iespējot eksperimentālo fotorežģi", "experimental_settings_subtitle": "Izmanto uzņemoties risku!", "experimental_settings_title": "Eksperimentāls", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favorites", "favorites_page_no_favorites": "Nav atrasti iecienītākie aktīvi", "favorites_page_title": "Izlase", "filename_search": "File name or extension", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Iestatīt haptisku reakciju", "haptic_feedback_title": "Haptiska Reakcija", "header_settings_add_header_tip": "Add Header", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Vecākais fotoattēls", "library_page_sort_most_recent_photo": "Jaunākais fotoattēls", "library_page_sort_title": "Albuma virsraksts", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Izvēlēties uz kartes", "location_picker_latitude": "Ģeogrāfiskais platums", "location_picker_latitude_error": "Ievadiet korektu ģeogrāfisko platumu", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Nevar rediģēt read only aktīva(-u) datumu, notiek izlaišana", "multiselect_grid_edit_gps_err_read_only": "Nevar rediģēt atrašanās vietu read only aktīva(-u) datumu, notiek izlaišana", "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "Nav uzrādāmo aktīvu", "no_name": "No name", "notification_permission_dialog_cancel": "Atcelt", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Atļauja ierobežota. Lai atļautu Immich dublēšanu un varētu pārvaldīt visu galeriju kolekciju, sadaļā Iestatījumi piešķiriet fotoattēlu un video atļaujas.", "permission_onboarding_request": "Immich nepieciešama atļauja skatīt jūsu fotoattēlus un videoklipus.", "places": "Places", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Iestatījumi", "profile_drawer_app_logs": "Žurnāli", "profile_drawer_client_out_of_date_major": "Mobilā Aplikācija ir novecojusi. Lūdzu atjaunojiet to uz jaunāko lielo versiju", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Atkritne", "recently_added": "Recently added", "recently_added_page_title": "Nesen Pievienotais", + "save": "Save", "save_to_gallery": "Save to gallery", "scaffold_body_error_occurred": "Radās kļūda", "search_albums": "Search albums", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Ieteikumi", "select_user_for_sharing_page_err_album": "Neizdevās izveidot albumu", "select_user_for_sharing_page_share_suggestions": "Ieteikumi", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Aplikācijas Versija", "server_info_box_latest_release": "Jaunākā Versija", "server_info_box_server_url": "Servera URL", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Ielādēt priekšskatījuma attēlu", "setting_image_viewer_title": "Attēli", "setting_languages_apply": "Lietot", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Valodas", "setting_notifications_notify_failures_grace_period": "Paziņot par fona dublēšanas kļūmēm: {}", "setting_notifications_notify_hours": "{} stundas", @@ -612,6 +639,8 @@ "upload_dialog_info": "Vai vēlaties veikt izvēlētā(-o) aktīva(-u) dublējumu uz servera?", "upload_dialog_ok": "Augšupielādēt", "upload_dialog_title": "Augšupielādēt Aktīvu", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Atzīt", "version_announcement_overlay_release_notes": "informācija par laidienu", "version_announcement_overlay_text_1": "Sveiks draugs, ir jauns izlaidums no", @@ -621,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "Noņemt no Steka", "viewer_stack_use_as_main_asset": "Izmantot kā Galveno Aktīvu", - "viewer_unstack": "At-Stekot" + "viewer_unstack": "At-Stekot", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/mn-MN.json b/mobile/assets/i18n/mn-MN.json index 66392ed47a60d..ef45a29e99b39 100644 --- a/mobile/assets/i18n/mn-MN.json +++ b/mobile/assets/i18n/mn-MN.json @@ -7,6 +7,7 @@ "action_common_select": "Select", "action_common_update": "Update", "add_a_name": "Add a name", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Added to {album}", "add_to_album_bottom_sheet_already_exists": "Already in {album}", "advanced_settings_log_level_title": "Log level: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} asset(s) restored successfully", "assets_trashed": "{} asset(s) trashed", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Asset Viewer", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Albums on device ({})", "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", @@ -131,6 +137,7 @@ "backup_manual_success": "Success", "backup_manual_title": "Upload status", "backup_options_page_title": "Backup options", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", "cache_settings_clear_cache_button": "Clear cache", "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Control the local storage behaviour", "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Caching Settings", + "cancel": "Cancel", "change_password_form_confirm_password": "Confirm Password", "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", @@ -199,6 +210,7 @@ "crop": "Crop", "curated_location_page_title": "Places", "curated_object_page_title": "Things", + "current_server_address": "Current server address", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", "edit_location_dialog_title": "Location", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Error: {}", "exif_bottom_sheet_description": "Add Description...", "exif_bottom_sheet_details": "DETAILS", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Enable experimental photo grid", "experimental_settings_subtitle": "Use at your own risk!", "experimental_settings_title": "Experimental", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favorites", "favorites_page_no_favorites": "No favorite assets found", "favorites_page_title": "Favorites", "filename_search": "File name or extension", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Enable haptic feedback", "haptic_feedback_title": "Haptic Feedback", "header_settings_add_header_tip": "Add Header", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Oldest photo", "library_page_sort_most_recent_photo": "Most recent photo", "library_page_sort_title": "Album title", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Choose on map", "location_picker_latitude": "Latitude", "location_picker_latitude_error": "Enter a valid latitude", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "No assets to show", "no_name": "No name", "notification_permission_dialog_cancel": "Цуцлах", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.", "permission_onboarding_request": "Immich requires permission to view your photos and videos.", "places": "Places", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Preferences", "profile_drawer_app_logs": "Logs", "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Trash", "recently_added": "Recently added", "recently_added_page_title": "Recently Added", + "save": "Save", "save_to_gallery": "Save to gallery", "scaffold_body_error_occurred": "Error occurred", "search_albums": "Search albums", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Suggestions", "select_user_for_sharing_page_err_album": "Failed to create album", "select_user_for_sharing_page_share_suggestions": "Suggestions", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "App Version", "server_info_box_latest_release": "Latest Version", "server_info_box_server_url": "Server URL", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Load preview image", "setting_image_viewer_title": "Images", "setting_languages_apply": "Apply", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Languages", "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", "setting_notifications_notify_hours": "{} hours", @@ -612,6 +639,8 @@ "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", "upload_dialog_ok": "Upload", "upload_dialog_title": "Upload Asset", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Acknowledge", "version_announcement_overlay_release_notes": "release notes", "version_announcement_overlay_text_1": "Hi friend, there is a new release of", @@ -621,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" + "viewer_unstack": "Un-Stack", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/nb-NO.json b/mobile/assets/i18n/nb-NO.json index 80c9db2804672..e58b0e1c2e75e 100644 --- a/mobile/assets/i18n/nb-NO.json +++ b/mobile/assets/i18n/nb-NO.json @@ -7,6 +7,7 @@ "action_common_select": "Velg", "action_common_update": "Oppdater", "add_a_name": "Legg til navn", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Lagt til i {album}", "add_to_album_bottom_sheet_already_exists": "Allerede i {album}", "advanced_settings_log_level_title": "Loggnivå: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} objekt(er) gjenopprettet", "assets_trashed": "{} objekt(er) slettet", "assets_trashed_from_server": "{} objekt(er) slettet fra Immich serveren", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Objektviser", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Album på enhet ({})", "backup_album_selection_page_albums_tap": "Trykk for å inkludere, dobbelttrykk for å ekskludere", "backup_album_selection_page_assets_scatter": "Objekter kan bli spredd over flere album. Album kan derfor bli inkludert eller ekskludert under sikkerhetskopieringen.", @@ -131,6 +137,7 @@ "backup_manual_success": "Vellykket", "backup_manual_title": "Opplastingsstatus", "backup_options_page_title": "Backupinnstillinger", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Bibliotekminiatyrbilder ({} objekter)", "cache_settings_clear_cache_button": "Tøm buffer", "cache_settings_clear_cache_button_title": "Tømmer app-ens buffer. Dette vil ha betydelig innvirkning på appens ytelse inntil bufferen er gjenoppbygd.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Kontroller lokal lagring", "cache_settings_tile_title": "Lokal lagring", "cache_settings_title": "Bufringsinnstillinger", + "cancel": "Cancel", "change_password_form_confirm_password": "Bekreft passord", "change_password_form_description": "Hei {name}!\n\nDette er enten første gang du logger på systemet, eller det er sendt en forespørsel om å endre passordet ditt. Vennligst skriv inn det nye passordet nedenfor.", "change_password_form_new_password": "Nytt passord", "change_password_form_password_mismatch": "Passordene stemmer ikke", "change_password_form_reenter_new_password": "Skriv nytt passord igjen", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Skriv inn passord", "client_cert_import": "Importer", @@ -199,6 +210,7 @@ "crop": "Beskjær", "curated_location_page_title": "Plasseringer", "curated_object_page_title": "Ting", + "current_server_address": "Current server address", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Tidssone", "edit_image_title": "Endre", "edit_location_dialog_title": "Lokasjon", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Feil: {}", "exif_bottom_sheet_description": "Legg til beskrivelse ...", "exif_bottom_sheet_details": "DETALJER", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Aktiver eksperimentell rutenettsvisning", "experimental_settings_subtitle": "Bruk på egen risiko!", "experimental_settings_title": "Eksperimentelt", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favoritter", "favorites_page_no_favorites": "Ingen favorittobjekter funnet", "favorites_page_title": "Favoritter", "filename_search": "Filnavn eller filtype", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Aktivert haptisk tilbakemelding", "haptic_feedback_title": "Haptisk tilbakemelding", "header_settings_add_header_tip": "Legg til header", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Eldste bilde", "library_page_sort_most_recent_photo": "Siste bilde", "library_page_sort_title": "Albumtittel", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Velg på kart", "location_picker_latitude": "Breddegrad", "location_picker_latitude_error": "Skriv inn en gyldig bredddegrad", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Kan ikke endre dato på objekt(er) med kun lese-rettigheter, hopper over", "multiselect_grid_edit_gps_err_read_only": "Kan ikke endre lokasjon på objekt(er) med kun lese-rettigheter, hopper over", "my_albums": "Mine albumer", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "Ingen objekter å vise", "no_name": "Ingen navn", "notification_permission_dialog_cancel": "Avbryt", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Begrenset tilgang. For å la Immich sikkerhetskopiere og håndtere galleriet, tillatt bilde- og video-tilgang i Innstillinger.", "permission_onboarding_request": "Immich trenger tilgang til å se dine bilder og videoer", "places": "Steder", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Innstillinger", "profile_drawer_app_logs": "Logg", "profile_drawer_client_out_of_date_major": "Mobilapp er utdatert. Vennligst oppdater til nyeste versjon.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Søppelbøtte", "recently_added": "Nylig lagt til", "recently_added_page_title": "Nylig lagt til", + "save": "Save", "save_to_gallery": "Lagre til galleriet", "scaffold_body_error_occurred": "Feil oppstått", "search_albums": "Søk i albumer", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Forslag", "select_user_for_sharing_page_err_album": "Feilet ved oppretting av album", "select_user_for_sharing_page_share_suggestions": "Forslag", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "App-versjon", "server_info_box_latest_release": "Siste versjon", "server_info_box_server_url": "Server-adresse", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Last forhåndsvisningsbilde", "setting_image_viewer_title": "Bilder", "setting_languages_apply": "Bekreft", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Språk", "setting_notifications_notify_failures_grace_period": "Varsle om sikkerhetskopieringsfeil i bakgrunnen: {}", "setting_notifications_notify_hours": "{} timer", @@ -612,6 +639,8 @@ "upload_dialog_info": "Vil du utføre backup av valgte objekt(er) til serveren?", "upload_dialog_ok": "Last opp", "upload_dialog_title": "Last opp objekt", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Bekreft", "version_announcement_overlay_release_notes": "endringsloggen", "version_announcement_overlay_text_1": "Hei, det er en ny versjon av", @@ -621,5 +650,7 @@ "videos": "Videoer", "viewer_remove_from_stack": "Fjern fra stabling", "viewer_stack_use_as_main_asset": "Bruk som hovedobjekt", - "viewer_unstack": "avstable" + "viewer_unstack": "avstable", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/nl-NL.json b/mobile/assets/i18n/nl-NL.json index 2bf277da1294b..2814144cda5a1 100644 --- a/mobile/assets/i18n/nl-NL.json +++ b/mobile/assets/i18n/nl-NL.json @@ -7,6 +7,7 @@ "action_common_select": "Selecteren", "action_common_update": "Bijwerken", "add_a_name": "Naam toevoegen", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Toegevoegd aan {album}", "add_to_album_bottom_sheet_already_exists": "Staat al in {album}", "advanced_settings_log_level_title": "Log niveau: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} asset(s) succesvol hersteld", "assets_trashed": "{} asset(s) naar de prullenbak verplaatst", "assets_trashed_from_server": "{} asset(s) naar de prullenbak verplaatst op de Immich server", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Foto weergave", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Albums op apparaat ({})", "backup_album_selection_page_albums_tap": "Tik om in te voegen, dubbel tik om uit te sluiten", "backup_album_selection_page_assets_scatter": "Assets kunnen over verschillende albums verdeeld zijn, dus albums kunnen inbegrepen of uitgesloten zijn van het backup proces.", @@ -131,6 +137,7 @@ "backup_manual_success": "Succes", "backup_manual_title": "Uploadstatus", "backup_options_page_title": "Back-up instellingen", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Thumbnails bibliotheekpagina ({} assets)", "cache_settings_clear_cache_button": "Cache wissen", "cache_settings_clear_cache_button_title": "Wist de cache van de app. Dit zal de presentaties van de app aanzienlijk beïnvloeden totdat de cache opnieuw is opgebouwd.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Beheer het gedrag van lokale opslag", "cache_settings_tile_title": "Lokale opslag", "cache_settings_title": "Cache-instellingen", + "cancel": "Cancel", "change_password_form_confirm_password": "Bevestig wachtwoord", "change_password_form_description": "Hallo {name},\n\nDit is ofwel de eerste keer dat je inlogt, of er is een verzoek gedaan om je wachtwoord te wijzigen. Vul hieronder een nieuw wachtwoord in.", "change_password_form_new_password": "Nieuw wachtwoord", "change_password_form_password_mismatch": "Wachtwoorden komen niet overeen", "change_password_form_reenter_new_password": "Vul het wachtwoord opnieuw in", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "Ok", "client_cert_enter_password": "Voer wachtwoord in", "client_cert_import": "Importeren", @@ -199,6 +210,7 @@ "crop": "Bijsnijden", "curated_location_page_title": "Plaatsen", "curated_object_page_title": "Dingen", + "current_server_address": "Current server address", "daily_title_text_date": "E dd MMM", "daily_title_text_date_year": "E dd MMM yyyy", "date_format": "E d LLL y • H:mm", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Tijdzone", "edit_image_title": "Bewerken", "edit_location_dialog_title": "Locatie", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Fout: {}", "exif_bottom_sheet_description": "Beschrijving toevoegen...", "exif_bottom_sheet_details": "DETAILS", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Experimenteel fotoraster inschakelen", "experimental_settings_subtitle": "Gebruik op eigen risico!", "experimental_settings_title": "Experimenteel", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favorieten", "favorites_page_no_favorites": "Geen favoriete assets gevonden", "favorites_page_title": "Favorieten", "filename_search": "Bestandsnaam of extensie", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Aanraaktrillingen inschakelen", "haptic_feedback_title": "Aanraaktrillingen", "header_settings_add_header_tip": "Header toevoegen", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Oudste foto", "library_page_sort_most_recent_photo": "Meest recente foto", "library_page_sort_title": "Albumtitel", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Kies op kaart", "location_picker_latitude": "Breedtegraad", "location_picker_latitude_error": "Voer een geldige breedtegraad in", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Kan datum van alleen-lezen asset(s) niet wijzigen, overslaan", "multiselect_grid_edit_gps_err_read_only": "Kan locatie van alleen-lezen asset(s) niet wijzigen, overslaan", "my_albums": "Mijn albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "Geen foto's om te laten zien", "no_name": "Geen naam", "notification_permission_dialog_cancel": "Annuleren", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Beperkte toestemming. Geef toestemming tot foto's en video's in Instellingen om Immich een back-up te laten maken van je galerij en deze te beheren.", "permission_onboarding_request": "Immich heeft toestemming nodig om je foto's en video's te bekijken.", "places": "Plaatsen", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Voorkeuren", "profile_drawer_app_logs": "Logboek", "profile_drawer_client_out_of_date_major": "Mobiele app is verouderd. Werk bij naar de nieuwste hoofdversie.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Prullenbak", "recently_added": "Onlangs toegevoegd", "recently_added_page_title": "Recent toegevoegd", + "save": "Save", "save_to_gallery": "Opslaan in galerij", "scaffold_body_error_occurred": "Fout opgetreden", "search_albums": "Albums zoeken", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Suggesties", "select_user_for_sharing_page_err_album": "Album aanmaken mislukt", "select_user_for_sharing_page_share_suggestions": "Suggesties", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Appversie", "server_info_box_latest_release": "Laatste Versie", "server_info_box_server_url": "Server URL", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Voorbeeldafbeelding laden", "setting_image_viewer_title": "Afbeeldingen", "setting_languages_apply": "Toepassen", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Taal", "setting_notifications_notify_failures_grace_period": "Fouten van de achtergrond back-up melden: {}", "setting_notifications_notify_hours": "{} uur", @@ -612,6 +639,8 @@ "upload_dialog_info": "Wil je een backup maken van de geselecteerde asset(s) op de server?", "upload_dialog_ok": "Uploaden", "upload_dialog_title": "Asset uploaden", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Bevestig", "version_announcement_overlay_release_notes": "releaseopmerkingen", "version_announcement_overlay_text_1": "Hoi, er is een nieuwe versie beschikbaar van", @@ -621,5 +650,7 @@ "videos": "Video's", "viewer_remove_from_stack": "Verwijder van Stapel", "viewer_stack_use_as_main_asset": "Gebruik als Hoofd Asset", - "viewer_unstack": "Ontstapel" + "viewer_unstack": "Ontstapel", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/pl-PL.json b/mobile/assets/i18n/pl-PL.json index 12a7e6faf2fdd..0141f8b5d5e85 100644 --- a/mobile/assets/i18n/pl-PL.json +++ b/mobile/assets/i18n/pl-PL.json @@ -7,6 +7,7 @@ "action_common_select": "Wybierz", "action_common_update": "Aktualizuj", "add_a_name": "Dodaj nazwę", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Dodano do {album}", "add_to_album_bottom_sheet_already_exists": "Już w {album}", "advanced_settings_log_level_title": "Poziom dziennika: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": " {} zasoby pomyślnie przywrócono", "assets_trashed": "{} zasoby zostały usunięte", "assets_trashed_from_server": "{} zasoby usunięte z serwera Immich", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Przeglądarka zasobów", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Albumy na urządzeniu ({})", "backup_album_selection_page_albums_tap": "Stuknij, aby włączyć, stuknij dwukrotnie, aby wykluczyć", "backup_album_selection_page_assets_scatter": "Pliki mogą być rozproszone w wielu albumach. Dzięki temu albumy mogą być włączane lub wyłączane podczas procesu tworzenia kopii zapasowej.", @@ -131,6 +137,7 @@ "backup_manual_success": "Sukces", "backup_manual_title": "Stan przesyłania", "backup_options_page_title": "Opcje kopi zapasowej", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Miniatury stron bibliotek ({} zasobów)", "cache_settings_clear_cache_button": "Wyczyść Cache", "cache_settings_clear_cache_button_title": "Czyści pamięć podręczną aplikacji. Wpłynie to znacząco na wydajność aplikacji, dopóki pamięć podręczna nie zostanie odbudowana.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Kontroluj zachowanie lokalnego magazynu", "cache_settings_tile_title": "Lokalny magazyn", "cache_settings_title": "Ustawienia Buforowania", + "cancel": "Cancel", "change_password_form_confirm_password": "Potwierdź Hasło", "change_password_form_description": "Cześć {name},\n\nPierwszy raz logujesz się do systemu, albo złożono prośbę o zmianę hasła. Wpisz poniżej nowe hasło.", "change_password_form_new_password": "Nowe Hasło", "change_password_form_password_mismatch": "Hasła nie są zgodne", "change_password_form_reenter_new_password": "Wprowadź ponownie Nowe Hasło", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Wprowadź hasło", "client_cert_import": "Importuj", @@ -199,6 +210,7 @@ "crop": "Przytnij", "curated_location_page_title": "Miejsca", "curated_object_page_title": "Rzeczy", + "current_server_address": "Current server address", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Strefa czasowa", "edit_image_title": "Edytuj", "edit_location_dialog_title": "Lokalizacja", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Błąd: {}", "exif_bottom_sheet_description": "Dodaj Opis...", "exif_bottom_sheet_details": "SZCZEGÓŁY", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Włącz eksperymentalną układ zdjęć", "experimental_settings_subtitle": "Używaj na własne ryzyko!", "experimental_settings_title": "Eksperymentalny", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Ulubione", "favorites_page_no_favorites": "Nie znaleziono ulubionych zasobów", "favorites_page_title": "Ulubione", "filename_search": "Nazwa pliku lub rozszerzenie", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Enable haptic feedback", "haptic_feedback_title": "Haptic Feedback", "header_settings_add_header_tip": "Dodaj nagłówek", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Najstarsze zdjęcie", "library_page_sort_most_recent_photo": "Najnowsze zdjęcie", "library_page_sort_title": "Tytuł albumu", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Wybierz na mapie", "location_picker_latitude": "Szerokość geograficzna", "location_picker_latitude_error": "Wprowadź prawidłową szerokość geograficzną", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Nie można edytować daty zasobów tylko do odczytu, pomijanie", "multiselect_grid_edit_gps_err_read_only": "Nie można edytować lokalizacji zasobów tylko do odczytu, pomijanie", "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "Brak zasobów do pokazania", "no_name": "Bez nazwy", "notification_permission_dialog_cancel": "Anuluj", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Pozwolenie ograniczone. Aby umożliwić Immichowi tworzenie kopii zapasowych całej kolekcji galerii i zarządzanie nią, przyznaj uprawnienia do zdjęć i filmów w Ustawieniach.", "permission_onboarding_request": "Immich potrzebuje pozwolenia na przeglądanie Twoich zdjęć i filmów.", "places": "Miejsca", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Ustawienia", "profile_drawer_app_logs": "Logi", "profile_drawer_client_out_of_date_major": "Aplikacja mobilna jest nieaktualna. Zaktualizuj do najnowszej wersji głównej.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Kosz", "recently_added": "Recently added", "recently_added_page_title": "Ostatnio Dodane", + "save": "Save", "save_to_gallery": "Zapisz w galerii", "scaffold_body_error_occurred": "Wystąpił błąd", "search_albums": "Przeszukaj albumy", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Propozycje", "select_user_for_sharing_page_err_album": "Nie udało się utworzyć albumu", "select_user_for_sharing_page_share_suggestions": "Propozycje", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Wersja Aplikacji", "server_info_box_latest_release": "Ostatnia wersja", "server_info_box_server_url": "Adres URL", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Załaduj obraz podglądu", "setting_image_viewer_title": "Zdjęcia", "setting_languages_apply": "Zastosuj", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Języki", "setting_notifications_notify_failures_grace_period": "Powiadomienie o awariach kopii zapasowych w tle: {}", "setting_notifications_notify_hours": "{} godzin", @@ -612,6 +639,8 @@ "upload_dialog_info": "Czy chcesz wykonać kopię zapasową wybranych zasobów na serwerze?", "upload_dialog_ok": "Prześlij", "upload_dialog_title": "Prześlij Zasób", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Potwierdzam", "version_announcement_overlay_release_notes": "informacje o wydaniu", "version_announcement_overlay_text_1": "Cześć przyjacielu, jest nowe wydanie", @@ -621,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "Usuń ze stosu", "viewer_stack_use_as_main_asset": "Użyj jako głównego zasobu", - "viewer_unstack": "Usuń stos" + "viewer_unstack": "Usuń stos", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/pt-PT.json b/mobile/assets/i18n/pt-PT.json index a17bae556707f..aa8654e2593cc 100644 --- a/mobile/assets/i18n/pt-PT.json +++ b/mobile/assets/i18n/pt-PT.json @@ -7,6 +7,7 @@ "action_common_select": "Selecionar", "action_common_update": "Atualizar", "add_a_name": "Adicionar nome", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Adicionado a {album}", "add_to_album_bottom_sheet_already_exists": "Já existe em {album}", "advanced_settings_log_level_title": "Nível de log: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} arquivo(s) restaurados com sucesso", "assets_trashed": "{} arquivo(s) enviados para a lixeira", "assets_trashed_from_server": "{} arquivo(s) do servidor foram enviados para a lixeira", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Visualizador", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Álbuns no dispositivo ({})", "backup_album_selection_page_albums_tap": "Toque para incluir, duplo toque para excluir", "backup_album_selection_page_assets_scatter": "Os arquivos podem estar espalhados em vários álbuns. Assim, os álbuns podem ser incluídos ou excluídos durante o processo de backup.", @@ -131,6 +137,7 @@ "backup_manual_success": "Sucesso", "backup_manual_title": "Estado do envio", "backup_options_page_title": "Opções de backup", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Miniaturas da página da biblioteca ({} arquivos)", "cache_settings_clear_cache_button": "Limpar cache", "cache_settings_clear_cache_button_title": "Limpa o cache do aplicativo. Isso afetará significativamente o desempenho do aplicativo até que o cache seja reconstruído.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Controlar o comportamento do armazenamento local", "cache_settings_tile_title": "Armazenamento local", "cache_settings_title": "Configurações de cache", + "cancel": "Cancel", "change_password_form_confirm_password": "Confirme a senha", "change_password_form_description": "Esta é a primeira vez que você está acessando o sistema ou foi feita uma solicitação para alterar sua senha. Por favor, insira a nova senha abaixo.", "change_password_form_new_password": "Nova senha", "change_password_form_password_mismatch": "As senhas não estão iguais", "change_password_form_reenter_new_password": "Confirme a nova senha", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Digite a senha", "client_cert_import": "Importar", @@ -199,6 +210,7 @@ "crop": "Cortar", "curated_location_page_title": "Locais", "curated_object_page_title": "Objetos", + "current_server_address": "Current server address", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "date_format": "E, d LLL, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Fuso horário", "edit_image_title": "Editar", "edit_location_dialog_title": "Localização", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Erro: {}", "exif_bottom_sheet_description": "Adicionar Descrição...", "exif_bottom_sheet_details": "DETALHES", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Ativar visualização de grade experimental", "experimental_settings_subtitle": "Use por sua conta e risco!", "experimental_settings_title": "Experimental", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favoritos", "favorites_page_no_favorites": "Nenhum favorito encontrado", "favorites_page_title": "Favoritos", "filename_search": "Nome do arquivo ou extensão", "filter": "Filtro", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Habilitar vibração", "haptic_feedback_title": "Vibração", "header_settings_add_header_tip": "Adicionar cabeçalho", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Foto mais antiga", "library_page_sort_most_recent_photo": "Foto mais recente", "library_page_sort_title": "Título do álbum", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Escolha no mapa", "location_picker_latitude": "Latitude", "location_picker_latitude_error": "Digite uma latitude válida", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Não é possível editar a data de arquivo só leitura, ignorando", "multiselect_grid_edit_gps_err_read_only": "Não é possível editar a localização de arquivo só leitura, ignorando", "my_albums": "Meus álbuns", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "Não há arquivos para exibir", "no_name": "Sem nome", "notification_permission_dialog_cancel": "Cancelar", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Permissão limitada. Para permitir que o Immich faça backups e gerencie sua galeria, conceda permissões para fotos e vídeos nas configurações.", "permission_onboarding_request": "O Immich requer autorização para ver as suas fotos e vídeos.", "places": "Lugares", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Preferências", "profile_drawer_app_logs": "Logs", "profile_drawer_client_out_of_date_major": "O aplicativo está desatualizado. Por favor, atualize para a versão mais recente.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Lixeira", "recently_added": "Adicionados Recentemente", "recently_added_page_title": "Adicionado recentemente", + "save": "Save", "save_to_gallery": "Salvar na galeria", "scaffold_body_error_occurred": "Ocorreu um erro", "search_albums": "Pesquisar Álbuns", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Sugestões", "select_user_for_sharing_page_err_album": "Falha ao criar o álbum", "select_user_for_sharing_page_share_suggestions": "Sugestões", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Versão do app", "server_info_box_latest_release": "Versão mais recente", "server_info_box_server_url": "URL do servidor", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Carregar imagem de pré-visualização", "setting_image_viewer_title": "Imagens", "setting_languages_apply": "Aplicar", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Idioma", "setting_notifications_notify_failures_grace_period": "Notifique falhas de backup em segundo plano: {}", "setting_notifications_notify_hours": "{} horas", @@ -612,6 +639,8 @@ "upload_dialog_info": "Deseja fazer o backup dos arquivos selecionados no servidor?", "upload_dialog_ok": "Enviar", "upload_dialog_title": "Enviar arquivo", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Entendi", "version_announcement_overlay_release_notes": "notas da versão", "version_announcement_overlay_text_1": "Olá, há um novo lançamento de", @@ -621,5 +650,7 @@ "videos": "Vídeos", "viewer_remove_from_stack": "Remover da pilha", "viewer_stack_use_as_main_asset": "Usar como foto principal", - "viewer_unstack": "Desempilhar" + "viewer_unstack": "Desempilhar", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/ro-RO.json b/mobile/assets/i18n/ro-RO.json index 255940263320e..38f0139ba6fbf 100644 --- a/mobile/assets/i18n/ro-RO.json +++ b/mobile/assets/i18n/ro-RO.json @@ -7,6 +7,7 @@ "action_common_select": "Select", "action_common_update": "Actualizează", "add_a_name": "Add a name", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Adăugat în {album}", "add_to_album_bottom_sheet_already_exists": "Deja în {album}", "advanced_settings_log_level_title": "Nivel log: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} asset(s) restored successfully", "assets_trashed": "{} asset(s) trashed", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Asset Viewer", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Albume în dispozitiv ({})", "backup_album_selection_page_albums_tap": "Apasă odata pentru a include, de două ori pentru a exclude", "backup_album_selection_page_assets_scatter": "Resursele pot fi împrăștiate în mai multe albume. Prin urmare, albumele pot fi incluse sau excluse în timpul procesului de backup.", @@ -131,6 +137,7 @@ "backup_manual_success": "Succes", "backup_manual_title": "Status încărcare", "backup_options_page_title": "Backup options", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Miniaturi pagină galerie ({} resurse)", "cache_settings_clear_cache_button": "Șterge cache", "cache_settings_clear_cache_button_title": "Șterge memoria cache a aplicatiei. Performanța aplicației va fi semnificativ afectată până când va fi reconstruită.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Controlează modul stocării locale", "cache_settings_tile_title": "Stocare locală", "cache_settings_title": "Setări pentru memoria cache", + "cancel": "Cancel", "change_password_form_confirm_password": "Confirmă parola", "change_password_form_description": "Salut {name},\n\nAceasta este fie prima dată când te conectazi la sistem, fie s-a făcut o cerere pentru schimbarea parolei. Te rugăm să introduci noua parolă mai jos.", "change_password_form_new_password": "Parolă nouă", "change_password_form_password_mismatch": "Parolele nu se potrivesc", "change_password_form_reenter_new_password": "Reintrodu noua parolă", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", @@ -199,6 +210,7 @@ "crop": "Crop", "curated_location_page_title": "Locuri", "curated_object_page_title": "Obiecte", + "current_server_address": "Current server address", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Fus orar", "edit_image_title": "Edit", "edit_location_dialog_title": "Locație", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Error: {}", "exif_bottom_sheet_description": "Adaugă Descriere...", "exif_bottom_sheet_details": "DETALII", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Activează grila experimentală de fotografii.", "experimental_settings_subtitle": "Folosește pe propria răspundere!", "experimental_settings_title": "Experimental", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favorites", "favorites_page_no_favorites": "Nu au fost găsite resurse favorite", "favorites_page_title": "Favorite", "filename_search": "File name or extension", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Enable haptic feedback", "haptic_feedback_title": "Haptic Feedback", "header_settings_add_header_tip": "Add Header", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Cea mai veche fotografie", "library_page_sort_most_recent_photo": "Cea mai recentă fotografie", "library_page_sort_title": "Titlu album", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Alege pe hartă", "location_picker_latitude": "Latitudine", "location_picker_latitude_error": "Introdu o latitudine validă", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Nu se poate edita data fișierului(lor) cu permisiuni doar pentru citire, omitere", "multiselect_grid_edit_gps_err_read_only": "Nu se poate edita locația fișierului(lor) cu permisiuni doar pentru citire, omitere", "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "No assets to show", "no_name": "No name", "notification_permission_dialog_cancel": "Anulează", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Permisiune limitată. Pentru a permite Immich să facă copii de siguranță și să gestioneze întreaga colecție de galerii, acordă permisiuni pentru fotografii și videoclipuri în Setări.", "permission_onboarding_request": "Immich necesită permisiunea de a vizualiza fotografiile și videoclipurile tale.", "places": "Places", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Preferences", "profile_drawer_app_logs": "Log-uri", "profile_drawer_client_out_of_date_major": "Aplicația nu folosește ultima versiune. Te rugăm să actulizezi la ultima versiune majoră.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Coș", "recently_added": "Recently added", "recently_added_page_title": "Adăugate recent", + "save": "Save", "save_to_gallery": "Save to gallery", "scaffold_body_error_occurred": "A apărut o eroare", "search_albums": "Search albums", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Sugestii", "select_user_for_sharing_page_err_album": "Creare album eșuată", "select_user_for_sharing_page_share_suggestions": "Sugestii", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Versiune Aplicatie", "server_info_box_latest_release": "Ultima versiune", "server_info_box_server_url": "URL-ul server-ului", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Încarcă imaginea de previzualizare", "setting_image_viewer_title": "Images", "setting_languages_apply": "Apply", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Languages", "setting_notifications_notify_failures_grace_period": "Notificare eșuări backup în fundal: {}", "setting_notifications_notify_hours": "{} ore", @@ -612,6 +639,8 @@ "upload_dialog_info": "Vrei să backup resursele selectate pe server?", "upload_dialog_ok": "Incarcă", "upload_dialog_title": "Încarcă resursă", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Confirm", "version_announcement_overlay_release_notes": "informații update", "version_announcement_overlay_text_1": "Salut, există un update nou pentru", @@ -621,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "Șterge din grup", "viewer_stack_use_as_main_asset": "Folosește ca resursă principală", - "viewer_unstack": "Anulează grup" + "viewer_unstack": "Anulează grup", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/ru-RU.json b/mobile/assets/i18n/ru-RU.json index c79fdfb8c3b77..f1089834bfa5f 100644 --- a/mobile/assets/i18n/ru-RU.json +++ b/mobile/assets/i18n/ru-RU.json @@ -7,6 +7,7 @@ "action_common_select": "Выбрать", "action_common_update": "Обновить", "add_a_name": "Добавить имя", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Добавлено в {album}", "add_to_album_bottom_sheet_already_exists": "Уже в {album}", "advanced_settings_log_level_title": "Уровень логирования:", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} объект(ы) успешно восстановлен(ы)", "assets_trashed": "{} объект(ы) помещен(ы) в корзину", "assets_trashed_from_server": "{} объект(ы) помещен(ы) в корзину на сервере Immich", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Просмотр изображений", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Альбомы на устройстве ({})", "backup_album_selection_page_albums_tap": "Нажмите, чтобы включить,\nнажмите дважды, чтобы исключить", "backup_album_selection_page_assets_scatter": "Ваши изображения и видео могут находиться в разных альбомах. Вы можете выбрать, какие альбомы включить, а какие исключить из резервного копирования.", @@ -131,6 +137,7 @@ "backup_manual_success": "Успешно", "backup_manual_title": "Статус загрузки", "backup_options_page_title": "Резервное копирование", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Миниатюры страниц библиотеки ({} объектов)", "cache_settings_clear_cache_button": "Очистить кэш", "cache_settings_clear_cache_button_title": "Очищает кэш приложения. Это негативно повлияет на производительность, пока кэш не будет создан заново.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Управление локальным хранилищем", "cache_settings_tile_title": "Локальное хранилище", "cache_settings_title": "Настройки кэширования", + "cancel": "Cancel", "change_password_form_confirm_password": "Подтвердите пароль", "change_password_form_description": "Привет, {name}!\n\nЛибо ваш первый вход в систему, либо вы запросили смену пароля. Пожалуйста, введите новый пароль ниже.", "change_password_form_new_password": "Новый пароль", "change_password_form_password_mismatch": "Пароли не совпадают", "change_password_form_reenter_new_password": "Повторно введите новый пароль", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Введите пароль", "client_cert_import": "Импорт", @@ -199,6 +210,7 @@ "crop": "Обрезать", "curated_location_page_title": "Места", "curated_object_page_title": "Предметы", + "current_server_address": "Current server address", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Часовой пояс", "edit_image_title": "Редактировать", "edit_location_dialog_title": "Местоположение", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Ошибка: {}", "exif_bottom_sheet_description": "Добавить описание...", "exif_bottom_sheet_details": "ПОДРОБНОСТИ", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Включить экспериментальную сетку фотографий", "experimental_settings_subtitle": "Используйте на свой страх и риск!", "experimental_settings_title": "Экспериментальные функции", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Избранное", "favorites_page_no_favorites": "В избранном сейчас пусто", "favorites_page_title": "Избранное", "filename_search": "Имя или расширение файла", "filter": "Фильтр", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Включить тактильную отдачу", "haptic_feedback_title": "Тактильная отдача", "header_settings_add_header_tip": "Добавить заголовок", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Старые фото", "library_page_sort_most_recent_photo": "Последние фото", "library_page_sort_title": "Название альбома", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Выбрать на карте", "location_picker_latitude": "Широта", "location_picker_latitude_error": "Укажите правильную широту", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Невозможно изменить дату файлов только для чтения, пропуск", "multiselect_grid_edit_gps_err_read_only": "Невозможно изменить местоположение файлов только для чтения, пропуск", "my_albums": "Мои альбомы", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "Медиа отсутствуют", "no_name": "Без имени", "notification_permission_dialog_cancel": "Отмена", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Доступ к файлам ограничен. Чтобы Immich мог создавать резервные копии и управлять вашей галереей, пожалуйста, предоставьте приложению разрешение на доступ к \"Фото и видео\" в настройках.", "permission_onboarding_request": "Приложению необходимо разрешение на доступ к вашим фото и видео", "places": "Места", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Параметры", "profile_drawer_app_logs": "Журнал", "profile_drawer_client_out_of_date_major": "Версия мобильного приложения устарела. Пожалуйста, обновите его.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Корзина", "recently_added": "Недавно добавленные", "recently_added_page_title": "Недавно добавленные", + "save": "Save", "save_to_gallery": "Сохранить в галерею", "scaffold_body_error_occurred": "Возникла ошибка", "search_albums": "Поиск альбома", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Предложения", "select_user_for_sharing_page_err_album": "Не удалось создать альбом", "select_user_for_sharing_page_share_suggestions": "Предложения", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Версия приложения", "server_info_box_latest_release": "Последняя версия", "server_info_box_server_url": "URL сервера", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Загружать уменьшенное изображение", "setting_image_viewer_title": "Изображения", "setting_languages_apply": "Применить", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Язык", "setting_notifications_notify_failures_grace_period": "Уведомлять об ошибках фонового резервного копирования: {}", "setting_notifications_notify_hours": "{} ч.", @@ -612,6 +639,8 @@ "upload_dialog_info": "Хотите создать резервную копию выбранных объектов на сервере?", "upload_dialog_ok": "Загрузить", "upload_dialog_title": "Загрузить объект", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Понятно", "version_announcement_overlay_release_notes": "примечания к выпуску", "version_announcement_overlay_text_1": "Привет, друг! Вышла новая версия", @@ -621,5 +650,7 @@ "videos": "Видео", "viewer_remove_from_stack": "Удалить из стека", "viewer_stack_use_as_main_asset": "Использовать в качестве основного объекта", - "viewer_unstack": "Разобрать стек" + "viewer_unstack": "Разобрать стек", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/sk-SK.json b/mobile/assets/i18n/sk-SK.json index eb4e304f2d161..ccea3c99f1db4 100644 --- a/mobile/assets/i18n/sk-SK.json +++ b/mobile/assets/i18n/sk-SK.json @@ -7,6 +7,7 @@ "action_common_select": "Select", "action_common_update": "Aktualizovať", "add_a_name": "Add a name", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Pridané do {album}", "add_to_album_bottom_sheet_already_exists": "Už v {album}", "advanced_settings_log_level_title": "Úroveň logovania: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} asset(s) restored successfully", "assets_trashed": "{} asset(s) trashed", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Zobrazovač položiek", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Albumy v zariadení ({})", "backup_album_selection_page_albums_tap": "Ťuknutím na položku ju zahrniete, dvojitým ťuknutím ju vylúčite", "backup_album_selection_page_assets_scatter": "Súbory môžu byť roztrúsené vo viacerých albumoch. To umožňuje zahrnúť alebo vylúčiť albumy počas procesu zálohovania.", @@ -131,6 +137,7 @@ "backup_manual_success": "Úspech", "backup_manual_title": "Stav nahrávania", "backup_options_page_title": "Možnosti zálohovania", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Náhľady stránok knižnice (položiek {})", "cache_settings_clear_cache_button": "Vymazať vyrovnávaciu pamäť", "cache_settings_clear_cache_button_title": "Vymaže vyrovnávaciu pamäť aplikácie. To výrazne ovplyvní výkon aplikácie, kým sa vyrovnávacia pamäť neobnoví.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Ovládanie správania lokálneho úložiska", "cache_settings_tile_title": "Lokálne úložisko", "cache_settings_title": "Nastavenia vyrovnávacej pamäte", + "cancel": "Cancel", "change_password_form_confirm_password": "Potvrďte heslo", "change_password_form_description": "Dobrý deň, {name},\n\nBuď sa do systému prihlasujete prvýkrát, alebo bola podaná žiadosť o zmenu hesla. Prosím, zadajte nové heslo nižšie.", "change_password_form_new_password": "Nové heslo", "change_password_form_password_mismatch": "Heslá sa nezhodujú", "change_password_form_reenter_new_password": "Znova zadajte nové heslo", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", @@ -199,6 +210,7 @@ "crop": "Crop", "curated_location_page_title": "Miesta", "curated_object_page_title": "Veci", + "current_server_address": "Current server address", "daily_title_text_date": "EEEE, d. MMMM", "daily_title_text_date_year": "EEEE, d. MMMM y", "date_format": "EEEE, d. MMMM y • H:mm", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Časové pásmo", "edit_image_title": "Edit", "edit_location_dialog_title": "Poloha", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Error: {}", "exif_bottom_sheet_description": "Pridať popis...", "exif_bottom_sheet_details": "PODROBNOSTI", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Povolenie experimentálnej mriežky fotografií", "experimental_settings_subtitle": "Používajte na vlastné riziko!", "experimental_settings_title": "Experimentálne", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favorites", "favorites_page_no_favorites": "Žiadne obľúbené médiá", "favorites_page_title": "Obľúbené", "filename_search": "File name or extension", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Povoliť hmatovú odozvu", "haptic_feedback_title": "Hmatová odozva", "header_settings_add_header_tip": "Add Header", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Najstaršia fotka", "library_page_sort_most_recent_photo": "Najnovšia fotka", "library_page_sort_title": "Podľa názvu albumu", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Zvoľte mapu", "location_picker_latitude": "Zemepisná dĺžka", "location_picker_latitude_error": "Zadajte platnú zemepisnú dĺžku", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Nemožno upraviť dátum položky len na čítanie, preskakujem", "multiselect_grid_edit_gps_err_read_only": "Nemožno upraviť polohu položky len na čítanie, preskakujem", "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "Žiadne položky", "no_name": "No name", "notification_permission_dialog_cancel": "Zrušiť", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Povolenie obmedzené. Ak chcete, aby Immich zálohoval a spravoval celú vašu zbierku galérie, udeľte v Nastaveniach povolenia na fotografie a videá.", "permission_onboarding_request": "Immich vyžaduje povolenie na prezeranie vašich fotografií a videí.", "places": "Places", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Preferencie", "profile_drawer_app_logs": "Logy", "profile_drawer_client_out_of_date_major": "Mobilná aplikácia je zastaralá. Prosím aktualizujte na najnovšiu verziu.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Kôš", "recently_added": "Recently added", "recently_added_page_title": "Nedávno pridané", + "save": "Save", "save_to_gallery": "Save to gallery", "scaffold_body_error_occurred": "Vyskytla sa chyba", "search_albums": "Search albums", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Návrhy", "select_user_for_sharing_page_err_album": "Nepodarilo sa vytvoriť album", "select_user_for_sharing_page_share_suggestions": "Návrhy", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Verzia aplikácie", "server_info_box_latest_release": "Najnovšia verzia", "server_info_box_server_url": "URL Serveru", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Načítať náhľad obrázka", "setting_image_viewer_title": "Obrázky", "setting_languages_apply": "Použiť", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Jazyky", "setting_notifications_notify_failures_grace_period": "Oznámenie o zlyhaní zálohovania na pozadí: {}", "setting_notifications_notify_hours": "{} hodín", @@ -612,6 +639,8 @@ "upload_dialog_info": "Chcete zálohovať zvolené médiá na server?", "upload_dialog_ok": "Nahrať", "upload_dialog_title": "Nahrať médiá", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Potvrdiť", "version_announcement_overlay_release_notes": "poznámky k vydaniu", "version_announcement_overlay_text_1": "Ahoj, je tu nová verzia", @@ -621,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "Odstrániť zo zoskupenia", "viewer_stack_use_as_main_asset": "Použiť ako hlavnú fotku", - "viewer_unstack": "Odskupiť" + "viewer_unstack": "Odskupiť", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/sl-SI.json b/mobile/assets/i18n/sl-SI.json index 1d7ef33a4ef42..bc9bd2405d1a1 100644 --- a/mobile/assets/i18n/sl-SI.json +++ b/mobile/assets/i18n/sl-SI.json @@ -7,6 +7,7 @@ "action_common_select": "Select", "action_common_update": "Posodobi", "add_a_name": "Add a name", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Dodano v {album}", "add_to_album_bottom_sheet_already_exists": "Že v {albumu}", "advanced_settings_log_level_title": "Nivo dnevnika: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} asset(s) restored successfully", "assets_trashed": "{} asset(s) trashed", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Pregledovalnik sredstev", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Albumi v napravi ({})", "backup_album_selection_page_albums_tap": "Tapnite za vključitev, dvakrat tapnite za izključitev", "backup_album_selection_page_assets_scatter": "Sredstva so lahko razpršena po več albumih. Tako je mogoče med postopkom varnostnega kopiranja albume vključiti ali izključiti.", @@ -131,6 +137,7 @@ "backup_manual_success": "Uspeh", "backup_manual_title": "Status nalaganja", "backup_options_page_title": "Možnosti varnostne kopije", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Sličice strani knjižnice ({} sredstev)", "cache_settings_clear_cache_button": "Počisti predpomnilnik", "cache_settings_clear_cache_button_title": "Počisti predpomnilnik aplikacije. To bo znatno vplivalo na delovanje aplikacije, dokler se predpomnilnik ne obnovi.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Nadzoruj vedenje lokalnega shranjevanja", "cache_settings_tile_title": "Lokalna shramba", "cache_settings_title": "Nastavitve predpomnjenja", + "cancel": "Cancel", "change_password_form_confirm_password": "Potrdi geslo", "change_password_form_description": "Pozdravljeni {name},\n\nTo je bodisi prvič, da se vpisujete v sistem ali pa je bila podana zahteva za spremembo vašega gesla. Spodaj vnesite novo geslo.", "change_password_form_new_password": "Novo geslo", "change_password_form_password_mismatch": "Gesli se ne ujemata", "change_password_form_reenter_new_password": "Znova vnesi novo geslo", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", @@ -199,6 +210,7 @@ "crop": "Crop", "curated_location_page_title": "Lokacije", "curated_object_page_title": "Stvari", + "current_server_address": "Current server address", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Časovni pas", "edit_image_title": "Edit", "edit_location_dialog_title": "Lokacija", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Error: {}", "exif_bottom_sheet_description": "Dodaj opis..", "exif_bottom_sheet_details": "PODROBNOSTI", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Omogoči eksperimentalno mrežo fotografij", "experimental_settings_subtitle": "Uporabljajte na lastno odgovornost!", "experimental_settings_title": "Eksperimentalno", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favorites", "favorites_page_no_favorites": "Ni priljubljenih sredstev", "favorites_page_title": "Priljubljene", "filename_search": "File name or extension", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Uporabi haptičen odziv", "haptic_feedback_title": "Haptičen odziv", "header_settings_add_header_tip": "Dodaj glavo", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Najstarejša fotografija", "library_page_sort_most_recent_photo": "Najnovejša fotografija", "library_page_sort_title": "Naslov albuma", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Izberi na zemljevidu", "location_picker_latitude": "Zemljepisna širina", "location_picker_latitude_error": "Vnesi veljavno zemljepisno širino", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Ni mogoče urediti datuma sredstev samo za branje, preskočim", "multiselect_grid_edit_gps_err_read_only": "Ni mogoče urediti lokacije sredstev samo za branje, preskočim", "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "Ni sredstev za prikaz", "no_name": "No name", "notification_permission_dialog_cancel": "Prekliči", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Dovoljenje je omejeno. Če želite Immichu dovoliti varnostno kopiranje in upravljanje vaše celotne zbirke galerij, v nastavitvah podelite dovoljenja za fotografije in videoposnetke.", "permission_onboarding_request": "Immich potrebuje dovoljenje za ogled vaših fotografij in videoposnetkov.", "places": "Places", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Nastavitve", "profile_drawer_app_logs": "Dnevniki", "profile_drawer_client_out_of_date_major": "Mobilna aplikacija je zastarela. Posodobite na najnovejšo glavno različico.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Smetnjak", "recently_added": "Recently added", "recently_added_page_title": "Nedavno dodano", + "save": "Save", "save_to_gallery": "Save to gallery", "scaffold_body_error_occurred": "Prišlo je do napake", "search_albums": "Search albums", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Predlogi", "select_user_for_sharing_page_err_album": "Albuma ni bilo mogoče ustvariti", "select_user_for_sharing_page_share_suggestions": "Predlogi", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Različica aplikacije", "server_info_box_latest_release": "Zadnja verzija", "server_info_box_server_url": "URL strežnika", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Naloži predogled slike", "setting_image_viewer_title": "Slike", "setting_languages_apply": "Uporabi", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Jeziki", "setting_notifications_notify_failures_grace_period": "Obvesti o napakah varnostnega kopiranja v ozadju: {}", "setting_notifications_notify_hours": "{} ur", @@ -612,6 +639,8 @@ "upload_dialog_info": "Ali želite varnostno kopirati izbrana sredstva na strežnik?", "upload_dialog_ok": "Naloži", "upload_dialog_title": "Naloži sredstvo", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Preverite", "version_announcement_overlay_release_notes": "opombe ob izdaji", "version_announcement_overlay_text_1": "Živjo prijatelj, na voljo je nova izdaja", @@ -621,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "Odstrani iz sklada", "viewer_stack_use_as_main_asset": "Uporabi kot glavno sredstvo", - "viewer_unstack": "Razkladi" + "viewer_unstack": "Razkladi", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/sr-Cyrl.json b/mobile/assets/i18n/sr-Cyrl.json index 0075f65de0557..6fb2ed4ff58ba 100644 --- a/mobile/assets/i18n/sr-Cyrl.json +++ b/mobile/assets/i18n/sr-Cyrl.json @@ -7,6 +7,7 @@ "action_common_select": "Select", "action_common_update": "Update", "add_a_name": "Add a name", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Added to {album}", "add_to_album_bottom_sheet_already_exists": "Already in {album}", "advanced_settings_log_level_title": "Log level: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} asset(s) restored successfully", "assets_trashed": "{} asset(s) trashed", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Asset Viewer", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Albums on device ({})", "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", @@ -131,6 +137,7 @@ "backup_manual_success": "Success", "backup_manual_title": "Upload status", "backup_options_page_title": "Backup options", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", "cache_settings_clear_cache_button": "Clear cache", "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Control the local storage behaviour", "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Caching Settings", + "cancel": "Cancel", "change_password_form_confirm_password": "Confirm Password", "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", @@ -199,6 +210,7 @@ "crop": "Crop", "curated_location_page_title": "Places", "curated_object_page_title": "Things", + "current_server_address": "Current server address", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", "edit_location_dialog_title": "Location", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Error: {}", "exif_bottom_sheet_description": "Add Description...", "exif_bottom_sheet_details": "DETAILS", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Enable experimental photo grid", "experimental_settings_subtitle": "Use at your own risk!", "experimental_settings_title": "Experimental", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favorites", "favorites_page_no_favorites": "No favorite assets found", "favorites_page_title": "Favorites", "filename_search": "File name or extension", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Enable haptic feedback", "haptic_feedback_title": "Haptic Feedback", "header_settings_add_header_tip": "Add Header", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Oldest photo", "library_page_sort_most_recent_photo": "Most recent photo", "library_page_sort_title": "Album title", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Choose on map", "location_picker_latitude": "Latitude", "location_picker_latitude_error": "Enter a valid latitude", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "No assets to show", "no_name": "No name", "notification_permission_dialog_cancel": "Cancel", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.", "permission_onboarding_request": "Immich requires permission to view your photos and videos.", "places": "Places", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Preferences", "profile_drawer_app_logs": "Logs", "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Trash", "recently_added": "Recently added", "recently_added_page_title": "Recently Added", + "save": "Save", "save_to_gallery": "Save to gallery", "scaffold_body_error_occurred": "Error occurred", "search_albums": "Search albums", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Suggestions", "select_user_for_sharing_page_err_album": "Failed to create album", "select_user_for_sharing_page_share_suggestions": "Suggestions", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "App Version", "server_info_box_latest_release": "Latest Version", "server_info_box_server_url": "Server URL", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Load preview image", "setting_image_viewer_title": "Images", "setting_languages_apply": "Apply", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Languages", "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", "setting_notifications_notify_hours": "{} hours", @@ -612,6 +639,8 @@ "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", "upload_dialog_ok": "Upload", "upload_dialog_title": "Upload Asset", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Acknowledge", "version_announcement_overlay_release_notes": "release notes", "version_announcement_overlay_text_1": "Hi friend, there is a new release of", @@ -621,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" + "viewer_unstack": "Un-Stack", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/sr-Latn.json b/mobile/assets/i18n/sr-Latn.json index 3e11d73e08a7e..9c3058ee94e92 100644 --- a/mobile/assets/i18n/sr-Latn.json +++ b/mobile/assets/i18n/sr-Latn.json @@ -7,6 +7,7 @@ "action_common_select": "Select", "action_common_update": "Update", "add_a_name": "Add a name", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Dodato u {album}", "add_to_album_bottom_sheet_already_exists": "Već u {album}", "advanced_settings_log_level_title": "Log level: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} asset(s) restored successfully", "assets_trashed": "{} asset(s) trashed", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Asset Viewer", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Albuma na uređaju ({})", "backup_album_selection_page_albums_tap": "Dodirni da uključiš, dodirni dvaput da isključiš", "backup_album_selection_page_assets_scatter": "Zapisi se mogu naći u više različitih albuma. Odatle albumi se mogu uključiti ili isključiti tokom procesa pravljenja pozadinskih kopija.", @@ -131,6 +137,7 @@ "backup_manual_success": "Success", "backup_manual_title": "Upload status", "backup_options_page_title": "Backup options", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Sličice na stranici biblioteke", "cache_settings_clear_cache_button": "Obriši keš memoriju", "cache_settings_clear_cache_button_title": "Ova opcija briše keš memoriju aplikacije. Ovo će bitno uticati na performanse aplikacije dok se keš memorija ne učita ponovo.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Control the local storage behaviour", "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Opcije za keširanje", + "cancel": "Cancel", "change_password_form_confirm_password": "Ponovo unesite šifru", "change_password_form_description": "Ćao, {name}\n\nOvo je verovatno Vaše prvo pristupanje sistemu, ili je podnešen zahtev za promenu šifre. Molimo Vas, unesite novu šifru ispod", "change_password_form_new_password": "Nova šifra", "change_password_form_password_mismatch": "Šifre se ne podudaraju", "change_password_form_reenter_new_password": "Ponovo unesite novu šifru", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", @@ -199,6 +210,7 @@ "crop": "Crop", "curated_location_page_title": "Places", "curated_object_page_title": "Things", + "current_server_address": "Current server address", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", "edit_location_dialog_title": "Location", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Error: {}", "exif_bottom_sheet_description": "Dodaj opis...", "exif_bottom_sheet_details": "DETALJI", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Aktiviraj eksperimentalni mrežni prikaz fotografija", "experimental_settings_subtitle": "Koristiti na sopstvenu odgovornost!", "experimental_settings_title": "Eksperimentalno", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favorites", "favorites_page_no_favorites": "No favorite assets found", "favorites_page_title": "Omiljeno", "filename_search": "File name or extension", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Enable haptic feedback", "haptic_feedback_title": "Haptic Feedback", "header_settings_add_header_tip": "Add Header", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Oldest photo", "library_page_sort_most_recent_photo": "Most recent photo", "library_page_sort_title": "Naziv albuma", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Choose on map", "location_picker_latitude": "Latitude", "location_picker_latitude_error": "Enter a valid latitude", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "No assets to show", "no_name": "No name", "notification_permission_dialog_cancel": "Odustani", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.", "permission_onboarding_request": "Immich requires permission to view your photos and videos.", "places": "Places", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Preferences", "profile_drawer_app_logs": "Evidencija", "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Trash", "recently_added": "Recently added", "recently_added_page_title": "Recently Added", + "save": "Save", "save_to_gallery": "Save to gallery", "scaffold_body_error_occurred": "Error occurred", "search_albums": "Search albums", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Sugsetije", "select_user_for_sharing_page_err_album": "Neuspešno kreiranje albuma", "select_user_for_sharing_page_share_suggestions": "Sugestije", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Verzija Aplikacije", "server_info_box_latest_release": "Latest Version", "server_info_box_server_url": "Server URL", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Pregledaj sliku", "setting_image_viewer_title": "Images", "setting_languages_apply": "Apply", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Languages", "setting_notifications_notify_failures_grace_period": "Neuspešne rezervne kopije: {}", "setting_notifications_notify_hours": "{} sati", @@ -612,6 +639,8 @@ "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", "upload_dialog_ok": "Upload", "upload_dialog_title": "Upload Asset", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Priznati", "version_announcement_overlay_release_notes": "novine nove verzije", "version_announcement_overlay_text_1": "Ćao, nova verzija", @@ -621,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" + "viewer_unstack": "Un-Stack", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/sv-FI.json b/mobile/assets/i18n/sv-FI.json index 0075f65de0557..6fb2ed4ff58ba 100644 --- a/mobile/assets/i18n/sv-FI.json +++ b/mobile/assets/i18n/sv-FI.json @@ -7,6 +7,7 @@ "action_common_select": "Select", "action_common_update": "Update", "add_a_name": "Add a name", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Added to {album}", "add_to_album_bottom_sheet_already_exists": "Already in {album}", "advanced_settings_log_level_title": "Log level: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} asset(s) restored successfully", "assets_trashed": "{} asset(s) trashed", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Asset Viewer", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Albums on device ({})", "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", @@ -131,6 +137,7 @@ "backup_manual_success": "Success", "backup_manual_title": "Upload status", "backup_options_page_title": "Backup options", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", "cache_settings_clear_cache_button": "Clear cache", "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Control the local storage behaviour", "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Caching Settings", + "cancel": "Cancel", "change_password_form_confirm_password": "Confirm Password", "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", @@ -199,6 +210,7 @@ "crop": "Crop", "curated_location_page_title": "Places", "curated_object_page_title": "Things", + "current_server_address": "Current server address", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", "edit_location_dialog_title": "Location", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Error: {}", "exif_bottom_sheet_description": "Add Description...", "exif_bottom_sheet_details": "DETAILS", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Enable experimental photo grid", "experimental_settings_subtitle": "Use at your own risk!", "experimental_settings_title": "Experimental", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favorites", "favorites_page_no_favorites": "No favorite assets found", "favorites_page_title": "Favorites", "filename_search": "File name or extension", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Enable haptic feedback", "haptic_feedback_title": "Haptic Feedback", "header_settings_add_header_tip": "Add Header", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Oldest photo", "library_page_sort_most_recent_photo": "Most recent photo", "library_page_sort_title": "Album title", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Choose on map", "location_picker_latitude": "Latitude", "location_picker_latitude_error": "Enter a valid latitude", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "No assets to show", "no_name": "No name", "notification_permission_dialog_cancel": "Cancel", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.", "permission_onboarding_request": "Immich requires permission to view your photos and videos.", "places": "Places", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Preferences", "profile_drawer_app_logs": "Logs", "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Trash", "recently_added": "Recently added", "recently_added_page_title": "Recently Added", + "save": "Save", "save_to_gallery": "Save to gallery", "scaffold_body_error_occurred": "Error occurred", "search_albums": "Search albums", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Suggestions", "select_user_for_sharing_page_err_album": "Failed to create album", "select_user_for_sharing_page_share_suggestions": "Suggestions", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "App Version", "server_info_box_latest_release": "Latest Version", "server_info_box_server_url": "Server URL", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Load preview image", "setting_image_viewer_title": "Images", "setting_languages_apply": "Apply", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Languages", "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", "setting_notifications_notify_hours": "{} hours", @@ -612,6 +639,8 @@ "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", "upload_dialog_ok": "Upload", "upload_dialog_title": "Upload Asset", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Acknowledge", "version_announcement_overlay_release_notes": "release notes", "version_announcement_overlay_text_1": "Hi friend, there is a new release of", @@ -621,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" + "viewer_unstack": "Un-Stack", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/sv-SE.json b/mobile/assets/i18n/sv-SE.json index 76ec0e7e1d0eb..99b612c4587f4 100644 --- a/mobile/assets/i18n/sv-SE.json +++ b/mobile/assets/i18n/sv-SE.json @@ -7,6 +7,7 @@ "action_common_select": "Välj", "action_common_update": "Uppdatera", "add_a_name": "Lägg till namn", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Tillagd till {album}", "add_to_album_bottom_sheet_already_exists": "Redan i {album}", "advanced_settings_log_level_title": "Loggnivå: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} objekt har återställts", "assets_trashed": "{} objekt raderade", "assets_trashed_from_server": "{} objekt raderade från Immich-servern", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Objektvisare", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Album på enhet ({})", "backup_album_selection_page_albums_tap": "Tryck en gång för att inkludera, tryck två gånger för att exkludera", "backup_album_selection_page_assets_scatter": "Objekt kan vara utspridda över flera album. Därför kan album inkluderas eller exkluderas under säkerhetskopieringsprocessen", @@ -131,6 +137,7 @@ "backup_manual_success": "Klart", "backup_manual_title": "Uppladdningsstatus", "backup_options_page_title": "Säkerhetskopieringsinställningar", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Miniatyrbilder för bibliotek ({} bilder och videor)", "cache_settings_clear_cache_button": "Rensa cacheminnet", "cache_settings_clear_cache_button_title": "Rensar appens cacheminne. Detta kommer att avsevärt påverka appens prestanda tills cachen har byggts om.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Kontrollera beteende för lokal lagring", "cache_settings_tile_title": "Lokal Lagring", "cache_settings_title": "Cache Inställningar", + "cancel": "Cancel", "change_password_form_confirm_password": "Bekräfta lösenord", "change_password_form_description": "Hej {name},\n\nDet är antingen första gången du loggar in i systemet, eller så har det skett en förfrågan om återställning av ditt lösenord. Ange ditt nya lösenord nedan.", "change_password_form_new_password": "Nytt lösenord", "change_password_form_password_mismatch": "Lösenorden matchar inte", "change_password_form_reenter_new_password": "Ange Nytt Lösenord Igen", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Kontrollera", + "check_corrupt_asset_backup_description": "Kör kontrollen endast över Wi-Fi och när alla resurser har säkerhetskopierats. Det kan ta några minuter.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Ange Lösenord", "client_cert_import": "Importera", @@ -199,6 +210,7 @@ "crop": "Beskär", "curated_location_page_title": "Platser", "curated_object_page_title": "Objekt", + "current_server_address": "Current server address", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "date_format": "E d. LLL y • hh:mm", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Tidszon", "edit_image_title": "Redigera", "edit_location_dialog_title": "Plats", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Fel: {}", "exif_bottom_sheet_description": "Lägg till beskrivning...", "exif_bottom_sheet_details": "DETALJER", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Aktivera experimentellt fotorutnät", "experimental_settings_subtitle": "Använd på egen risk!", "experimental_settings_title": "Experimentellt", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favoriter", "favorites_page_no_favorites": "Inga favoritobjekt hittades", "favorites_page_title": "Favoriter", "filename_search": "Filnamn eller filändelse", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Aktivera haptisk feedback", "haptic_feedback_title": "Haptisk Feedback", "header_settings_add_header_tip": "Lägg Till Header", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Äldsta foto", "library_page_sort_most_recent_photo": "Senaste foto", "library_page_sort_title": "Albumtitel", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Välj på karta", "location_picker_latitude": "Latitud", "location_picker_latitude_error": "Ange en giltig latitud", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Kan inte ändra datum på skrivskyddade objekt, hoppar över", "multiselect_grid_edit_gps_err_read_only": "Kan inte ändra plats på skrivskyddade objekt, hoppar över", "my_albums": "Mina album", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "Inga objekt att visa", "no_name": "Inget namn", "notification_permission_dialog_cancel": "Avbryt", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Rättighet begränsad. För att låta Immich säkerhetskopiera och hantera hela ditt galleri, tillåt foto- och video-rättigheter i Inställningar.", "permission_onboarding_request": "Immich kräver tillstånd för att se dina foton och videor.", "places": "Platser", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Inställningar", "profile_drawer_app_logs": "Loggar", "profile_drawer_client_out_of_date_major": "Mobilappen är utdaterad. Uppdatera till senaste huvudversionen.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Papperskorg", "recently_added": "Nyligen tillagda", "recently_added_page_title": "Nyligen tillagda", + "save": "Save", "save_to_gallery": "Spara i galleri", "scaffold_body_error_occurred": "Fel uppstod", "search_albums": "Sök i album", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Förslag", "select_user_for_sharing_page_err_album": "Kunde inte skapa nytt album", "select_user_for_sharing_page_share_suggestions": "Förslag", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "App-version", "server_info_box_latest_release": "Senaste Version", "server_info_box_server_url": "Server-URL", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Ladda förhandsgranskning av bild", "setting_image_viewer_title": "Bilder", "setting_languages_apply": "Verkställ", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Språk", "setting_notifications_notify_failures_grace_period": "Rapportera säkerhetskopieringsfel i bakgrunden: {}", "setting_notifications_notify_hours": "{} timmar", @@ -612,6 +639,8 @@ "upload_dialog_info": "Vill du säkerhetskopiera de valda objekten till servern?", "upload_dialog_ok": "Ladda Upp", "upload_dialog_title": "Ladda Upp Objekt", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Bekräfta", "version_announcement_overlay_release_notes": "versionsinformation", "version_announcement_overlay_text_1": "Hej vännen, det finns en ny version av", @@ -621,5 +650,7 @@ "videos": "Videor", "viewer_remove_from_stack": "Ta bort från Stapeln", "viewer_stack_use_as_main_asset": "Använd som Huvudobjekt", - "viewer_unstack": "Stapla Av" + "viewer_unstack": "Stapla Av", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/th-TH.json b/mobile/assets/i18n/th-TH.json index b6013ceed4685..a825270fd7519 100644 --- a/mobile/assets/i18n/th-TH.json +++ b/mobile/assets/i18n/th-TH.json @@ -7,6 +7,7 @@ "action_common_select": "Select", "action_common_update": "อัปเดต", "add_a_name": "Add a name", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "เพิ่มไปยัง {album}", "add_to_album_bottom_sheet_already_exists": "อยู่ใน {album} อยู่แล้ว", "advanced_settings_log_level_title": "ระดับการ Log: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} asset(s) restored successfully", "assets_trashed": "{} asset(s) trashed", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "ตัวดูทรัพยากร", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "อัลบั้มบนเครื่อง ({})", "backup_album_selection_page_albums_tap": "กดเพื่อรวม กดสองครั้งเพื่อยกเว้น", "backup_album_selection_page_assets_scatter": "ทรัพยาการสามารถกระจายไปในหลายอัลบั้ม ดังนั้นอัลบั้มสามารถถูกรวมหรือยกเว้นในกระบวนการสำรองข้อมูล", @@ -131,6 +137,7 @@ "backup_manual_success": "สำเร็จ", "backup_manual_title": "สถานะอัพโหลด", "backup_options_page_title": "ตัวเลือกการสำรองข้อมูล", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "รูปย่อคลังภาพ ({} ทรัพยากร)", "cache_settings_clear_cache_button": "ล้างแคช", "cache_settings_clear_cache_button_title": "ล้างแคชของแอพ จะส่งผลกระทบต่อประสิทธิภาพแอพจนกว่าแคชจะถูกสร้างใหม่", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "ควบคุมพฤติกรรมของที่จัดเก็บในตัวเครื่อง", "cache_settings_tile_title": "ที่จัดเก็บในตัวเครื่อง", "cache_settings_title": "ตั้งค่าแคช", + "cancel": "Cancel", "change_password_form_confirm_password": "ยืนยันรหัสผ่าน", "change_password_form_description": "สวัสดี {name},\n\nครั้งนี้อาจจะเป็นครั้งแรกที่คุณเข้าสู่ระบบ หรือมีคำขอเพื่อที่จะเปลี่ยนรหัสผ่านของคุI กรุณาเพิ่มรหัสผ่านใหม่ข้างล่าง", "change_password_form_new_password": "รหัสผ่านใหม่", "change_password_form_password_mismatch": "รหัสผ่านไม่ตรงกัน", "change_password_form_reenter_new_password": "กรอกรหัสผ่านใหม่", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", @@ -199,6 +210,7 @@ "crop": "Crop", "curated_location_page_title": "สถานที่", "curated_object_page_title": "สิ่งของ", + "current_server_address": "Current server address", "daily_title_text_date": "E dd MMM", "daily_title_text_date_year": "E dd MMM yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "เขดเวลา", "edit_image_title": "Edit", "edit_location_dialog_title": "ตำแหน่ง", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Error: {}", "exif_bottom_sheet_description": "เพิ่มคำอธิบาย", "exif_bottom_sheet_details": "รายละเอียด", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "เปิดตารางรูปภาพที่กำลังทดลอง", "experimental_settings_subtitle": "ใช้ภายใต้ความเสี่ยงของคุณเอง!", "experimental_settings_title": "ทดลอง", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favorites", "favorites_page_no_favorites": "ไม่พบทรัพยากรในรายการโปรด", "favorites_page_title": "รายการโปรด", "filename_search": "File name or extension", "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "เปิดการตอบสนองแบบสัมผัส", "haptic_feedback_title": "การตอบสนองแบบสัมผัส", "header_settings_add_header_tip": "Add Header", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "รูปภาพที่เก่าที่สุด", "library_page_sort_most_recent_photo": "รูปล่าสุด", "library_page_sort_title": "ชื่ออัลบั้ม", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "เลือกบนแผนที่", "location_picker_latitude": "ละติจูต", "location_picker_latitude_error": "กรุณาเพิ่มละติจูตที่ถูกต้อง", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "ไม่สามารถแก้ไขวันที่ทรัพยากรแบบอ่านอย่างเดียว กำลังข้าม", "multiselect_grid_edit_gps_err_read_only": "ไม่สามารถแก้ตำแหน่งของทรัพยากรแบบอ่านอย่างเดียว กำลังข้าม", "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "ไม่มีทรัพยากรให้แสดง", "no_name": "No name", "notification_permission_dialog_cancel": "ยกเลิก", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "สิทธ์จำกัด เพื่อให้ Immich สำรองข้อมูลและจัดการคลังภาพได้ ตั้งค่าสิทธิเข้าถึงรูปภาพและวิดีโอ", "permission_onboarding_request": "Immich จำเป็นจะต้องได้รับสิทธิ์ดูรูปภาพและวิดีโอ", "places": "Places", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "การตั้งค่า", "profile_drawer_app_logs": "การบันทึก", "profile_drawer_client_out_of_date_major": "แอปพลิเคชันมีอัพเดต โปรดอัปเดตเป็นเวอร์ชันหลักล่าสุด", @@ -412,6 +436,7 @@ "profile_drawer_trash": "ขยะ", "recently_added": "Recently added", "recently_added_page_title": "เพิ่มล่าสุด", + "save": "Save", "save_to_gallery": "Save to gallery", "scaffold_body_error_occurred": "เกิดข้อผิดพลาด", "search_albums": "Search albums", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "ข้อเสนอแนะ", "select_user_for_sharing_page_err_album": "สร้างอัลบั้มล้มเหลว", "select_user_for_sharing_page_share_suggestions": "ข้อเสนอแนะ", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "เวอร์ชันแอพ", "server_info_box_latest_release": "เวอร์ชันล่าสุด", "server_info_box_server_url": "URL เซิร์ฟเวอร์", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "โหลดรูปภาพตัวอย่าง", "setting_image_viewer_title": "รูปภาพ", "setting_languages_apply": "บันทึก", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "ภาษา", "setting_notifications_notify_failures_grace_period": "แจ้งการสำรองข้อมูลในเบื้องหลังล้มเหลว: {}", "setting_notifications_notify_hours": "{} ชั่วโมง", @@ -612,6 +639,8 @@ "upload_dialog_info": "คุณต้องการอัพโหลดทรัพยากรดังกล่าวบนเซิร์ฟเวอร์หรือไม่?", "upload_dialog_ok": "อัปโหลด", "upload_dialog_title": "อัปโหลดทรัพยากร", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "รับทราบ", "version_announcement_overlay_release_notes": "รายงานการอัพเดท", "version_announcement_overlay_text_1": "สวัสดีเพื่อน ขณะนี้มีเวอร์ชั้นใหม่ของ", @@ -621,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "เอาออกจากที่ซ้อน", "viewer_stack_use_as_main_asset": "ใช้เป็นทรัพยากรหลัก", - "viewer_unstack": "หยุดซ้อน" + "viewer_unstack": "หยุดซ้อน", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/uk-UA.json b/mobile/assets/i18n/uk-UA.json index 8f9b6370ec698..ff782bf12c0c1 100644 --- a/mobile/assets/i18n/uk-UA.json +++ b/mobile/assets/i18n/uk-UA.json @@ -7,6 +7,7 @@ "action_common_select": "Вибрати", "action_common_update": "Оновити", "add_a_name": "Додати ім'я", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Додати до {album}", "add_to_album_bottom_sheet_already_exists": "Вже є в {album}", "advanced_settings_log_level_title": "Log level: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "{} елемент(и) успішно відновлено", "assets_trashed": "{} елемент(и) поміщено до кошика", "assets_trashed_from_server": "{} елемент(и) поміщено до кошика на сервері Immich", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Переглядач зображень", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Альбоми на пристрої ({})", "backup_album_selection_page_albums_tap": "Торкніться, щоб включити,\nторкніться двічі, щоб виключити", "backup_album_selection_page_assets_scatter": "Елементи можуть належати до кількох альбомів водночас. Таким чином, альбоми можуть бути включені або вилучені під час резервного копіювання.", @@ -131,6 +137,7 @@ "backup_manual_success": "Успіх", "backup_manual_title": "Стан завантаження", "backup_options_page_title": "Резервне копіювання", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Мініатюри сторінок бібліотеки ({} елементи)", "cache_settings_clear_cache_button": "Очистити кеш", "cache_settings_clear_cache_button_title": "Очищає кеш програми. Це суттєво знизить продуктивність програми, доки кеш не буде перебудовано.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Керування поведінкою локального сховища", "cache_settings_tile_title": "Локальне сховище", "cache_settings_title": "Налаштування кешування", + "cancel": "Cancel", "change_password_form_confirm_password": "Підтвердити пароль", "change_password_form_description": "Привіт {name},\n\nВи або або вперше входите у систему, або було зроблено запит на зміну вашого пароля. \nВведіть ваш новий пароль.", "change_password_form_new_password": "Новий пароль", "change_password_form_password_mismatch": "Паролі не співпадають", "change_password_form_reenter_new_password": "Повторіть новий пароль", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Введіть пароль", "client_cert_import": "Імпорт", @@ -199,6 +210,7 @@ "crop": "Кадрувати", "curated_location_page_title": "Місця", "curated_object_page_title": "Речі", + "current_server_address": "Current server address", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Часовий пояс", "edit_image_title": "Редагувати", "edit_location_dialog_title": "Місцезнаходження", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Помилка: {}", "exif_bottom_sheet_description": "Додати опис...", "exif_bottom_sheet_details": "ПОДРОБИЦІ", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "Експериментальний макет знімків", "experimental_settings_subtitle": "На власний ризик!", "experimental_settings_title": "Експериментальні", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Вибране", "favorites_page_no_favorites": "Немає улюблених елементів", "favorites_page_title": "Улюблені", "filename_search": "Ім'я або розширення файлу", "filter": "Фільтр", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "Увімкнути тактильну віддачу", "haptic_feedback_title": "Тактильна віддача", "header_settings_add_header_tip": "Додати заголовок", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Найдавніші фото", "library_page_sort_most_recent_photo": "Найновіші фото", "library_page_sort_title": "Назва альбому", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Обрати на мапі", "location_picker_latitude": "Широта", "location_picker_latitude_error": "Вкажіть дійсну широту", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Неможливо редагувати дату елементів лише для читання, пропущено", "multiselect_grid_edit_gps_err_read_only": "Неможливо редагувати місцезнаходження елементів лише для читання, пропущено", "my_albums": "Мої альбоми", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "Елементи відсутні", "no_name": "Без імені", "notification_permission_dialog_cancel": "Скасувати", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Обмежений доступ. Аби дозволити Immich резервне копіювання та керування вашою галереєю, надайте доступ до знімків та відео у Налаштуваннях", "permission_onboarding_request": "Immich потребує доступу до ваших знімків та відео.", "places": "Місця", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Параметри", "profile_drawer_app_logs": "Журнал", "profile_drawer_client_out_of_date_major": "Мобільний додаток застарів. Будь ласка, оновіть до останньої мажорної версії.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Кошик", "recently_added": "Нещодавно додані", "recently_added_page_title": "Нещодавні", + "save": "Save", "save_to_gallery": "Зберегти в галерею", "scaffold_body_error_occurred": "Виникла помилка", "search_albums": "Пошук альбому", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Пропозиції", "select_user_for_sharing_page_err_album": "Не вдалося створити альбом", "select_user_for_sharing_page_share_suggestions": "Пропозиції", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Версія додатка", "server_info_box_latest_release": "Остання версія", "server_info_box_server_url": "URL сервера", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Завантажувати зображення попереднього перегляду", "setting_image_viewer_title": "Зображення", "setting_languages_apply": "Застосувати", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Мова", "setting_notifications_notify_failures_grace_period": "Повідомити про помилки фонового резервного копіювання: {}", "setting_notifications_notify_hours": "{} годин", @@ -612,6 +639,8 @@ "upload_dialog_info": "Бажаєте створити резервну копію вибраних елементів на сервері?", "upload_dialog_ok": "Завантажити", "upload_dialog_title": "Завантажити Елементи", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Прийняти", "version_announcement_overlay_release_notes": "примітки до випуску", "version_announcement_overlay_text_1": "Вітаємо, є новий випуск ", @@ -621,5 +650,7 @@ "videos": "Відео", "viewer_remove_from_stack": "Видалити зі стеку", "viewer_stack_use_as_main_asset": "Використовувати як основний елементи", - "viewer_unstack": "Розібрати стек" + "viewer_unstack": "Розібрати стек", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/vi-VN.json b/mobile/assets/i18n/vi-VN.json index 0a26e6dd70fc6..ba42c6365507c 100644 --- a/mobile/assets/i18n/vi-VN.json +++ b/mobile/assets/i18n/vi-VN.json @@ -7,6 +7,7 @@ "action_common_select": "Chọn", "action_common_update": "Cập nhật", "add_a_name": "Add a name", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "Thêm vào {album}", "add_to_album_bottom_sheet_already_exists": "Đã có sẵn trong {album}", "advanced_settings_log_level_title": "Phân loại nhật ký: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "Đã khôi phục {} mục thành công", "assets_trashed": "Đã chuyển {} mục vào thùng rác", "assets_trashed_from_server": "Đã chuyển {} mục từ máy chủ Immich vào thùng rác", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Trình xem ảnh", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Album trên thiết bị ({})", "backup_album_selection_page_albums_tap": "Nhấn để chọn, nhấn đúp để bỏ qua", "backup_album_selection_page_assets_scatter": "Ảnh có thể có trong nhiều album khác nhau. Trong quá trình sao lưu, bạn có thể chọn để sao lưu tất cả các album hoặc chỉ một số album nhất định.", @@ -121,7 +127,7 @@ "backup_controller_page_total": "Tổng số", "backup_controller_page_total_sub": "Tất cả ảnh và video không trùng lập từ các album được chọn", "backup_controller_page_turn_off": "Tắt sao lưu khi ứng dụng hoạt động", - "backup_controller_page_turn_on": "Bật sao lưu khi ứng dụng hoạt động", + "backup_controller_page_turn_on": "Bật sao lưu khi mở ứng dụng", "backup_controller_page_uploading_file_info": "Thông tin tệp đang tải lên", "backup_err_only_album": "Không thể xóa album duy nhất", "backup_info_card_assets": "ảnh", @@ -131,6 +137,7 @@ "backup_manual_success": "Thành công", "backup_manual_title": "Trạng thái tải lên", "backup_options_page_title": "Tuỳ chỉnh sao lưu", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "Trang thư viện hình thu nhỏ ({} ảnh)", "cache_settings_clear_cache_button": "Xoá bộ nhớ đệm", "cache_settings_clear_cache_button_title": "Xóa bộ nhớ đệm của ứng dụng. Điều này sẽ ảnh hưởng đến hiệu suất của ứng dụng đến khi bộ nhớ đệm được tạo lại.", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "Kiểm soát cách xử lý lưu trữ cục bộ", "cache_settings_tile_title": "Lưu trữ cục bộ", "cache_settings_title": "Cài đặt bộ nhớ đệm", + "cancel": "Cancel", "change_password_form_confirm_password": "Xác nhận mật khẩu", "change_password_form_description": "Xin chào {name},\n\nĐây là lần đầu tiên bạn đăng nhập vào hệ thống hoặc đã có yêu cầu thay đổi mật khẩu. Vui lòng nhập mật khẩu mới bên dưới.", "change_password_form_new_password": "Mật khẩu mới", "change_password_form_password_mismatch": "Mật khẩu không giống nhau", "change_password_form_reenter_new_password": "Nhập lại mật khẩu mới", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "Đồng ý", "client_cert_enter_password": "Nhập mật khẩu", "client_cert_import": "Nhập", @@ -199,6 +210,7 @@ "crop": "Cắt", "curated_location_page_title": "Địa điểm", "curated_object_page_title": "Sự vật", + "current_server_address": "Current server address", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "Múi giờ", "edit_image_title": "Sửa", "edit_location_dialog_title": "Vị trí", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "Lỗi: {}", "exif_bottom_sheet_description": "Thêm mô tả...", "exif_bottom_sheet_details": "CHI TIẾT", @@ -246,13 +259,17 @@ "experimental_settings_new_asset_list_title": "Bật lưới ảnh thử nghiệm", "experimental_settings_subtitle": "Sử dụng có thể rủi ro!", "experimental_settings_title": "Chưa hoàn thiện", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "Favorites", "favorites_page_no_favorites": "Không tìm thấy ảnh yêu thích", "favorites_page_title": "Ảnh yêu thích", "filename_search": "Tên hoặc phần mở rộng tập tin", "filter": "Bộ lọc", - "haptic_feedback_switch": "Bật phản hồi haptic\n", - "haptic_feedback_title": "Haptic Feedback\n", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", + "haptic_feedback_switch": "Bật phản hồi haptic", + "haptic_feedback_title": "Phản hồi Hapic", "header_settings_add_header_tip": "Thêm Header", "header_settings_field_validator_msg": "Trường này không được để trống", "header_settings_header_name_input": "Tên Header", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "Ảnh cũ nhất", "library_page_sort_most_recent_photo": "Ảnh gần đây nhất", "library_page_sort_title": "Tiêu đề album", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Chọn trên bản đồ", "location_picker_latitude": "Vĩ độ", "location_picker_latitude_error": "Nhập vĩ độ hợp lệ", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Không thể chỉnh sửa ngày của ảnh chỉ có quyền đọc, bỏ qua", "multiselect_grid_edit_gps_err_read_only": "Không thể chỉnh sửa vị trí của ảnh chỉ có quyền đọc, bỏ qua", "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "Không có mục nào để hiển thị", "no_name": "Không có tên", "notification_permission_dialog_cancel": "Từ chối", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "Quyền truy cập vào ảnh của bạn bị hạn chế. Để Immich sao lưu và quản lý toàn bộ thư viện ảnh của bạn, hãy cấp quyền truy cập toàn bộ ảnh trong Cài đặt.", "permission_onboarding_request": "Immich cần quyền để xem ảnh và video của bạn", "places": "Places", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Tuỳ chỉnh", "profile_drawer_app_logs": "Nhật ký", "profile_drawer_client_out_of_date_major": "Ứng dụng đã lỗi thời. Vui lòng cập nhật lên phiên bản chính mới nhất.", @@ -412,6 +436,7 @@ "profile_drawer_trash": "Thùng rác", "recently_added": "Recently added", "recently_added_page_title": "Mới thêm gần đây", + "save": "Save", "save_to_gallery": "Lưu vào thư viện", "scaffold_body_error_occurred": "Xảy ra lỗi", "search_albums": "Search albums", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "Gợi ý", "select_user_for_sharing_page_err_album": "Tạo album thất bại", "select_user_for_sharing_page_share_suggestions": "Gợi ý", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Phiên bản ứng dụng", "server_info_box_latest_release": "Phiên bản mới nhất", "server_info_box_server_url": "Địa chỉ máy chủ", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "Tải ảnh xem trước", "setting_image_viewer_title": "Hình ảnh", "setting_languages_apply": "Áp dụng", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Ngôn ngữ", "setting_notifications_notify_failures_grace_period": "Thông báo sao lưu nền thất bại: {}", "setting_notifications_notify_hours": "{} giờ", @@ -612,6 +639,8 @@ "upload_dialog_info": "Bạn có muốn sao lưu những mục đã chọn tới máy chủ không?", "upload_dialog_ok": "Tải lên", "upload_dialog_title": "Tải lên ảnh", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "Công nhận", "version_announcement_overlay_release_notes": "ghi chú phát hành", "version_announcement_overlay_text_1": "Chào bạn, có một bản phát hành mới của", @@ -621,5 +650,7 @@ "videos": "Videos", "viewer_remove_from_stack": "Xoá khỏi nhóm", "viewer_stack_use_as_main_asset": "Đặt làm lựa chọn hàng đầu", - "viewer_unstack": "Huỷ xếp nhóm" + "viewer_unstack": "Huỷ xếp nhóm", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/zh-CN.json b/mobile/assets/i18n/zh-CN.json index 0da7c3b2db4c3..1b8dd215826a2 100644 --- a/mobile/assets/i18n/zh-CN.json +++ b/mobile/assets/i18n/zh-CN.json @@ -7,6 +7,7 @@ "action_common_select": "选择", "action_common_update": "更新", "add_a_name": "添加姓名", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "添加到 {album}", "add_to_album_bottom_sheet_already_exists": "已在 {album} 中", "advanced_settings_log_level_title": "日志等级:{}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "已成功恢复{}个项目", "assets_trashed": "{}个回收站项目", "assets_trashed_from_server": "{}个项目已放入回收站", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "资源查看器", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "设备上的相册({})", "backup_album_selection_page_albums_tap": "单击选中,双击取消", "backup_album_selection_page_assets_scatter": "项目会分散在多个相册中。因此,可以在备份过程中包含或排除相册。", @@ -131,6 +137,7 @@ "backup_manual_success": "成功", "backup_manual_title": "上传状态", "backup_options_page_title": "备份选项", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "图库缩略图({} 项)", "cache_settings_clear_cache_button": "清除缓存", "cache_settings_clear_cache_button_title": "清除应用缓存。在重新生成缓存之前,将显著影响应用的性能。", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "设置本地存储行为", "cache_settings_tile_title": "本地存储", "cache_settings_title": "缓存设置", + "cancel": "Cancel", "change_password_form_confirm_password": "确认密码", "change_password_form_description": "{name} 您好,\n\n这是您首次登录系统,或被管理员要求更改密码。\n请在下方输入新密码。", "change_password_form_new_password": "新密码", "change_password_form_password_mismatch": "密码不匹配", "change_password_form_reenter_new_password": "再次输入新密码", + "check_corrupt_asset_backup": "检查备份是否损坏", + "check_corrupt_asset_backup_button": "执行检查", + "check_corrupt_asset_backup_description": "仅在连接到Wi-Fi并完成所有项目备份后执行此检查。该过程可能需要几分钟。", "client_cert_dialog_msg_confirm": "确定", "client_cert_enter_password": "输入密码", "client_cert_import": "导入", @@ -199,6 +210,7 @@ "crop": "裁剪", "curated_location_page_title": "地点", "curated_object_page_title": "事物", + "current_server_address": "Current server address", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "时区", "edit_image_title": "编辑", "edit_location_dialog_title": "位置", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "错误:{}", "exif_bottom_sheet_description": "添加描述...", "exif_bottom_sheet_details": "详情", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "启用实验性照片网格", "experimental_settings_subtitle": "使用风险自负!", "experimental_settings_title": "实验性功能", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "收藏", "favorites_page_no_favorites": "未找到收藏项目", "favorites_page_title": "收藏", "filename_search": "文件名或扩展名", "filter": "筛选", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "启用振动反馈", "haptic_feedback_title": "振动反馈", "header_settings_add_header_tip": "添加标头", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "最早的照片", "library_page_sort_most_recent_photo": "最近的项目", "library_page_sort_title": "相册标题", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "在地图上选择", "location_picker_latitude": "纬度", "location_picker_latitude_error": "输入有效的纬度值", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "无法编辑只读项目的日期,跳过", "multiselect_grid_edit_gps_err_read_only": "无法编辑只读项目的位置信息,跳过", "my_albums": "我的相册", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "无项目展示", "no_name": "无姓名", "notification_permission_dialog_cancel": "取消", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "权限受限:要让 Immich 备份和管理您的整个图库收藏,请在“设置”中授予照片和视频权限。", "permission_onboarding_request": "Immich 需要权限才能查看您的照片和视频。", "places": "地点", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "偏好设置", "profile_drawer_app_logs": "日志", "profile_drawer_client_out_of_date_major": "客户端有大版本升级,请尽快升级至最新版。", @@ -412,6 +436,7 @@ "profile_drawer_trash": "回收站", "recently_added": "近期添加", "recently_added_page_title": "最近添加", + "save": "Save", "save_to_gallery": "保存到图库", "scaffold_body_error_occurred": "发生错误", "search_albums": "搜索相册", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "建议", "select_user_for_sharing_page_err_album": "创建相册失败", "select_user_for_sharing_page_share_suggestions": "建议", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "App 版本", "server_info_box_latest_release": "最新版本", "server_info_box_server_url": "服务器地址", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "加载预览图", "setting_image_viewer_title": "图片", "setting_languages_apply": "应用", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "语言", "setting_notifications_notify_failures_grace_period": "后台备份失败通知:{}", "setting_notifications_notify_hours": "{} 小时", @@ -612,6 +639,8 @@ "upload_dialog_info": "是否要将所选项目备份到服务器?", "upload_dialog_ok": "上传", "upload_dialog_title": "上传项目", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "我知道了", "version_announcement_overlay_release_notes": "发行说明", "version_announcement_overlay_text_1": "号外号外,有新版本的", @@ -621,5 +650,7 @@ "videos": "视频", "viewer_remove_from_stack": "从堆叠中移除", "viewer_stack_use_as_main_asset": "作为主项目使用", - "viewer_unstack": "取消堆叠" + "viewer_unstack": "取消堆叠", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/zh-Hans.json b/mobile/assets/i18n/zh-Hans.json index 21a7fc2e4e67b..93b1d8ce557fa 100644 --- a/mobile/assets/i18n/zh-Hans.json +++ b/mobile/assets/i18n/zh-Hans.json @@ -7,6 +7,7 @@ "action_common_select": "选择", "action_common_update": "更新", "add_a_name": "添加姓名", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "添加到 {album}", "add_to_album_bottom_sheet_already_exists": "已在 {album} 中", "advanced_settings_log_level_title": "日志等级:{}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "已成功恢复{}个项目", "assets_trashed": "{}个回收站项目", "assets_trashed_from_server": "{}个项目已放入回收站", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "资源查看器", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "设备上的相册({})", "backup_album_selection_page_albums_tap": "单击选中,双击取消", "backup_album_selection_page_assets_scatter": "项目会分散在多个相册中。因此,可以在备份过程中包含或排除相册。", @@ -131,6 +137,7 @@ "backup_manual_success": "成功", "backup_manual_title": "上传状态", "backup_options_page_title": "备份选项", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "图库缩略图({} 项)", "cache_settings_clear_cache_button": "清除缓存", "cache_settings_clear_cache_button_title": "清除应用缓存。在重新生成缓存之前,将显著影响应用的性能。", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "设置本地存储行为", "cache_settings_tile_title": "本地存储", "cache_settings_title": "缓存设置", + "cancel": "Cancel", "change_password_form_confirm_password": "确认密码", "change_password_form_description": "{name} 您好,\n\n这是您首次登录系统,或被管理员要求更改密码。\n请在下方输入新密码。", "change_password_form_new_password": "新密码", "change_password_form_password_mismatch": "密码不匹配", "change_password_form_reenter_new_password": "再次输入新密码", + "check_corrupt_asset_backup": "检查备份是否损坏", + "check_corrupt_asset_backup_button": "执行检查", + "check_corrupt_asset_backup_description": "仅在连接到Wi-Fi并完成所有项目备份后执行此检查。该过程可能需要几分钟。", "client_cert_dialog_msg_confirm": "确定", "client_cert_enter_password": "输入密码", "client_cert_import": "导入", @@ -199,6 +210,7 @@ "crop": "裁剪", "curated_location_page_title": "地点", "curated_object_page_title": "事物", + "current_server_address": "Current server address", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "时区", "edit_image_title": "编辑", "edit_location_dialog_title": "位置", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "错误:{}", "exif_bottom_sheet_description": "添加描述...", "exif_bottom_sheet_details": "详情", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "启用实验性照片网格", "experimental_settings_subtitle": "使用风险自负!", "experimental_settings_title": "实验性功能", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "收藏", "favorites_page_no_favorites": "未找到收藏项目", "favorites_page_title": "收藏", "filename_search": "文件名或扩展名", "filter": "筛选", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "启用振动反馈", "haptic_feedback_title": "振动反馈", "header_settings_add_header_tip": "添加标头", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "最早的照片", "library_page_sort_most_recent_photo": "最近的项目", "library_page_sort_title": "相册标题", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "在地图上选择", "location_picker_latitude": "纬度", "location_picker_latitude_error": "输入有效的纬度值", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "无法编辑只读项目的日期,跳过", "multiselect_grid_edit_gps_err_read_only": "无法编辑只读项目的位置信息,跳过", "my_albums": "我的相册", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "无项目展示", "no_name": "无姓名", "notification_permission_dialog_cancel": "取消", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "权限受限:要让 Immich 备份和管理您的整个图库收藏,请在“设置”中授予照片和视频权限。", "permission_onboarding_request": "Immich 需要权限才能查看您的照片和视频。", "places": "地点", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "偏好设置", "profile_drawer_app_logs": "日志", "profile_drawer_client_out_of_date_major": "客户端有大版本升级,请尽快升级至最新版。", @@ -412,6 +436,7 @@ "profile_drawer_trash": "回收站", "recently_added": "近期添加", "recently_added_page_title": "最近添加", + "save": "Save", "save_to_gallery": "保存到图库", "scaffold_body_error_occurred": "发生错误", "search_albums": "搜索相册", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "建议", "select_user_for_sharing_page_err_album": "创建相册失败", "select_user_for_sharing_page_share_suggestions": "建议", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "App 版本", "server_info_box_latest_release": "最新版本", "server_info_box_server_url": "服务器地址", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "加载预览图", "setting_image_viewer_title": "图片", "setting_languages_apply": "应用", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "语言", "setting_notifications_notify_failures_grace_period": "后台备份失败通知:{}", "setting_notifications_notify_hours": "{} 小时", @@ -612,6 +639,8 @@ "upload_dialog_info": "是否要将所选项目备份到服务器?", "upload_dialog_ok": "上传", "upload_dialog_title": "上传项目", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "我知道了", "version_announcement_overlay_release_notes": "发行说明", "version_announcement_overlay_text_1": "号外号外,有新版本的", @@ -621,5 +650,7 @@ "videos": "视频", "viewer_remove_from_stack": "从堆叠中移除", "viewer_stack_use_as_main_asset": "作为主项目使用", - "viewer_unstack": "取消堆叠" + "viewer_unstack": "取消堆叠", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file diff --git a/mobile/assets/i18n/zh-TW.json b/mobile/assets/i18n/zh-TW.json index 9fd3a19e5ed45..84120c50f7186 100644 --- a/mobile/assets/i18n/zh-TW.json +++ b/mobile/assets/i18n/zh-TW.json @@ -7,6 +7,7 @@ "action_common_select": "選擇", "action_common_update": "更新", "add_a_name": "新增姓名", + "add_endpoint": "Add endpoint", "add_to_album_bottom_sheet_added": "新增到 {album}", "add_to_album_bottom_sheet_already_exists": "已在 {album} 中", "advanced_settings_log_level_title": "日誌等級: {}", @@ -65,7 +66,12 @@ "assets_restored_successfully": "已成功恢復 {} 個項目", "assets_trashed": "{} 個回收桶項目", "assets_trashed_from_server": "{} 個項目已放入回收桶", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "資源查看器", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "裝置上的相簿( {} )", "backup_album_selection_page_albums_tap": "單擊選中,雙擊取消", "backup_album_selection_page_assets_scatter": "項目會分散在多個相簿中。因此,可以在備份過程中包含或排除相簿。", @@ -131,6 +137,7 @@ "backup_manual_success": "成功", "backup_manual_title": "上傳狀態", "backup_options_page_title": "備份選項", + "backup_setting_subtitle": "Manage background and foreground upload settings", "cache_settings_album_thumbnails": "圖庫縮圖( {} 項)", "cache_settings_clear_cache_button": "清除緩存", "cache_settings_clear_cache_button_title": "清除套用緩存。在重新生成緩存之前,將顯著影響套用的性能。", @@ -149,11 +156,15 @@ "cache_settings_tile_subtitle": "設定本地存儲行為", "cache_settings_tile_title": "本地存儲", "cache_settings_title": "緩存設定", + "cancel": "Cancel", "change_password_form_confirm_password": "確認密碼", "change_password_form_description": "您好 {name} :\n\n這是您首次登入系統,或被管理員要求更改密碼。\n請在下方輸入新密碼。", "change_password_form_new_password": "新密碼", "change_password_form_password_mismatch": "密碼不一致", "change_password_form_reenter_new_password": "再次輸入新密碼", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "client_cert_dialog_msg_confirm": "確定", "client_cert_enter_password": "輸入密碼", "client_cert_import": "匯入", @@ -199,6 +210,7 @@ "crop": "裁剪", "curated_location_page_title": "地點", "curated_object_page_title": "事物", + "current_server_address": "Current server address", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -235,6 +247,7 @@ "edit_date_time_dialog_timezone": "時區", "edit_image_title": "編輯", "edit_location_dialog_title": "位置", + "enter_wifi_name": "Enter WiFi name", "error_saving_image": "錯誤: {} ", "exif_bottom_sheet_description": "新增描述...", "exif_bottom_sheet_details": "詳情", @@ -246,11 +259,15 @@ "experimental_settings_new_asset_list_title": "啓用實驗性照片網格", "experimental_settings_subtitle": "使用風險自負!", "experimental_settings_title": "實驗性功能", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "favorites": "收藏", "favorites_page_no_favorites": "未找到收藏項目", "favorites_page_title": "收藏", "filename_search": "文件名或副檔名", "filter": "篩選", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", "haptic_feedback_switch": "啓用振動反饋", "haptic_feedback_title": "振動反饋", "header_settings_add_header_tip": "新增標頭", @@ -296,6 +313,10 @@ "library_page_sort_most_oldest_photo": "最早的照片", "library_page_sort_most_recent_photo": "最近的項目", "library_page_sort_title": "相簿標題", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "在地圖上選擇", "location_picker_latitude": "緯度", "location_picker_latitude_error": "輸入有效的緯度值", @@ -365,6 +386,8 @@ "multiselect_grid_edit_date_time_err_read_only": "無法編輯唯讀項目的日期,略過", "multiselect_grid_edit_gps_err_read_only": "無法編輯唯讀項目的位置資訊,略過", "my_albums": "我的相簿", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", "no_assets_to_show": "無項目展示", "no_name": "無姓名", "notification_permission_dialog_cancel": "取消", @@ -398,6 +421,7 @@ "permission_onboarding_permission_limited": "權限受限:要讓 Immich 備份和管理您的整個圖庫收藏,請在「設定」中授予照片和短片權限。", "permission_onboarding_request": "Immich 需要權限才能查看您的照片和短片。", "places": "地點", + "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "偏好設定", "profile_drawer_app_logs": "日誌", "profile_drawer_client_out_of_date_major": "客戶端有大版本升級,請盡快升級至最新版。", @@ -412,6 +436,7 @@ "profile_drawer_trash": "回收桶", "recently_added": "近期新增", "recently_added_page_title": "最近新增", + "save": "Save", "save_to_gallery": "儲存到圖庫", "scaffold_body_error_occurred": "發生錯誤", "search_albums": "搜尋相簿", @@ -469,6 +494,7 @@ "select_additional_user_for_sharing_page_suggestions": "建議", "select_user_for_sharing_page_err_album": "新增相簿失敗", "select_user_for_sharing_page_share_suggestions": "建議", + "server_endpoint": "Server Endpoint", "server_info_box_app_version": "App 版本", "server_info_box_latest_release": "最新版本", "server_info_box_server_url": "伺服器地址", @@ -480,6 +506,7 @@ "setting_image_viewer_preview_title": "載入預覽圖", "setting_image_viewer_title": "圖片", "setting_languages_apply": "套用", + "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "語言", "setting_notifications_notify_failures_grace_period": "背景備份失敗通知: {} ", "setting_notifications_notify_hours": " {} 小時", @@ -612,6 +639,8 @@ "upload_dialog_info": "是否要將所選項目備份到伺服器?", "upload_dialog_ok": "上傳", "upload_dialog_title": "上傳項目", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", "version_announcement_overlay_ack": "我知道了", "version_announcement_overlay_release_notes": "發行說明", "version_announcement_overlay_text_1": "好消息,有新版本的", @@ -621,5 +650,7 @@ "videos": "短片", "viewer_remove_from_stack": "從堆疊中移除", "viewer_stack_use_as_main_asset": "作為主項目使用", - "viewer_unstack": "取消堆疊" + "viewer_unstack": "取消堆疊", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" } \ No newline at end of file From 37220a342a9a3cde67bedfc7a791a6660fa145e1 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 16:29:46 +0000 Subject: [PATCH 11/19] chore: version v1.122.0 --- cli/package-lock.json | 8 ++++---- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package-lock.json | 16 ++++++++-------- e2e/package.json | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/openapi/devtools_options.yaml | 3 --- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 19 files changed, 36 insertions(+), 35 deletions(-) delete mode 100644 mobile/openapi/devtools_options.yaml diff --git a/cli/package-lock.json b/cli/package-lock.json index 8bb364ee23d21..7681417b76d50 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.32", + "version": "2.2.33", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.32", + "version": "2.2.33", "license": "GNU Affero General Public License version 3", "dependencies": { "fast-glob": "^3.3.2", @@ -31,7 +31,7 @@ "byte-size": "^9.0.0", "cli-progress": "^3.12.0", "commander": "^12.0.0", - "eslint": "9.14.0", + "eslint": "^9.14.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^55.0.0", @@ -52,7 +52,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.121.0", + "version": "1.122.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/cli/package.json b/cli/package.json index f0ab3aedc1965..ab66ac31180d4 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.32", + "version": "2.2.33", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 32f14e8639939..049f79dba63cd 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.122.0", + "url": "https://v1.122.0.archive.immich.app" + }, { "label": "v1.121.0", "url": "https://v1.121.0.archive.immich.app" diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 90750b5ed32b7..50bd5c6ce31f7 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.121.0", + "version": "1.122.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.121.0", + "version": "1.122.0", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -23,7 +23,7 @@ "@typescript-eslint/eslint-plugin": "^8.15.0", "@typescript-eslint/parser": "^8.15.0", "@vitest/coverage-v8": "^2.0.5", - "eslint": "9.14.0", + "eslint": "^9.14.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^55.0.0", @@ -45,7 +45,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.32", + "version": "2.2.33", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -65,13 +65,13 @@ "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", "@types/node": "^22.9.0", - "@typescript-eslint/eslint-plugin": "^8.0.0", - "@typescript-eslint/parser": "^8.0.0", + "@typescript-eslint/eslint-plugin": "^8.15.0", + "@typescript-eslint/parser": "^8.15.0", "@vitest/coverage-v8": "^2.0.5", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", "commander": "^12.0.0", - "eslint": "^9.0.0", + "eslint": "^9.14.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^55.0.0", @@ -92,7 +92,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.121.0", + "version": "1.122.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index a030ecdb1b3c2..9727fbd50d9ed 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.121.0", + "version": "1.122.0", "description": "", "main": "index.js", "type": "module", diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index cfb55bdc6bba4..d6baffcef517f 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.121.0" +version = "1.122.0" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 4c62a45ad74fb..7e2ea4c8e8d06 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 168, - "android.injected.version.name" => "1.121.0", + "android.injected.version.code" => 169, + "android.injected.version.name" => "1.122.0", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 4a484351033b7..9f1a78fcd7fbb 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.121.0" + version_number: "1.122.0" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index b97ff5411cbba..6d881e6d3d633 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.121.0 +- API version: 1.122.0 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen diff --git a/mobile/openapi/devtools_options.yaml b/mobile/openapi/devtools_options.yaml deleted file mode 100644 index fa0b357c4f4a2..0000000000000 --- a/mobile/openapi/devtools_options.yaml +++ /dev/null @@ -1,3 +0,0 @@ -description: This file stores settings for Dart & Flutter DevTools. -documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states -extensions: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index e8bee37653e19..863d1c1a752a6 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.121.0+168 +version: 1.122.0+169 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 43985cae8141d..16e9c93b329c4 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7436,7 +7436,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.121.0", + "version": "1.122.0", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 7239a3c5079d2..f8a044879947f 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.121.0", + "version": "1.122.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.121.0", + "version": "1.122.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index eedea811a4f8c..efcd085424124 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.121.0", + "version": "1.122.0", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 20d0c5715fa91..61f56f44048b6 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.121.0 + * 1.122.0 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index 6f0b2998e8476..a7eb1f50c9340 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.121.0", + "version": "1.122.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.121.0", + "version": "1.122.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^10.0.1", diff --git a/server/package.json b/server/package.json index 9332217c00382..28b0a44289224 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.121.0", + "version": "1.122.0", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index 15edeb0c289cc..615b17de534f9 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.121.0", + "version": "1.122.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.121.0", + "version": "1.122.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.7.8", @@ -74,7 +74,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.121.0", + "version": "1.122.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index 4f0062fe15aab..93b00cde60c1d 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.121.0", + "version": "1.122.0", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From 4380ecf7bbd16d8424e1ae004357fa03d09ca7b8 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 5 Dec 2024 14:10:08 -0600 Subject: [PATCH 12/19] fix(web): misaligned icon on Firefox (#14500) --- .../shared-components/side-bar/side-bar-link.svelte | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/src/lib/components/shared-components/side-bar/side-bar-link.svelte b/web/src/lib/components/shared-components/side-bar/side-bar-link.svelte index 4da73b6288408..c6e430fe99c14 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar-link.svelte +++ b/web/src/lib/components/shared-components/side-bar/side-bar-link.svelte @@ -36,7 +36,7 @@ }); - +

{#if hasDropdown}
+ {#if hasDropdown && dropdownOpen} {@render hasDropdown?.()} {/if} From d36477381ab958634865eba1fc121588d0598360 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 19:31:27 -0500 Subject: [PATCH 13/19] chore(deps): update dependency @sveltejs/kit to v2.8.3 [security] (#14342) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- web/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 615b17de534f9..40a1378fa6a9d 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1965,9 +1965,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.8.1.tgz", - "integrity": "sha512-uuOfFwZ4xvnfPsiTB6a4H1ljjTUksGhWnYq5X/Y9z4x5+3uM2Md8q/YVeHL+7w+mygAwoEFdgKZ8YkUuk+VKww==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.8.3.tgz", + "integrity": "sha512-DVBVwugfzzn0SxKA+eAmKqcZ7aHZROCHxH7/pyrOi+HLtQ721eEsctGb9MkhEuqj6q/9S/OFYdn37vdxzFPdvw==", "dev": true, "hasInstallScript": true, "license": "MIT", From 07096bdcee0c8979e3930caaefb4c8be47b87593 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Fri, 6 Dec 2024 08:43:58 -0500 Subject: [PATCH 14/19] fix(server): images with non-ascii names failing to load (#14512) * utf-8 filename * Update file.ts Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> --------- Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> --- server/src/utils/file.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/utils/file.ts b/server/src/utils/file.ts index ba487840e5e84..869e4d78765ea 100644 --- a/server/src/utils/file.ts +++ b/server/src/utils/file.ts @@ -58,7 +58,7 @@ export const sendFile = async ( res.header('Content-Type', file.contentType); if (file.fileName) { - res.header('Content-Disposition', `inline; filename="${file.fileName}"`); + res.header('Content-Disposition', `inline; filename*=UTF-8''${encodeURIComponent(file.fileName)}`); } const options: SendFileOptions = { dotfiles: 'allow' }; From 97c1eb72897c34bd00891d1d336504ddddebb857 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 13:49:14 +0000 Subject: [PATCH 15/19] chore: version v1.122.1 --- cli/package-lock.json | 6 +++--- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package-lock.json | 8 ++++---- e2e/package.json | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 2 +- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 18 files changed, 30 insertions(+), 26 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 7681417b76d50..b0611f1af0577 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.33", + "version": "2.2.34", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.33", + "version": "2.2.34", "license": "GNU Affero General Public License version 3", "dependencies": { "fast-glob": "^3.3.2", @@ -52,7 +52,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.122.0", + "version": "1.122.1", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/cli/package.json b/cli/package.json index ab66ac31180d4..b69a36cf199fb 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.33", + "version": "2.2.34", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 049f79dba63cd..0d664e1272cdf 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.122.1", + "url": "https://v1.122.1.archive.immich.app" + }, { "label": "v1.122.0", "url": "https://v1.122.0.archive.immich.app" diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 50bd5c6ce31f7..d962cb3368810 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.122.0", + "version": "1.122.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.122.0", + "version": "1.122.1", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -45,7 +45,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.33", + "version": "2.2.34", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -92,7 +92,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.122.0", + "version": "1.122.1", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index 9727fbd50d9ed..42ea62d64b3c8 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.122.0", + "version": "1.122.1", "description": "", "main": "index.js", "type": "module", diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index d6baffcef517f..e4d1d7fbf59e1 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.122.0" +version = "1.122.1" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 7e2ea4c8e8d06..9e384e859193e 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -36,7 +36,7 @@ platform :android do build_type: 'Release', properties: { "android.injected.version.code" => 169, - "android.injected.version.name" => "1.122.0", + "android.injected.version.name" => "1.122.1", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 9f1a78fcd7fbb..1c28c050aa0cb 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.122.0" + version_number: "1.122.1" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 6d881e6d3d633..7cfd48d83dc30 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.122.0 +- API version: 1.122.1 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 863d1c1a752a6..6974c560a2c87 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.122.0+169 +version: 1.122.1+169 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 16e9c93b329c4..a13fb6e696b41 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7436,7 +7436,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.122.0", + "version": "1.122.1", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index f8a044879947f..2e69c4cfc6f1b 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.122.0", + "version": "1.122.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.122.0", + "version": "1.122.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index efcd085424124..bc3a3023e44ee 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.122.0", + "version": "1.122.1", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 61f56f44048b6..ef82b82954bdf 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.122.0 + * 1.122.1 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index a7eb1f50c9340..3a01c83fa1a4a 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.122.0", + "version": "1.122.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.122.0", + "version": "1.122.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^10.0.1", diff --git a/server/package.json b/server/package.json index 28b0a44289224..84cd0e4aaec7a 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.122.0", + "version": "1.122.1", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index 40a1378fa6a9d..4669730ae137f 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.122.0", + "version": "1.122.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.122.0", + "version": "1.122.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.7.8", @@ -74,7 +74,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.122.0", + "version": "1.122.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index 93b00cde60c1d..6bac30054a9fd 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.122.0", + "version": "1.122.1", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From c02e3e2a2ed15534454a520c28728a0a3303262c Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Dec 2024 20:04:02 -0600 Subject: [PATCH 16/19] chore(mobile): post release tasks (#14520) --- mobile/ios/Runner/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 4389b39114f59..e85afdc8520c5 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -58,7 +58,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.121.0 + 1.122.0 CFBundleSignature ???? CFBundleVersion From e2b36476e77af2eeec98bebb46cdba33d48ca952 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 21:10:47 -0500 Subject: [PATCH 17/19] chore(deps): update grafana/grafana docker tag to v11.3.1 (#14476) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker/docker-compose.prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 8d80003ee440a..704f3bdfc8377 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -103,7 +103,7 @@ services: command: ['./run.sh', '-disable-reporting'] ports: - 3000:3000 - image: grafana/grafana:11.3.0-ubuntu@sha256:51587e148ac0214d7938e7f3fe8512182e4eb6141892a3ffb88bba1901b49285 + image: grafana/grafana:11.3.1-ubuntu@sha256:7ca40d20250157abd70a907a93617a70c9b0ad9d7e59e8e6b5c8140781350d6a volumes: - grafana-data:/var/lib/grafana From 5e955a1b030a50ebe5c4dd8ae374729c643ce678 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Sat, 7 Dec 2024 17:24:00 +0100 Subject: [PATCH 18/19] fix(web): recent albums sort (#14545) --- .../side-bar/recent-albums.spec.ts | 28 +++++++++++++++++++ .../side-bar/recent-albums.svelte | 4 +-- 2 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 web/src/lib/components/shared-components/side-bar/recent-albums.spec.ts diff --git a/web/src/lib/components/shared-components/side-bar/recent-albums.spec.ts b/web/src/lib/components/shared-components/side-bar/recent-albums.spec.ts new file mode 100644 index 0000000000000..d5c197c0039f3 --- /dev/null +++ b/web/src/lib/components/shared-components/side-bar/recent-albums.spec.ts @@ -0,0 +1,28 @@ +import { sdkMock } from '$lib/__mocks__/sdk.mock'; +import RecentAlbums from '$lib/components/shared-components/side-bar/recent-albums.svelte'; +import { albumFactory } from '@test-data/factories/album-factory'; +import { render, screen } from '@testing-library/svelte'; +import { tick } from 'svelte'; + +describe('RecentAlbums component', () => { + it('sorts albums by most recently updated', async () => { + const albums = [ + albumFactory.build({ updatedAt: '2024-01-01T00:00:00Z' }), + albumFactory.build({ updatedAt: '2024-01-09T00:00:01Z' }), + albumFactory.build({ updatedAt: '2024-01-10T00:00:00Z' }), + albumFactory.build({ updatedAt: '2024-01-09T00:00:00Z' }), + ]; + + sdkMock.getAllAlbums.mockResolvedValueOnce([...albums]); + render(RecentAlbums); + + expect(sdkMock.getAllAlbums).toBeCalledTimes(1); + await tick(); + + const links = screen.getAllByRole('link'); + expect(links).toHaveLength(3); + expect(links[0]).toHaveAttribute('href', `/albums/${albums[2].id}`); + expect(links[1]).toHaveAttribute('href', `/albums/${albums[1].id}`); + expect(links[2]).toHaveAttribute('href', `/albums/${albums[3].id}`); + }); +}); diff --git a/web/src/lib/components/shared-components/side-bar/recent-albums.svelte b/web/src/lib/components/shared-components/side-bar/recent-albums.svelte index a412d5cc42ed5..d90d7dec01c87 100644 --- a/web/src/lib/components/shared-components/side-bar/recent-albums.svelte +++ b/web/src/lib/components/shared-components/side-bar/recent-albums.svelte @@ -10,9 +10,7 @@ onMount(async () => { try { const allAlbums = await getAllAlbums({}); - albums = allAlbums - .sort((album1, album2) => (album1.lastModifiedAssetTimestamp! > album2.lastModifiedAssetTimestamp! ? 1 : 0)) - .slice(0, 3); + albums = allAlbums.sort((a, b) => (a.updatedAt > b.updatedAt ? -1 : 1)).slice(0, 3); } catch (error) { handleError(error, $t('failed_to_load_assets')); } From e99edc47b7a6298f62bad7f3d12389b51472ca02 Mon Sep 17 00:00:00 2001 From: Cotterman-b <119392287+Cotterman-b@users.noreply.github.com> Date: Sat, 7 Dec 2024 10:46:19 -0600 Subject: [PATCH 19/19] fix(mobile): fix translations on search page (#14533) * Update en-US.json * Update search.page.dart --- mobile/assets/i18n/en-US.json | 3 ++- mobile/lib/pages/search/search.page.dart | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 6fb2ed4ff58ba..46e9758d85688 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -482,6 +482,7 @@ "search_page_places": "Places", "search_page_recently_added": "Recently added", "search_page_screenshots": "Screenshots", + "search_page_search_photos_videos": "Search for your photos and videos", "search_page_selfies": "Selfies", "search_page_things": "Things", "search_page_videos": "Videos", @@ -653,4 +654,4 @@ "viewer_unstack": "Un-Stack", "wifi_name": "WiFi Name", "your_wifi_name": "Your WiFi name" -} \ No newline at end of file +} diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart index 9f2ddee446149..01119485cfe4e 100644 --- a/mobile/lib/pages/search/search.page.dart +++ b/mobile/lib/pages/search/search.page.dart @@ -680,7 +680,7 @@ class SearchEmptyContent extends StatelessWidget { const SizedBox(height: 16), Center( child: Text( - "Search for your photos and videos", + 'search_page_search_photos_videos'.tr(), style: context.textTheme.labelLarge, ), ),