Compare commits
36 Commits
localizely
...
asset-user
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7277ea3d7a | ||
|
|
da580d4685 | ||
|
|
cb6d94c7a7 | ||
|
|
060300de8a | ||
|
|
c2ba1cc202 | ||
|
|
08db77db23 | ||
|
|
92dff839d0 | ||
|
|
fe1e09e51f | ||
|
|
f44669447f | ||
|
|
92412ca2f7 | ||
|
|
64d926581f | ||
|
|
c139e05170 | ||
|
|
0fe62298e1 | ||
|
|
e5794e6cfc | ||
|
|
f6cbc9db06 | ||
|
|
8dab5d3798 | ||
|
|
e864811a85 | ||
|
|
72a55c13b6 | ||
|
|
206412267a | ||
|
|
f780a56e24 | ||
|
|
7bbffccf76 | ||
|
|
05a446c259 | ||
|
|
4f725b95e1 | ||
|
|
64b92cb24c | ||
|
|
19f2f888ee | ||
|
|
d12b1c907d | ||
|
|
947c053c15 | ||
|
|
79592701dd | ||
|
|
39697cd973 | ||
|
|
10e518db42 | ||
|
|
72fa31f9e9 | ||
|
|
9871a04d54 | ||
|
|
ba01b40e7c | ||
|
|
f5a3d7ba23 | ||
|
|
d4a9eed4a1 | ||
|
|
9d8072b994 |
6
cli/package-lock.json
generated
6
cli/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.42",
|
||||
"version": "2.2.47",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.42",
|
||||
"version": "2.2.47",
|
||||
"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.125.1",
|
||||
"version": "1.125.6",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.42",
|
||||
"version": "2.2.47",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"type": "module",
|
||||
"exports": "./dist/index.js",
|
||||
|
||||
@@ -99,6 +99,11 @@ const projects: CommunityProjectProps[] = [
|
||||
description: 'Downloads a configurable number of random photos based on people or album ID.',
|
||||
url: 'https://github.com/jon6fingrs/immich-dl',
|
||||
},
|
||||
{
|
||||
title: 'Immich Upload Optimizer',
|
||||
description: 'Automatically optimize files uploaded to Immich in order to save storage space',
|
||||
url: 'https://github.com/miguelangel-nubla/immich-upload-optimizer',
|
||||
},
|
||||
];
|
||||
|
||||
function CommunityProject({ title, description, url }: CommunityProjectProps): JSX.Element {
|
||||
|
||||
20
docs/static/archived-versions.json
vendored
20
docs/static/archived-versions.json
vendored
@@ -1,4 +1,24 @@
|
||||
[
|
||||
{
|
||||
"label": "v1.125.6",
|
||||
"url": "https://v1.125.6.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.125.5",
|
||||
"url": "https://v1.125.5.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.125.4",
|
||||
"url": "https://v1.125.4.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.125.3",
|
||||
"url": "https://v1.125.3.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.125.2",
|
||||
"url": "https://v1.125.2.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.125.1",
|
||||
"url": "https://v1.125.1.archive.immich.app"
|
||||
|
||||
8
e2e/package-lock.json
generated
8
e2e/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "1.125.1",
|
||||
"version": "1.125.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich-e2e",
|
||||
"version": "1.125.1",
|
||||
"version": "1.125.6",
|
||||
"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.42",
|
||||
"version": "2.2.47",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
@@ -92,7 +92,7 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.125.1",
|
||||
"version": "1.125.6",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "1.125.1",
|
||||
"version": "1.125.6",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -22,79 +22,92 @@ const user1NotShared = 'user1NotShared';
|
||||
const user2SharedUser = 'user2SharedUser';
|
||||
const user2SharedLink = 'user2SharedLink';
|
||||
const user2NotShared = 'user2NotShared';
|
||||
const user4DeletedAsset = 'user4DeletedAsset';
|
||||
const user4Empty = 'user4Empty';
|
||||
|
||||
describe('/albums', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let user1: LoginResponseDto;
|
||||
let user1Asset1: AssetMediaResponseDto;
|
||||
let user1Asset2: AssetMediaResponseDto;
|
||||
let user4Asset1: AssetMediaResponseDto;
|
||||
let user1Albums: AlbumResponseDto[];
|
||||
let user2: LoginResponseDto;
|
||||
let user2Albums: AlbumResponseDto[];
|
||||
let deletedAssetAlbum: AlbumResponseDto;
|
||||
let user3: LoginResponseDto; // deleted
|
||||
let user4: LoginResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
|
||||
admin = await utils.adminSetup();
|
||||
|
||||
[user1, user2, user3] = await Promise.all([
|
||||
[user1, user2, user3, user4] = await Promise.all([
|
||||
utils.userSetup(admin.accessToken, createUserDto.user1),
|
||||
utils.userSetup(admin.accessToken, createUserDto.user2),
|
||||
utils.userSetup(admin.accessToken, createUserDto.user3),
|
||||
utils.userSetup(admin.accessToken, createUserDto.user4),
|
||||
]);
|
||||
|
||||
[user1Asset1, user1Asset2] = await Promise.all([
|
||||
[user1Asset1, user1Asset2, user4Asset1] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken, { isFavorite: true }),
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
user1Albums = await Promise.all([
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1SharedEditorUser,
|
||||
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }],
|
||||
assetIds: [user1Asset1.id],
|
||||
}),
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1SharedLink,
|
||||
assetIds: [user1Asset1.id],
|
||||
}),
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1NotShared,
|
||||
assetIds: [user1Asset1.id, user1Asset2.id],
|
||||
}),
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1SharedViewerUser,
|
||||
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
|
||||
assetIds: [user1Asset1.id],
|
||||
[user1Albums, user2Albums, deletedAssetAlbum] = await Promise.all([
|
||||
Promise.all([
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1SharedEditorUser,
|
||||
albumUsers: [
|
||||
{ userId: admin.userId, role: AlbumUserRole.Editor },
|
||||
{ userId: user2.userId, role: AlbumUserRole.Editor },
|
||||
],
|
||||
assetIds: [user1Asset1.id],
|
||||
}),
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1SharedLink,
|
||||
assetIds: [user1Asset1.id],
|
||||
}),
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1NotShared,
|
||||
assetIds: [user1Asset1.id, user1Asset2.id],
|
||||
}),
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1SharedViewerUser,
|
||||
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
|
||||
assetIds: [user1Asset1.id],
|
||||
}),
|
||||
]),
|
||||
Promise.all([
|
||||
utils.createAlbum(user2.accessToken, {
|
||||
albumName: user2SharedUser,
|
||||
albumUsers: [
|
||||
{ userId: user1.userId, role: AlbumUserRole.Editor },
|
||||
{ userId: user3.userId, role: AlbumUserRole.Editor },
|
||||
],
|
||||
}),
|
||||
utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
|
||||
utils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
|
||||
]),
|
||||
utils.createAlbum(user4.accessToken, { albumName: user4DeletedAsset }),
|
||||
utils.createAlbum(user4.accessToken, { albumName: user4Empty }),
|
||||
utils.createAlbum(user3.accessToken, {
|
||||
albumName: 'Deleted',
|
||||
albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }],
|
||||
}),
|
||||
]);
|
||||
|
||||
user2Albums = await Promise.all([
|
||||
utils.createAlbum(user2.accessToken, {
|
||||
albumName: user2SharedUser,
|
||||
albumUsers: [
|
||||
{ userId: user1.userId, role: AlbumUserRole.Editor },
|
||||
{ userId: user3.userId, role: AlbumUserRole.Editor },
|
||||
],
|
||||
}),
|
||||
utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
|
||||
utils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
|
||||
]);
|
||||
|
||||
await utils.createAlbum(user3.accessToken, {
|
||||
albumName: 'Deleted',
|
||||
albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }],
|
||||
});
|
||||
|
||||
await addAssetsToAlbum(
|
||||
{ id: user2Albums[0].id, bulkIdsDto: { ids: [user1Asset1.id, user1Asset2.id] } },
|
||||
{ headers: asBearerAuth(user1.accessToken) },
|
||||
);
|
||||
|
||||
user2Albums[0] = await getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) });
|
||||
|
||||
await Promise.all([
|
||||
addAssetsToAlbum(
|
||||
{ id: user2Albums[0].id, bulkIdsDto: { ids: [user1Asset1.id, user1Asset2.id] } },
|
||||
{ headers: asBearerAuth(user1.accessToken) },
|
||||
),
|
||||
addAssetsToAlbum(
|
||||
{ id: deletedAssetAlbum.id, bulkIdsDto: { ids: [user4Asset1.id] } },
|
||||
{ headers: asBearerAuth(user4.accessToken) },
|
||||
),
|
||||
// add shared link to user1SharedLink album
|
||||
utils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
@@ -107,7 +120,11 @@ describe('/albums', () => {
|
||||
}),
|
||||
]);
|
||||
|
||||
await deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
||||
[user2Albums[0]] = await Promise.all([
|
||||
getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) }),
|
||||
deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) }),
|
||||
utils.deleteAssets(user1.accessToken, [user4Asset1.id]),
|
||||
]);
|
||||
});
|
||||
|
||||
describe('GET /albums', () => {
|
||||
@@ -284,6 +301,25 @@ describe('/albums', () => {
|
||||
expect(status).toBe(200);
|
||||
expect(body).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('should return empty albums and albums where all assets are deleted', async () => {
|
||||
const { status, body } = await request(app).get('/albums').set('Authorization', `Bearer ${user4.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
ownerId: user4.userId,
|
||||
albumName: user4DeletedAsset,
|
||||
shared: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
ownerId: user4.userId,
|
||||
albumName: user4Empty,
|
||||
shared: false,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /albums/:id', () => {
|
||||
@@ -362,6 +398,26 @@ describe('/albums', () => {
|
||||
shared: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not count trashed assets', async () => {
|
||||
await utils.deleteAssets(user1.accessToken, [user1Asset2.id]);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get(`/albums/${user2Albums[0].id}?withoutAssets=true`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
...user2Albums[0],
|
||||
assets: [],
|
||||
assetCount: 1,
|
||||
lastModifiedAssetTimestamp: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
startDate: expect.any(String),
|
||||
albumUsers: expect.any(Array),
|
||||
shared: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /albums/statistics', () => {
|
||||
|
||||
@@ -701,6 +701,20 @@ describe('/asset', () => {
|
||||
expect(status).toEqual(200);
|
||||
});
|
||||
|
||||
it('should set the negative rating', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/assets/${user1Assets[0].id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ rating: -1 });
|
||||
expect(body).toMatchObject({
|
||||
id: user1Assets[0].id,
|
||||
exifInfo: expect.objectContaining({
|
||||
rating: -1,
|
||||
}),
|
||||
});
|
||||
expect(status).toEqual(200);
|
||||
});
|
||||
|
||||
it('should reject invalid rating', async () => {
|
||||
for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: null }]) {
|
||||
const { status, body } = await request(app)
|
||||
|
||||
@@ -97,8 +97,8 @@ download:
|
||||
- file: mobile/assets/i18n/id-ID.json
|
||||
locale_code: id-ID
|
||||
- file: mobile/assets/i18n/gl.json
|
||||
locale_code: gl-ES
|
||||
locale_code: gl
|
||||
- file: mobile/assets/i18n/ga.json
|
||||
locale_code: ga-IE
|
||||
locale_code: ga
|
||||
- file: mobile/assets/i18n/tr-TR.json
|
||||
locale_code: tr-TR
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "machine-learning"
|
||||
version = "1.125.1"
|
||||
version = "1.125.6"
|
||||
description = ""
|
||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||
readme = "README.md"
|
||||
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 177,
|
||||
"android.injected.version.name" => "1.125.1",
|
||||
"android.injected.version.code" => 182,
|
||||
"android.injected.version.name" => "1.125.6",
|
||||
}
|
||||
)
|
||||
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')
|
||||
|
||||
@@ -248,6 +248,7 @@
|
||||
"download_waiting_to_retry": "Waiting to retry",
|
||||
"edit_date_time_dialog_date_time": "Date and Time",
|
||||
"edit_date_time_dialog_timezone": "Timezone",
|
||||
"edit_date_time_dialog_search_timezone": "Search timezone...",
|
||||
"edit_image_title": "Edit",
|
||||
"edit_location_dialog_title": "Location",
|
||||
"end_date": "End date",
|
||||
|
||||
673
mobile/assets/i18n/ga.json
Normal file
673
mobile/assets/i18n/ga.json
Normal file
@@ -0,0 +1,673 @@
|
||||
{
|
||||
"action_common_back": "Back",
|
||||
"action_common_cancel": "Cancel",
|
||||
"action_common_clear": "Clear",
|
||||
"action_common_confirm": "Confirm",
|
||||
"action_common_save": "Save",
|
||||
"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: {}",
|
||||
"advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.",
|
||||
"advanced_settings_prefer_remote_title": "Prefer remote images",
|
||||
"advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
|
||||
"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 (EXPERIMENTAL)",
|
||||
"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",
|
||||
"album_info_card_backup_album_excluded": "EXCLUDED",
|
||||
"album_info_card_backup_album_included": "INCLUDED",
|
||||
"albums": "Albums",
|
||||
"album_thumbnail_card_item": "1 item",
|
||||
"album_thumbnail_card_items": "{} items",
|
||||
"album_thumbnail_card_shared": " · Shared",
|
||||
"album_thumbnail_owned": "Owned",
|
||||
"album_thumbnail_shared_by": "Shared by {}",
|
||||
"album_viewer_appbar_delete_confirm": "Are you sure you want to delete this album from your account?",
|
||||
"album_viewer_appbar_share_delete": "Delete album",
|
||||
"album_viewer_appbar_share_err_delete": "Failed to delete album",
|
||||
"album_viewer_appbar_share_err_leave": "Failed to leave album",
|
||||
"album_viewer_appbar_share_err_remove": "There are problems in removing assets from album",
|
||||
"album_viewer_appbar_share_err_title": "Failed to change album title",
|
||||
"album_viewer_appbar_share_leave": "Leave album",
|
||||
"album_viewer_appbar_share_remove": "Remove from album",
|
||||
"album_viewer_appbar_share_to": "Share To",
|
||||
"album_viewer_page_share_add_users": "Add users",
|
||||
"all": "All",
|
||||
"all_people_page_title": "People",
|
||||
"all_videos_page_title": "Videos",
|
||||
"app_bar_signout_dialog_content": "Are you sure you want to sign out?",
|
||||
"app_bar_signout_dialog_ok": "Yes",
|
||||
"app_bar_signout_dialog_title": "Sign out",
|
||||
"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",
|
||||
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
|
||||
"asset_list_group_by_sub_title": "Group by",
|
||||
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
|
||||
"asset_list_layout_settings_group_automatically": "Automatic",
|
||||
"asset_list_layout_settings_group_by": "Group assets by",
|
||||
"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",
|
||||
"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_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.",
|
||||
"backup_album_selection_page_select_albums": "Select albums",
|
||||
"backup_album_selection_page_selection_info": "Selection Info",
|
||||
"backup_album_selection_page_total_assets": "Total unique assets",
|
||||
"backup_all": "All",
|
||||
"backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…",
|
||||
"backup_background_service_connection_failed_message": "Failed to connect to the server. Retrying…",
|
||||
"backup_background_service_current_upload_notification": "Uploading {}",
|
||||
"backup_background_service_default_notification": "Checking for new assets…",
|
||||
"backup_background_service_error_title": "Backup error",
|
||||
"backup_background_service_in_progress_notification": "Backing up your assets…",
|
||||
"backup_background_service_upload_failure_notification": "Failed to upload {}",
|
||||
"backup_controller_page_albums": "Backup Albums",
|
||||
"backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.",
|
||||
"backup_controller_page_background_app_refresh_disabled_title": "Background app refresh disabled",
|
||||
"backup_controller_page_background_app_refresh_enable_button_text": "Go to settings",
|
||||
"backup_controller_page_background_battery_info_link": "Show me how",
|
||||
"backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.",
|
||||
"backup_controller_page_background_battery_info_ok": "OK",
|
||||
"backup_controller_page_background_battery_info_title": "Battery optimizations",
|
||||
"backup_controller_page_background_charging": "Only while charging",
|
||||
"backup_controller_page_background_configure_error": "Failed to configure the background service",
|
||||
"backup_controller_page_background_delay": "Delay new assets backup: {}",
|
||||
"backup_controller_page_background_description": "Turn on the background service to automatically backup any new assets without needing to open the app",
|
||||
"backup_controller_page_background_is_off": "Automatic background backup is off",
|
||||
"backup_controller_page_background_is_on": "Automatic background backup is on",
|
||||
"backup_controller_page_background_turn_off": "Turn off background service",
|
||||
"backup_controller_page_background_turn_on": "Turn on background service",
|
||||
"backup_controller_page_background_wifi": "Only on WiFi",
|
||||
"backup_controller_page_backup": "Backup",
|
||||
"backup_controller_page_backup_selected": "Selected: ",
|
||||
"backup_controller_page_backup_sub": "Backed up photos and videos",
|
||||
"backup_controller_page_cancel": "Cancel",
|
||||
"backup_controller_page_created": "Created on: {}",
|
||||
"backup_controller_page_desc_backup": "Turn on foreground backup to automatically upload new assets to the server when opening the app.",
|
||||
"backup_controller_page_excluded": "Excluded: ",
|
||||
"backup_controller_page_failed": "Failed ({})",
|
||||
"backup_controller_page_filename": "File name: {} [{}]",
|
||||
"backup_controller_page_id": "ID: {}",
|
||||
"backup_controller_page_info": "Backup Information",
|
||||
"backup_controller_page_none_selected": "None selected",
|
||||
"backup_controller_page_remainder": "Remainder",
|
||||
"backup_controller_page_remainder_sub": "Remaining photos and videos to back up from selection",
|
||||
"backup_controller_page_select": "Select",
|
||||
"backup_controller_page_server_storage": "Server Storage",
|
||||
"backup_controller_page_start_backup": "Start Backup",
|
||||
"backup_controller_page_status_off": "Automatic foreground backup is off",
|
||||
"backup_controller_page_status_on": "Automatic foreground backup is on",
|
||||
"backup_controller_page_storage_format": "{} of {} used",
|
||||
"backup_controller_page_to_backup": "Albums to be backed up",
|
||||
"backup_controller_page_total": "Total",
|
||||
"backup_controller_page_total_sub": "All unique photos and videos from selected albums",
|
||||
"backup_controller_page_turn_off": "Turn off foreground backup",
|
||||
"backup_controller_page_turn_on": "Turn on foreground backup",
|
||||
"backup_controller_page_uploading_file_info": "Uploading file info",
|
||||
"backup_err_only_album": "Cannot remove the only album",
|
||||
"backup_info_card_assets": "assets",
|
||||
"backup_manual_cancelled": "Cancelled",
|
||||
"backup_manual_failed": "Failed",
|
||||
"backup_manual_in_progress": "Upload already in progress. Try after sometime",
|
||||
"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.",
|
||||
"cache_settings_duplicated_assets_clear_button": "CLEAR",
|
||||
"cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app",
|
||||
"cache_settings_duplicated_assets_title": "Duplicated Assets ({})",
|
||||
"cache_settings_image_cache_size": "Image cache size ({} assets)",
|
||||
"cache_settings_statistics_album": "Library thumbnails",
|
||||
"cache_settings_statistics_assets": "{} assets ({})",
|
||||
"cache_settings_statistics_full": "Full images",
|
||||
"cache_settings_statistics_shared": "Shared album thumbnails",
|
||||
"cache_settings_statistics_thumbnail": "Thumbnails",
|
||||
"cache_settings_statistics_title": "Cache usage",
|
||||
"cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application",
|
||||
"cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)",
|
||||
"cache_settings_tile_subtitle": "Control the local storage behaviour",
|
||||
"cache_settings_tile_title": "Local Storage",
|
||||
"cache_settings_title": "Caching Settings",
|
||||
"cancel": "Cancel",
|
||||
"canceled": "Canceled",
|
||||
"change_display_order": "Change display order",
|
||||
"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",
|
||||
"client_cert_import_success_msg": "Client certificate is imported",
|
||||
"client_cert_invalid_msg": "Invalid certificate file or wrong password",
|
||||
"client_cert_remove": "Remove",
|
||||
"client_cert_remove_msg": "Client certificate is removed",
|
||||
"client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login",
|
||||
"client_cert_title": "SSL Client Certificate (EXPERIMENTAL)",
|
||||
"common_add_to_album": "Add to album",
|
||||
"common_change_password": "Change Password",
|
||||
"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",
|
||||
"completed": "Completed",
|
||||
"contextual_search": "Sunrise on the beach",
|
||||
"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",
|
||||
"control_bottom_app_bar_archive": "Archive",
|
||||
"control_bottom_app_bar_create_new_album": "Create new album",
|
||||
"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_edit_location": "Edit Location",
|
||||
"control_bottom_app_bar_edit_time": "Edit Date & Time",
|
||||
"control_bottom_app_bar_favorite": "Favorite",
|
||||
"control_bottom_app_bar_share": "Share",
|
||||
"control_bottom_app_bar_share_to": "Share To",
|
||||
"control_bottom_app_bar_stack": "Stack",
|
||||
"control_bottom_app_bar_trash_from_immich": "Move to Trash",
|
||||
"control_bottom_app_bar_unarchive": "Unarchive",
|
||||
"control_bottom_app_bar_unfavorite": "Unfavorite",
|
||||
"control_bottom_app_bar_upload": "Upload",
|
||||
"create_album": "Create album",
|
||||
"create_album_page_untitled": "Untitled",
|
||||
"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",
|
||||
"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",
|
||||
"delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
|
||||
"delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server",
|
||||
"delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
|
||||
"delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
|
||||
"delete_dialog_cancel": "Cancel",
|
||||
"delete_dialog_ok": "Delete",
|
||||
"delete_dialog_ok_force": "Delete Anyway",
|
||||
"delete_dialog_title": "Delete Permanently",
|
||||
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
|
||||
"delete_local_dialog_ok_force": "Delete Anyway",
|
||||
"delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?",
|
||||
"delete_shared_link_dialog_title": "Delete Shared Link",
|
||||
"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",
|
||||
"edit_date_time_dialog_date_time": "Date and Time",
|
||||
"edit_date_time_dialog_timezone": "Timezone",
|
||||
"edit_image_title": "Edit",
|
||||
"edit_location_dialog_title": "Location",
|
||||
"end_date": "End date",
|
||||
"enqueued": "Enqueued",
|
||||
"enter_wifi_name": "Enter WiFi name",
|
||||
"error_change_sort_album": "Failed to change album sort order",
|
||||
"error_saving_image": "Error: {}",
|
||||
"exif_bottom_sheet_description": "Add Description...",
|
||||
"exif_bottom_sheet_details": "DETAILS",
|
||||
"exif_bottom_sheet_location": "LOCATION",
|
||||
"exif_bottom_sheet_location_add": "Add a location",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_person_add_person": "Add name",
|
||||
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
||||
"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",
|
||||
"failed": "Failed",
|
||||
"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",
|
||||
"header_settings_field_validator_msg": "Value cannot be empty",
|
||||
"header_settings_header_name_input": "Header name",
|
||||
"header_settings_header_value_input": "Header value",
|
||||
"header_settings_page_title": "Proxy Headers (EXPERIMENTAL)",
|
||||
"headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request",
|
||||
"headers_settings_tile_title": "Custom proxy headers",
|
||||
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
|
||||
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
|
||||
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
|
||||
"home_page_album_err_partner": "Can not add partner assets to an album yet, skipping",
|
||||
"home_page_archive_err_local": "Can not archive local assets yet, skipping",
|
||||
"home_page_archive_err_partner": "Can not archive partner assets, skipping",
|
||||
"home_page_building_timeline": "Building the timeline",
|
||||
"home_page_delete_err_partner": "Can not delete partner assets, skipping",
|
||||
"home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping",
|
||||
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
|
||||
"home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping",
|
||||
"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": "Can not share local assets via link, skipping",
|
||||
"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",
|
||||
"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",
|
||||
"library_page_albums": "Albums",
|
||||
"library_page_archive": "Archive",
|
||||
"library_page_device_albums": "Albums on Device",
|
||||
"library_page_favorites": "Favorites",
|
||||
"library_page_new_album": "New album",
|
||||
"library_page_sharing": "Sharing",
|
||||
"library_page_sort_asset_count": "Number of assets",
|
||||
"library_page_sort_created": "Created date",
|
||||
"library_page_sort_last_modified": "Last modified",
|
||||
"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",
|
||||
"location_picker_latitude_hint": "Enter your latitude here",
|
||||
"location_picker_longitude": "Longitude",
|
||||
"location_picker_longitude_error": "Enter a valid longitude",
|
||||
"location_picker_longitude_hint": "Enter your longitude here",
|
||||
"login_disabled": "Login has been disabled",
|
||||
"login_form_api_exception": "API exception. Please check the server URL and try again.",
|
||||
"login_form_back_button_text": "Back",
|
||||
"login_form_button_text": "Login",
|
||||
"login_form_email_hint": "youremail@email.com",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port",
|
||||
"login_form_endpoint_url": "Server Endpoint URL",
|
||||
"login_form_err_http": "Please specify http:// or https://",
|
||||
"login_form_err_invalid_email": "Invalid Email",
|
||||
"login_form_err_invalid_url": "Invalid URL",
|
||||
"login_form_err_leading_whitespace": "Leading whitespace",
|
||||
"login_form_err_trailing_whitespace": "Trailing whitespace",
|
||||
"login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL",
|
||||
"login_form_failed_get_oauth_server_disable": "OAuth feature is not available on this server",
|
||||
"login_form_failed_login": "Error logging you in, check server URL, email and password",
|
||||
"login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.",
|
||||
"login_form_label_email": "Email",
|
||||
"login_form_label_password": "Password",
|
||||
"login_form_next_button": "Next",
|
||||
"login_form_password_hint": "password",
|
||||
"login_form_save_login": "Stay logged in",
|
||||
"login_form_server_empty": "Enter a server URL.",
|
||||
"login_form_server_error": "Could not connect to server.",
|
||||
"login_password_changed_error": "There was an error updating your password",
|
||||
"login_password_changed_success": "Password updated successfully",
|
||||
"map_assets_in_bound": "{} photo",
|
||||
"map_assets_in_bounds": "{} photos",
|
||||
"map_cannot_get_user_location": "Cannot get user's location",
|
||||
"map_location_dialog_cancel": "Cancel",
|
||||
"map_location_dialog_yes": "Yes",
|
||||
"map_location_picker_page_use_location": "Use this location",
|
||||
"map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?",
|
||||
"map_location_service_disabled_title": "Location Service disabled",
|
||||
"map_no_assets_in_bounds": "No photos in this area",
|
||||
"map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
|
||||
"map_no_location_permission_title": "Location Permission denied",
|
||||
"map_settings_dark_mode": "Dark mode",
|
||||
"map_settings_date_range_option_all": "All",
|
||||
"map_settings_date_range_option_day": "Past 24 hours",
|
||||
"map_settings_date_range_option_days": "Past {} days",
|
||||
"map_settings_date_range_option_year": "Past year",
|
||||
"map_settings_date_range_option_years": "Past {} years",
|
||||
"map_settings_dialog_cancel": "Cancel",
|
||||
"map_settings_dialog_save": "Save",
|
||||
"map_settings_dialog_title": "Map Settings",
|
||||
"map_settings_include_show_archived": "Include Archived",
|
||||
"map_settings_include_show_partners": "Include Partners",
|
||||
"map_settings_only_relative_range": "Date range",
|
||||
"map_settings_only_show_favorites": "Show Favorite Only",
|
||||
"map_settings_theme_settings": "Map Theme",
|
||||
"map_zoom_to_see_photos": "Zoom out to see photos",
|
||||
"memories_all_caught_up": "All caught up",
|
||||
"memories_check_back_tomorrow": "Check back tomorrow for more memories",
|
||||
"memories_start_over": "Start Over",
|
||||
"memories_swipe_to_close": "Swipe up to close",
|
||||
"memories_year_ago": "A year ago",
|
||||
"memories_years_ago": "{} years ago",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"not_selected": "Not selected",
|
||||
"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",
|
||||
"partner_page_empty_message": "Your photos are not yet shared with any partner.",
|
||||
"partner_page_no_more_users": "No more users to add",
|
||||
"partner_page_partner_add_failed": "Failed to add partner",
|
||||
"partner_page_select_partner": "Select partner",
|
||||
"partner_page_shared_to_title": "Shared to",
|
||||
"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",
|
||||
"paused": "Paused",
|
||||
"people": "People",
|
||||
"permission_onboarding_back": "Back",
|
||||
"permission_onboarding_continue_anyway": "Continue anyway",
|
||||
"permission_onboarding_get_started": "Get started",
|
||||
"permission_onboarding_go_to_settings": "Go to settings",
|
||||
"permission_onboarding_grant_permission": "Grant permission",
|
||||
"permission_onboarding_log_out": "Log out",
|
||||
"permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.",
|
||||
"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",
|
||||
"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.",
|
||||
"profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.",
|
||||
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
|
||||
"profile_drawer_documentation": "Documentation",
|
||||
"profile_drawer_github": "GitHub",
|
||||
"profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.",
|
||||
"profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.",
|
||||
"profile_drawer_settings": "Settings",
|
||||
"profile_drawer_sign_out": "Sign Out",
|
||||
"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",
|
||||
"search_bar_hint": "Search your photos",
|
||||
"search_filter_apply": "Apply filter",
|
||||
"search_filter_camera": "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_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_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_media_type_all": "All",
|
||||
"search_filter_media_type_image": "Image",
|
||||
"search_filter_media_type_title": "Select media type",
|
||||
"search_filter_media_type_video": "Video",
|
||||
"search_filter_people": "People",
|
||||
"search_filter_people_title": "Select people",
|
||||
"search_page_categories": "Categories",
|
||||
"search_page_favorites": "Favorites",
|
||||
"search_page_motion_photos": "Motion Photos",
|
||||
"search_page_no_objects": "No Objects Info Available",
|
||||
"search_page_no_places": "No Places Info Available",
|
||||
"search_page_people": "People",
|
||||
"search_page_person_add_name_dialog_cancel": "Cancel",
|
||||
"search_page_person_add_name_dialog_hint": "Name",
|
||||
"search_page_person_add_name_dialog_save": "Save",
|
||||
"search_page_person_add_name_dialog_title": "Add a name",
|
||||
"search_page_person_add_name_subtitle": "Find them fast by name with search",
|
||||
"search_page_person_add_name_title": "Add a name",
|
||||
"search_page_person_edit_name": "Edit name",
|
||||
"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",
|
||||
"search_page_view_all_button": "View all",
|
||||
"search_page_your_activity": "Your activity",
|
||||
"search_page_your_map": "Your Map",
|
||||
"search_result_page_new_search_hint": "New Search",
|
||||
"search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ",
|
||||
"search_suggestion_list_smart_search_hint_2": "m:your-search-term",
|
||||
"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",
|
||||
"server_info_box_server_version": "Server Version",
|
||||
"setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).",
|
||||
"setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).",
|
||||
"setting_image_viewer_original_title": "Load original image",
|
||||
"setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.",
|
||||
"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",
|
||||
"setting_notifications_notify_immediately": "immediately",
|
||||
"setting_notifications_notify_minutes": "{} minutes",
|
||||
"setting_notifications_notify_never": "never",
|
||||
"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",
|
||||
"setting_pages_app_bar_settings": "Settings",
|
||||
"settings_require_restart": "Please restart Immich to apply this setting",
|
||||
"setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
|
||||
"setting_video_viewer_looping_title": "Looping",
|
||||
"setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.",
|
||||
"setting_video_viewer_original_video_title": "Force original video",
|
||||
"setting_video_viewer_title": "Videos",
|
||||
"share_add": "Add",
|
||||
"share_add_photos": "Add photos",
|
||||
"share_add_title": "Add a title",
|
||||
"share_assets_selected": "{} selected",
|
||||
"share_create_album": "Create album",
|
||||
"shared_album_activities_input_disable": "Comment is disabled",
|
||||
"shared_album_activities_input_hint": "Say something",
|
||||
"shared_album_activity_remove_content": "Do you want to delete this activity?",
|
||||
"shared_album_activity_remove_title": "Delete Activity",
|
||||
"shared_album_activity_setting_subtitle": "Let others respond",
|
||||
"shared_album_activity_setting_title": "Comments & likes",
|
||||
"shared_album_section_people_action_error": "Error leaving/removing from album",
|
||||
"shared_album_section_people_action_leave": "Remove user from album",
|
||||
"shared_album_section_people_action_remove_user": "Remove user from album",
|
||||
"shared_album_section_people_owner_label": "Owner",
|
||||
"shared_album_section_people_title": "PEOPLE",
|
||||
"share_dialog_preparing": "Preparing...",
|
||||
"shared_intent_upload_button_progress_text": "{} / {} Uploaded",
|
||||
"shared_link_app_bar_title": "Shared Links",
|
||||
"shared_link_clipboard_copied_massage": "Copied to clipboard",
|
||||
"shared_link_clipboard_text": "Link: {}\nPassword: {}",
|
||||
"shared_link_create_app_bar_title": "Create link to share",
|
||||
"shared_link_create_error": "Error while creating shared link",
|
||||
"shared_link_create_info": "Let anyone with the link see the selected photo(s)",
|
||||
"shared_link_create_submit_button": "Create link",
|
||||
"shared_link_edit_allow_download": "Allow public user to download",
|
||||
"shared_link_edit_allow_upload": "Allow public user to upload",
|
||||
"shared_link_edit_app_bar_title": "Edit link",
|
||||
"shared_link_edit_change_expiry": "Change expiration time",
|
||||
"shared_link_edit_description": "Description",
|
||||
"shared_link_edit_description_hint": "Enter the share description",
|
||||
"shared_link_edit_expire_after": "Expire after",
|
||||
"shared_link_edit_expire_after_option_day": "1 day",
|
||||
"shared_link_edit_expire_after_option_days": "{} days",
|
||||
"shared_link_edit_expire_after_option_hour": "1 hour",
|
||||
"shared_link_edit_expire_after_option_hours": "{} hours",
|
||||
"shared_link_edit_expire_after_option_minute": "1 minute",
|
||||
"shared_link_edit_expire_after_option_minutes": "{} minutes",
|
||||
"shared_link_edit_expire_after_option_months": "{} months",
|
||||
"shared_link_edit_expire_after_option_never": "Never",
|
||||
"shared_link_edit_expire_after_option_year": "{} year",
|
||||
"shared_link_edit_password": "Password",
|
||||
"shared_link_edit_password_hint": "Enter the share password",
|
||||
"shared_link_edit_show_meta": "Show metadata",
|
||||
"shared_link_edit_submit_button": "Update link",
|
||||
"shared_link_empty": "You don't have any shared links",
|
||||
"shared_link_error_server_url_fetch": "Cannot fetch the server url",
|
||||
"shared_link_expired": "Expired",
|
||||
"shared_link_expires_day": "Expires in {} day",
|
||||
"shared_link_expires_days": "Expires in {} days",
|
||||
"shared_link_expires_hour": "Expires in {} hour",
|
||||
"shared_link_expires_hours": "Expires in {} hours",
|
||||
"shared_link_expires_minute": "Expires in {} minute",
|
||||
"shared_link_expires_minutes": "Expires in {} minutes",
|
||||
"shared_link_expires_never": "Expires ∞",
|
||||
"shared_link_expires_second": "Expires in {} second",
|
||||
"shared_link_expires_seconds": "Expires in {} seconds",
|
||||
"shared_link_individual_shared": "Individual shared",
|
||||
"shared_link_info_chip_download": "Download",
|
||||
"shared_link_info_chip_metadata": "EXIF",
|
||||
"shared_link_info_chip_upload": "Upload",
|
||||
"shared_link_manage_links": "Manage Shared links",
|
||||
"shared_link_public_album": "Public album",
|
||||
"shared_links": "Shared links",
|
||||
"share_done": "Done",
|
||||
"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.",
|
||||
"sharing_page_empty_list": "EMPTY LIST",
|
||||
"sharing_silver_appbar_create_shared_album": "New shared album",
|
||||
"sharing_silver_appbar_shared_links": "Shared links",
|
||||
"sharing_silver_appbar_share_partner": "Share with partner",
|
||||
"start_date": "Start date",
|
||||
"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",
|
||||
"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_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_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_page_delete": "Delete",
|
||||
"trash_page_delete_all": "Delete All",
|
||||
"trash_page_empty_trash_btn": "Empty trash",
|
||||
"trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich",
|
||||
"trash_page_empty_trash_dialog_ok": "Ok",
|
||||
"trash_page_info": "Trashed items will be permanently deleted after {} days",
|
||||
"trash_page_no_assets": "No trashed assets",
|
||||
"trash_page_restore": "Restore",
|
||||
"trash_page_restore_all": "Restore All",
|
||||
"trash_page_select_assets_btn": "Select assets",
|
||||
"trash_page_select_btn": "Select",
|
||||
"trash_page_title": "Trash ({})",
|
||||
"upload": "Upload",
|
||||
"upload_dialog_cancel": "Cancel",
|
||||
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
|
||||
"upload_dialog_ok": "Upload",
|
||||
"upload_dialog_title": "Upload Asset",
|
||||
"uploading": "Uploading",
|
||||
"upload_to_immich": "Upload to Immich ({})",
|
||||
"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",
|
||||
"viewer_remove_from_stack": "Remove from Stack",
|
||||
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
||||
"viewer_unstack": "Un-Stack",
|
||||
"wifi_name": "WiFi Name",
|
||||
"your_wifi_name": "Your WiFi name"
|
||||
}
|
||||
673
mobile/assets/i18n/gl.json
Normal file
673
mobile/assets/i18n/gl.json
Normal file
@@ -0,0 +1,673 @@
|
||||
{
|
||||
"action_common_back": "Back",
|
||||
"action_common_cancel": "Cancel",
|
||||
"action_common_clear": "Clear",
|
||||
"action_common_confirm": "Confirm",
|
||||
"action_common_save": "Save",
|
||||
"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: {}",
|
||||
"advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.",
|
||||
"advanced_settings_prefer_remote_title": "Prefer remote images",
|
||||
"advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
|
||||
"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 (EXPERIMENTAL)",
|
||||
"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",
|
||||
"album_info_card_backup_album_excluded": "EXCLUDED",
|
||||
"album_info_card_backup_album_included": "INCLUDED",
|
||||
"albums": "Albums",
|
||||
"album_thumbnail_card_item": "1 item",
|
||||
"album_thumbnail_card_items": "{} items",
|
||||
"album_thumbnail_card_shared": " · Shared",
|
||||
"album_thumbnail_owned": "Owned",
|
||||
"album_thumbnail_shared_by": "Shared by {}",
|
||||
"album_viewer_appbar_delete_confirm": "Are you sure you want to delete this album from your account?",
|
||||
"album_viewer_appbar_share_delete": "Delete album",
|
||||
"album_viewer_appbar_share_err_delete": "Failed to delete album",
|
||||
"album_viewer_appbar_share_err_leave": "Failed to leave album",
|
||||
"album_viewer_appbar_share_err_remove": "There are problems in removing assets from album",
|
||||
"album_viewer_appbar_share_err_title": "Failed to change album title",
|
||||
"album_viewer_appbar_share_leave": "Leave album",
|
||||
"album_viewer_appbar_share_remove": "Remove from album",
|
||||
"album_viewer_appbar_share_to": "Share To",
|
||||
"album_viewer_page_share_add_users": "Add users",
|
||||
"all": "All",
|
||||
"all_people_page_title": "People",
|
||||
"all_videos_page_title": "Videos",
|
||||
"app_bar_signout_dialog_content": "Are you sure you want to sign out?",
|
||||
"app_bar_signout_dialog_ok": "Yes",
|
||||
"app_bar_signout_dialog_title": "Sign out",
|
||||
"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",
|
||||
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
|
||||
"asset_list_group_by_sub_title": "Group by",
|
||||
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
|
||||
"asset_list_layout_settings_group_automatically": "Automatic",
|
||||
"asset_list_layout_settings_group_by": "Group assets by",
|
||||
"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",
|
||||
"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_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.",
|
||||
"backup_album_selection_page_select_albums": "Select albums",
|
||||
"backup_album_selection_page_selection_info": "Selection Info",
|
||||
"backup_album_selection_page_total_assets": "Total unique assets",
|
||||
"backup_all": "All",
|
||||
"backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…",
|
||||
"backup_background_service_connection_failed_message": "Failed to connect to the server. Retrying…",
|
||||
"backup_background_service_current_upload_notification": "Uploading {}",
|
||||
"backup_background_service_default_notification": "Checking for new assets…",
|
||||
"backup_background_service_error_title": "Backup error",
|
||||
"backup_background_service_in_progress_notification": "Backing up your assets…",
|
||||
"backup_background_service_upload_failure_notification": "Failed to upload {}",
|
||||
"backup_controller_page_albums": "Backup Albums",
|
||||
"backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.",
|
||||
"backup_controller_page_background_app_refresh_disabled_title": "Background app refresh disabled",
|
||||
"backup_controller_page_background_app_refresh_enable_button_text": "Go to settings",
|
||||
"backup_controller_page_background_battery_info_link": "Show me how",
|
||||
"backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.",
|
||||
"backup_controller_page_background_battery_info_ok": "OK",
|
||||
"backup_controller_page_background_battery_info_title": "Battery optimizations",
|
||||
"backup_controller_page_background_charging": "Only while charging",
|
||||
"backup_controller_page_background_configure_error": "Failed to configure the background service",
|
||||
"backup_controller_page_background_delay": "Delay new assets backup: {}",
|
||||
"backup_controller_page_background_description": "Turn on the background service to automatically backup any new assets without needing to open the app",
|
||||
"backup_controller_page_background_is_off": "Automatic background backup is off",
|
||||
"backup_controller_page_background_is_on": "Automatic background backup is on",
|
||||
"backup_controller_page_background_turn_off": "Turn off background service",
|
||||
"backup_controller_page_background_turn_on": "Turn on background service",
|
||||
"backup_controller_page_background_wifi": "Only on WiFi",
|
||||
"backup_controller_page_backup": "Backup",
|
||||
"backup_controller_page_backup_selected": "Selected: ",
|
||||
"backup_controller_page_backup_sub": "Backed up photos and videos",
|
||||
"backup_controller_page_cancel": "Cancel",
|
||||
"backup_controller_page_created": "Created on: {}",
|
||||
"backup_controller_page_desc_backup": "Turn on foreground backup to automatically upload new assets to the server when opening the app.",
|
||||
"backup_controller_page_excluded": "Excluded: ",
|
||||
"backup_controller_page_failed": "Failed ({})",
|
||||
"backup_controller_page_filename": "File name: {} [{}]",
|
||||
"backup_controller_page_id": "ID: {}",
|
||||
"backup_controller_page_info": "Backup Information",
|
||||
"backup_controller_page_none_selected": "None selected",
|
||||
"backup_controller_page_remainder": "Remainder",
|
||||
"backup_controller_page_remainder_sub": "Remaining photos and videos to back up from selection",
|
||||
"backup_controller_page_select": "Select",
|
||||
"backup_controller_page_server_storage": "Server Storage",
|
||||
"backup_controller_page_start_backup": "Start Backup",
|
||||
"backup_controller_page_status_off": "Automatic foreground backup is off",
|
||||
"backup_controller_page_status_on": "Automatic foreground backup is on",
|
||||
"backup_controller_page_storage_format": "{} of {} used",
|
||||
"backup_controller_page_to_backup": "Albums to be backed up",
|
||||
"backup_controller_page_total": "Total",
|
||||
"backup_controller_page_total_sub": "All unique photos and videos from selected albums",
|
||||
"backup_controller_page_turn_off": "Turn off foreground backup",
|
||||
"backup_controller_page_turn_on": "Turn on foreground backup",
|
||||
"backup_controller_page_uploading_file_info": "Uploading file info",
|
||||
"backup_err_only_album": "Cannot remove the only album",
|
||||
"backup_info_card_assets": "assets",
|
||||
"backup_manual_cancelled": "Cancelled",
|
||||
"backup_manual_failed": "Failed",
|
||||
"backup_manual_in_progress": "Upload already in progress. Try after sometime",
|
||||
"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.",
|
||||
"cache_settings_duplicated_assets_clear_button": "CLEAR",
|
||||
"cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app",
|
||||
"cache_settings_duplicated_assets_title": "Duplicated Assets ({})",
|
||||
"cache_settings_image_cache_size": "Image cache size ({} assets)",
|
||||
"cache_settings_statistics_album": "Library thumbnails",
|
||||
"cache_settings_statistics_assets": "{} assets ({})",
|
||||
"cache_settings_statistics_full": "Full images",
|
||||
"cache_settings_statistics_shared": "Shared album thumbnails",
|
||||
"cache_settings_statistics_thumbnail": "Thumbnails",
|
||||
"cache_settings_statistics_title": "Cache usage",
|
||||
"cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application",
|
||||
"cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)",
|
||||
"cache_settings_tile_subtitle": "Control the local storage behaviour",
|
||||
"cache_settings_tile_title": "Local Storage",
|
||||
"cache_settings_title": "Caching Settings",
|
||||
"cancel": "Cancel",
|
||||
"canceled": "Canceled",
|
||||
"change_display_order": "Change display order",
|
||||
"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",
|
||||
"client_cert_import_success_msg": "Client certificate is imported",
|
||||
"client_cert_invalid_msg": "Invalid certificate file or wrong password",
|
||||
"client_cert_remove": "Remove",
|
||||
"client_cert_remove_msg": "Client certificate is removed",
|
||||
"client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login",
|
||||
"client_cert_title": "SSL Client Certificate (EXPERIMENTAL)",
|
||||
"common_add_to_album": "Add to album",
|
||||
"common_change_password": "Change Password",
|
||||
"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",
|
||||
"completed": "Completed",
|
||||
"contextual_search": "Sunrise on the beach",
|
||||
"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",
|
||||
"control_bottom_app_bar_archive": "Archive",
|
||||
"control_bottom_app_bar_create_new_album": "Create new album",
|
||||
"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_edit_location": "Edit Location",
|
||||
"control_bottom_app_bar_edit_time": "Edit Date & Time",
|
||||
"control_bottom_app_bar_favorite": "Favorite",
|
||||
"control_bottom_app_bar_share": "Share",
|
||||
"control_bottom_app_bar_share_to": "Share To",
|
||||
"control_bottom_app_bar_stack": "Stack",
|
||||
"control_bottom_app_bar_trash_from_immich": "Move to Trash",
|
||||
"control_bottom_app_bar_unarchive": "Unarchive",
|
||||
"control_bottom_app_bar_unfavorite": "Unfavorite",
|
||||
"control_bottom_app_bar_upload": "Upload",
|
||||
"create_album": "Create album",
|
||||
"create_album_page_untitled": "Untitled",
|
||||
"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",
|
||||
"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",
|
||||
"delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
|
||||
"delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server",
|
||||
"delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
|
||||
"delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
|
||||
"delete_dialog_cancel": "Cancel",
|
||||
"delete_dialog_ok": "Delete",
|
||||
"delete_dialog_ok_force": "Delete Anyway",
|
||||
"delete_dialog_title": "Delete Permanently",
|
||||
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
|
||||
"delete_local_dialog_ok_force": "Delete Anyway",
|
||||
"delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?",
|
||||
"delete_shared_link_dialog_title": "Delete Shared Link",
|
||||
"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",
|
||||
"edit_date_time_dialog_date_time": "Date and Time",
|
||||
"edit_date_time_dialog_timezone": "Timezone",
|
||||
"edit_image_title": "Edit",
|
||||
"edit_location_dialog_title": "Location",
|
||||
"end_date": "End date",
|
||||
"enqueued": "Enqueued",
|
||||
"enter_wifi_name": "Enter WiFi name",
|
||||
"error_change_sort_album": "Failed to change album sort order",
|
||||
"error_saving_image": "Error: {}",
|
||||
"exif_bottom_sheet_description": "Add Description...",
|
||||
"exif_bottom_sheet_details": "DETAILS",
|
||||
"exif_bottom_sheet_location": "LOCATION",
|
||||
"exif_bottom_sheet_location_add": "Add a location",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_person_add_person": "Add name",
|
||||
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
||||
"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",
|
||||
"failed": "Failed",
|
||||
"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",
|
||||
"header_settings_field_validator_msg": "Value cannot be empty",
|
||||
"header_settings_header_name_input": "Header name",
|
||||
"header_settings_header_value_input": "Header value",
|
||||
"header_settings_page_title": "Proxy Headers (EXPERIMENTAL)",
|
||||
"headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request",
|
||||
"headers_settings_tile_title": "Custom proxy headers",
|
||||
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
|
||||
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
|
||||
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
|
||||
"home_page_album_err_partner": "Can not add partner assets to an album yet, skipping",
|
||||
"home_page_archive_err_local": "Can not archive local assets yet, skipping",
|
||||
"home_page_archive_err_partner": "Can not archive partner assets, skipping",
|
||||
"home_page_building_timeline": "Building the timeline",
|
||||
"home_page_delete_err_partner": "Can not delete partner assets, skipping",
|
||||
"home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping",
|
||||
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
|
||||
"home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping",
|
||||
"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": "Can not share local assets via link, skipping",
|
||||
"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",
|
||||
"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",
|
||||
"library_page_albums": "Albums",
|
||||
"library_page_archive": "Archive",
|
||||
"library_page_device_albums": "Albums on Device",
|
||||
"library_page_favorites": "Favorites",
|
||||
"library_page_new_album": "New album",
|
||||
"library_page_sharing": "Sharing",
|
||||
"library_page_sort_asset_count": "Number of assets",
|
||||
"library_page_sort_created": "Created date",
|
||||
"library_page_sort_last_modified": "Last modified",
|
||||
"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",
|
||||
"location_picker_latitude_hint": "Enter your latitude here",
|
||||
"location_picker_longitude": "Longitude",
|
||||
"location_picker_longitude_error": "Enter a valid longitude",
|
||||
"location_picker_longitude_hint": "Enter your longitude here",
|
||||
"login_disabled": "Login has been disabled",
|
||||
"login_form_api_exception": "API exception. Please check the server URL and try again.",
|
||||
"login_form_back_button_text": "Back",
|
||||
"login_form_button_text": "Login",
|
||||
"login_form_email_hint": "youremail@email.com",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port",
|
||||
"login_form_endpoint_url": "Server Endpoint URL",
|
||||
"login_form_err_http": "Please specify http:// or https://",
|
||||
"login_form_err_invalid_email": "Invalid Email",
|
||||
"login_form_err_invalid_url": "Invalid URL",
|
||||
"login_form_err_leading_whitespace": "Leading whitespace",
|
||||
"login_form_err_trailing_whitespace": "Trailing whitespace",
|
||||
"login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL",
|
||||
"login_form_failed_get_oauth_server_disable": "OAuth feature is not available on this server",
|
||||
"login_form_failed_login": "Error logging you in, check server URL, email and password",
|
||||
"login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.",
|
||||
"login_form_label_email": "Email",
|
||||
"login_form_label_password": "Password",
|
||||
"login_form_next_button": "Next",
|
||||
"login_form_password_hint": "password",
|
||||
"login_form_save_login": "Stay logged in",
|
||||
"login_form_server_empty": "Enter a server URL.",
|
||||
"login_form_server_error": "Could not connect to server.",
|
||||
"login_password_changed_error": "There was an error updating your password",
|
||||
"login_password_changed_success": "Password updated successfully",
|
||||
"map_assets_in_bound": "{} photo",
|
||||
"map_assets_in_bounds": "{} photos",
|
||||
"map_cannot_get_user_location": "Cannot get user's location",
|
||||
"map_location_dialog_cancel": "Cancel",
|
||||
"map_location_dialog_yes": "Yes",
|
||||
"map_location_picker_page_use_location": "Use this location",
|
||||
"map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?",
|
||||
"map_location_service_disabled_title": "Location Service disabled",
|
||||
"map_no_assets_in_bounds": "No photos in this area",
|
||||
"map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
|
||||
"map_no_location_permission_title": "Location Permission denied",
|
||||
"map_settings_dark_mode": "Dark mode",
|
||||
"map_settings_date_range_option_all": "All",
|
||||
"map_settings_date_range_option_day": "Past 24 hours",
|
||||
"map_settings_date_range_option_days": "Past {} days",
|
||||
"map_settings_date_range_option_year": "Past year",
|
||||
"map_settings_date_range_option_years": "Past {} years",
|
||||
"map_settings_dialog_cancel": "Cancel",
|
||||
"map_settings_dialog_save": "Save",
|
||||
"map_settings_dialog_title": "Map Settings",
|
||||
"map_settings_include_show_archived": "Include Archived",
|
||||
"map_settings_include_show_partners": "Include Partners",
|
||||
"map_settings_only_relative_range": "Date range",
|
||||
"map_settings_only_show_favorites": "Show Favorite Only",
|
||||
"map_settings_theme_settings": "Map Theme",
|
||||
"map_zoom_to_see_photos": "Zoom out to see photos",
|
||||
"memories_all_caught_up": "All caught up",
|
||||
"memories_check_back_tomorrow": "Check back tomorrow for more memories",
|
||||
"memories_start_over": "Start Over",
|
||||
"memories_swipe_to_close": "Swipe up to close",
|
||||
"memories_year_ago": "A year ago",
|
||||
"memories_years_ago": "{} years ago",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"not_selected": "Not selected",
|
||||
"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",
|
||||
"partner_page_empty_message": "Your photos are not yet shared with any partner.",
|
||||
"partner_page_no_more_users": "No more users to add",
|
||||
"partner_page_partner_add_failed": "Failed to add partner",
|
||||
"partner_page_select_partner": "Select partner",
|
||||
"partner_page_shared_to_title": "Shared to",
|
||||
"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",
|
||||
"paused": "Paused",
|
||||
"people": "People",
|
||||
"permission_onboarding_back": "Back",
|
||||
"permission_onboarding_continue_anyway": "Continue anyway",
|
||||
"permission_onboarding_get_started": "Get started",
|
||||
"permission_onboarding_go_to_settings": "Go to settings",
|
||||
"permission_onboarding_grant_permission": "Grant permission",
|
||||
"permission_onboarding_log_out": "Log out",
|
||||
"permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.",
|
||||
"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",
|
||||
"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.",
|
||||
"profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.",
|
||||
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
|
||||
"profile_drawer_documentation": "Documentation",
|
||||
"profile_drawer_github": "GitHub",
|
||||
"profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.",
|
||||
"profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.",
|
||||
"profile_drawer_settings": "Settings",
|
||||
"profile_drawer_sign_out": "Sign Out",
|
||||
"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",
|
||||
"search_bar_hint": "Search your photos",
|
||||
"search_filter_apply": "Apply filter",
|
||||
"search_filter_camera": "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_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_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_media_type_all": "All",
|
||||
"search_filter_media_type_image": "Image",
|
||||
"search_filter_media_type_title": "Select media type",
|
||||
"search_filter_media_type_video": "Video",
|
||||
"search_filter_people": "People",
|
||||
"search_filter_people_title": "Select people",
|
||||
"search_page_categories": "Categories",
|
||||
"search_page_favorites": "Favorites",
|
||||
"search_page_motion_photos": "Motion Photos",
|
||||
"search_page_no_objects": "No Objects Info Available",
|
||||
"search_page_no_places": "No Places Info Available",
|
||||
"search_page_people": "People",
|
||||
"search_page_person_add_name_dialog_cancel": "Cancel",
|
||||
"search_page_person_add_name_dialog_hint": "Name",
|
||||
"search_page_person_add_name_dialog_save": "Save",
|
||||
"search_page_person_add_name_dialog_title": "Add a name",
|
||||
"search_page_person_add_name_subtitle": "Find them fast by name with search",
|
||||
"search_page_person_add_name_title": "Add a name",
|
||||
"search_page_person_edit_name": "Edit name",
|
||||
"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",
|
||||
"search_page_view_all_button": "View all",
|
||||
"search_page_your_activity": "Your activity",
|
||||
"search_page_your_map": "Your Map",
|
||||
"search_result_page_new_search_hint": "New Search",
|
||||
"search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ",
|
||||
"search_suggestion_list_smart_search_hint_2": "m:your-search-term",
|
||||
"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",
|
||||
"server_info_box_server_version": "Server Version",
|
||||
"setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).",
|
||||
"setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).",
|
||||
"setting_image_viewer_original_title": "Load original image",
|
||||
"setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.",
|
||||
"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",
|
||||
"setting_notifications_notify_immediately": "immediately",
|
||||
"setting_notifications_notify_minutes": "{} minutes",
|
||||
"setting_notifications_notify_never": "never",
|
||||
"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",
|
||||
"setting_pages_app_bar_settings": "Settings",
|
||||
"settings_require_restart": "Please restart Immich to apply this setting",
|
||||
"setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
|
||||
"setting_video_viewer_looping_title": "Looping",
|
||||
"setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.",
|
||||
"setting_video_viewer_original_video_title": "Force original video",
|
||||
"setting_video_viewer_title": "Videos",
|
||||
"share_add": "Add",
|
||||
"share_add_photos": "Add photos",
|
||||
"share_add_title": "Add a title",
|
||||
"share_assets_selected": "{} selected",
|
||||
"share_create_album": "Create album",
|
||||
"shared_album_activities_input_disable": "Comment is disabled",
|
||||
"shared_album_activities_input_hint": "Say something",
|
||||
"shared_album_activity_remove_content": "Do you want to delete this activity?",
|
||||
"shared_album_activity_remove_title": "Delete Activity",
|
||||
"shared_album_activity_setting_subtitle": "Let others respond",
|
||||
"shared_album_activity_setting_title": "Comments & likes",
|
||||
"shared_album_section_people_action_error": "Error leaving/removing from album",
|
||||
"shared_album_section_people_action_leave": "Remove user from album",
|
||||
"shared_album_section_people_action_remove_user": "Remove user from album",
|
||||
"shared_album_section_people_owner_label": "Owner",
|
||||
"shared_album_section_people_title": "PEOPLE",
|
||||
"share_dialog_preparing": "Preparing...",
|
||||
"shared_intent_upload_button_progress_text": "{} / {} Uploaded",
|
||||
"shared_link_app_bar_title": "Shared Links",
|
||||
"shared_link_clipboard_copied_massage": "Copied to clipboard",
|
||||
"shared_link_clipboard_text": "Link: {}\nPassword: {}",
|
||||
"shared_link_create_app_bar_title": "Create link to share",
|
||||
"shared_link_create_error": "Error while creating shared link",
|
||||
"shared_link_create_info": "Let anyone with the link see the selected photo(s)",
|
||||
"shared_link_create_submit_button": "Create link",
|
||||
"shared_link_edit_allow_download": "Allow public user to download",
|
||||
"shared_link_edit_allow_upload": "Allow public user to upload",
|
||||
"shared_link_edit_app_bar_title": "Edit link",
|
||||
"shared_link_edit_change_expiry": "Change expiration time",
|
||||
"shared_link_edit_description": "Description",
|
||||
"shared_link_edit_description_hint": "Enter the share description",
|
||||
"shared_link_edit_expire_after": "Expire after",
|
||||
"shared_link_edit_expire_after_option_day": "1 day",
|
||||
"shared_link_edit_expire_after_option_days": "{} days",
|
||||
"shared_link_edit_expire_after_option_hour": "1 hour",
|
||||
"shared_link_edit_expire_after_option_hours": "{} hours",
|
||||
"shared_link_edit_expire_after_option_minute": "1 minute",
|
||||
"shared_link_edit_expire_after_option_minutes": "{} minutes",
|
||||
"shared_link_edit_expire_after_option_months": "{} months",
|
||||
"shared_link_edit_expire_after_option_never": "Never",
|
||||
"shared_link_edit_expire_after_option_year": "{} year",
|
||||
"shared_link_edit_password": "Password",
|
||||
"shared_link_edit_password_hint": "Enter the share password",
|
||||
"shared_link_edit_show_meta": "Show metadata",
|
||||
"shared_link_edit_submit_button": "Update link",
|
||||
"shared_link_empty": "You don't have any shared links",
|
||||
"shared_link_error_server_url_fetch": "Cannot fetch the server url",
|
||||
"shared_link_expired": "Expired",
|
||||
"shared_link_expires_day": "Expires in {} day",
|
||||
"shared_link_expires_days": "Expires in {} days",
|
||||
"shared_link_expires_hour": "Expires in {} hour",
|
||||
"shared_link_expires_hours": "Expires in {} hours",
|
||||
"shared_link_expires_minute": "Expires in {} minute",
|
||||
"shared_link_expires_minutes": "Expires in {} minutes",
|
||||
"shared_link_expires_never": "Expires ∞",
|
||||
"shared_link_expires_second": "Expires in {} second",
|
||||
"shared_link_expires_seconds": "Expires in {} seconds",
|
||||
"shared_link_individual_shared": "Individual shared",
|
||||
"shared_link_info_chip_download": "Download",
|
||||
"shared_link_info_chip_metadata": "EXIF",
|
||||
"shared_link_info_chip_upload": "Upload",
|
||||
"shared_link_manage_links": "Manage Shared links",
|
||||
"shared_link_public_album": "Public album",
|
||||
"shared_links": "Shared links",
|
||||
"share_done": "Done",
|
||||
"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.",
|
||||
"sharing_page_empty_list": "EMPTY LIST",
|
||||
"sharing_silver_appbar_create_shared_album": "New shared album",
|
||||
"sharing_silver_appbar_shared_links": "Shared links",
|
||||
"sharing_silver_appbar_share_partner": "Share with partner",
|
||||
"start_date": "Start date",
|
||||
"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",
|
||||
"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_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_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_page_delete": "Delete",
|
||||
"trash_page_delete_all": "Delete All",
|
||||
"trash_page_empty_trash_btn": "Empty trash",
|
||||
"trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich",
|
||||
"trash_page_empty_trash_dialog_ok": "Ok",
|
||||
"trash_page_info": "Trashed items will be permanently deleted after {} days",
|
||||
"trash_page_no_assets": "No trashed assets",
|
||||
"trash_page_restore": "Restore",
|
||||
"trash_page_restore_all": "Restore All",
|
||||
"trash_page_select_assets_btn": "Select assets",
|
||||
"trash_page_select_btn": "Select",
|
||||
"trash_page_title": "Trash ({})",
|
||||
"upload": "Upload",
|
||||
"upload_dialog_cancel": "Cancel",
|
||||
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
|
||||
"upload_dialog_ok": "Upload",
|
||||
"upload_dialog_title": "Upload Asset",
|
||||
"uploading": "Uploading",
|
||||
"upload_to_immich": "Upload to Immich ({})",
|
||||
"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",
|
||||
"viewer_remove_from_stack": "Remove from Stack",
|
||||
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
||||
"viewer_unstack": "Un-Stack",
|
||||
"wifi_name": "WiFi Name",
|
||||
"your_wifi_name": "Your WiFi name"
|
||||
}
|
||||
@@ -7,10 +7,10 @@
|
||||
"action_common_select": "Вибрати",
|
||||
"action_common_update": "Оновити",
|
||||
"add_a_name": "Додати ім'я",
|
||||
"add_endpoint": "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: {}",
|
||||
"advanced_settings_log_level_title": "Рівень журналу: {}",
|
||||
"advanced_settings_prefer_remote_subtitle": "Деякі пристрої вельми повільно завантажують мініатюри із елементів на пристрої. Активуйте для завантаження віддалених мініатюр натомість.",
|
||||
"advanced_settings_prefer_remote_title": "Перевага віддаленим зображенням",
|
||||
"advanced_settings_proxy_headers_subtitle": "Визначте заголовки проксі-сервера, які Immich має надсилати з кожним мережевим запитом.",
|
||||
@@ -66,12 +66,12 @@
|
||||
"assets_restored_successfully": "{} елемент(и) успішно відновлено",
|
||||
"assets_trashed": "{} елемент(и) поміщено до кошика",
|
||||
"assets_trashed_from_server": "{} елемент(и) поміщено до кошика на сервері Immich",
|
||||
"asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
|
||||
"asset_viewer_settings_subtitle": "Керувати налаштуваннями переглядача галереї",
|
||||
"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",
|
||||
"automatic_endpoint_switching_subtitle": "Підключатися локально через визначену Wi-Fi мережу, коли вона доступна, і використовувати альтернативні з'єднання в інших випадках",
|
||||
"automatic_endpoint_switching_title": "Автоперемикання URL-адрес",
|
||||
"background_location_permission": "Дозвіл на місцезнаходження у фоні",
|
||||
"background_location_permission_content": "Щоб перемикатися між мережами під час роботи у фоновому режимі, Immich *завжди* повинен мати доступ до точного місцезнаходження, щоб додаток міг зчитувати назву Wi-Fi мережі",
|
||||
"backup_album_selection_page_albums_device": "Альбоми на пристрої ({})",
|
||||
"backup_album_selection_page_albums_tap": "Торкніться, щоб включити,\nторкніться двічі, щоб виключити",
|
||||
"backup_album_selection_page_assets_scatter": "Елементи можуть належати до кількох альбомів водночас. Таким чином, альбоми можуть бути включені або вилучені під час резервного копіювання.",
|
||||
@@ -137,7 +137,7 @@
|
||||
"backup_manual_success": "Успіх",
|
||||
"backup_manual_title": "Стан завантаження",
|
||||
"backup_options_page_title": "Резервне копіювання",
|
||||
"backup_setting_subtitle": "Manage background and foreground upload settings",
|
||||
"backup_setting_subtitle": "Керування завантаженням у фоновому та передньому режимах",
|
||||
"cache_settings_album_thumbnails": "Мініатюри сторінок бібліотеки ({} елементи)",
|
||||
"cache_settings_clear_cache_button": "Очистити кеш",
|
||||
"cache_settings_clear_cache_button_title": "Очищає кеш програми. Це суттєво знизить продуктивність програми, доки кеш не буде перебудовано.",
|
||||
@@ -156,17 +156,17 @@
|
||||
"cache_settings_tile_subtitle": "Керування поведінкою локального сховища",
|
||||
"cache_settings_tile_title": "Локальне сховище",
|
||||
"cache_settings_title": "Налаштування кешування",
|
||||
"cancel": "Cancel",
|
||||
"canceled": "Canceled",
|
||||
"change_display_order": "Change display order",
|
||||
"cancel": "Скасувати",
|
||||
"canceled": "Скасовано",
|
||||
"change_display_order": "Змінити порядок відображення",
|
||||
"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.",
|
||||
"check_corrupt_asset_backup": "Перевірити пошкоджені резервні копії",
|
||||
"check_corrupt_asset_backup_button": "Виконати перевірку",
|
||||
"check_corrupt_asset_backup_description": "Виконуйте цю перевірку лише через Wi-Fi та після того, як усі активи будуть заархівовані. Процес може зайняти кілька хвилин.",
|
||||
"client_cert_dialog_msg_confirm": "OK",
|
||||
"client_cert_enter_password": "Введіть пароль",
|
||||
"client_cert_import": "Імпорт",
|
||||
@@ -181,7 +181,7 @@
|
||||
"common_create_new_album": "Створити новий альбом",
|
||||
"common_server_error": "Будь ласка, перевірте з'єднання, переконайтеся, що сервер доступний і версія програми/сервера сумісна.",
|
||||
"common_shared": "Спільні",
|
||||
"completed": "Completed",
|
||||
"completed": "Завершено",
|
||||
"contextual_search": "Схід сонця на пляжі",
|
||||
"control_bottom_app_bar_add_to_album": "Додати у альбом",
|
||||
"control_bottom_app_bar_album_info": "{} елементи",
|
||||
@@ -199,7 +199,7 @@
|
||||
"control_bottom_app_bar_share": "Поділитися",
|
||||
"control_bottom_app_bar_share_to": "Поділитися",
|
||||
"control_bottom_app_bar_stack": "Стек",
|
||||
"control_bottom_app_bar_trash_from_immich": "Перемістити до кошика",
|
||||
"control_bottom_app_bar_trash_from_immich": "В кошик",
|
||||
"control_bottom_app_bar_unarchive": "Розархівувати",
|
||||
"control_bottom_app_bar_unfavorite": "Видалити з улюблених",
|
||||
"control_bottom_app_bar_upload": "Завантажити",
|
||||
@@ -213,7 +213,7 @@
|
||||
"crop": "Кадрувати",
|
||||
"curated_location_page_title": "Місця",
|
||||
"curated_object_page_title": "Речі",
|
||||
"current_server_address": "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",
|
||||
@@ -250,10 +250,10 @@
|
||||
"edit_date_time_dialog_timezone": "Часовий пояс",
|
||||
"edit_image_title": "Редагувати",
|
||||
"edit_location_dialog_title": "Місцезнаходження",
|
||||
"end_date": "End date",
|
||||
"enqueued": "Enqueued",
|
||||
"enter_wifi_name": "Enter WiFi name",
|
||||
"error_change_sort_album": "Failed to change album sort order",
|
||||
"end_date": "Дата завершення",
|
||||
"enqueued": "В черзі",
|
||||
"enter_wifi_name": "Введіть назву Wi-Fi",
|
||||
"error_change_sort_album": "Не вдалося змінити порядок сортування альбому",
|
||||
"error_saving_image": "Помилка: {}",
|
||||
"exif_bottom_sheet_description": "Додати опис...",
|
||||
"exif_bottom_sheet_details": "ПОДРОБИЦІ",
|
||||
@@ -265,16 +265,16 @@
|
||||
"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",
|
||||
"failed": "Failed",
|
||||
"external_network": "Зовнішня мережа",
|
||||
"external_network_sheet_info": "Якщо ви не підключені до бажаної Wi-Fi мережі, додаток підключиться до сервера через першу доступну URL-адресу зверху вниз",
|
||||
"failed": "Не вдалося",
|
||||
"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",
|
||||
"get_wifiname_error": "Не вдалося отримати назву Wi-Fi мережі. Переконайтеся, що ви надали необхідні дозволи та підключені до Wi-Fi мережі",
|
||||
"grant_permission": "Надати дозвіл",
|
||||
"haptic_feedback_switch": "Увімкнути тактильну віддачу",
|
||||
"haptic_feedback_title": "Тактильна віддача",
|
||||
"header_settings_add_header_tip": "Додати заголовок",
|
||||
@@ -320,10 +320,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",
|
||||
"local_network": "Локальна мережа",
|
||||
"local_network_sheet_info": "Додаток підключатиметься до сервера через цю URL-адресу при використанні вказаної Wi-Fi мережі",
|
||||
"location_permission": "Дозвіл до місцезнаходження",
|
||||
"location_permission_content": "Для використання функції автоперемикання Immich потрібен дозвіл на точне місцезнаходження, щоб зчитувати назву поточної Wi-Fi мережі",
|
||||
"location_picker_choose_on_map": "Обрати на мапі",
|
||||
"location_picker_latitude": "Широта",
|
||||
"location_picker_latitude_error": "Вкажіть дійсну широту",
|
||||
@@ -393,8 +393,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",
|
||||
"networking_settings": "Мережа",
|
||||
"networking_subtitle": "Керувати налаштуваннями кінцевої точки сервера",
|
||||
"no_assets_to_show": "Елементи відсутні",
|
||||
"no_name": "Без імені",
|
||||
"notification_permission_dialog_cancel": "Скасувати",
|
||||
@@ -403,7 +403,7 @@
|
||||
"notification_permission_list_tile_content": "Надати дозвіл для сповіщень.",
|
||||
"notification_permission_list_tile_enable_button": "Увімкнути Сповіщення",
|
||||
"notification_permission_list_tile_title": "Дозвіл на Сповіщення",
|
||||
"not_selected": "Not selected",
|
||||
"not_selected": "Не вибрано",
|
||||
"on_this_device": "На цьому пристрої",
|
||||
"partner_list_user_photos": "Фотографії {user}",
|
||||
"partner_list_view_all": "Переглянути усі",
|
||||
@@ -417,7 +417,7 @@
|
||||
"partner_page_stop_sharing_title": "Припинити надання ваших знімків?",
|
||||
"partner_page_title": "Партнер",
|
||||
"partners": "\nПартнери",
|
||||
"paused": "Paused",
|
||||
"paused": "Призупинено",
|
||||
"people": "Люди",
|
||||
"permission_onboarding_back": "Назад",
|
||||
"permission_onboarding_continue_anyway": "Все одно продовжити",
|
||||
@@ -430,7 +430,7 @@
|
||||
"permission_onboarding_permission_limited": "Обмежений доступ. Аби дозволити Immich резервне копіювання та керування вашою галереєю, надайте доступ до знімків та відео у Налаштуваннях",
|
||||
"permission_onboarding_request": "Immich потребує доступу до ваших знімків та відео.",
|
||||
"places": "Місця",
|
||||
"preferences_settings_subtitle": "Manage the app's preferences",
|
||||
"preferences_settings_subtitle": "Керувати налаштуваннями додатка",
|
||||
"preferences_settings_title": "Параметри",
|
||||
"profile_drawer_app_logs": "Журнал",
|
||||
"profile_drawer_client_out_of_date_major": "Мобільний додаток застарів. Будь ласка, оновіть до останньої мажорної версії.",
|
||||
@@ -445,7 +445,7 @@
|
||||
"profile_drawer_trash": "Кошик",
|
||||
"recently_added": "Нещодавно додані",
|
||||
"recently_added_page_title": "Нещодавні",
|
||||
"save": "Save",
|
||||
"save": "Зберегти",
|
||||
"save_to_gallery": "Зберегти в галерею",
|
||||
"scaffold_body_error_occurred": "Виникла помилка",
|
||||
"search_albums": "Пошук альбому",
|
||||
@@ -491,7 +491,7 @@
|
||||
"search_page_places": "Місця",
|
||||
"search_page_recently_added": "Нещодавно додані",
|
||||
"search_page_screenshots": "Знімки екрану",
|
||||
"search_page_search_photos_videos": "Search for your photos and videos",
|
||||
"search_page_search_photos_videos": "Шукайте ваші фотографії та відео",
|
||||
"search_page_selfies": "Селфі",
|
||||
"search_page_things": "Речі",
|
||||
"search_page_videos": "Відео",
|
||||
@@ -504,7 +504,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_endpoint": "Кінцева точка сервера",
|
||||
"server_info_box_app_version": "Версія додатка",
|
||||
"server_info_box_latest_release": "Остання версія",
|
||||
"server_info_box_server_url": "URL сервера",
|
||||
@@ -516,7 +516,7 @@
|
||||
"setting_image_viewer_preview_title": "Завантажувати зображення попереднього перегляду",
|
||||
"setting_image_viewer_title": "Зображення",
|
||||
"setting_languages_apply": "Застосувати",
|
||||
"setting_languages_subtitle": "Change the app's language",
|
||||
"setting_languages_subtitle": "Змінити мову додатка",
|
||||
"setting_languages_title": "Мова",
|
||||
"setting_notifications_notify_failures_grace_period": "Повідомити про помилки фонового резервного копіювання: {}",
|
||||
"setting_notifications_notify_hours": "{} годин",
|
||||
@@ -534,8 +534,8 @@
|
||||
"settings_require_restart": "Перезавантажте програму для застосування цього налаштування",
|
||||
"setting_video_viewer_looping_subtitle": "Увімкнути циклічне відтворення відео",
|
||||
"setting_video_viewer_looping_title": "Циклічне відтворення",
|
||||
"setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.",
|
||||
"setting_video_viewer_original_video_title": "Force original video",
|
||||
"setting_video_viewer_original_video_subtitle": "При трансляції відео з сервера відтворювати оригінал, навіть якщо доступне транскодоване відео. Це може призвести до буферизації. Відео, доступні локально, відтворюються в оригінальній якості, незалежно від цього налаштування.",
|
||||
"setting_video_viewer_original_video_title": "Примусово відтворювати оригінальне відео",
|
||||
"setting_video_viewer_title": "Відео",
|
||||
"share_add": "Додати",
|
||||
"share_add_photos": "Додати знімки",
|
||||
@@ -554,7 +554,7 @@
|
||||
"shared_album_section_people_owner_label": "Власник",
|
||||
"shared_album_section_people_title": "ЛЮДИ",
|
||||
"share_dialog_preparing": "Підготовка...",
|
||||
"shared_intent_upload_button_progress_text": "{} / {} Uploaded",
|
||||
"shared_intent_upload_button_progress_text": "{} / {} Завантажено",
|
||||
"shared_link_app_bar_title": "Спільні посилання",
|
||||
"shared_link_clipboard_copied_massage": "Скопійовано в буфер обміну",
|
||||
"shared_link_clipboard_text": "Посилання: {}\nПароль: {}",
|
||||
@@ -649,15 +649,15 @@
|
||||
"trash_page_select_assets_btn": "Вибрані елементи",
|
||||
"trash_page_select_btn": "Вибрати",
|
||||
"trash_page_title": "Кошик ({})",
|
||||
"upload": "Upload",
|
||||
"upload": "Завантажити",
|
||||
"upload_dialog_cancel": "Скасувати",
|
||||
"upload_dialog_info": "Бажаєте створити резервну копію вибраних елементів на сервері?",
|
||||
"upload_dialog_ok": "Завантажити",
|
||||
"upload_dialog_title": "Завантажити Елементи",
|
||||
"uploading": "Uploading",
|
||||
"upload_to_immich": "Upload to Immich ({})",
|
||||
"use_current_connection": "use current connection",
|
||||
"validate_endpoint_error": "Please enter a valid URL",
|
||||
"uploading": "Завантажується",
|
||||
"upload_to_immich": "Завантажити в Immich ({})",
|
||||
"use_current_connection": "використовувати поточне з'єднання",
|
||||
"validate_endpoint_error": "Будь ласка, введіть дійсну URL-адресу.",
|
||||
"version_announcement_overlay_ack": "Прийняти",
|
||||
"version_announcement_overlay_release_notes": "примітки до випуску",
|
||||
"version_announcement_overlay_text_1": "Вітаємо, є новий випуск ",
|
||||
@@ -668,6 +668,6 @@
|
||||
"viewer_remove_from_stack": "Видалити зі стеку",
|
||||
"viewer_stack_use_as_main_asset": "Використовувати як основний елементи",
|
||||
"viewer_unstack": "Розібрати стек",
|
||||
"wifi_name": "WiFi Name",
|
||||
"your_wifi_name": "Your WiFi name"
|
||||
}
|
||||
"wifi_name": "Назва Wi-Fi",
|
||||
"your_wifi_name": "Ваша Wi-Fi мережа"
|
||||
}
|
||||
|
||||
@@ -541,7 +541,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -685,7 +685,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -715,7 +715,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -748,7 +748,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -791,7 +791,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -831,7 +831,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.125.1</string>
|
||||
<string>1.125.2</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@@ -93,7 +93,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>189</string>
|
||||
<string>190</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Release"
|
||||
lane :release do
|
||||
increment_version_number(
|
||||
version_number: "1.125.1"
|
||||
version_number: "1.125.6"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -277,7 +277,6 @@ class SearchPage extends HookConsumerWidget {
|
||||
fieldEndHintText: 'end_date'.tr(),
|
||||
initialEntryMode: DatePickerEntryMode.calendar,
|
||||
keyboardType: TextInputType.text,
|
||||
locale: context.locale,
|
||||
);
|
||||
|
||||
if (date == null) {
|
||||
|
||||
@@ -4,6 +4,7 @@ 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/extensions/duration_extensions.dart';
|
||||
import 'package:immich_mobile/widgets/common/dropdown_search_menu.dart';
|
||||
import 'package:timezone/timezone.dart' as tz;
|
||||
import 'package:timezone/timezone.dart';
|
||||
|
||||
@@ -24,7 +25,7 @@ Future<String?> showDateTimePicker({
|
||||
}
|
||||
|
||||
String _getFormattedOffset(int offsetInMilli, tz.Location location) {
|
||||
return "${location.name} (UTC${Duration(milliseconds: offsetInMilli).formatAsOffset()})";
|
||||
return "${location.name} (${Duration(milliseconds: offsetInMilli).formatAsOffset()})";
|
||||
}
|
||||
|
||||
class _DateTimePicker extends HookWidget {
|
||||
@@ -73,7 +74,6 @@ class _DateTimePicker extends HookWidget {
|
||||
// returns a list of location<name> along with it's offset in duration
|
||||
List<_TimeZoneOffset> getAllTimeZones() {
|
||||
return tz.timeZoneDatabase.locations.values
|
||||
.where((l) => !l.currentTimeZone.abbreviation.contains("0"))
|
||||
.map(_TimeZoneOffset.fromLocation)
|
||||
.sorted()
|
||||
.toList();
|
||||
@@ -133,83 +133,78 @@ class _DateTimePicker extends HookWidget {
|
||||
context.pop(dtWithOffset);
|
||||
}
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraint) => AlertDialog(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(vertical: 32, horizontal: 18),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(
|
||||
"action_common_cancel",
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.colorScheme.error,
|
||||
return AlertDialog(
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 32, horizontal: 18),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(
|
||||
"action_common_cancel",
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.colorScheme.error,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: popWithDateTime,
|
||||
child: Text(
|
||||
"action_common_update",
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"edit_date_time_dialog_date_time",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
).tr(),
|
||||
const SizedBox(height: 32),
|
||||
ListTile(
|
||||
tileColor: context.colorScheme.surfaceContainerHighest,
|
||||
shape: ShapeBorder.lerp(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
1,
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.edit_outlined,
|
||||
size: 18,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
title: Text(
|
||||
DateFormat("dd-MM-yyyy hh:mm a").format(date.value),
|
||||
style: context.textTheme.bodyMedium,
|
||||
).tr(),
|
||||
onTap: pickDate,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: popWithDateTime,
|
||||
child: Text(
|
||||
"action_common_update",
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
).tr(),
|
||||
const SizedBox(height: 24),
|
||||
DropdownSearchMenu(
|
||||
trailingIcon: Icon(
|
||||
Icons.arrow_drop_down,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
hintText: "edit_date_time_dialog_timezone".tr(),
|
||||
label: const Text('edit_date_time_dialog_timezone').tr(),
|
||||
textStyle: context.textTheme.bodyMedium,
|
||||
onSelected: (value) => tzOffset.value = value,
|
||||
initialSelection: tzOffset.value,
|
||||
dropdownMenuEntries: menuEntries,
|
||||
),
|
||||
],
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"edit_date_time_dialog_date_time",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
).tr(),
|
||||
const SizedBox(height: 32),
|
||||
ListTile(
|
||||
tileColor: context.colorScheme.surfaceContainerHighest,
|
||||
shape: ShapeBorder.lerp(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
1,
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.edit_outlined,
|
||||
size: 18,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
title: Text(
|
||||
DateFormat("dd-MM-yyyy hh:mm a").format(date.value),
|
||||
style: context.textTheme.bodyMedium,
|
||||
).tr(),
|
||||
onTap: pickDate,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
DropdownMenu(
|
||||
width: 275,
|
||||
menuHeight: 300,
|
||||
trailingIcon: Icon(
|
||||
Icons.arrow_drop_down,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
hintText: "edit_date_time_dialog_timezone".tr(),
|
||||
label: const Text('edit_date_time_dialog_timezone').tr(),
|
||||
textStyle: context.textTheme.bodyMedium,
|
||||
onSelected: (value) => tzOffset.value = value!,
|
||||
initialSelection: tzOffset.value,
|
||||
dropdownMenuEntries: menuEntries,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
169
mobile/lib/widgets/common/dropdown_search_menu.dart
Normal file
169
mobile/lib/widgets/common/dropdown_search_menu.dart
Normal file
@@ -0,0 +1,169 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
|
||||
class DropdownSearchMenu<T> extends HookWidget {
|
||||
const DropdownSearchMenu({
|
||||
super.key,
|
||||
required this.dropdownMenuEntries,
|
||||
this.initialSelection,
|
||||
this.onSelected,
|
||||
this.trailingIcon,
|
||||
this.hintText,
|
||||
this.label,
|
||||
this.textStyle,
|
||||
this.menuConstraints,
|
||||
});
|
||||
|
||||
final List<DropdownMenuEntry<T>> dropdownMenuEntries;
|
||||
final T? initialSelection;
|
||||
final ValueChanged<T>? onSelected;
|
||||
final Widget? trailingIcon;
|
||||
final String? hintText;
|
||||
final Widget? label;
|
||||
final TextStyle? textStyle;
|
||||
final BoxConstraints? menuConstraints;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedItem = useState<DropdownMenuEntry<T>?>(
|
||||
dropdownMenuEntries
|
||||
.firstWhereOrNull((item) => item.value == initialSelection),
|
||||
);
|
||||
final showTimeZoneDropdown = useState<bool>(false);
|
||||
|
||||
final effectiveConstraints = menuConstraints ??
|
||||
const BoxConstraints(
|
||||
minWidth: 280,
|
||||
maxWidth: 280,
|
||||
minHeight: 0,
|
||||
maxHeight: 280,
|
||||
);
|
||||
|
||||
final inputDecoration = InputDecoration(
|
||||
contentPadding: const EdgeInsets.fromLTRB(12, 4, 12, 4),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: trailingIcon,
|
||||
label: label,
|
||||
hintText: hintText,
|
||||
).applyDefaults(context.themeData.inputDecorationTheme);
|
||||
|
||||
if (!showTimeZoneDropdown.value) {
|
||||
return ConstrainedBox(
|
||||
constraints: effectiveConstraints,
|
||||
child: GestureDetector(
|
||||
onTap: () => showTimeZoneDropdown.value = true,
|
||||
child: InputDecorator(
|
||||
decoration: inputDecoration,
|
||||
child: selectedItem.value != null
|
||||
? Text(
|
||||
selectedItem.value!.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textStyle,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: effectiveConstraints,
|
||||
child: Autocomplete<DropdownMenuEntry<T>>(
|
||||
displayStringForOption: (option) => option.label,
|
||||
optionsBuilder: (textEditingValue) {
|
||||
return dropdownMenuEntries.where(
|
||||
(item) => item.label
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.contains(textEditingValue.text.toLowerCase().trim()),
|
||||
);
|
||||
},
|
||||
onSelected: (option) {
|
||||
selectedItem.value = option;
|
||||
showTimeZoneDropdown.value = false;
|
||||
onSelected?.call(option.value);
|
||||
},
|
||||
fieldViewBuilder: (context, textEditingController, focusNode, _) {
|
||||
return TextField(
|
||||
autofocus: true,
|
||||
focusNode: focusNode,
|
||||
controller: textEditingController,
|
||||
decoration: inputDecoration.copyWith(
|
||||
hintText: "edit_date_time_dialog_search_timezone".tr(),
|
||||
),
|
||||
maxLines: 1,
|
||||
style: context.textTheme.bodyMedium,
|
||||
expands: false,
|
||||
onTapOutside: (event) {
|
||||
showTimeZoneDropdown.value = false;
|
||||
focusNode.unfocus();
|
||||
},
|
||||
onSubmitted: (_) {
|
||||
showTimeZoneDropdown.value = false;
|
||||
},
|
||||
);
|
||||
},
|
||||
optionsViewBuilder: (context, onSelected, options) {
|
||||
// This widget is a copy of the default implementation.
|
||||
// We have only changed the `constraints` parameter.
|
||||
return Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: ConstrainedBox(
|
||||
constraints: effectiveConstraints,
|
||||
child: Material(
|
||||
elevation: 4.0,
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
itemCount: options.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final option = options.elementAt(index);
|
||||
return InkWell(
|
||||
onTap: () => onSelected(option),
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final bool highlight =
|
||||
AutocompleteHighlightedOption.of(context) ==
|
||||
index;
|
||||
if (highlight) {
|
||||
SchedulerBinding.instance.addPostFrameCallback(
|
||||
(Duration timeStamp) {
|
||||
Scrollable.ensureVisible(
|
||||
context,
|
||||
alignment: 0.5,
|
||||
);
|
||||
},
|
||||
debugLabel: 'AutocompleteOptions.ensureVisible',
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
color: highlight
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.12)
|
||||
: null,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
option.label,
|
||||
style: textStyle,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,7 @@ class UserCircleAvatar extends ConsumerWidget {
|
||||
final profileImageUrl =
|
||||
'${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${Random().nextInt(1024)}';
|
||||
|
||||
final textIcon = Text(
|
||||
user.name[0].toUpperCase(),
|
||||
final textIcon = DefaultTextStyle(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
@@ -37,6 +36,7 @@ class UserCircleAvatar extends ConsumerWidget {
|
||||
? Colors.black
|
||||
: Colors.white,
|
||||
),
|
||||
child: Text(user.name[0].toUpperCase()),
|
||||
);
|
||||
return CircleAvatar(
|
||||
backgroundColor: user.avatarColor.toColor(),
|
||||
|
||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.125.1
|
||||
- API version: 1.125.6
|
||||
- Generator version: 7.8.0
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ class AssetBulkUpdateDto {
|
||||
///
|
||||
num? longitude;
|
||||
|
||||
/// Minimum value: 0
|
||||
/// Minimum value: -1
|
||||
/// Maximum value: 5
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
|
||||
2
mobile/openapi/lib/model/update_asset_dto.dart
generated
2
mobile/openapi/lib/model/update_asset_dto.dart
generated
@@ -73,7 +73,7 @@ class UpdateAssetDto {
|
||||
///
|
||||
num? longitude;
|
||||
|
||||
/// Minimum value: 0
|
||||
/// Minimum value: -1
|
||||
/// Maximum value: 5
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
|
||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: 'none'
|
||||
version: 1.125.1+177
|
||||
version: 1.125.6+182
|
||||
|
||||
environment:
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
|
||||
@@ -7454,7 +7454,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "1.125.1",
|
||||
"version": "1.125.6",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [],
|
||||
@@ -7951,7 +7951,7 @@
|
||||
},
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": 0,
|
||||
"minimum": -1,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
@@ -12780,7 +12780,7 @@
|
||||
},
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": 0,
|
||||
"minimum": -1,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
|
||||
4
open-api/typescript-sdk/package-lock.json
generated
4
open-api/typescript-sdk/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.125.1",
|
||||
"version": "1.125.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.125.1",
|
||||
"version": "1.125.6",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.125.1",
|
||||
"version": "1.125.6",
|
||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||
"type": "module",
|
||||
"main": "./build/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Immich
|
||||
* 1.125.1
|
||||
* 1.125.6
|
||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||
* See https://www.npmjs.com/package/oazapfts
|
||||
*/
|
||||
|
||||
@@ -40,12 +40,12 @@
|
||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||
</p>
|
||||
|
||||
## ข้อจำกัดความรับผิดชอบ
|
||||
## ข้อควรระวัง
|
||||
|
||||
- ⚠️ โพรเจกต์นี้กำลังอยู่ระหว่างการพัฒนา**ที่มีการเปลี่ยนแปลงบ่อยมาก**
|
||||
- ⚠️ คาดว่าจะมีข้อผิดพลาดและการเปลี่ยนแปลงที่ส่งผลเสีย
|
||||
- ⚠️ **ห้ามใช้แอปนี้เป็นวิธีการเดียวในการจัดเก็บภาพถ่ายและวิดีโอของคุณ**
|
||||
- ⚠️ ปฏิบัติตามแผนการสำรองข้อมูลแบบ [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) สำหรับภาพถ่ายและวิดีโอที่สำคัญของคุณอยู่เสมอ!
|
||||
- ⚠️ โพรเจกต์นี้กำลังอยู่ระหว่างการพัฒนา**มีการเปลี่ยนแปลงบ่อยมาก**
|
||||
- ⚠️ อาจจะเกิดข้อผิดพลาดและการเปลี่ยนแปลงที่ส่งผลเสีย
|
||||
- ⚠️ **ห้ามใช้ระบบนี้เป็นวิธีการเดียวในการจัดเก็บภาพถ่ายและวิดีโอของคุณ**
|
||||
- ⚠️ ปฏิบัติตามแผนการสำรองข้อมูลแบบ [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) สำหรับภาพถ่ายและวิดีโอที่สำคัญของคุณอยู่เสมอ
|
||||
|
||||
> [!NOTE]
|
||||
> คุณสามารถหาคู่มือหลัก รวมถึงคู่มือการติดตั้ง ได้ที่ https://immich.app/
|
||||
@@ -79,15 +79,15 @@
|
||||
| :----------------------------------------- | ------ | ------ |
|
||||
| อัปโหลดและดูวิดีโอและภาพถ่าย | ใช่ | ใช่ |
|
||||
| การสำรองข้อมูลอัตโนมัติเมื่อเปิดแอป | ใช่ | N/A |
|
||||
| ป้องกันการซ้ำซ้อนของไฟล์ | ใช่ | ใช่ |
|
||||
| ป้องกันการซ้ำของไฟล์ | ใช่ | ใช่ |
|
||||
| เลือกอัลบั้มสำหรับสำรองข้อมูล | ใช่ | N/A |
|
||||
| ดาวน์โหลดภาพถ่ายและวิดีโอไปยังอุปกรณ์ | ใช่ | ใช่ |
|
||||
| รองรับผู้ใช้หลายคน | ใช่ | ใช่ |
|
||||
| อัลบั้มและอัลบั้มแชร์ | ใช่ | ใช่ |
|
||||
| แถบเลื่อนแบบลากได้ | ใช่ | ใช่ |
|
||||
| รองรับรูปแบบไฟล์ RAW | ใช่ | ใช่ |
|
||||
| ดูข้อมูลเมตา (EXIF, แผนที่) | ใช่ | ใช่ |
|
||||
| ค้นหาจากข้อมูลเมตา วัตถุ ใบหน้า และ CLIP | ใช่ | ใช่ |
|
||||
| ดูข้อมูลเมตาดาต้า (EXIF, แผนที่) | ใช่ | ใช่ |
|
||||
| ค้นหาจากข้อมูลเมตาดาต้า วัตถุ ใบหน้า และ CLIP | ใช่ | ใช่ |
|
||||
| ฟังก์ชันการจัดการผู้ดูแลระบบ | ไม่ใช่ | ใช่ |
|
||||
| การสำรองข้อมูลพื้นหลัง | ใช่ | N/A |
|
||||
| การเลื่อนแบบเสมือน | ใช่ | ใช่ |
|
||||
@@ -100,7 +100,7 @@
|
||||
| การจัดเก็บและรายการโปรด | ใช่ | ใช่ |
|
||||
| แผนที่ทั่วโลก | ใช่ | ใช่ |
|
||||
| การแชร์กับคู่หู | ใช่ | ใช่ |
|
||||
| การจดจำใบหน้าและการจัดกลุ่ม | ใช่ | ใช่ |
|
||||
| ระบบจดจำใบหน้าและการจัดกลุ่ม | ใช่ | ใช่ |
|
||||
| ความทรงจำ (x ปีที่แล้ว) | ใช่ | ใช่ |
|
||||
| รองรับแบบออฟไลน์ | ใช่ | ไม่ใช่ |
|
||||
| แกลเลอรีแบบอ่านอย่างเดียว | ใช่ | ใช่ |
|
||||
@@ -108,13 +108,13 @@
|
||||
|
||||
## การแปลภาษา
|
||||
|
||||
อ่านเพิ่มเติมเกี่ยวกับการแปลภาษา [ที่นี่](https://immich.app/docs/developer/translations)
|
||||
อ่านเพิ่มเติมเกี่ยวกับการแปล [ที่นี่](https://immich.app/docs/developer/translations)
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/immich/">
|
||||
<img src="https://hosted.weblate.org/widget/immich/immich/multi-auto.svg" alt="สถานะการแปล" />
|
||||
</a>
|
||||
|
||||
## กิจกรรมของคลังเก็บข้อมูล
|
||||
## กิจกรรมของ Repository
|
||||
|
||||

|
||||
|
||||
|
||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.125.1",
|
||||
"version": "1.125.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich",
|
||||
"version": "1.125.1",
|
||||
"version": "1.125.6",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@nestjs/bullmq": "^11.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.125.1",
|
||||
"version": "1.125.6",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -3,9 +3,11 @@ import { Inject, Module, OnModuleDestroy, OnModuleInit, ValidationPipe } from '@
|
||||
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE, ModuleRef } from '@nestjs/core';
|
||||
import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||
import { ClsModule } from 'nestjs-cls';
|
||||
import { KyselyModule } from 'nestjs-kysely';
|
||||
import { OpenTelemetryModule } from 'nestjs-otel';
|
||||
import postgres from 'postgres';
|
||||
import { commands } from 'src/commands';
|
||||
import { IWorker } from 'src/constants';
|
||||
import { controllers } from 'src/controllers';
|
||||
@@ -57,7 +59,19 @@ const imports = [
|
||||
},
|
||||
}),
|
||||
TypeOrmModule.forFeature(entities),
|
||||
KyselyModule.forRoot(database.config.kysely),
|
||||
KyselyModule.forRoot({
|
||||
dialect: new PostgresJSDialect({ postgres: postgres(database.config.kysely) }),
|
||||
log(event) {
|
||||
if (event.level === 'error') {
|
||||
console.error('Query failed :', {
|
||||
durationMs: event.queryDurationMillis,
|
||||
error: event.error,
|
||||
sql: event.query.sql,
|
||||
params: event.query.parameters,
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
class BaseModule implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
@@ -4,10 +4,12 @@ import { Reflector } from '@nestjs/core';
|
||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||
import { KyselyModule } from 'nestjs-kysely';
|
||||
import { OpenTelemetryModule } from 'nestjs-otel';
|
||||
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import postgres from 'postgres';
|
||||
import { format } from 'sql-formatter';
|
||||
import { GENERATE_SQL_KEY, GenerateSqlQueries } from 'src/decorators';
|
||||
import { entities } from 'src/entities';
|
||||
@@ -84,7 +86,7 @@ class SqlGenerator {
|
||||
const moduleFixture = await Test.createTestingModule({
|
||||
imports: [
|
||||
KyselyModule.forRoot({
|
||||
...database.config.kysely,
|
||||
dialect: new PostgresJSDialect({ postgres: postgres(database.config.kysely) }),
|
||||
log: (event) => {
|
||||
if (event.level === 'query') {
|
||||
this.sqlLogger.logQuery(event.query.sql);
|
||||
|
||||
28
server/src/db.d.ts
vendored
28
server/src/db.d.ts
vendored
@@ -3,21 +3,16 @@
|
||||
* Please do not edit it manually.
|
||||
*/
|
||||
|
||||
import type { ColumnType } from "kysely";
|
||||
import type { ColumnType } from 'kysely';
|
||||
|
||||
export type ArrayType<T> = ArrayTypeImpl<T> extends (infer U)[]
|
||||
? U[]
|
||||
: ArrayTypeImpl<T>;
|
||||
export type ArrayType<T> = ArrayTypeImpl<T> extends (infer U)[] ? U[] : ArrayTypeImpl<T>;
|
||||
|
||||
export type ArrayTypeImpl<T> = T extends ColumnType<infer S, infer I, infer U>
|
||||
? ColumnType<S[], I[], U[]>
|
||||
: T[];
|
||||
export type ArrayTypeImpl<T> = T extends ColumnType<infer S, infer I, infer U> ? ColumnType<S[], I[], U[]> : T[];
|
||||
|
||||
export type AssetsStatusEnum = "active" | "deleted" | "trashed";
|
||||
export type AssetsStatusEnum = 'active' | 'deleted' | 'trashed';
|
||||
|
||||
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
|
||||
? ColumnType<S, I | undefined, U>
|
||||
: ColumnType<T, T | undefined, T>;
|
||||
export type Generated<T> =
|
||||
T extends ColumnType<infer S, infer I, infer U> ? ColumnType<S, I | undefined, U> : ColumnType<T, T | undefined, T>;
|
||||
|
||||
export type Int8 = ColumnType<string, bigint | number | string, bigint | number | string>;
|
||||
|
||||
@@ -33,7 +28,7 @@ export type JsonPrimitive = boolean | number | string | null;
|
||||
|
||||
export type JsonValue = JsonArray | JsonObject | JsonPrimitive;
|
||||
|
||||
export type Sourcetype = "exif" | "machine-learning";
|
||||
export type Sourcetype = 'exif' | 'machine-learning';
|
||||
|
||||
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
||||
|
||||
@@ -154,6 +149,12 @@ export interface AssetStack {
|
||||
primaryAssetId: string;
|
||||
}
|
||||
|
||||
export interface AssetUser {
|
||||
assetId: string;
|
||||
createdAt: Timestamp;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface Audit {
|
||||
action: string;
|
||||
createdAt: Generated<Timestamp>;
|
||||
@@ -413,6 +414,7 @@ export interface DB {
|
||||
asset_files: AssetFiles;
|
||||
asset_job_status: AssetJobStatus;
|
||||
asset_stack: AssetStack;
|
||||
asset_user: AssetUser;
|
||||
assets: Assets;
|
||||
audit: Audit;
|
||||
exif: Exif;
|
||||
@@ -438,6 +440,6 @@ export interface DB {
|
||||
tags_closure: TagsClosure;
|
||||
user_metadata: UserMetadata;
|
||||
users: Users;
|
||||
"vectors.pg_vector_index_stat": VectorsPgVectorIndexStat;
|
||||
'vectors.pg_vector_index_stat': VectorsPgVectorIndexStat;
|
||||
version_history: VersionHistory;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import { albumStub } from 'test/fixtures/album.stub';
|
||||
describe('mapAlbum', () => {
|
||||
it('should set start and end dates', () => {
|
||||
const dto = mapAlbum(albumStub.twoAssets, false);
|
||||
expect(dto.startDate).toEqual(new Date('2023-02-22T05:06:29.716Z'));
|
||||
expect(dto.endDate).toEqual(new Date('2023-02-23T05:06:29.716Z'));
|
||||
expect(dto.startDate).toEqual(new Date('2020-12-31T23:59:00.000Z'));
|
||||
expect(dto.endDate).toEqual(new Date('2025-01-01T01:02:03.456Z'));
|
||||
});
|
||||
|
||||
it('should not set start and end dates for empty assets', () => {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { AlbumUserRole, AssetOrder } from 'src/enum';
|
||||
import { getAssetDateTime } from 'src/utils/date-time';
|
||||
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class AlbumInfoDto {
|
||||
@@ -165,8 +164,8 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt
|
||||
const hasSharedLink = entity.sharedLinks?.length > 0;
|
||||
const hasSharedUser = sharedUsers.length > 0;
|
||||
|
||||
let startDate = getAssetDateTime(assets.at(0));
|
||||
let endDate = getAssetDateTime(assets.at(-1));
|
||||
let startDate = assets.at(0)?.localDateTime;
|
||||
let endDate = assets.at(-1)?.localDateTime;
|
||||
// Swap dates if start date is greater than end date.
|
||||
if (startDate && endDate && startDate > endDate) {
|
||||
[startDate, endDate] = [endDate, startDate];
|
||||
|
||||
@@ -52,7 +52,7 @@ export class UpdateAssetBase {
|
||||
@Optional()
|
||||
@IsInt()
|
||||
@Max(5)
|
||||
@Min(0)
|
||||
@Min(-1)
|
||||
rating?: number;
|
||||
}
|
||||
|
||||
|
||||
22
server/src/entities/asset-user.entity.ts
Normal file
22
server/src/entities/asset-user.entity.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { Column, Entity, Index, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity('asset_user')
|
||||
@Index('IDX_assetId_userId', ['assetId', 'userId'])
|
||||
export class AssetUserEntity {
|
||||
@PrimaryColumn()
|
||||
assetId!: string;
|
||||
|
||||
@ManyToOne(() => AssetEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||
asset!: AssetEntity;
|
||||
|
||||
@PrimaryColumn()
|
||||
userId!: string;
|
||||
|
||||
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||
user!: UserEntity;
|
||||
|
||||
@Column()
|
||||
createdAt!: Date;
|
||||
}
|
||||
@@ -193,7 +193,7 @@ export function withExifInner<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
||||
export function withSmartSearch<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
||||
return qb
|
||||
.leftJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
||||
.select(sql<number[]>`smart_search.embedding`.as('embedding'));
|
||||
.select((eb) => eb.fn.toJson(eb.table('smart_search')).as('smartSearch'));
|
||||
}
|
||||
|
||||
export function withFaces(eb: ExpressionBuilder<DB, 'assets'>) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { APIKeyEntity } from 'src/entities/api-key.entity';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { AssetFileEntity } from 'src/entities/asset-files.entity';
|
||||
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
||||
import { AssetUserEntity } from 'src/entities/asset-user.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AuditEntity } from 'src/entities/audit.entity';
|
||||
import { ExifEntity } from 'src/entities/exif.entity';
|
||||
@@ -34,6 +35,7 @@ export const entities = [
|
||||
AssetEntity,
|
||||
AssetFaceEntity,
|
||||
AssetFileEntity,
|
||||
AssetUserEntity,
|
||||
AssetJobStatusEntity,
|
||||
AuditEntity,
|
||||
ExifEntity,
|
||||
|
||||
@@ -9,8 +9,8 @@ export const IAlbumRepository = 'IAlbumRepository';
|
||||
export interface AlbumAssetCount {
|
||||
albumId: string;
|
||||
assetCount: number;
|
||||
startDate: Date | undefined;
|
||||
endDate: Date | undefined;
|
||||
startDate: Date | null;
|
||||
endDate: Date | null;
|
||||
}
|
||||
|
||||
export interface AlbumInfoOptions {
|
||||
|
||||
@@ -13,7 +13,7 @@ if (immichApp) {
|
||||
let apiProcess: ChildProcess | undefined;
|
||||
|
||||
const onError = (name: string, error: Error) => {
|
||||
console.error(`${name} worker error: ${error}`);
|
||||
console.error(`${name} worker error: ${error}, stack: ${error.stack}`);
|
||||
};
|
||||
|
||||
const onExit = (name: string, exitCode: number | null) => {
|
||||
|
||||
@@ -3,10 +3,10 @@ import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
export class AddTimeBucketIndices1734574016301 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX idx_local_date_time_month ON public.assets ((date_trunc('MONTH', "localDateTime" at time zone 'UTC') at time zone 'UTC'))`,
|
||||
`CREATE INDEX idx_local_date_time_month ON assets ((date_trunc('MONTH', "localDateTime" at time zone 'UTC') at time zone 'UTC'))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX idx_local_date_time ON public.assets ((("localDateTime" at time zone 'UTC')::date))`,
|
||||
`CREATE INDEX idx_local_date_time ON assets ((("localDateTime" at time zone 'UTC')::date))`,
|
||||
);
|
||||
await queryRunner.query(`DROP INDEX "IDX_day_of_month"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_month"`);
|
||||
|
||||
20
server/src/migrations/1738099775096-AddAssetUserTable.ts
Normal file
20
server/src/migrations/1738099775096-AddAssetUserTable.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddAssetUserTable1738099775096 implements MigrationInterface {
|
||||
name = 'AddAssetUserTable1738099775096'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "asset_user" ("assetId" uuid NOT NULL, "userId" uuid NOT NULL, "createdAt" TIMESTAMP NOT NULL, CONSTRAINT "PK_f3d7f17ab93d60e007282726058" PRIMARY KEY ("assetId", "userId"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_assetId_userId" ON "asset_user" ("assetId", "userId") `);
|
||||
await queryRunner.query(`ALTER TABLE "asset_user" ADD CONSTRAINT "FK_07c8478e0936e78b553aaeceedb" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_user" ADD CONSTRAINT "FK_85e2ef24493bdf649dfdfb769a2" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "asset_user" DROP CONSTRAINT "FK_85e2ef24493bdf649dfdfb769a2"`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_user" DROP CONSTRAINT "FK_07c8478e0936e78b553aaeceedb"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_assetId_userId"`);
|
||||
await queryRunner.query(`DROP TABLE "asset_user"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -90,7 +90,7 @@ select
|
||||
(
|
||||
select
|
||||
"assets".*,
|
||||
to_json("exif") as "exifInfo"
|
||||
"exif" as "exifInfo"
|
||||
from
|
||||
"assets"
|
||||
inner join "exif" on "assets"."id" = "exif"."assetId"
|
||||
@@ -98,7 +98,6 @@ select
|
||||
where
|
||||
"albums_assets_assets"."albumsId" = "albums"."id"
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."isArchived" = $1
|
||||
order by
|
||||
"assets"."fileCreatedAt" desc
|
||||
) as "asset"
|
||||
@@ -106,7 +105,7 @@ select
|
||||
from
|
||||
"albums"
|
||||
where
|
||||
"albums"."id" = $2
|
||||
"albums"."id" = $1
|
||||
and "albums"."deletedAt" is null
|
||||
|
||||
-- AlbumRepository.getByAssetId
|
||||
@@ -181,19 +180,20 @@ select
|
||||
) as "albumUsers"
|
||||
from
|
||||
"albums"
|
||||
left join "albums_assets_assets" as "album_assets" on "album_assets"."albumsId" = "albums"."id"
|
||||
left join "albums_shared_users_users" as "album_users" on "album_users"."albumsId" = "albums"."id"
|
||||
inner join "albums_assets_assets" as "album_assets" on "album_assets"."albumsId" = "albums"."id"
|
||||
where
|
||||
(
|
||||
(
|
||||
"albums"."ownerId" = $1
|
||||
and "album_assets"."assetsId" = $2
|
||||
)
|
||||
or (
|
||||
"album_users"."usersId" = $3
|
||||
and "album_assets"."assetsId" = $4
|
||||
"albums"."ownerId" = $1
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"albums_shared_users_users" as "album_users"
|
||||
where
|
||||
"album_users"."albumsId" = "albums"."id"
|
||||
and "album_users"."usersId" = $2
|
||||
)
|
||||
)
|
||||
and "album_assets"."assetsId" = $3
|
||||
and "albums"."deletedAt" is null
|
||||
order by
|
||||
"albums"."createdAt" desc,
|
||||
@@ -201,16 +201,17 @@ order by
|
||||
|
||||
-- AlbumRepository.getMetadataForIds
|
||||
select
|
||||
"albums"."id",
|
||||
min("assets"."fileCreatedAt") as "startDate",
|
||||
max("assets"."fileCreatedAt") as "endDate",
|
||||
count("assets"."id") as "assetCount"
|
||||
"albums"."id" as "albumId",
|
||||
min("assets"."localDateTime") as "startDate",
|
||||
max("assets"."localDateTime") as "endDate",
|
||||
count("assets"."id")::int as "assetCount"
|
||||
from
|
||||
"albums"
|
||||
left join "albums_assets_assets" as "album_assets" on "album_assets"."albumsId" = "albums"."id"
|
||||
left join "assets" on "assets"."id" = "album_assets"."assetsId"
|
||||
inner join "albums_assets_assets" as "album_assets" on "album_assets"."albumsId" = "albums"."id"
|
||||
inner join "assets" on "assets"."id" = "album_assets"."assetsId"
|
||||
where
|
||||
"albums"."id" in ($1)
|
||||
and "assets"."deletedAt" is null
|
||||
group by
|
||||
"albums"."id"
|
||||
|
||||
@@ -306,8 +307,8 @@ order by
|
||||
"albums"."createdAt" desc
|
||||
|
||||
-- AlbumRepository.getShared
|
||||
select distinct
|
||||
on ("albums"."createdAt") "albums".*,
|
||||
select
|
||||
"albums".*,
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
@@ -390,15 +391,26 @@ select distinct
|
||||
) as "sharedLinks"
|
||||
from
|
||||
"albums"
|
||||
left join "albums_shared_users_users" as "shared_albums" on "shared_albums"."albumsId" = "albums"."id"
|
||||
left join "shared_links" on "shared_links"."albumId" = "albums"."id"
|
||||
where
|
||||
(
|
||||
"shared_albums"."usersId" = $1
|
||||
or "shared_links"."userId" = $2
|
||||
or (
|
||||
"albums"."ownerId" = $3
|
||||
and "shared_albums"."usersId" is not null
|
||||
exists (
|
||||
select
|
||||
from
|
||||
"albums_shared_users_users" as "album_users"
|
||||
where
|
||||
"album_users"."albumsId" = "albums"."id"
|
||||
and (
|
||||
"albums"."ownerId" = $1
|
||||
or "album_users"."usersId" = $2
|
||||
)
|
||||
)
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"shared_links"
|
||||
where
|
||||
"shared_links"."albumId" = "albums"."id"
|
||||
and "shared_links"."userId" = $3
|
||||
)
|
||||
)
|
||||
and "albums"."deletedAt" is null
|
||||
@@ -406,48 +418,8 @@ order by
|
||||
"albums"."createdAt" desc
|
||||
|
||||
-- AlbumRepository.getNotShared
|
||||
select distinct
|
||||
on ("albums"."createdAt") "albums".*,
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"album_users".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "album_users"."usersId"
|
||||
) as obj
|
||||
) as "user"
|
||||
from
|
||||
"albums_shared_users_users" as "album_users"
|
||||
where
|
||||
"album_users"."albumsId" = "albums"."id"
|
||||
) as agg
|
||||
) as "albumUsers",
|
||||
select
|
||||
"albums".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
@@ -474,29 +446,26 @@ select distinct
|
||||
where
|
||||
"users"."id" = "albums"."ownerId"
|
||||
) as obj
|
||||
) as "owner",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*
|
||||
from
|
||||
"shared_links"
|
||||
where
|
||||
"shared_links"."albumId" = "albums"."id"
|
||||
) as agg
|
||||
) as "sharedLinks"
|
||||
) as "owner"
|
||||
from
|
||||
"albums"
|
||||
left join "albums_shared_users_users" as "shared_albums" on "shared_albums"."albumsId" = "albums"."id"
|
||||
left join "shared_links" on "shared_links"."albumId" = "albums"."id"
|
||||
where
|
||||
"albums"."ownerId" = $1
|
||||
and "shared_albums"."usersId" is null
|
||||
and "shared_links"."userId" is null
|
||||
and "albums"."deletedAt" is null
|
||||
and not exists (
|
||||
select
|
||||
from
|
||||
"albums_shared_users_users" as "album_users"
|
||||
where
|
||||
"album_users"."albumsId" = "albums"."id"
|
||||
)
|
||||
and not exists (
|
||||
select
|
||||
from
|
||||
"shared_links"
|
||||
where
|
||||
"shared_links"."albumId" = "albums"."id"
|
||||
)
|
||||
order by
|
||||
"albums"."createdAt" desc
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ offset
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."id" < $6
|
||||
order by
|
||||
"assets"."id"
|
||||
random()
|
||||
limit
|
||||
$7
|
||||
)
|
||||
@@ -56,7 +56,7 @@ union all
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."id" > $13
|
||||
order by
|
||||
"assets"."id"
|
||||
random()
|
||||
limit
|
||||
$14
|
||||
)
|
||||
|
||||
@@ -59,11 +59,10 @@ const withAssets = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
||||
.selectFrom('assets')
|
||||
.selectAll('assets')
|
||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||
.select((eb) => eb.fn.toJson('exif').as('exifInfo'))
|
||||
.select((eb) => eb.table('exif').as('exifInfo'))
|
||||
.innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id')
|
||||
.whereRef('albums_assets_assets.albumsId', '=', 'albums.id')
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.isArchived', '=', false)
|
||||
.orderBy('assets.fileCreatedAt', 'desc')
|
||||
.as('asset'),
|
||||
)
|
||||
@@ -94,14 +93,19 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
.selectAll('albums')
|
||||
.leftJoin('albums_assets_assets as album_assets', 'album_assets.albumsId', 'albums.id')
|
||||
.leftJoin('albums_shared_users_users as album_users', 'album_users.albumsId', 'albums.id')
|
||||
.innerJoin('albums_assets_assets as album_assets', 'album_assets.albumsId', 'albums.id')
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb.and([eb('albums.ownerId', '=', ownerId), eb('album_assets.assetsId', '=', assetId)]),
|
||||
eb.and([eb('album_users.usersId', '=', ownerId), eb('album_assets.assetsId', '=', assetId)]),
|
||||
eb('albums.ownerId', '=', ownerId),
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('albums_shared_users_users as album_users')
|
||||
.whereRef('album_users.albumsId', '=', 'albums.id')
|
||||
.where('album_users.usersId', '=', ownerId),
|
||||
),
|
||||
]),
|
||||
)
|
||||
.where('album_assets.assetsId', '=', assetId)
|
||||
.where('albums.deletedAt', 'is', null)
|
||||
.orderBy('albums.createdAt', 'desc')
|
||||
.select(withOwner)
|
||||
@@ -118,24 +122,18 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
return [];
|
||||
}
|
||||
|
||||
const metadatas = await this.db
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
.leftJoin('albums_assets_assets as album_assets', 'album_assets.albumsId', 'albums.id')
|
||||
.leftJoin('assets', 'assets.id', 'album_assets.assetsId')
|
||||
.select('albums.id')
|
||||
.select((eb) => eb.fn.min('assets.fileCreatedAt').as('startDate'))
|
||||
.select((eb) => eb.fn.max('assets.fileCreatedAt').as('endDate'))
|
||||
.select((eb) => eb.fn.count('assets.id').as('assetCount'))
|
||||
.innerJoin('albums_assets_assets as album_assets', 'album_assets.albumsId', 'albums.id')
|
||||
.innerJoin('assets', 'assets.id', 'album_assets.assetsId')
|
||||
.select('albums.id as albumId')
|
||||
.select((eb) => eb.fn.min('assets.localDateTime').as('startDate'))
|
||||
.select((eb) => eb.fn.max('assets.localDateTime').as('endDate'))
|
||||
.select((eb) => sql<number>`${eb.fn.count('assets.id')}::int`.as('assetCount'))
|
||||
.where('albums.id', 'in', ids)
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.groupBy('albums.id')
|
||||
.execute();
|
||||
|
||||
return metadatas.map((metadatas) => ({
|
||||
albumId: metadatas.id,
|
||||
assetCount: Number(metadatas.assetCount),
|
||||
startDate: metadatas.startDate ? new Date(metadatas.startDate) : undefined,
|
||||
endDate: metadatas.endDate ? new Date(metadatas.endDate) : undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
@@ -160,14 +158,20 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
.selectAll('albums')
|
||||
.distinctOn('albums.createdAt')
|
||||
.leftJoin('albums_shared_users_users as shared_albums', 'shared_albums.albumsId', 'albums.id')
|
||||
.leftJoin('shared_links', 'shared_links.albumId', 'albums.id')
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb('shared_albums.usersId', '=', ownerId),
|
||||
eb('shared_links.userId', '=', ownerId),
|
||||
eb.and([eb('albums.ownerId', '=', ownerId), eb('shared_albums.usersId', 'is not', null)]),
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('albums_shared_users_users as album_users')
|
||||
.whereRef('album_users.albumsId', '=', 'albums.id')
|
||||
.where((eb) => eb.or([eb('albums.ownerId', '=', ownerId), eb('album_users.usersId', '=', ownerId)])),
|
||||
),
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('shared_links')
|
||||
.whereRef('shared_links.albumId', '=', 'albums.id')
|
||||
.where('shared_links.userId', '=', ownerId),
|
||||
),
|
||||
]),
|
||||
)
|
||||
.where('albums.deletedAt', 'is', null)
|
||||
@@ -186,16 +190,21 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
.selectAll('albums')
|
||||
.distinctOn('albums.createdAt')
|
||||
.leftJoin('albums_shared_users_users as shared_albums', 'shared_albums.albumsId', 'albums.id')
|
||||
.leftJoin('shared_links', 'shared_links.albumId', 'albums.id')
|
||||
.where('albums.ownerId', '=', ownerId)
|
||||
.where('shared_albums.usersId', 'is', null)
|
||||
.where('shared_links.userId', 'is', null)
|
||||
.where('albums.deletedAt', 'is', null)
|
||||
.select(withAlbumUsers)
|
||||
.where((eb) =>
|
||||
eb.not(
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('albums_shared_users_users as album_users')
|
||||
.whereRef('album_users.albumsId', '=', 'albums.id'),
|
||||
),
|
||||
),
|
||||
)
|
||||
.where((eb) =>
|
||||
eb.not(eb.exists(eb.selectFrom('shared_links').whereRef('shared_links.albumId', '=', 'albums.id'))),
|
||||
)
|
||||
.select(withOwner)
|
||||
.select(withSharedLink)
|
||||
.orderBy('albums.createdAt', 'desc')
|
||||
.execute() as unknown as Promise<AlbumEntity[]>;
|
||||
}
|
||||
@@ -282,7 +291,6 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
.selectAll()
|
||||
.where('id', '=', newAlbum.id)
|
||||
.select(withOwner)
|
||||
.select(withSharedLink)
|
||||
.select(withAssets)
|
||||
.select(withAlbumUsers)
|
||||
.executeTakeFirst() as unknown as Promise<AlbumEntity>;
|
||||
@@ -292,7 +300,7 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
update(id: string, album: Updateable<Albums>): Promise<AlbumEntity> {
|
||||
return this.db
|
||||
.updateTable('albums')
|
||||
.set({ ...album, updatedAt: new Date() })
|
||||
.set(album)
|
||||
.where('id', '=', id)
|
||||
.returningAll('albums')
|
||||
.returning(withOwner)
|
||||
@@ -335,7 +343,6 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
.select('album_assets.assetsId')
|
||||
.orderBy('assets.fileCreatedAt', 'desc')
|
||||
.limit(1),
|
||||
updatedAt: new Date(),
|
||||
}))
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
|
||||
@@ -81,7 +81,24 @@ export class AssetRepository implements IAssetRepository {
|
||||
}
|
||||
|
||||
create(asset: Insertable<Assets>): Promise<AssetEntity> {
|
||||
return this.db.insertInto('assets').values(asset).returningAll().executeTakeFirst() as any as Promise<AssetEntity>;
|
||||
return this.db.transaction().execute(async (tx) => {
|
||||
const newAsset = (await tx
|
||||
.insertInto('assets')
|
||||
.values(asset)
|
||||
.returningAll()
|
||||
.executeTakeFirst()) as any as AssetEntity;
|
||||
|
||||
await tx
|
||||
.insertInto('asset_user')
|
||||
.values({
|
||||
assetId: newAsset.id,
|
||||
userId: newAsset.ownerId,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
.execute();
|
||||
|
||||
return newAsset;
|
||||
});
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] })
|
||||
@@ -495,7 +512,6 @@ export class AssetRepository implements IAssetRepository {
|
||||
.$if(property === WithoutProperty.THUMBNAIL, (qb) =>
|
||||
qb
|
||||
.innerJoin('asset_job_status as job_status', 'assetId', 'assets.id')
|
||||
.select(withFiles)
|
||||
.where('assets.isVisible', '=', true)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||
import { ImmichTelemetry } from 'src/enum';
|
||||
import { clearEnvCache, ConfigRepository } from 'src/repositories/config.repository';
|
||||
|
||||
@@ -81,10 +80,13 @@ describe('getEnv', () => {
|
||||
const { database } = getEnv();
|
||||
expect(database).toEqual({
|
||||
config: {
|
||||
kysely: {
|
||||
dialect: expect.any(PostgresJSDialect),
|
||||
log: expect.any(Function),
|
||||
},
|
||||
kysely: expect.objectContaining({
|
||||
host: 'database',
|
||||
port: 5432,
|
||||
database: 'immich',
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
}),
|
||||
typeorm: expect.objectContaining({
|
||||
type: 'postgres',
|
||||
host: 'database',
|
||||
@@ -104,6 +106,94 @@ describe('getEnv', () => {
|
||||
const { database } = getEnv();
|
||||
expect(database).toMatchObject({ skipMigrations: true });
|
||||
});
|
||||
|
||||
it('should use DB_URL', () => {
|
||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich';
|
||||
const { database } = getEnv();
|
||||
expect(database.config.kysely).toMatchObject({
|
||||
host: 'database1',
|
||||
password: 'postgres2',
|
||||
user: 'postgres1',
|
||||
port: 54_320,
|
||||
database: 'immich',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle sslmode=require', () => {
|
||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=require';
|
||||
|
||||
const { database } = getEnv();
|
||||
|
||||
expect(database.config.kysely).toMatchObject({ ssl: {} });
|
||||
});
|
||||
|
||||
it('should handle sslmode=prefer', () => {
|
||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=prefer';
|
||||
|
||||
const { database } = getEnv();
|
||||
|
||||
expect(database.config.kysely).toMatchObject({ ssl: {} });
|
||||
});
|
||||
|
||||
it('should handle sslmode=verify-ca', () => {
|
||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=verify-ca';
|
||||
|
||||
const { database } = getEnv();
|
||||
|
||||
expect(database.config.kysely).toMatchObject({ ssl: {} });
|
||||
});
|
||||
|
||||
it('should handle sslmode=verify-full', () => {
|
||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=verify-full';
|
||||
|
||||
const { database } = getEnv();
|
||||
|
||||
expect(database.config.kysely).toMatchObject({ ssl: {} });
|
||||
});
|
||||
|
||||
it('should handle sslmode=no-verify', () => {
|
||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=no-verify';
|
||||
|
||||
const { database } = getEnv();
|
||||
|
||||
expect(database.config.kysely).toMatchObject({ ssl: { rejectUnauthorized: false } });
|
||||
});
|
||||
|
||||
it('should handle ssl=true', () => {
|
||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?ssl=true';
|
||||
|
||||
const { database } = getEnv();
|
||||
|
||||
expect(database.config.kysely).toMatchObject({ ssl: true });
|
||||
});
|
||||
|
||||
it('should reject invalid ssl', () => {
|
||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?ssl=invalid';
|
||||
|
||||
expect(() => getEnv()).toThrowError('Invalid ssl option: invalid');
|
||||
});
|
||||
|
||||
it('should handle socket: URLs', () => {
|
||||
process.env.DB_URL = 'socket:/run/postgresql?db=database1';
|
||||
|
||||
const { database } = getEnv();
|
||||
|
||||
expect(database.config.kysely).toMatchObject({
|
||||
host: '/run/postgresql',
|
||||
database: 'database1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle sockets in postgres: URLs', () => {
|
||||
process.env.DB_URL = 'postgres:///database2?host=/path/to/socket';
|
||||
|
||||
const { database } = getEnv();
|
||||
|
||||
expect(database.config.kysely).toMatchObject({
|
||||
host: '/path/to/socket',
|
||||
database: 'database2',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('redis', () => {
|
||||
|
||||
@@ -5,12 +5,11 @@ import { plainToInstance } from 'class-transformer';
|
||||
import { validateSync } from 'class-validator';
|
||||
import { Request, Response } from 'express';
|
||||
import { RedisOptions } from 'ioredis';
|
||||
import { KyselyConfig } from 'kysely';
|
||||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||
import { CLS_ID, ClsModuleOptions } from 'nestjs-cls';
|
||||
import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces';
|
||||
import { join, resolve } from 'node:path';
|
||||
import postgres, { Notice } from 'postgres';
|
||||
import { parse } from 'pg-connection-string';
|
||||
import { Notice } from 'postgres';
|
||||
import { citiesFile, excludePaths, IWorker } from 'src/constants';
|
||||
import { Telemetry } from 'src/decorators';
|
||||
import { EnvDto } from 'src/dtos/env.dto';
|
||||
@@ -20,6 +19,20 @@ import { QueueName } from 'src/interfaces/job.interface';
|
||||
import { setDifference } from 'src/utils/set';
|
||||
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js';
|
||||
|
||||
type Ssl = 'require' | 'allow' | 'prefer' | 'verify-full' | boolean | object;
|
||||
type PostgresConnectionConfig = {
|
||||
host?: string;
|
||||
password?: string;
|
||||
user?: string;
|
||||
port?: number;
|
||||
database?: string;
|
||||
client_encoding?: string;
|
||||
ssl?: Ssl;
|
||||
application_name?: string;
|
||||
fallback_application_name?: string;
|
||||
options?: string;
|
||||
};
|
||||
|
||||
export interface EnvData {
|
||||
host?: string;
|
||||
port: number;
|
||||
@@ -53,7 +66,7 @@ export interface EnvData {
|
||||
};
|
||||
|
||||
database: {
|
||||
config: { typeorm: PostgresConnectionOptions & DatabaseConnectionParams; kysely: KyselyConfig };
|
||||
config: { typeorm: PostgresConnectionOptions & DatabaseConnectionParams; kysely: PostgresConnectionConfig };
|
||||
skipMigrations: boolean;
|
||||
vectorExtension: VectorExtension;
|
||||
};
|
||||
@@ -124,6 +137,9 @@ const asSet = <T>(value: string | undefined, defaults: T[]) => {
|
||||
return new Set(values.length === 0 ? defaults : (values as T[]));
|
||||
};
|
||||
|
||||
const isValidSsl = (ssl?: string | boolean | object): ssl is Ssl =>
|
||||
typeof ssl !== 'string' || ssl === 'require' || ssl === 'allow' || ssl === 'prefer' || ssl === 'verify-full';
|
||||
|
||||
const getEnv = (): EnvData => {
|
||||
const dto = plainToInstance(EnvDto, process.env);
|
||||
const errors = validateSync(dto);
|
||||
@@ -185,6 +201,31 @@ const getEnv = (): EnvData => {
|
||||
}
|
||||
}
|
||||
|
||||
const parts = {
|
||||
connectionType: 'parts',
|
||||
host: dto.DB_HOSTNAME || 'database',
|
||||
port: dto.DB_PORT || 5432,
|
||||
username: dto.DB_USERNAME || 'postgres',
|
||||
password: dto.DB_PASSWORD || 'postgres',
|
||||
database: dto.DB_DATABASE_NAME || 'immich',
|
||||
} as const;
|
||||
|
||||
let parsedOptions: PostgresConnectionConfig = parts;
|
||||
if (dto.DB_URL) {
|
||||
const parsed = parse(dto.DB_URL);
|
||||
if (!isValidSsl(parsed.ssl)) {
|
||||
throw new Error(`Invalid ssl option: ${parsed.ssl}`);
|
||||
}
|
||||
|
||||
parsedOptions = {
|
||||
...parsed,
|
||||
ssl: parsed.ssl,
|
||||
host: parsed.host ?? undefined,
|
||||
port: parsed.port ? Number(parsed.port) : undefined,
|
||||
database: parsed.database ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const driverOptions = {
|
||||
onnotice: (notice: Notice) => {
|
||||
if (notice['severity'] !== 'NOTICE') {
|
||||
@@ -206,17 +247,9 @@ const getEnv = (): EnvData => {
|
||||
serialize: (value: number) => value.toString(),
|
||||
},
|
||||
},
|
||||
...parsedOptions,
|
||||
};
|
||||
|
||||
const parts = {
|
||||
connectionType: 'parts',
|
||||
host: dto.DB_HOSTNAME || 'database',
|
||||
port: dto.DB_PORT || 5432,
|
||||
username: dto.DB_USERNAME || 'postgres',
|
||||
password: dto.DB_PASSWORD || 'postgres',
|
||||
database: dto.DB_DATABASE_NAME || 'immich',
|
||||
} as const;
|
||||
|
||||
return {
|
||||
host: dto.IMMICH_HOST,
|
||||
port: dto.IMMICH_PORT || 2283,
|
||||
@@ -282,21 +315,7 @@ const getEnv = (): EnvData => {
|
||||
parseInt8: true,
|
||||
...(databaseUrl ? { connectionType: 'url', url: databaseUrl } : parts),
|
||||
},
|
||||
kysely: {
|
||||
dialect: new PostgresJSDialect({
|
||||
postgres: databaseUrl ? postgres(databaseUrl, driverOptions) : postgres({ ...parts, ...driverOptions }),
|
||||
}),
|
||||
log(event) {
|
||||
if (event.level === 'error') {
|
||||
console.error('Query failed :', {
|
||||
durationMs: event.queryDurationMillis,
|
||||
error: event.error,
|
||||
sql: event.query.sql,
|
||||
params: event.query.parameters,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
kysely: driverOptions,
|
||||
},
|
||||
|
||||
skipMigrations: dto.DB_SKIP_MIGRATIONS ?? false,
|
||||
|
||||
@@ -100,7 +100,6 @@ export class PersonRepository implements IPersonRepository {
|
||||
.$if(!!options.personId, (qb) => qb.where('asset_faces.personId', '=', options.personId!))
|
||||
.$if(!!options.sourceType, (qb) => qb.where('asset_faces.sourceType', '=', options.sourceType!))
|
||||
.$if(!!options.assetId, (qb) => qb.where('asset_faces.assetId', '=', options.assetId!))
|
||||
.$if(!!options.assetId, (qb) => qb.where('asset_faces.assetId', '=', options.assetId!))
|
||||
.stream() as AsyncIterableIterator<AssetFaceEntity>;
|
||||
}
|
||||
|
||||
@@ -109,7 +108,7 @@ export class PersonRepository implements IPersonRepository {
|
||||
.selectFrom('person')
|
||||
.selectAll('person')
|
||||
.$if(!!options.ownerId, (qb) => qb.where('person.ownerId', '=', options.ownerId!))
|
||||
.$if(!!options.thumbnailPath, (qb) => qb.where('person.thumbnailPath', '=', options.thumbnailPath!))
|
||||
.$if(options.thumbnailPath !== undefined, (qb) => qb.where('person.thumbnailPath', '=', options.thumbnailPath!))
|
||||
.$if(options.faceAssetId === null, (qb) => qb.where('person.faceAssetId', 'is', null))
|
||||
.$if(!!options.faceAssetId, (qb) => qb.where('person.faceAssetId', '=', options.faceAssetId!))
|
||||
.$if(options.isHidden !== undefined, (qb) => qb.where('person.isHidden', '=', options.isHidden!))
|
||||
|
||||
@@ -72,8 +72,14 @@ export class SearchRepository implements ISearchRepository {
|
||||
async searchRandom(size: number, options: AssetSearchOptions): Promise<AssetEntity[]> {
|
||||
const uuid = randomUUID();
|
||||
const builder = searchAssetBuilder(this.db, options);
|
||||
const lessThan = builder.where('assets.id', '<', uuid).orderBy('assets.id').limit(size);
|
||||
const greaterThan = builder.where('assets.id', '>', uuid).orderBy('assets.id').limit(size);
|
||||
const lessThan = builder
|
||||
.where('assets.id', '<', uuid)
|
||||
.orderBy(sql`random()`)
|
||||
.limit(size);
|
||||
const greaterThan = builder
|
||||
.where('assets.id', '>', uuid)
|
||||
.orderBy(sql`random()`)
|
||||
.limit(size);
|
||||
const { rows } = await sql`${lessThan} union all ${greaterThan} limit ${size}`.execute(this.db);
|
||||
return rows as any as AssetEntity[];
|
||||
}
|
||||
@@ -292,7 +298,7 @@ export class SearchRepository implements ISearchRepository {
|
||||
await sql`truncate ${sql.table('smart_search')}`.execute(trx);
|
||||
await trx.schema
|
||||
.alterTable('smart_search')
|
||||
.alterColumn('embedding', (col) => col.setDataType(sql.lit(`vector(${dimSize})`)))
|
||||
.alterColumn('embedding', (col) => col.setDataType(sql.raw(`vector(${dimSize})`)))
|
||||
.execute();
|
||||
await sql`reindex index clip_index`.execute(trx);
|
||||
});
|
||||
|
||||
@@ -52,8 +52,8 @@ describe(AlbumService.name, () => {
|
||||
it('gets list of albums for auth user', async () => {
|
||||
albumMock.getOwned.mockResolvedValue([albumStub.empty, albumStub.sharedWithUser]);
|
||||
albumMock.getMetadataForIds.mockResolvedValue([
|
||||
{ albumId: albumStub.empty.id, assetCount: 0, startDate: undefined, endDate: undefined },
|
||||
{ albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: undefined, endDate: undefined },
|
||||
{ albumId: albumStub.empty.id, assetCount: 0, startDate: null, endDate: null },
|
||||
{ albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: null, endDate: null },
|
||||
]);
|
||||
|
||||
const result = await sut.getAll(authStub.admin, {});
|
||||
@@ -82,7 +82,7 @@ describe(AlbumService.name, () => {
|
||||
it('gets list of albums that are shared', async () => {
|
||||
albumMock.getShared.mockResolvedValue([albumStub.sharedWithUser]);
|
||||
albumMock.getMetadataForIds.mockResolvedValue([
|
||||
{ albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: undefined, endDate: undefined },
|
||||
{ albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: null, endDate: null },
|
||||
]);
|
||||
|
||||
const result = await sut.getAll(authStub.admin, { shared: true });
|
||||
@@ -94,7 +94,7 @@ describe(AlbumService.name, () => {
|
||||
it('gets list of albums that are NOT shared', async () => {
|
||||
albumMock.getNotShared.mockResolvedValue([albumStub.empty]);
|
||||
albumMock.getMetadataForIds.mockResolvedValue([
|
||||
{ albumId: albumStub.empty.id, assetCount: 0, startDate: undefined, endDate: undefined },
|
||||
{ albumId: albumStub.empty.id, assetCount: 0, startDate: null, endDate: null },
|
||||
]);
|
||||
|
||||
const result = await sut.getAll(authStub.admin, { shared: false });
|
||||
|
||||
@@ -55,13 +55,7 @@ export class AlbumService extends BaseService {
|
||||
const results = await this.albumRepository.getMetadataForIds(albums.map((album) => album.id));
|
||||
const albumMetadata: Record<string, AlbumAssetCount> = {};
|
||||
for (const metadata of results) {
|
||||
const { albumId, assetCount, startDate, endDate } = metadata;
|
||||
albumMetadata[albumId] = {
|
||||
albumId,
|
||||
assetCount,
|
||||
startDate,
|
||||
endDate,
|
||||
};
|
||||
albumMetadata[metadata.albumId] = metadata;
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
@@ -70,9 +64,9 @@ export class AlbumService extends BaseService {
|
||||
return {
|
||||
...mapAlbumWithoutAssets(album),
|
||||
sharedLinks: undefined,
|
||||
startDate: albumMetadata[album.id].startDate,
|
||||
endDate: albumMetadata[album.id].endDate,
|
||||
assetCount: albumMetadata[album.id].assetCount,
|
||||
startDate: albumMetadata[album.id]?.startDate ?? undefined,
|
||||
endDate: albumMetadata[album.id]?.endDate ?? undefined,
|
||||
assetCount: albumMetadata[album.id]?.assetCount ?? 0,
|
||||
lastModifiedAssetTimestamp: lastModifiedAsset?.updatedAt,
|
||||
};
|
||||
}),
|
||||
@@ -89,9 +83,9 @@ export class AlbumService extends BaseService {
|
||||
|
||||
return {
|
||||
...mapAlbum(album, withAssets, auth),
|
||||
startDate: albumMetadataForIds.startDate,
|
||||
endDate: albumMetadataForIds.endDate,
|
||||
assetCount: albumMetadataForIds.assetCount,
|
||||
startDate: albumMetadataForIds?.startDate ?? undefined,
|
||||
endDate: albumMetadataForIds?.endDate ?? undefined,
|
||||
assetCount: albumMetadataForIds?.assetCount ?? 0,
|
||||
lastModifiedAssetTimestamp: lastModifiedAsset?.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -416,6 +416,34 @@ describe(AssetService.name, () => {
|
||||
await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], isArchived: true });
|
||||
expect(assetMock.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true });
|
||||
});
|
||||
|
||||
it('should not update Assets table if no relevant fields are provided', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
await sut.updateAll(authStub.admin, {
|
||||
ids: ['asset-1'],
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
isArchived: undefined,
|
||||
isFavorite: undefined,
|
||||
duplicateId: undefined,
|
||||
rating: undefined,
|
||||
});
|
||||
expect(assetMock.updateAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update Assets table if isArchived field is provided', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
await sut.updateAll(authStub.admin, {
|
||||
ids: ['asset-1'],
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
isArchived: undefined,
|
||||
isFavorite: false,
|
||||
duplicateId: undefined,
|
||||
rating: undefined,
|
||||
});
|
||||
expect(assetMock.updateAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteAll', () => {
|
||||
|
||||
@@ -142,7 +142,14 @@ export class AssetService extends BaseService {
|
||||
await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude });
|
||||
}
|
||||
|
||||
await this.assetRepository.updateAll(ids, options);
|
||||
if (
|
||||
options.isArchived != undefined ||
|
||||
options.isFavorite != undefined ||
|
||||
options.duplicateId != undefined ||
|
||||
options.rating != undefined
|
||||
) {
|
||||
await this.assetRepository.updateAll(ids, options);
|
||||
}
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.ASSET_DELETION_CHECK, queue: QueueName.BACKGROUND_TASK })
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||
import {
|
||||
DatabaseExtension,
|
||||
EXTENSION_NAMES,
|
||||
@@ -62,8 +61,11 @@ describe(DatabaseService.name, () => {
|
||||
database: {
|
||||
config: {
|
||||
kysely: {
|
||||
dialect: expect.any(PostgresJSDialect),
|
||||
log: ['error'],
|
||||
host: 'database',
|
||||
port: 5432,
|
||||
user: 'postgres',
|
||||
password: 'postgres',
|
||||
database: 'immich',
|
||||
},
|
||||
typeorm: {
|
||||
connectionType: 'parts',
|
||||
@@ -298,8 +300,11 @@ describe(DatabaseService.name, () => {
|
||||
database: {
|
||||
config: {
|
||||
kysely: {
|
||||
dialect: expect.any(PostgresJSDialect),
|
||||
log: ['error'],
|
||||
host: 'database',
|
||||
port: 5432,
|
||||
user: 'postgres',
|
||||
password: 'postgres',
|
||||
database: 'immich',
|
||||
},
|
||||
typeorm: {
|
||||
connectionType: 'parts',
|
||||
@@ -328,8 +333,11 @@ describe(DatabaseService.name, () => {
|
||||
database: {
|
||||
config: {
|
||||
kysely: {
|
||||
dialect: expect.any(PostgresJSDialect),
|
||||
log: ['error'],
|
||||
host: 'database',
|
||||
port: 5432,
|
||||
user: 'postgres',
|
||||
password: 'postgres',
|
||||
database: 'immich',
|
||||
},
|
||||
typeorm: {
|
||||
connectionType: 'parts',
|
||||
|
||||
@@ -337,12 +337,31 @@ describe(LibraryService.name, () => {
|
||||
|
||||
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.trashedOffline.id], {
|
||||
deletedAt: null,
|
||||
fileCreatedAt: assetStub.trashedOffline.fileModifiedAt,
|
||||
fileModifiedAt: assetStub.trashedOffline.fileModifiedAt,
|
||||
isOffline: false,
|
||||
originalFileName: 'path.jpg',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not touch fileCreatedAt when un-trashing an asset previously marked as offline', async () => {
|
||||
const mockAssetJob: ILibraryAssetJob = {
|
||||
id: assetStub.external.id,
|
||||
importPaths: ['/'],
|
||||
exclusionPatterns: [],
|
||||
};
|
||||
|
||||
assetMock.getById.mockResolvedValue(assetStub.trashedOffline);
|
||||
storageMock.stat.mockResolvedValue({ mtime: assetStub.trashedOffline.fileModifiedAt } as Stats);
|
||||
|
||||
await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||
|
||||
expect(assetMock.updateAll).toHaveBeenCalledWith(
|
||||
[assetStub.trashedOffline.id],
|
||||
expect.not.objectContaining({
|
||||
fileCreatedAt: expect.anything(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should update file when mtime has changed', async () => {
|
||||
@@ -360,7 +379,6 @@ describe(LibraryService.name, () => {
|
||||
|
||||
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.external.id], {
|
||||
fileModifiedAt: newMTime,
|
||||
fileCreatedAt: newMTime,
|
||||
isOffline: false,
|
||||
originalFileName: 'photo.jpg',
|
||||
deletedAt: null,
|
||||
|
||||
@@ -511,7 +511,6 @@ export class LibraryService extends BaseService {
|
||||
await this.assetRepository.updateAll([asset.id], {
|
||||
isOffline: false,
|
||||
deletedAt: null,
|
||||
fileCreatedAt: mtime,
|
||||
fileModifiedAt: mtime,
|
||||
originalFileName: parse(asset.originalPath).base,
|
||||
});
|
||||
|
||||
@@ -194,7 +194,7 @@ export class MediaService extends BaseService {
|
||||
await Promise.all(pathsToDelete.map((path) => this.storageRepository.unlink(path)));
|
||||
}
|
||||
|
||||
if (asset.thumbhash != generated.thumbhash) {
|
||||
if (!asset.thumbhash || Buffer.compare(asset.thumbhash, generated.thumbhash) !== 0) {
|
||||
await this.assetRepository.update({ id: asset.id, thumbhash: generated.thumbhash });
|
||||
}
|
||||
|
||||
|
||||
@@ -335,8 +335,8 @@ describe(MetadataService.name, () => {
|
||||
expect(assetMock.update).toHaveBeenCalledWith({
|
||||
id: assetStub.image.id,
|
||||
duration: null,
|
||||
fileCreatedAt: assetStub.image.createdAt,
|
||||
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
|
||||
fileCreatedAt: assetStub.image.fileCreatedAt,
|
||||
localDateTime: assetStub.image.fileCreatedAt,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1162,6 +1162,17 @@ describe(MetadataService.name, () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
it('should handle valid negative rating value', async () => {
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
mockReadTags({ Rating: -1 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
expect(assetMock.upsertExif).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rating: -1,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleQueueSidecar', () => {
|
||||
|
||||
@@ -204,7 +204,7 @@ export class MetadataService extends BaseService {
|
||||
// comments
|
||||
description: String(exifTags.ImageDescription || exifTags.Description || '').trim(),
|
||||
profileDescription: exifTags.ProfileDescription || null,
|
||||
rating: validateRange(exifTags.Rating, 0, 5),
|
||||
rating: validateRange(exifTags.Rating, -1, 5),
|
||||
|
||||
// grouping
|
||||
livePhotoCID: (exifTags.ContentIdentifier || exifTags.MediaGroupUUID) ?? null,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
|
||||
export const getAssetDateTime = (asset: AssetEntity | undefined) => {
|
||||
return asset?.exifInfo?.dateTimeOriginal || asset?.fileCreatedAt;
|
||||
};
|
||||
@@ -62,7 +62,7 @@ async function bootstrap() {
|
||||
app.use(app.get(ApiService).ssr(excludePaths));
|
||||
|
||||
const server = await (host ? app.listen(port, host) : app.listen(port));
|
||||
server.requestTimeout = 30 * 60 * 1000;
|
||||
server.requestTimeout = 24 * 60 * 60 * 1000;
|
||||
|
||||
logger.log(`Immich Server is listening on ${await app.getUrl()} [v${serverVersion}] [${environment}] `);
|
||||
}
|
||||
|
||||
4
server/test/fixtures/asset.stub.ts
vendored
4
server/test/fixtures/asset.stub.ts
vendored
@@ -210,7 +210,7 @@ export const assetStub = {
|
||||
encodedVideoPath: null,
|
||||
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
|
||||
localDateTime: new Date('2025-01-01T01:02:03.456Z'),
|
||||
isFavorite: true,
|
||||
isArchived: false,
|
||||
duration: null,
|
||||
@@ -574,7 +574,7 @@ export const assetStub = {
|
||||
encodedVideoPath: null,
|
||||
createdAt: new Date('2023-02-22T05:06:29.716Z'),
|
||||
updatedAt: new Date('2023-02-22T05:06:29.716Z'),
|
||||
localDateTime: new Date('2023-02-22T05:06:29.716Z'),
|
||||
localDateTime: new Date('2020-12-31T23:59:00.000Z'),
|
||||
isFavorite: false,
|
||||
isArchived: false,
|
||||
isExternal: false,
|
||||
|
||||
2
server/test/fixtures/shared-link.stub.ts
vendored
2
server/test/fixtures/shared-link.stub.ts
vendored
@@ -311,7 +311,7 @@ export const sharedLinkResponseStub = {
|
||||
allowUpload: false,
|
||||
allowDownload: false,
|
||||
showMetadata: false,
|
||||
album: { ...albumResponse, startDate: assetResponse.fileCreatedAt, endDate: assetResponse.fileCreatedAt },
|
||||
album: { ...albumResponse, startDate: assetResponse.localDateTime, endDate: assetResponse.localDateTime },
|
||||
assets: [{ ...assetResponseWithoutMetadata, exifInfo: undefined }],
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||
import postgres from 'postgres';
|
||||
import { ImmichEnvironment, ImmichWorker } from 'src/enum';
|
||||
import { DatabaseExtension } from 'src/interfaces/database.interface';
|
||||
import { EnvData } from 'src/repositories/config.repository';
|
||||
@@ -24,12 +22,7 @@ const envData: EnvData = {
|
||||
|
||||
database: {
|
||||
config: {
|
||||
kysely: {
|
||||
dialect: new PostgresJSDialect({
|
||||
postgres: postgres({ database: 'immich', host: 'database', port: 5432 }),
|
||||
}),
|
||||
log: ['error'],
|
||||
},
|
||||
kysely: { database: 'immich', host: 'database', port: 5432 },
|
||||
typeorm: {
|
||||
connectionType: 'parts',
|
||||
database: 'immich',
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Mocked, vitest } from 'vitest';
|
||||
export const newMediaRepositoryMock = (): Mocked<IMediaRepository> => {
|
||||
return {
|
||||
generateThumbnail: vitest.fn().mockImplementation(() => Promise.resolve()),
|
||||
generateThumbhash: vitest.fn().mockImplementation(() => Promise.resolve()),
|
||||
generateThumbhash: vitest.fn().mockResolvedValue(Buffer.from('')),
|
||||
decodeImage: vitest.fn().mockResolvedValue({ data: Buffer.from(''), info: {} }),
|
||||
extract: vitest.fn().mockResolvedValue(false),
|
||||
probe: vitest.fn(),
|
||||
|
||||
27
web/package-lock.json
generated
27
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "1.125.1",
|
||||
"version": "1.125.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich-web",
|
||||
"version": "1.125.1",
|
||||
"version": "1.125.6",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||
@@ -16,6 +16,8 @@
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@photo-sphere-viewer/core": "^5.11.5",
|
||||
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.11.5",
|
||||
"@photo-sphere-viewer/resolution-plugin": "^5.11.5",
|
||||
"@photo-sphere-viewer/settings-plugin": "^5.11.5",
|
||||
"@photo-sphere-viewer/video-plugin": "^5.11.5",
|
||||
"@zoom-image/svelte": "^0.3.0",
|
||||
"dom-to-image": "^2.6.0",
|
||||
@@ -75,7 +77,7 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.125.1",
|
||||
"version": "1.125.6",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
@@ -1669,6 +1671,25 @@
|
||||
"@photo-sphere-viewer/video-plugin": "5.11.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@photo-sphere-viewer/resolution-plugin": {
|
||||
"version": "5.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/resolution-plugin/-/resolution-plugin-5.11.5.tgz",
|
||||
"integrity": "sha512-Dbvp5bBtozD3IWt1Q0wORVaZBcB1bV9xUeoOS9A7F7b3EkQ2pkC5/jot/1AyM4wtU5wJ63NWHskQ1d7m6WWazQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@photo-sphere-viewer/core": "5.11.5",
|
||||
"@photo-sphere-viewer/settings-plugin": "5.11.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@photo-sphere-viewer/settings-plugin": {
|
||||
"version": "5.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/settings-plugin/-/settings-plugin-5.11.5.tgz",
|
||||
"integrity": "sha512-ZgYaWjiBMhsoRH5ddW3h+v4J4LPmofsT7BBRq5UCssWw2Fsrvv7mFFRi4UbZ1qzeKmvNUOr8BaFQgX1ZLvUWfQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@photo-sphere-viewer/core": "5.11.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@photo-sphere-viewer/video-plugin": {
|
||||
"version": "5.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.11.5.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "1.125.1",
|
||||
"version": "1.125.6",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"scripts": {
|
||||
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
||||
@@ -72,6 +72,8 @@
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@photo-sphere-viewer/core": "^5.11.5",
|
||||
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.11.5",
|
||||
"@photo-sphere-viewer/resolution-plugin": "^5.11.5",
|
||||
"@photo-sphere-viewer/settings-plugin": "^5.11.5",
|
||||
"@photo-sphere-viewer/video-plugin": "^5.11.5",
|
||||
"@zoom-image/svelte": "^0.3.0",
|
||||
"dom-to-image": "^2.6.0",
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
let { album }: Props = $props();
|
||||
|
||||
const formatDate = (date?: string) => {
|
||||
return date ? new Date(date).toLocaleDateString($locale, dateFormats.album) : undefined;
|
||||
const dateWithoutTimeZone = date?.slice(0, -1);
|
||||
return dateWithoutTimeZone
|
||||
? new Date(dateWithoutTimeZone).toLocaleDateString($locale, dateFormats.album)
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const getDateRange = (start?: string, end?: string) => {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
{:then [data, { default: PhotoSphereViewer }]}
|
||||
<PhotoSphereViewer
|
||||
panorama={data}
|
||||
originalImageUrl={isWebCompatibleImage(asset) ? getAssetOriginalUrl(asset.id) : undefined}
|
||||
originalPanorama={isWebCompatibleImage(asset) ? getAssetOriginalUrl(asset.id) : undefined}
|
||||
/>
|
||||
{:catch}
|
||||
{$t('errors.failed_to_load_asset')}
|
||||
|
||||
@@ -7,18 +7,21 @@
|
||||
type AdapterConstructor,
|
||||
type PluginConstructor,
|
||||
} from '@photo-sphere-viewer/core';
|
||||
import { SettingsPlugin } from '@photo-sphere-viewer/settings-plugin';
|
||||
import { ResolutionPlugin } from '@photo-sphere-viewer/resolution-plugin';
|
||||
import '@photo-sphere-viewer/core/index.css';
|
||||
import '@photo-sphere-viewer/settings-plugin/index.css';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
panorama: string | { source: string };
|
||||
originalImageUrl?: string;
|
||||
originalPanorama?: string | { source: string };
|
||||
adapter?: AdapterConstructor | [AdapterConstructor, unknown];
|
||||
plugins?: (PluginConstructor | [PluginConstructor, unknown])[];
|
||||
navbar?: boolean;
|
||||
}
|
||||
|
||||
let { panorama, originalImageUrl, adapter = EquirectangularAdapter, plugins = [], navbar = false }: Props = $props();
|
||||
let { panorama, originalPanorama, adapter = EquirectangularAdapter, plugins = [], navbar = false }: Props = $props();
|
||||
|
||||
let container: HTMLDivElement | undefined = $state();
|
||||
let viewer: Viewer;
|
||||
@@ -30,9 +33,33 @@
|
||||
|
||||
viewer = new Viewer({
|
||||
adapter,
|
||||
plugins,
|
||||
plugins: [
|
||||
SettingsPlugin,
|
||||
[
|
||||
ResolutionPlugin,
|
||||
{
|
||||
defaultResolution: $alwaysLoadOriginalFile && originalPanorama ? 'original' : 'default',
|
||||
resolutions: [
|
||||
{
|
||||
id: 'default',
|
||||
label: 'Default',
|
||||
panorama,
|
||||
},
|
||||
...(originalPanorama
|
||||
? [
|
||||
{
|
||||
id: 'original',
|
||||
label: 'Original',
|
||||
panorama: originalPanorama,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
],
|
||||
...plugins,
|
||||
],
|
||||
container,
|
||||
panorama,
|
||||
touchmoveTwoFingers: false,
|
||||
mousewheelCtrlKey: false,
|
||||
navbar,
|
||||
@@ -40,15 +67,14 @@
|
||||
maxFov: 120,
|
||||
fisheye: false,
|
||||
});
|
||||
const resolutionPlugin = viewer.getPlugin(ResolutionPlugin) as ResolutionPlugin;
|
||||
|
||||
if (originalImageUrl && !$alwaysLoadOriginalFile) {
|
||||
if (originalPanorama && !$alwaysLoadOriginalFile) {
|
||||
const zoomHandler = ({ zoomLevel }: events.ZoomUpdatedEvent) => {
|
||||
// zoomLevel range: [0, 100]
|
||||
if (Math.round(zoomLevel) >= 75) {
|
||||
// Replace the preview with the original
|
||||
viewer.setPanorama(originalImageUrl, { showLoader: false, speed: 150 }).catch(() => {
|
||||
viewer.setPanorama(panorama, { showLoader: false, speed: 0 }).catch(() => {});
|
||||
});
|
||||
void resolutionPlugin.setResolution('original');
|
||||
viewer.removeEventListener(events.ZoomUpdatedEvent.type, zoomHandler);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { getAssetOriginalUrl } from '$lib/utils';
|
||||
import { getAssetPlaybackUrl, getAssetOriginalUrl } from '$lib/utils';
|
||||
import { fade } from 'svelte/transition';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
@@ -22,7 +22,13 @@
|
||||
{#await modules}
|
||||
<LoadingSpinner />
|
||||
{:then [PhotoSphereViewer, adapter, videoPlugin]}
|
||||
<PhotoSphereViewer panorama={{ source: getAssetOriginalUrl(assetId) }} plugins={[videoPlugin]} {adapter} navbar />
|
||||
<PhotoSphereViewer
|
||||
panorama={{ source: getAssetPlaybackUrl(assetId) }}
|
||||
originalPanorama={{ source: getAssetOriginalUrl(assetId) }}
|
||||
plugins={[videoPlugin]}
|
||||
{adapter}
|
||||
navbar
|
||||
/>
|
||||
{:catch}
|
||||
{$t('errors.failed_to_load_asset')}
|
||||
{/await}
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
|
||||
<section class="min-w-screen flex min-h-dvh items-center justify-center relative">
|
||||
<div class="absolute -z-10 w-full h-full flex place-items-center place-content-center">
|
||||
<img src={immichLogo} class="max-w-screen-md mx-auto h-full mb-2 antialiased -z-10" alt="Immich logo" />
|
||||
<img
|
||||
src={immichLogo}
|
||||
class="max-w-screen-md mx-auto h-full mb-2 antialiased -z-10 overflow-hidden"
|
||||
alt="Immich logo"
|
||||
/>
|
||||
<div
|
||||
class="w-full h-[99%] absolute left-0 top-0 backdrop-blur-[200px] bg-transparent dark:bg-immich-dark-bg/20"
|
||||
></div>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</script>
|
||||
|
||||
<ul class="list-none ml-2">
|
||||
{#each Object.entries(items) as [path, tree]}
|
||||
{#each Object.entries(items).sort() as [path, tree]}
|
||||
{@const value = normalizeTreePath(`${parent}/${path}`)}
|
||||
{@const key = value + getColor(value)}
|
||||
{#key key}
|
||||
|
||||
@@ -24,7 +24,6 @@ class FoldersStore {
|
||||
|
||||
const uniquePaths = await getUniqueOriginalPaths();
|
||||
this.uniquePaths.push(...uniquePaths);
|
||||
this.uniquePaths.sort();
|
||||
}
|
||||
|
||||
bustAssetCache() {
|
||||
|
||||
@@ -157,7 +157,6 @@ async function fileUploader(
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error calculating sha1 file=${assetFile.name})`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
let pathSegments = $derived(data.path ? data.path.split('/') : []);
|
||||
let tree = $derived(buildTree(foldersStore.uniquePaths));
|
||||
let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || '');
|
||||
let currentTreeItems = $derived(currentPath ? data.currentFolders : Object.keys(tree));
|
||||
let currentTreeItems = $derived(currentPath ? data.currentFolders : Object.keys(tree).sort());
|
||||
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export const load = (async ({ params, url }) => {
|
||||
return {
|
||||
asset,
|
||||
path,
|
||||
currentFolders: Object.keys(tree || {}),
|
||||
currentFolders: Object.keys(tree || {}).sort(),
|
||||
pathAssets,
|
||||
meta: {
|
||||
title: $t('folders'),
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
let personMerge1: PersonResponseDto | undefined = $state();
|
||||
let personMerge2: PersonResponseDto | undefined = $state();
|
||||
let potentialMergePeople: PersonResponseDto[] = $state([]);
|
||||
let isSuggestionSelectedByUser = $state(false);
|
||||
|
||||
let personName = '';
|
||||
let suggestedPeople: PersonResponseDto[] = $state([]);
|
||||
@@ -233,15 +234,22 @@
|
||||
personName = person.name;
|
||||
personMerge1 = person;
|
||||
personMerge2 = person2;
|
||||
isSuggestionSelectedByUser = true;
|
||||
viewMode = PersonPageViewMode.SUGGEST_MERGE;
|
||||
};
|
||||
|
||||
const changeName = async () => {
|
||||
viewMode = PersonPageViewMode.VIEW_ASSETS;
|
||||
person.name = personName;
|
||||
try {
|
||||
isEditingName = false;
|
||||
isEditingName = false;
|
||||
|
||||
if (isSuggestionSelectedByUser) {
|
||||
// User canceled the merge
|
||||
isSuggestionSelectedByUser = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
person = await updatePerson({ id: person.id, personUpdateDto: { name: personName } });
|
||||
|
||||
notificationController.show({
|
||||
|
||||
Reference in New Issue
Block a user