Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b0fff3b3d | ||
|
|
ec7015be88 | ||
|
|
19fafd8c10 | ||
|
|
e47a89b274 | ||
|
|
66650f5944 | ||
|
|
0529076ed7 | ||
|
|
7f854432ae | ||
|
|
15a2e6feeb | ||
|
|
4ed68cf673 | ||
|
|
8337da183c | ||
|
|
6dfa9e1146 | ||
|
|
282bccaca5 | ||
|
|
62d307321a | ||
|
|
f7afc0334e | ||
|
|
28e8e539f6 | ||
|
|
7cc19b50fc | ||
|
|
97c340b8a4 | ||
|
|
7b1d4a6787 | ||
|
|
0714d119d7 | ||
|
|
700622e521 | ||
|
|
3682e76dee | ||
|
|
cd0e537e3e | ||
|
|
0849dbd1af | ||
|
|
4ab4a35eba | ||
|
|
e5d9372708 | ||
|
|
8edc2fb46f | ||
|
|
e520c0d1f5 | ||
|
|
506f9f6fb9 | ||
|
|
3cb8f54307 | ||
|
|
ee4d9fff16 | ||
|
|
27be813011 | ||
|
|
861b72ef04 | ||
|
|
fd83280b70 | ||
|
|
169d9d18b0 | ||
|
|
245535ee04 | ||
|
|
5bc9158724 | ||
|
|
6a4bc777a2 | ||
|
|
34cbb18ecd | ||
|
|
e2d5a8c0bb | ||
|
|
94cd806675 | ||
|
|
b6af7788e1 | ||
|
|
c4bb9f49ff | ||
|
|
8e5695f06d | ||
|
|
395c28f5fa | ||
|
|
3e5183606c | ||
|
|
6a36bbd1d1 | ||
|
|
4b39d37cae | ||
|
|
25c9b779e4 | ||
|
|
fcc3b81745 | ||
|
|
6f677b4fae | ||
|
|
78f202603c | ||
|
|
b8c5363a15 | ||
|
|
b8b3c487d4 | ||
|
|
ec48fccb30 | ||
|
|
3f61019ca1 |
@@ -1,4 +1,4 @@
|
||||
FROM node:20-alpine3.19@sha256:bf77dc26e48ea95fca9d1aceb5acfa69d2e546b765ec2abfb502975f1a2d4def as core
|
||||
FROM node:20-alpine3.19@sha256:ef3f47741e161900ddd07addcaca7e76534a9205e4cd73b2ed091ba339004a75 as core
|
||||
|
||||
WORKDIR /usr/src/open-api/typescript-sdk
|
||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||
|
||||
114
cli/package-lock.json
generated
114
cli/package-lock.json
generated
@@ -47,7 +47,7 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.100.0",
|
||||
"version": "1.101.0",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
@@ -1251,16 +1251,16 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.3.1.tgz",
|
||||
"integrity": "sha512-STEDMVQGww5lhCuNXVSQfbfuNII5E08QWkvAw5Qwf+bj2WT+JkG1uc+5/vXA3AOYMDHVOSpL+9rcbEUiHIm2dw==",
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz",
|
||||
"integrity": "sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.5.1",
|
||||
"@typescript-eslint/scope-manager": "7.3.1",
|
||||
"@typescript-eslint/type-utils": "7.3.1",
|
||||
"@typescript-eslint/utils": "7.3.1",
|
||||
"@typescript-eslint/visitor-keys": "7.3.1",
|
||||
"@typescript-eslint/scope-manager": "7.4.0",
|
||||
"@typescript-eslint/type-utils": "7.4.0",
|
||||
"@typescript-eslint/utils": "7.4.0",
|
||||
"@typescript-eslint/visitor-keys": "7.4.0",
|
||||
"debug": "^4.3.4",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.2.4",
|
||||
@@ -1286,15 +1286,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.3.1.tgz",
|
||||
"integrity": "sha512-Rq49+pq7viTRCH48XAbTA+wdLRrB/3sRq4Lpk0oGDm0VmnjBrAOVXH/Laalmwsv2VpekiEfVFwJYVk6/e8uvQw==",
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz",
|
||||
"integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "7.3.1",
|
||||
"@typescript-eslint/types": "7.3.1",
|
||||
"@typescript-eslint/typescript-estree": "7.3.1",
|
||||
"@typescript-eslint/visitor-keys": "7.3.1",
|
||||
"@typescript-eslint/scope-manager": "7.4.0",
|
||||
"@typescript-eslint/types": "7.4.0",
|
||||
"@typescript-eslint/typescript-estree": "7.4.0",
|
||||
"@typescript-eslint/visitor-keys": "7.4.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1314,13 +1314,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.3.1.tgz",
|
||||
"integrity": "sha512-fVS6fPxldsKY2nFvyT7IP78UO1/I2huG+AYu5AMjCT9wtl6JFiDnsv4uad4jQ0GTFzcUV5HShVeN96/17bTBag==",
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz",
|
||||
"integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.3.1",
|
||||
"@typescript-eslint/visitor-keys": "7.3.1"
|
||||
"@typescript-eslint/types": "7.4.0",
|
||||
"@typescript-eslint/visitor-keys": "7.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@@ -1331,13 +1331,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.3.1.tgz",
|
||||
"integrity": "sha512-iFhaysxFsMDQlzJn+vr3OrxN8NmdQkHks4WaqD4QBnt5hsq234wcYdyQ9uquzJJIDAj5W4wQne3yEsYA6OmXGw==",
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz",
|
||||
"integrity": "sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "7.3.1",
|
||||
"@typescript-eslint/utils": "7.3.1",
|
||||
"@typescript-eslint/typescript-estree": "7.4.0",
|
||||
"@typescript-eslint/utils": "7.4.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
},
|
||||
@@ -1358,9 +1358,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.3.1.tgz",
|
||||
"integrity": "sha512-2tUf3uWggBDl4S4183nivWQ2HqceOZh1U4hhu4p1tPiIJoRRXrab7Y+Y0p+dozYwZVvLPRI6r5wKe9kToF9FIw==",
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.4.0.tgz",
|
||||
"integrity": "sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@@ -1371,13 +1371,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.3.1.tgz",
|
||||
"integrity": "sha512-tLpuqM46LVkduWP7JO7yVoWshpJuJzxDOPYIVWUUZbW+4dBpgGeUdl/fQkhuV0A8eGnphYw3pp8d2EnvPOfxmQ==",
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz",
|
||||
"integrity": "sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.3.1",
|
||||
"@typescript-eslint/visitor-keys": "7.3.1",
|
||||
"@typescript-eslint/types": "7.4.0",
|
||||
"@typescript-eslint/visitor-keys": "7.4.0",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -1399,17 +1399,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.3.1.tgz",
|
||||
"integrity": "sha512-jIERm/6bYQ9HkynYlNZvXpzmXWZGhMbrOvq3jJzOSOlKXsVjrrolzWBjDW6/TvT5Q3WqaN4EkmcfdQwi9tDjBQ==",
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.4.0.tgz",
|
||||
"integrity": "sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@types/json-schema": "^7.0.12",
|
||||
"@types/semver": "^7.5.0",
|
||||
"@typescript-eslint/scope-manager": "7.3.1",
|
||||
"@typescript-eslint/types": "7.3.1",
|
||||
"@typescript-eslint/typescript-estree": "7.3.1",
|
||||
"@typescript-eslint/scope-manager": "7.4.0",
|
||||
"@typescript-eslint/types": "7.4.0",
|
||||
"@typescript-eslint/typescript-estree": "7.4.0",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1424,12 +1424,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.3.1.tgz",
|
||||
"integrity": "sha512-9RMXwQF8knsZvfv9tdi+4D/j7dMG28X/wMJ8Jj6eOHyHWwDW4ngQJcqEczSsqIKKjFiLFr40Mnr7a5ulDD3vmw==",
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz",
|
||||
"integrity": "sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.3.1",
|
||||
"@typescript-eslint/types": "7.4.0",
|
||||
"eslint-visitor-keys": "^3.4.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2555,16 +2555,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.3.10",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
|
||||
"integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
|
||||
"version": "10.3.12",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz",
|
||||
"integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^2.3.5",
|
||||
"jackspeak": "^2.3.6",
|
||||
"minimatch": "^9.0.1",
|
||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
|
||||
"path-scurry": "^1.10.1"
|
||||
"minipass": "^7.0.4",
|
||||
"path-scurry": "^1.10.2"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
@@ -3411,12 +3411,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
|
||||
"integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz",
|
||||
"integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^9.1.1 || ^10.0.0",
|
||||
"lru-cache": "^10.2.0",
|
||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4456,13 +4456,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.3.tgz",
|
||||
"integrity": "sha512-+i1oagbvkVIhEy9TnEV+fgXsng13nZM90JQbrcPrf6DvW2mXARlz+DK7DLiDP+qeKoD1FCVx/1SpFL1CLq9Mhw==",
|
||||
"version": "5.2.7",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.7.tgz",
|
||||
"integrity": "sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.20.1",
|
||||
"postcss": "^8.4.36",
|
||||
"postcss": "^8.4.38",
|
||||
"rollup": "^4.13.0"
|
||||
},
|
||||
"bin": {
|
||||
|
||||
@@ -76,7 +76,7 @@ services:
|
||||
container_name: immich_prometheus
|
||||
ports:
|
||||
- 9090:9090
|
||||
image: prom/prometheus@sha256:5ccad477d0057e62a7cd1981ffcc43785ac10c5a35522dc207466ff7e7ec845f
|
||||
image: prom/prometheus@sha256:dec2018ae55885fed717f25c289b8c9cff0bf5fbb9e619fb49b6161ac493c016
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus-data:/prometheus
|
||||
|
||||
@@ -253,8 +253,19 @@ The initial backup is the most intensive due to the number of jobs running. The
|
||||
|
||||
### Can I limit the amount of CPU and RAM usage?
|
||||
|
||||
By default, a container has no resource constraints and can use as much of a given resource as the host's kernel scheduler allows.
|
||||
You can look at the [original docker docs](https://docs.docker.com/config/containers/resource_constraints/) or use this [guide](https://www.baeldung.com/ops/docker-memory-limit) to learn how to limit this.
|
||||
By default, a container has no resource constraints and can use as much of a given resource as the host's kernel scheduler allows. To limit this, you can add the following to the `docker-compose.yml` block of any containers that you want to have limited resources.
|
||||
|
||||
```yaml
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
# Number of CPU threads
|
||||
cpus: '1.00'
|
||||
# Gigabytes of memory
|
||||
memory: '1G'
|
||||
```
|
||||
|
||||
For more details, you can look at the [original docker docs](https://docs.docker.com/config/containers/resource_constraints/) or use this [guide](https://www.baeldung.com/ops/docker-memory-limit).
|
||||
|
||||
### How can I boost machine learning speed?
|
||||
|
||||
@@ -296,6 +307,17 @@ You may need to add mount points or docker volumes for the following internal co
|
||||
|
||||
The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION`.
|
||||
|
||||
For a further hardened system, you can add the following block to every container except for `immich_postgres`.
|
||||
|
||||
```yaml
|
||||
security_opt:
|
||||
# Prevent escalation of privileges after container is started
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
# Prevent access to raw network traffic
|
||||
- NET_RAW
|
||||
```
|
||||
|
||||
### How can I **purge** data from Immich?
|
||||
|
||||
Data for Immich comes in two forms:
|
||||
|
||||
@@ -20,7 +20,7 @@ The recommended way to backup and restore the Immich database is to use the `pg_
|
||||
<Tabs>
|
||||
<TabItem value="Linux system based Backup" label="Linux system based Backup" default>
|
||||
|
||||
```bash title='Bash'
|
||||
```bash title='Backup'
|
||||
docker exec -t immich_postgres pg_dumpall -c -U postgres | gzip > "/path/to/backup/dump.sql.gz"
|
||||
```
|
||||
|
||||
|
||||
54
docs/docs/administration/postgres-standalone.md
Normal file
54
docs/docs/administration/postgres-standalone.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Pre-existing Postgres
|
||||
|
||||
While not officially recommended, it is possible to run Immich using a pre-existing Postgres server. To use this setup, you should have a baseline level of familiarity with Postgres and the Linux command line. If you do not have these, we recommend using the default setup with a dedicated Postgres container.
|
||||
|
||||
By default, Immich expects superuser permission on the Postgres database and requires certain extensions to be installed. This guide outlines the steps required to prepare a pre-existing Postgres server to be used by Immich.
|
||||
|
||||
:::tip
|
||||
Running with a pre-existing Postgres server can unlock powerful administrative features, including logical replication, data page checksums, and streaming write-ahead log backups using programs like pgBackRest or Barman.
|
||||
:::
|
||||
|
||||
## Prerequisites
|
||||
|
||||
You must install pgvecto.rs using their [instructions](https://docs.pgvecto.rs/getting-started/installation.html). After installation, add `shared_preload_libraries = 'vectors.so'` to your `postgresql.conf`. If you already have some `shared_preload_libraries` set, you can separate each extension with a comma. For example, `shared_preload_libraries = 'pg_stat_statements, vectors.so'`.
|
||||
|
||||
:::note
|
||||
Make sure the installed version of pgvecto.rs is compatible with your version of Immich. For example, if your Immich version uses the dedicated database image `tensorchord/pgvecto-rs:pg14-v0.2.1`, you must install pgvecto.rs `>= 0.2.1, < 0.3.0`.
|
||||
:::
|
||||
|
||||
## Specifying the connection URL
|
||||
|
||||
You can connect to your pre-existing Postgres server by setting the `DB_URL` environment variable in the `.env` file.
|
||||
|
||||
```
|
||||
DB_URL='postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename'
|
||||
|
||||
# require a SSL connection to Postgres
|
||||
# DB_URL='postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename?sslmode=require'
|
||||
|
||||
# require a SSL connection, but don't enforce checking the certificate name
|
||||
# DB_URL='postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename?sslmode=require&sslmode=no-verify'
|
||||
```
|
||||
|
||||
## Without superuser permissions
|
||||
|
||||
### Initial installation
|
||||
|
||||
Immich can run without superuser permissions by following the below instructions at the `psql` prompt to prepare the database.
|
||||
|
||||
```sql title="Set up Postgres for Immich"
|
||||
CREATE DATABASE <immichdatabasename>;
|
||||
\c <immichdatabasename>
|
||||
BEGIN;
|
||||
ALTER DATABASE <immichdatabasename> OWNER TO <immichdbusername>;
|
||||
CREATE EXTENSION vectors;
|
||||
CREATE EXTENSION earthdistance CASCADE;
|
||||
ALTER DATABASE <immichdatabasename> SET search_path TO "$user", public, vectors;
|
||||
GRANT USAGE ON SCHEMA vectors TO <immichdbusername>;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA vectors GRANT SELECT ON TABLES TO <immichdbusername>;
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
### Updating pgvecto.rs
|
||||
|
||||
When installing a new version of pgvecto.rs, you will need to manually update the extension by connecting to the Immich database and running `ALTER EXTENSION vectors UPDATE;`.
|
||||
@@ -1,29 +1,41 @@
|
||||
# Reverse Proxy
|
||||
|
||||
Users can deploy a custom reverse proxy that forwards requests to Immich. This way, the reverse proxy can handle TLS termination, load balancing, or other advanced features. All reverse proxies between Immich and the user must forward all headers and set the `Host`, `X-Forwarded-Host`, `X-Forwarded-Proto` and `X-Forwarded-For` headers to their appropriate values. Additionally, your reverse proxy should allow for big enough uploads. By following these practices, you ensure that all custom reverse proxies are fully compatible with Immich.
|
||||
Users can deploy a custom reverse proxy that forwards requests to Immich. This way, the reverse proxy can handle TLS termination, load balancing, or other advanced features. All reverse proxies between Immich and the user must forward all headers and set the `Host`, `X-Real-IP`, `X-Forwarded-Proto` and `X-Forwarded-For` headers to their appropriate values. Additionally, your reverse proxy should allow for big enough uploads. By following these practices, you ensure that all custom reverse proxies are fully compatible with Immich.
|
||||
|
||||
:::note
|
||||
The Repair page can take a long time to load. To avoid server timeouts or errors, we recommend specifying a timeout of at least 10 minutes on your proxy server.
|
||||
:::
|
||||
|
||||
### Nginx example config
|
||||
|
||||
Below is an example config for nginx. Make sure to include `client_max_body_size 50000M;` also in a `http` block in `/etc/nginx/nginx.conf`.
|
||||
Below is an example config for nginx. Make sure to set `public_url` to the front-facing URL of your instance, and `backend_url` to the path of the Immich server.
|
||||
|
||||
```nginx
|
||||
server {
|
||||
server_name <snip>
|
||||
server_name <public_url>;
|
||||
|
||||
# allow large file uploads
|
||||
client_max_body_size 50000M;
|
||||
|
||||
location / {
|
||||
proxy_pass http://<snip>:2283;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# Set headers
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# http://nginx.org/en/docs/http/websocket.html
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_redirect off;
|
||||
# enable websockets: http://nginx.org/en/docs/http/websocket.html
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_redirect off;
|
||||
|
||||
# set timeout
|
||||
proxy_read_timeout 600s;
|
||||
proxy_send_timeout 600s;
|
||||
send_timeout 600s;
|
||||
|
||||
location / {
|
||||
proxy_pass http://<backend_url>:2283;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -42,15 +54,13 @@ immich.example.org {
|
||||
|
||||
Below is an example config for Apache2 site configuration.
|
||||
|
||||
```
|
||||
```ApacheConf
|
||||
<VirtualHost *:80>
|
||||
ServerName <snip>
|
||||
ProxyRequests Off
|
||||
# set timeout in seconds
|
||||
ProxyPass / http://127.0.0.1:2283/ timeout=600 upgrade=websocket
|
||||
ProxyPassReverse / http://127.0.0.1:2283/
|
||||
ProxyPreserveHost On
|
||||
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
**timeout:** is measured in seconds, and it is particularly useful when long operations are triggered (i.e. Repair), so the server doesn't return an error.
|
||||
|
||||
@@ -18,12 +18,11 @@ Thanks for being interested in contributing 😊
|
||||
|
||||
### Server and web app
|
||||
|
||||
This environment includes the following services:
|
||||
This environment includes the services below. Additional details are available in each service's README.
|
||||
|
||||
- Core server - `/server/src/immich`
|
||||
- Machine learning - `/machine-learning`
|
||||
- Microservices - `/server/src/microservicess`
|
||||
- Web app - `/web`
|
||||
- Server - [`/server`](https://github.com/immich-app/immich/tree/main/server)
|
||||
- Web app - [`/web`](https://github.com/immich-app/immich/tree/main/web)
|
||||
- Machine learning - [`/machine-learning`](https://github.com/immich-app/immich/tree/main/machine-learning)
|
||||
- Redis
|
||||
- PostgreSQL development database with exposed port `5432` so you can use any database client to acess it
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ The metrics in immich are grouped into API (endpoint calls and response times),
|
||||
|
||||
Immich will not expose an endpoint for metrics by default. To enable this endpoint, you can add the `IMMICH_METRICS=true` environmental variable to your `.env` file. Note that only the server and microservices containers currently use this variable.
|
||||
|
||||
:::note
|
||||
`IMMICH_METRICS` is equivalent to enabling the following three environmental variables: `IMMICH_API_METRICS`, `IMMICH_HOST_METRICS`, and `IMMICH_IO_METRICS`. If you would like to only expose certain kinds of metrics, you can set only those environmental variables to `true`. Explicitly setting the environmental variable for a metric group overrides `IMMICH_METRICS` for that group.
|
||||
:::tip
|
||||
`IMMICH_METRICS` enables all metrics, but there are also [environmental variables](/docs/install/environment-variables.md#prometheus) to toggle specific metric groups. If you'd like to only expose certain kinds of metrics, you can set only those environmental variables to `true`. Explicitly setting the environmental variable for a metric group overrides `IMMICH_METRICS` for that group. For example, setting `IMMICH_METRICS=true` and `IMMICH_API_METRICS=false` will enable all metrics except API metrics.
|
||||
:::
|
||||
|
||||
The next step is to configure a new or existing Prometheus instance to scrape this endpoint. The following steps assume that you do not have an existing Prometheus instance, but the steps will be similar either way.
|
||||
|
||||
42
docs/docs/features/supported-formats.md
Normal file
42
docs/docs/features/supported-formats.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Supported formats
|
||||
|
||||
Immich supports a number of image and video formats, the most common of which are outlined here.
|
||||
|
||||
:::note
|
||||
For the full list, you can refer to the [Immich source code](https://github.com/immich-app/immich/blob/main/server/src/utils/mime-types.ts).
|
||||
:::
|
||||
|
||||
## Image formats
|
||||
|
||||
| Format | Extension(s) | Supported? | Notes |
|
||||
| :-------- | :---------------------------- | :----------------: | :-------------- |
|
||||
| `AVIF` | `.avif` | :white_check_mark: | |
|
||||
| `BMP` | `.bmp` | :white_check_mark: | |
|
||||
| `GIF` | `.gif` | :white_check_mark: | |
|
||||
| `HEIC` | `.heic` | :white_check_mark: | |
|
||||
| `HEIF` | `.heif` | :white_check_mark: | |
|
||||
| `JPEG` | `.jpeg` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
|
||||
| `JPEG XL` | `.jxl` | :white_check_mark: | |
|
||||
| `PNG` | `.png` | :white_check_mark: | |
|
||||
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
|
||||
| `RAW` | `.raw` | :white_check_mark: | |
|
||||
| `RW2` | `.rw2` | :white_check_mark: | |
|
||||
| `SVG` | `.svg` | :white_check_mark: | |
|
||||
| `TIFF` | `.tif` `.tiff` | :white_check_mark: | |
|
||||
| `WEBP` | `.webp` | :white_check_mark: | |
|
||||
|
||||
## Video formats
|
||||
|
||||
| Format | Extension(s) | Supported? | Notes |
|
||||
| :---------- | :-------------------- | :----------------: | :---- |
|
||||
| `3GPP` | `.3gp` `.3gpp` | :white_check_mark: | |
|
||||
| `AVI` | `.avi` | :white_check_mark: | |
|
||||
| `FLV` | `.flv` | :white_check_mark: | |
|
||||
| `M4V` | `.m4v` | :white_check_mark: | |
|
||||
| `MATROSKA` | `.mkv` | :white_check_mark: | |
|
||||
| `MP2T` | `.mts` `.m2ts` | :white_check_mark: | |
|
||||
| `MP4` | `.mp4` `.insv` | :white_check_mark: | |
|
||||
| `MPEG` | `.mpg` `.mpe` `.mpeg` | :white_check_mark: | |
|
||||
| `QUICKTIME` | `.mov` | :white_check_mark: | |
|
||||
| `WEBM` | `.webm` | :white_check_mark: | |
|
||||
| `WMV` | `.wmv` | :white_check_mark: | |
|
||||
@@ -45,7 +45,7 @@ SELECT * FROM "assets" JOIN "exif" ON "assets"."id" = "exif"."assetId" WHERE "ex
|
||||
```
|
||||
|
||||
```sql title="Without thumbnails"
|
||||
SELECT * FROM "assets" WHERE "assets"."resizePath" IS NULL OR "assets"."webpPath" IS NULL;
|
||||
SELECT * FROM "assets" WHERE "assets"."previewPath" IS NULL OR "assets"."thumbnailPath" IS NULL;
|
||||
```
|
||||
|
||||
```sql title="By type"
|
||||
|
||||
@@ -56,4 +56,4 @@ A remote reverse proxy like [Cloudflare](https://www.cloudflare.com/learning/cdn
|
||||
### Cons
|
||||
|
||||
- Complex configuration
|
||||
- Depending on your configuration, both the Immich web interface and API may be exposed to the internet. Immich is under very active developement and the existence of severe security vulnerabilities cannot be ruled out.
|
||||
- Depending on your configuration, both the Immich web interface and API may be exposed to the internet. Immich is under very active development and the existence of severe security vulnerabilities cannot be ruled out.
|
||||
|
||||
@@ -114,9 +114,11 @@ The default configuration looks like this:
|
||||
"hashVerificationEnabled": true,
|
||||
"template": "{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}"
|
||||
},
|
||||
"thumbnail": {
|
||||
"webpSize": 250,
|
||||
"jpegSize": 1440,
|
||||
"image": {
|
||||
"thumbnailFormat": "webp",
|
||||
"thumbnailSize": 250,
|
||||
"previewFormat": "jpeg",
|
||||
"previewSize": 1440,
|
||||
"quality": 80,
|
||||
"colorspace": "p3"
|
||||
},
|
||||
|
||||
@@ -17,10 +17,10 @@ If this should not work, try running `docker compose up -d --force-recreate`.
|
||||
|
||||
## Docker Compose
|
||||
|
||||
| Variable | Description | Default | Services |
|
||||
| :---------------- | :-------------------- | :-------: | :-------------------------------------------------- |
|
||||
| `IMMICH_VERSION` | Image tags | `release` | server, microservices, machine learning, web, proxy |
|
||||
| `UPLOAD_LOCATION` | Host Path for uploads | | server, microservices |
|
||||
| Variable | Description | Default | Services |
|
||||
| :---------------- | :-------------------- | :-------: | :-------------------------------------- |
|
||||
| `IMMICH_VERSION` | Image tags | `release` | server, microservices, machine learning |
|
||||
| `UPLOAD_LOCATION` | Host Path for uploads | | server, microservices |
|
||||
|
||||
:::tip
|
||||
|
||||
@@ -30,29 +30,26 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
||||
|
||||
## General
|
||||
|
||||
| Variable | Description | Default | Services |
|
||||
| :------------------------------ | :------------------------------------------- | :------------------: | :------------------------------------------- |
|
||||
| `TZ` | Timezone | | microservices |
|
||||
| `NODE_ENV` | Environment (production, development) | `production` | server, microservices, machine learning, web |
|
||||
| `LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, microservices |
|
||||
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload` | server, microservices |
|
||||
| `IMMICH_CONFIG_FILE` | Path to config file | | server, microservices |
|
||||
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server |
|
||||
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | microservices |
|
||||
| Variable | Description | Default | Services |
|
||||
| :------------------------------ | :------------------------------------------- | :------------------: | :-------------------------------------- |
|
||||
| `TZ` | Timezone | | microservices |
|
||||
| `NODE_ENV` | Environment (production, development) | `production` | server, microservices, machine learning |
|
||||
| `LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, microservices, machine learning |
|
||||
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload` | server, microservices |
|
||||
| `IMMICH_CONFIG_FILE` | Path to config file | | server, microservices |
|
||||
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server |
|
||||
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | microservices |
|
||||
|
||||
:::tip
|
||||
`TZ` should be set to a `TZ identifier` from [this list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List). For example, `TZ="Etc/UTC"`.
|
||||
|
||||
`TZ` is only used by the `exiftool` as a fallback in case the timezone cannot be determined from the image metadata.
|
||||
|
||||
`exiftool` is only present in the microservices container.
|
||||
|
||||
`TZ` is only used by `exiftool`, which is present in the microservices container, as a fallback in case the timezone cannot be determined from the image metadata.
|
||||
:::
|
||||
|
||||
## Ports
|
||||
|
||||
| Variable | Description | Default | Services |
|
||||
| :---------------------- | :-------------------- | :-------: | :--------------- |
|
||||
| `PORT` | Web Port | `3000` | web |
|
||||
| `SERVER_PORT` | Server Port | `3001` | server |
|
||||
| `MICROSERVICES_PORT` | Microservices Port | `3002` | microservices |
|
||||
| `MACHINE_LEARNING_HOST` | Machine Learning Host | `0.0.0.0` | machine learning |
|
||||
@@ -147,6 +144,18 @@ Other machine learning parameters can be tuned from the admin UI.
|
||||
|
||||
:::
|
||||
|
||||
## Prometheus
|
||||
|
||||
| Variable | Description | Default | Services |
|
||||
| :----------------------------- | :-------------------------------------------------------------------------------------------- | :-----: | :-------------------- |
|
||||
| `IMMICH_METRICS`<sup>\*1</sup> | Toggle all metrics (one of [`true`, `false`]) | | server, microservices |
|
||||
| `IMMICH_API_METRICS` | Toggle metrics for endpoints and response times (one of [`true`, `false`]) | | server, microservices |
|
||||
| `IMMICH_HOST_METRICS` | Toggle metrics for CPU and memory utilization for host and process (one of [`true`, `false`]) | | server, microservices |
|
||||
| `IMMICH_IO_METRICS` | Toggle metrics for database queries, image processing, etc. (one of [`true`, `false`]) | | server, microservices |
|
||||
| `IMMICH_JOB_METRICS` | Toggle metrics for jobs and queues (one of [`true`, `false`]) | | server, microservices |
|
||||
|
||||
\*1: Overridden for a metric group when its corresponding environmental variable is set.
|
||||
|
||||
## Docker Secrets
|
||||
|
||||
The following variables support the use of [Docker secrets](https://docs.docker.com/engine/swarm/secrets/) for additional security.
|
||||
@@ -154,13 +163,14 @@ The following variables support the use of [Docker secrets](https://docs.docker.
|
||||
To use any of these, replace the regular environment variable with the equivalent `_FILE` environment variable. The value of
|
||||
the `_FILE` variable should be set to the path of a file containing the variable value.
|
||||
|
||||
| Regular Variable | Equivalent Docker Secrets '\_FILE' Variable |
|
||||
| :----------------: | :-----------------------------------------: |
|
||||
| `DB_HOSTNAME` | `DB_HOSTNAME_FILE`<sup>\*1</sup> |
|
||||
| `DB_DATABASE_NAME` | `DB_DATABASE_NAME_FILE`<sup>\*1</sup> |
|
||||
| `DB_USERNAME` | `DB_USERNAME_FILE`<sup>\*1</sup> |
|
||||
| `DB_PASSWORD` | `DB_PASSWORD_FILE`<sup>\*1</sup> |
|
||||
| `REDIS_PASSWORD` | `REDIS_PASSWORD_FILE`<sup>\*2</sup> |
|
||||
| Regular Variable | Equivalent Docker Secrets '\_FILE' Variable |
|
||||
| :----------------- | :------------------------------------------ |
|
||||
| `DB_HOSTNAME` | `DB_HOSTNAME_FILE`<sup>\*1</sup> |
|
||||
| `DB_DATABASE_NAME` | `DB_DATABASE_NAME_FILE`<sup>\*1</sup> |
|
||||
| `DB_USERNAME` | `DB_USERNAME_FILE`<sup>\*1</sup> |
|
||||
| `DB_PASSWORD` | `DB_PASSWORD_FILE`<sup>\*1</sup> |
|
||||
| `DB_URL` | `DB_URL_FILE`<sup>\*1</sup> |
|
||||
| `REDIS_PASSWORD` | `REDIS_PASSWORD_FILE`<sup>\*2</sup> |
|
||||
|
||||
\*1: See the [official documentation](https://github.com/docker-library/docs/tree/master/postgres#docker-secrets) for
|
||||
details on how to use Docker Secrets in the Postgres image.
|
||||
|
||||
14
docs/package-lock.json
generated
14
docs/package-lock.json
generated
@@ -3429,9 +3429,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/docusaurus": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/docusaurus/-/docusaurus-2.0.2.tgz",
|
||||
"integrity": "sha512-12HWfYmgUl4M2o76/TFufGtI68wl2k/b8qPrIrG7ci9YJLrpAtadpy897Bz5v29Mlkr7a1Hq4KHdQTKtU+2rhQ==",
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/docusaurus/-/docusaurus-2.0.3.tgz",
|
||||
"integrity": "sha512-3l1L5PzWVa7l0691TjnsZ0yOIEwG9DziSqu5IPZPlI5Dowi7z42cEym8Y35GHbgHvPcBfNxfrbxm7Cncn4nByQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/acorn": {
|
||||
@@ -15781,9 +15781,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz",
|
||||
"integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==",
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz",
|
||||
"integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
@@ -15793,7 +15793,7 @@
|
||||
"fast-glob": "^3.3.0",
|
||||
"glob-parent": "^6.0.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"jiti": "^1.19.1",
|
||||
"jiti": "^1.21.0",
|
||||
"lilconfig": "^2.1.0",
|
||||
"micromatch": "^4.0.5",
|
||||
"normalize-path": "^3.0.0",
|
||||
|
||||
90
e2e/package-lock.json
generated
90
e2e/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "1.100.0",
|
||||
"version": "1.101.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich-e2e",
|
||||
"version": "1.100.0",
|
||||
"version": "1.101.0",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"devDependencies": {
|
||||
"@immich/cli": "file:../cli",
|
||||
@@ -80,7 +80,7 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.100.0",
|
||||
"version": "1.101.0",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
@@ -1277,16 +1277,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.3.1.tgz",
|
||||
"integrity": "sha512-STEDMVQGww5lhCuNXVSQfbfuNII5E08QWkvAw5Qwf+bj2WT+JkG1uc+5/vXA3AOYMDHVOSpL+9rcbEUiHIm2dw==",
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz",
|
||||
"integrity": "sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.5.1",
|
||||
"@typescript-eslint/scope-manager": "7.3.1",
|
||||
"@typescript-eslint/type-utils": "7.3.1",
|
||||
"@typescript-eslint/utils": "7.3.1",
|
||||
"@typescript-eslint/visitor-keys": "7.3.1",
|
||||
"@typescript-eslint/scope-manager": "7.4.0",
|
||||
"@typescript-eslint/type-utils": "7.4.0",
|
||||
"@typescript-eslint/utils": "7.4.0",
|
||||
"@typescript-eslint/visitor-keys": "7.4.0",
|
||||
"debug": "^4.3.4",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.2.4",
|
||||
@@ -1312,15 +1312,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.3.1.tgz",
|
||||
"integrity": "sha512-Rq49+pq7viTRCH48XAbTA+wdLRrB/3sRq4Lpk0oGDm0VmnjBrAOVXH/Laalmwsv2VpekiEfVFwJYVk6/e8uvQw==",
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz",
|
||||
"integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "7.3.1",
|
||||
"@typescript-eslint/types": "7.3.1",
|
||||
"@typescript-eslint/typescript-estree": "7.3.1",
|
||||
"@typescript-eslint/visitor-keys": "7.3.1",
|
||||
"@typescript-eslint/scope-manager": "7.4.0",
|
||||
"@typescript-eslint/types": "7.4.0",
|
||||
"@typescript-eslint/typescript-estree": "7.4.0",
|
||||
"@typescript-eslint/visitor-keys": "7.4.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1340,13 +1340,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.3.1.tgz",
|
||||
"integrity": "sha512-fVS6fPxldsKY2nFvyT7IP78UO1/I2huG+AYu5AMjCT9wtl6JFiDnsv4uad4jQ0GTFzcUV5HShVeN96/17bTBag==",
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz",
|
||||
"integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.3.1",
|
||||
"@typescript-eslint/visitor-keys": "7.3.1"
|
||||
"@typescript-eslint/types": "7.4.0",
|
||||
"@typescript-eslint/visitor-keys": "7.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@@ -1357,13 +1357,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.3.1.tgz",
|
||||
"integrity": "sha512-iFhaysxFsMDQlzJn+vr3OrxN8NmdQkHks4WaqD4QBnt5hsq234wcYdyQ9uquzJJIDAj5W4wQne3yEsYA6OmXGw==",
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz",
|
||||
"integrity": "sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "7.3.1",
|
||||
"@typescript-eslint/utils": "7.3.1",
|
||||
"@typescript-eslint/typescript-estree": "7.4.0",
|
||||
"@typescript-eslint/utils": "7.4.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
},
|
||||
@@ -1384,9 +1384,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.3.1.tgz",
|
||||
"integrity": "sha512-2tUf3uWggBDl4S4183nivWQ2HqceOZh1U4hhu4p1tPiIJoRRXrab7Y+Y0p+dozYwZVvLPRI6r5wKe9kToF9FIw==",
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.4.0.tgz",
|
||||
"integrity": "sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@@ -1397,13 +1397,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.3.1.tgz",
|
||||
"integrity": "sha512-tLpuqM46LVkduWP7JO7yVoWshpJuJzxDOPYIVWUUZbW+4dBpgGeUdl/fQkhuV0A8eGnphYw3pp8d2EnvPOfxmQ==",
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz",
|
||||
"integrity": "sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.3.1",
|
||||
"@typescript-eslint/visitor-keys": "7.3.1",
|
||||
"@typescript-eslint/types": "7.4.0",
|
||||
"@typescript-eslint/visitor-keys": "7.4.0",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -1449,17 +1449,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.3.1.tgz",
|
||||
"integrity": "sha512-jIERm/6bYQ9HkynYlNZvXpzmXWZGhMbrOvq3jJzOSOlKXsVjrrolzWBjDW6/TvT5Q3WqaN4EkmcfdQwi9tDjBQ==",
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.4.0.tgz",
|
||||
"integrity": "sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@types/json-schema": "^7.0.12",
|
||||
"@types/semver": "^7.5.0",
|
||||
"@typescript-eslint/scope-manager": "7.3.1",
|
||||
"@typescript-eslint/types": "7.3.1",
|
||||
"@typescript-eslint/typescript-estree": "7.3.1",
|
||||
"@typescript-eslint/scope-manager": "7.4.0",
|
||||
"@typescript-eslint/types": "7.4.0",
|
||||
"@typescript-eslint/typescript-estree": "7.4.0",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1474,12 +1474,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.3.1.tgz",
|
||||
"integrity": "sha512-9RMXwQF8knsZvfv9tdi+4D/j7dMG28X/wMJ8Jj6eOHyHWwDW4ngQJcqEczSsqIKKjFiLFr40Mnr7a5ulDD3vmw==",
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz",
|
||||
"integrity": "sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.3.1",
|
||||
"@typescript-eslint/types": "7.4.0",
|
||||
"eslint-visitor-keys": "^3.4.1"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "1.100.0",
|
||||
"version": "1.101.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
LibraryResponseDto,
|
||||
LoginResponseDto,
|
||||
SharedLinkType,
|
||||
TimeBucketSize,
|
||||
getAllLibraries,
|
||||
getAssetInfo,
|
||||
updateAssets,
|
||||
@@ -942,146 +941,6 @@ describe('/asset', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /asset/time-buckets', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/asset/time-buckets').query({ size: TimeBucketSize.Month });
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should get time buckets by month', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/asset/time-buckets')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ count: 3, timeBucket: '1970-02-01T00:00:00.000Z' },
|
||||
{ count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not allow access for unrelated shared links', async () => {
|
||||
const sharedLink = await utils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Individual,
|
||||
assetIds: user1Assets.map(({ id }) => id),
|
||||
});
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get('/asset/time-buckets')
|
||||
.query({ key: sharedLink.key, size: TimeBucketSize.Month });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.noPermission);
|
||||
});
|
||||
|
||||
it('should get time buckets by day', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/asset/time-buckets')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Day });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([
|
||||
{ count: 2, timeBucket: '1970-02-11T00:00:00.000Z' },
|
||||
{ count: 1, timeBucket: '1970-02-10T00:00:00.000Z' },
|
||||
{ count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /asset/time-bucket', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/asset/time-bucket').query({
|
||||
size: TimeBucketSize.Month,
|
||||
timeBucket: '1900-01-01T00:00:00.000Z',
|
||||
});
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should handle 5 digit years', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/asset/time-bucket')
|
||||
.query({ size: TimeBucketSize.Month, timeBucket: '+012345-01-01T00:00:00.000Z' })
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([]);
|
||||
});
|
||||
|
||||
// TODO enable date string validation while still accepting 5 digit years
|
||||
// it('should fail if time bucket is invalid', async () => {
|
||||
// const { status, body } = await request(app)
|
||||
// .get('/asset/time-bucket')
|
||||
// .set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
// .query({ size: TimeBucketSize.Month, timeBucket: 'foo' });
|
||||
|
||||
// expect(status).toBe(400);
|
||||
// expect(body).toEqual(errorDto.badRequest);
|
||||
// });
|
||||
|
||||
it('should return time bucket', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/asset/time-bucket')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month, timeBucket: '1970-02-10T00:00:00.000Z' });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return error if time bucket is requested with partners asset and archived', async () => {
|
||||
const req1 = await request(app)
|
||||
.get('/asset/time-buckets')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: true });
|
||||
|
||||
expect(req1.status).toBe(400);
|
||||
expect(req1.body).toEqual(errorDto.badRequest());
|
||||
|
||||
const req2 = await request(app)
|
||||
.get('/asset/time-buckets')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: undefined });
|
||||
|
||||
expect(req2.status).toBe(400);
|
||||
expect(req2.body).toEqual(errorDto.badRequest());
|
||||
});
|
||||
|
||||
it('should return error if time bucket is requested with partners asset and favorite', async () => {
|
||||
const req1 = await request(app)
|
||||
.get('/asset/time-buckets')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: true });
|
||||
|
||||
expect(req1.status).toBe(400);
|
||||
expect(req1.body).toEqual(errorDto.badRequest());
|
||||
|
||||
const req2 = await request(app)
|
||||
.get('/asset/time-buckets')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: false });
|
||||
|
||||
expect(req2.status).toBe(400);
|
||||
expect(req2.body).toEqual(errorDto.badRequest());
|
||||
});
|
||||
|
||||
it('should return error if time bucket is requested with partners asset and trash', async () => {
|
||||
const req = await request(app)
|
||||
.get('/asset/time-buckets')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month, withPartners: true, isTrashed: true });
|
||||
|
||||
expect(req.status).toBe(400);
|
||||
expect(req.body).toEqual(errorDto.badRequest());
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /asset', () => {
|
||||
it('should return stack data', async () => {
|
||||
const { status, body } = await request(app).get('/asset').set('Authorization', `Bearer ${stackUser.accessToken}`);
|
||||
|
||||
376
e2e/src/api/specs/memory.e2e-spec.ts
Normal file
376
e2e/src/api/specs/memory.e2e-spec.ts
Normal file
@@ -0,0 +1,376 @@
|
||||
import {
|
||||
AssetFileUploadResponseDto,
|
||||
LoginResponseDto,
|
||||
MemoryResponseDto,
|
||||
MemoryType,
|
||||
createMemory,
|
||||
getMemory,
|
||||
} from '@immich/sdk';
|
||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, asBearerAuth, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
describe('/memories', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let user: LoginResponseDto;
|
||||
let adminAsset: AssetFileUploadResponseDto;
|
||||
let userAsset1: AssetFileUploadResponseDto;
|
||||
let userAsset2: AssetFileUploadResponseDto;
|
||||
let userMemory: MemoryResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
|
||||
admin = await utils.adminSetup();
|
||||
user = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
||||
[adminAsset, userAsset1, userAsset2] = await Promise.all([
|
||||
utils.createAsset(admin.accessToken),
|
||||
utils.createAsset(user.accessToken),
|
||||
utils.createAsset(user.accessToken),
|
||||
]);
|
||||
userMemory = await createMemory(
|
||||
{
|
||||
memoryCreateDto: {
|
||||
type: MemoryType.OnThisDay,
|
||||
memoryAt: new Date(2021).toISOString(),
|
||||
data: { year: 2021 },
|
||||
assetIds: [],
|
||||
},
|
||||
},
|
||||
{ headers: asBearerAuth(user.accessToken) },
|
||||
);
|
||||
});
|
||||
|
||||
describe('GET /memories', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/memories');
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /memories', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).post('/memories');
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should validate data when type is on this day', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/memories')
|
||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||
.send({
|
||||
type: 'on_this_day',
|
||||
data: {},
|
||||
memoryAt: new Date(2021).toISOString(),
|
||||
});
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest(['data.year must be a positive number', 'data.year must be an integer number']),
|
||||
);
|
||||
});
|
||||
|
||||
it('should create a new memory', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/memories')
|
||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||
.send({
|
||||
type: 'on_this_day',
|
||||
data: { year: 2021 },
|
||||
memoryAt: new Date(2021).toISOString(),
|
||||
});
|
||||
|
||||
expect(status).toBe(201);
|
||||
expect(body).toEqual({
|
||||
id: expect.any(String),
|
||||
type: 'on_this_day',
|
||||
data: { year: 2021 },
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
deletedAt: null,
|
||||
seenAt: null,
|
||||
isSaved: false,
|
||||
memoryAt: expect.any(String),
|
||||
ownerId: user.userId,
|
||||
assets: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a new memory (with assets)', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/memories')
|
||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||
.send({
|
||||
type: 'on_this_day',
|
||||
data: { year: 2021 },
|
||||
memoryAt: new Date(2021).toISOString(),
|
||||
assetIds: [userAsset1.id, userAsset2.id],
|
||||
});
|
||||
|
||||
expect(status).toBe(201);
|
||||
expect(body).toMatchObject({
|
||||
id: expect.any(String),
|
||||
assets: expect.arrayContaining([
|
||||
expect.objectContaining({ id: userAsset1.id }),
|
||||
expect.objectContaining({ id: userAsset2.id }),
|
||||
]),
|
||||
});
|
||||
expect(body.assets).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should create a new memory and ignore assets the user does not have access to', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/memories')
|
||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||
.send({
|
||||
type: 'on_this_day',
|
||||
data: { year: 2021 },
|
||||
memoryAt: new Date(2021).toISOString(),
|
||||
assetIds: [userAsset1.id, adminAsset.id],
|
||||
});
|
||||
|
||||
expect(status).toBe(201);
|
||||
expect(body).toMatchObject({
|
||||
id: expect.any(String),
|
||||
assets: [expect.objectContaining({ id: userAsset1.id })],
|
||||
});
|
||||
expect(body.assets).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /memories/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get(`/memories/${uuidDto.invalid}`);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get(`/memories/${uuidDto.invalid}`)
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||
});
|
||||
|
||||
it('should require access', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get(`/memories/${userMemory.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.noPermission);
|
||||
});
|
||||
|
||||
it('should get the memory', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get(`/memories/${userMemory.id}`)
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({ id: userMemory.id });
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /memories/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).put(`/memories/${uuidDto.invalid}`).send({ isSaved: true });
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/memories/${uuidDto.invalid}`)
|
||||
.send({ isSaved: true })
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||
});
|
||||
|
||||
it('should require access', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/memories/${userMemory.id}`)
|
||||
.send({ isSaved: true })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.noPermission);
|
||||
});
|
||||
|
||||
it('should update the memory', async () => {
|
||||
const before = await getMemory({ id: userMemory.id }, { headers: asBearerAuth(user.accessToken) });
|
||||
expect(before.isSaved).toBe(false);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.put(`/memories/${userMemory.id}`)
|
||||
.send({ isSaved: true })
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({
|
||||
id: userMemory.id,
|
||||
isSaved: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /memories/:id/assets', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/memories/${userMemory.id}/assets`)
|
||||
.send({ ids: [userAsset1.id] });
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/memories/${uuidDto.invalid}/assets`)
|
||||
.send({ ids: [userAsset1.id] })
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||
});
|
||||
|
||||
it('should require access', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/memories/${userMemory.id}/assets`)
|
||||
.send({ ids: [userAsset1.id] })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.noPermission);
|
||||
});
|
||||
|
||||
it('should require a valid asset id', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/memories/${userMemory.id}/assets`)
|
||||
.send({ ids: [uuidDto.invalid] })
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID']));
|
||||
});
|
||||
|
||||
it('should require asset access', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/memories/${userMemory.id}/assets`)
|
||||
.send({ ids: [adminAsset.id] })
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toHaveLength(1);
|
||||
expect(body[0]).toEqual({
|
||||
id: adminAsset.id,
|
||||
success: false,
|
||||
error: 'no_permission',
|
||||
});
|
||||
});
|
||||
|
||||
it('should add assets to the memory', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/memories/${userMemory.id}/assets`)
|
||||
.send({ ids: [userAsset1.id] })
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toHaveLength(1);
|
||||
expect(body[0]).toEqual({ id: userAsset1.id, success: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /memories/:id/assets', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/memories/${userMemory.id}/assets`)
|
||||
.send({ ids: [userAsset1.id] });
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/memories/${uuidDto.invalid}/assets`)
|
||||
.send({ ids: [userAsset1.id] })
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||
});
|
||||
|
||||
it('should require access', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/memories/${userMemory.id}/assets`)
|
||||
.send({ ids: [userAsset1.id] })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.noPermission);
|
||||
});
|
||||
|
||||
it('should require a valid asset id', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/memories/${userMemory.id}/assets`)
|
||||
.send({ ids: [uuidDto.invalid] })
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID']));
|
||||
});
|
||||
|
||||
it('should only remove assets in the memory', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/memories/${userMemory.id}/assets`)
|
||||
.send({ ids: [adminAsset.id] })
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toHaveLength(1);
|
||||
expect(body[0]).toEqual({
|
||||
id: adminAsset.id,
|
||||
success: false,
|
||||
error: 'not_found',
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove assets from the memory', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/memories/${userMemory.id}/assets`)
|
||||
.send({ ids: [userAsset1.id] })
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toHaveLength(1);
|
||||
expect(body[0]).toEqual({ id: userAsset1.id, success: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /memories/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).delete(`/memories/${uuidDto.invalid}`);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/memories/${uuidDto.invalid}`)
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||
});
|
||||
|
||||
it('should require access', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/memories/${userMemory.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.noPermission);
|
||||
});
|
||||
|
||||
it('should delete the memory', async () => {
|
||||
const { status } = await request(app)
|
||||
.delete(`/memories/${userMemory.id}`)
|
||||
.send({ isSaved: true })
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(204);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AssetFileUploadResponseDto, LoginResponseDto, deleteAssets } from '@immich/sdk';
|
||||
import { AssetFileUploadResponseDto, LoginResponseDto, deleteAssets, updateAsset } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
@@ -7,7 +7,6 @@ import { errorDto } from 'src/responses';
|
||||
import { app, asBearerAuth, testAssetDir, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const today = DateTime.now();
|
||||
|
||||
describe('/search', () => {
|
||||
@@ -19,7 +18,7 @@ describe('/search', () => {
|
||||
let assetCyclamen: AssetFileUploadResponseDto;
|
||||
let assetNotocactus: AssetFileUploadResponseDto;
|
||||
let assetSilver: AssetFileUploadResponseDto;
|
||||
// let assetDensity: AssetFileUploadResponseDto;
|
||||
let assetDensity: AssetFileUploadResponseDto;
|
||||
// let assetPhiladelphia: AssetFileUploadResponseDto;
|
||||
// let assetOrychophragmus: AssetFileUploadResponseDto;
|
||||
// let assetRidge: AssetFileUploadResponseDto;
|
||||
@@ -79,6 +78,37 @@ describe('/search', () => {
|
||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
|
||||
}
|
||||
|
||||
// note: the coordinates here are not the actual coordinates of the images and are random for most of them
|
||||
const cities = [
|
||||
{ latitude: 48.853_41, longitude: 2.3488 }, // paris
|
||||
{ latitude: 63.0695, longitude: -151.0074 }, // denali
|
||||
{ latitude: 52.524_37, longitude: 13.410_53 }, // berlin
|
||||
{ latitude: 1.314_663_1, longitude: 103.845_409_3 }, // singapore
|
||||
{ latitude: 41.013_84, longitude: 28.949_66 }, // istanbul
|
||||
{ latitude: 5.556_02, longitude: -0.1969 }, // accra
|
||||
{ latitude: 37.544_270_6, longitude: -4.727_752_8 }, // andalusia
|
||||
{ latitude: 23.133_02, longitude: -82.383_04 }, // havana
|
||||
{ latitude: 41.694_11, longitude: 44.833_68 }, // tbilisi
|
||||
{ latitude: 31.222_22, longitude: 121.458_06 }, // shanghai
|
||||
{ latitude: 47.040_57, longitude: 9.068_04 }, // glarus
|
||||
{ latitude: 38.9711, longitude: -109.7137 }, // thompson springs
|
||||
{ latitude: 40.714_27, longitude: -74.005_97 }, // new york
|
||||
{ latitude: 32.771_52, longitude: -89.116_73 }, // philadelphia
|
||||
{ latitude: 31.634_16, longitude: -7.999_94 }, // marrakesh
|
||||
{ latitude: 38.523_735_4, longitude: -78.488_619_4 }, // tanners ridge
|
||||
{ latitude: 59.938_63, longitude: 30.314_13 }, // st. petersburg
|
||||
{ latitude: 35.6895, longitude: 139.691_71 }, // tokyo
|
||||
];
|
||||
|
||||
const updates = assets.map((asset, i) =>
|
||||
updateAsset({ id: asset.id, updateAssetDto: cities[i] }, { headers: asBearerAuth(admin.accessToken) }),
|
||||
);
|
||||
|
||||
await Promise.all(updates);
|
||||
for (const asset of assets) {
|
||||
await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id });
|
||||
}
|
||||
|
||||
[
|
||||
assetFalcon,
|
||||
assetDenali,
|
||||
@@ -92,7 +122,7 @@ describe('/search', () => {
|
||||
assetOneJpg5,
|
||||
assetGlarus,
|
||||
assetSprings,
|
||||
// assetDensity,
|
||||
assetDensity,
|
||||
// assetPhiladelphia,
|
||||
// assetOrychophragmus,
|
||||
// assetRidge,
|
||||
@@ -103,10 +133,10 @@ describe('/search', () => {
|
||||
assetLast = assets.at(-1) as AssetFileUploadResponseDto;
|
||||
|
||||
await deleteAssets({ assetBulkDeleteDto: { ids: [assetSilver.id] } }, { headers: asBearerAuth(admin.accessToken) });
|
||||
});
|
||||
}, 30_000);
|
||||
|
||||
afterAll(async () => {
|
||||
await utils.disconnectWebsocket(websocket);
|
||||
utils.disconnectWebsocket(websocket);
|
||||
});
|
||||
|
||||
describe('POST /search/metadata', () => {
|
||||
@@ -298,15 +328,15 @@ describe('/search', () => {
|
||||
},
|
||||
{
|
||||
should: 'should search by city',
|
||||
deferred: () => ({ dto: { city: 'Ralston' }, assets: [assetHeic] }),
|
||||
deferred: () => ({ dto: { city: 'Accra' }, assets: [assetHeic] }),
|
||||
},
|
||||
{
|
||||
should: 'should search by state',
|
||||
deferred: () => ({ dto: { state: 'Douglas County, Nebraska' }, assets: [assetHeic] }),
|
||||
deferred: () => ({ dto: { state: 'New York' }, assets: [assetDensity] }),
|
||||
},
|
||||
{
|
||||
should: 'should search by country',
|
||||
deferred: () => ({ dto: { country: 'United States of America' }, assets: [assetHeic] }),
|
||||
deferred: () => ({ dto: { country: 'France' }, assets: [assetFalcon] }),
|
||||
},
|
||||
{
|
||||
should: 'should search by make',
|
||||
@@ -370,13 +400,44 @@ describe('/search', () => {
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should get places', async () => {
|
||||
it('should get relevant places', async () => {
|
||||
const name = 'Paris';
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get('/search/places?name=Paris')
|
||||
.get(`/search/places?name=${name}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(Array.isArray(body)).toBe(true);
|
||||
expect(body.length).toBeGreaterThan(10);
|
||||
if (Array.isArray(body)) {
|
||||
expect(body.length).toBeGreaterThan(10);
|
||||
expect(body[0].name).toEqual(name);
|
||||
expect(body[0].admin2name).toEqual(name);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /search/cities', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/search/cities');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should get all cities', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/search/cities')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(Array.isArray(body)).toBe(true);
|
||||
if (Array.isArray(body)) {
|
||||
expect(body.length).toBeGreaterThan(10);
|
||||
const assetsWithCity = body.filter((asset) => !!asset.exifInfo?.city);
|
||||
expect(assetsWithCity.length).toEqual(body.length);
|
||||
const cities = new Set(assetsWithCity.map((asset) => asset.exifInfo.city));
|
||||
expect(cities.size).toEqual(body.length);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -391,7 +452,21 @@ describe('/search', () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/search/suggestions?type=country')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(body).toEqual(['United States of America']);
|
||||
expect(body).toEqual([
|
||||
'Cuba',
|
||||
'France',
|
||||
'Georgia',
|
||||
'Germany',
|
||||
'Ghana',
|
||||
'Japan',
|
||||
'Morocco',
|
||||
"People's Republic of China",
|
||||
'Russian Federation',
|
||||
'Singapore',
|
||||
'Spain',
|
||||
'Switzerland',
|
||||
'United States of America',
|
||||
]);
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
@@ -399,7 +474,23 @@ describe('/search', () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/search/suggestions?type=state')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(body).toEqual(['Douglas County, Nebraska', 'Mesa County, Colorado']);
|
||||
expect(body).toEqual([
|
||||
'Accra, Greater Accra',
|
||||
'Berlin',
|
||||
'Glarus, Glarus',
|
||||
'Havana',
|
||||
'Marrakech, Marrakesh-Safi',
|
||||
'Mesa County, Colorado',
|
||||
'Neshoba County, Mississippi',
|
||||
'New York',
|
||||
'Page County, Virginia',
|
||||
'Paris, Île-de-France',
|
||||
'Province of Córdoba, Andalusia',
|
||||
'Shanghai Municipality, Shanghai',
|
||||
'St.-Petersburg',
|
||||
'Tbilisi',
|
||||
'Tokyo',
|
||||
]);
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
@@ -407,7 +498,24 @@ describe('/search', () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/search/suggestions?type=city')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(body).toEqual(['Palisade', 'Ralston']);
|
||||
expect(body).toEqual([
|
||||
'Accra',
|
||||
'Berlin',
|
||||
'Glarus',
|
||||
'Havana',
|
||||
'Marrakesh',
|
||||
'Montalbán de Córdoba',
|
||||
'New York City',
|
||||
'Palisade',
|
||||
'Paris',
|
||||
'Philadelphia',
|
||||
'Saint Petersburg',
|
||||
'Shanghai',
|
||||
'Singapore',
|
||||
'Stanley',
|
||||
'Tbilisi',
|
||||
'Tokyo',
|
||||
]);
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LoginResponseDto, getConfig } from '@immich/sdk';
|
||||
import { AssetFileUploadResponseDto, LoginResponseDto, SharedLinkType, getConfig } from '@immich/sdk';
|
||||
import { createUserDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, asBearerAuth, utils } from 'src/utils';
|
||||
@@ -10,11 +10,14 @@ const getSystemConfig = (accessToken: string) => getConfig({ headers: asBearerAu
|
||||
describe('/system-config', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let nonAdmin: LoginResponseDto;
|
||||
let asset: AssetFileUploadResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
admin = await utils.adminSetup();
|
||||
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
||||
|
||||
asset = await utils.createAsset(admin.accessToken);
|
||||
});
|
||||
|
||||
describe('GET /system-config/map/style.json', () => {
|
||||
@@ -24,6 +27,19 @@ describe('/system-config', () => {
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should allow shared link access', async () => {
|
||||
const sharedLink = await utils.createSharedLink(admin.accessToken, {
|
||||
type: SharedLinkType.Individual,
|
||||
assetIds: [asset.id],
|
||||
});
|
||||
const { status, body } = await request(app)
|
||||
.get(`/system-config/map/style.json?key=${sharedLink.key}`)
|
||||
.query({ theme: 'dark' });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
|
||||
});
|
||||
|
||||
it('should throw an error if a theme is not light or dark', async () => {
|
||||
for (const theme of ['dark1', true, 123, '', null, undefined]) {
|
||||
const { status, body } = await request(app)
|
||||
|
||||
193
e2e/src/api/specs/timeline.e2e-spec.ts
Normal file
193
e2e/src/api/specs/timeline.e2e-spec.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import { AssetFileUploadResponseDto, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { createUserDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
// TODO this should probably be a test util function
|
||||
const today = DateTime.fromObject({
|
||||
year: 2023,
|
||||
month: 11,
|
||||
day: 3,
|
||||
}) as DateTime<true>;
|
||||
const yesterday = today.minus({ days: 1 });
|
||||
|
||||
describe('/timeline', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let user: LoginResponseDto;
|
||||
let timeBucketUser: LoginResponseDto;
|
||||
|
||||
let userAssets: AssetFileUploadResponseDto[];
|
||||
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
admin = await utils.adminSetup({ onboarding: false });
|
||||
[user, timeBucketUser] = await Promise.all([
|
||||
utils.userSetup(admin.accessToken, createUserDto.create('1')),
|
||||
utils.userSetup(admin.accessToken, createUserDto.create('time-bucket')),
|
||||
]);
|
||||
|
||||
userAssets = await Promise.all([
|
||||
utils.createAsset(user.accessToken),
|
||||
utils.createAsset(user.accessToken),
|
||||
utils.createAsset(user.accessToken, {
|
||||
isFavorite: true,
|
||||
isReadOnly: true,
|
||||
fileCreatedAt: yesterday.toISO(),
|
||||
fileModifiedAt: yesterday.toISO(),
|
||||
assetData: { filename: 'example.mp4' },
|
||||
}),
|
||||
utils.createAsset(user.accessToken),
|
||||
utils.createAsset(user.accessToken),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-01-01').toISOString() }),
|
||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-10').toISOString() }),
|
||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
|
||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
|
||||
]);
|
||||
});
|
||||
|
||||
describe('GET /timeline/buckets', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/timeline/buckets').query({ size: TimeBucketSize.Month });
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should get time buckets by month', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/timeline/buckets')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ count: 3, timeBucket: '1970-02-01T00:00:00.000Z' },
|
||||
{ count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not allow access for unrelated shared links', async () => {
|
||||
const sharedLink = await utils.createSharedLink(user.accessToken, {
|
||||
type: SharedLinkType.Individual,
|
||||
assetIds: userAssets.map(({ id }) => id),
|
||||
});
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get('/timeline/buckets')
|
||||
.query({ key: sharedLink.key, size: TimeBucketSize.Month });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.noPermission);
|
||||
});
|
||||
|
||||
it('should get time buckets by day', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/timeline/buckets')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Day });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([
|
||||
{ count: 2, timeBucket: '1970-02-11T00:00:00.000Z' },
|
||||
{ count: 1, timeBucket: '1970-02-10T00:00:00.000Z' },
|
||||
{ count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return error if time bucket is requested with partners asset and archived', async () => {
|
||||
const req1 = await request(app)
|
||||
.get('/timeline/buckets')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: true });
|
||||
|
||||
expect(req1.status).toBe(400);
|
||||
expect(req1.body).toEqual(errorDto.badRequest());
|
||||
|
||||
const req2 = await request(app)
|
||||
.get('/timeline/buckets')
|
||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: undefined });
|
||||
|
||||
expect(req2.status).toBe(400);
|
||||
expect(req2.body).toEqual(errorDto.badRequest());
|
||||
});
|
||||
|
||||
it('should return error if time bucket is requested with partners asset and favorite', async () => {
|
||||
const req1 = await request(app)
|
||||
.get('/timeline/buckets')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: true });
|
||||
|
||||
expect(req1.status).toBe(400);
|
||||
expect(req1.body).toEqual(errorDto.badRequest());
|
||||
|
||||
const req2 = await request(app)
|
||||
.get('/timeline/buckets')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: false });
|
||||
|
||||
expect(req2.status).toBe(400);
|
||||
expect(req2.body).toEqual(errorDto.badRequest());
|
||||
});
|
||||
|
||||
it('should return error if time bucket is requested with partners asset and trash', async () => {
|
||||
const req = await request(app)
|
||||
.get('/timeline/buckets')
|
||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month, withPartners: true, isTrashed: true });
|
||||
|
||||
expect(req.status).toBe(400);
|
||||
expect(req.body).toEqual(errorDto.badRequest());
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /timeline/bucket', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/timeline/bucket').query({
|
||||
size: TimeBucketSize.Month,
|
||||
timeBucket: '1900-01-01T00:00:00.000Z',
|
||||
});
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should handle 5 digit years', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/timeline/bucket')
|
||||
.query({ size: TimeBucketSize.Month, timeBucket: '+012345-01-01T00:00:00.000Z' })
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([]);
|
||||
});
|
||||
|
||||
// TODO enable date string validation while still accepting 5 digit years
|
||||
// it('should fail if time bucket is invalid', async () => {
|
||||
// const { status, body } = await request(app)
|
||||
// .get('/timeline/bucket')
|
||||
// .set('Authorization', `Bearer ${user.accessToken}`)
|
||||
// .query({ size: TimeBucketSize.Month, timeBucket: 'foo' });
|
||||
|
||||
// expect(status).toBe(400);
|
||||
// expect(body).toEqual(errorDto.badRequest);
|
||||
// });
|
||||
|
||||
it('should return time bucket', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/timeline/bucket')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month, timeBucket: '1970-02-10T00:00:00.000Z' });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -39,7 +39,7 @@ import { makeRandomImage } from 'src/generators';
|
||||
import request from 'supertest';
|
||||
|
||||
type CliResponse = { stdout: string; stderr: string; exitCode: number | null };
|
||||
type EventType = 'assetUpload' | 'assetDelete' | 'userDelete';
|
||||
type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete';
|
||||
type WaitOptions = { event: EventType; id?: string; total?: number; timeout?: number };
|
||||
type AdminSetupOptions = { onboarding?: boolean };
|
||||
type AssetData = { bytes?: Buffer; filename: string };
|
||||
@@ -82,6 +82,7 @@ let client: pg.Client | null = null;
|
||||
|
||||
const events: Record<EventType, Set<string>> = {
|
||||
assetUpload: new Set<string>(),
|
||||
assetUpdate: new Set<string>(),
|
||||
assetDelete: new Set<string>(),
|
||||
userDelete: new Set<string>(),
|
||||
};
|
||||
@@ -185,6 +186,7 @@ export const utils = {
|
||||
websocket
|
||||
.on('connect', () => resolve(websocket))
|
||||
.on('on_upload_success', (data: AssetResponseDto) => onEvent({ event: 'assetUpload', id: data.id }))
|
||||
.on('on_asset_update', (data: AssetResponseDto) => onEvent({ event: 'assetUpdate', id: data.id }))
|
||||
.on('on_asset_delete', (assetId: string) => onEvent({ event: 'assetDelete', id: assetId }))
|
||||
.on('on_user_delete', (userId: string) => onEvent({ event: 'userDelete', id: userId }))
|
||||
.connect();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
ARG DEVICE=cpu
|
||||
|
||||
FROM python:3.11-bookworm@sha256:991e20a11120277e977cadbc104e7a9b196a68a346597879821b19034285a403 as builder-cpu
|
||||
FROM python:3.11-bookworm@sha256:e2ed446c899827ed992f8a5a8875fa0853fcab32581e61418b650322061aa3c4 as builder-cpu
|
||||
|
||||
FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as builder-openvino
|
||||
USER root
|
||||
@@ -36,7 +36,7 @@ RUN python3 -m venv /opt/venv
|
||||
COPY poetry.lock pyproject.toml ./
|
||||
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:a2eb07f336e4f194358382611b4fea136c632b40baa6314cb27a366deeaf0144 as prod-cpu
|
||||
FROM python:3.11-slim-bookworm@sha256:90f8795536170fd08236d2ceb74fe7065dbf74f738d8b84bfbf263656654dc9b as prod-cpu
|
||||
|
||||
FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as prod-openvino
|
||||
USER root
|
||||
|
||||
@@ -22,3 +22,19 @@ You can change the models or adjust options like score thresholds through the Lo
|
||||
To get started, you can simply run `locust --web-host 127.0.0.1` and open `localhost:8089` in a browser to access the UI. See the [Locust documentation](https://docs.locust.io/en/stable/index.html) for more info on running Locust.
|
||||
|
||||
Note that in Locust's jargon, concurrency is measured in `users`, and each user runs one task at a time. To achieve a particular per-endpoint concurrency, multiply that number by the number of endpoints to be queried. For example, if there are 3 endpoints and you want each of them to receive 8 requests at a time, you should set the number of users to 24.
|
||||
|
||||
# Facial Recognition
|
||||
|
||||
## Acknowledgements
|
||||
This project utilizes facial recognition models from the [InsightFace](https://github.com/deepinsight/insightface/tree/master/model_zoo) project. We appreciate the work put into developing these models, which have been beneficial to the machine learning part of this project.
|
||||
|
||||
### Used Models
|
||||
* antelopev2
|
||||
* buffalo_l
|
||||
* buffalo_m
|
||||
* buffalo_s
|
||||
|
||||
## License and Use Restrictions
|
||||
We have received permission to use the InsightFace facial recognition models in our project, as granted via email by Jia Guo (guojia@insightface.ai) on 18th March 2023. However, it's important to note that this permission does not extend to the redistribution or commercial use of their models by third parties. Users and developers interested in using these models should review the licensing terms provided in the InsightFace GitHub repository.
|
||||
|
||||
For more information on the capabilities of the InsightFace models and to ensure compliance with their license, please refer to their [official repository](https://github.com/deepinsight/insightface). Adhering to the specified licensing terms is crucial for the respectful and lawful use of their work.
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM mambaorg/micromamba:bookworm-slim@sha256:881dbb68d115182b2c12e7e77dc54ea5005fd4e0123ca009d822adb5b0631785 as builder
|
||||
FROM mambaorg/micromamba:bookworm-slim@sha256:3624db3aee11d2f3f00d25f691aaaf8834b8bc4ec1b340dcdb48ef37281ea604 as builder
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
TRANSFORMERS_CACHE=/cache \
|
||||
|
||||
228
machine-learning/poetry.lock
generated
228
machine-learning/poetry.lock
generated
@@ -64,33 +64,33 @@ trio = ["trio (>=0.23)"]
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "24.2.0"
|
||||
version = "24.3.0"
|
||||
description = "The uncompromising code formatter."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"},
|
||||
{file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"},
|
||||
{file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"},
|
||||
{file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"},
|
||||
{file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"},
|
||||
{file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"},
|
||||
{file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"},
|
||||
{file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"},
|
||||
{file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"},
|
||||
{file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"},
|
||||
{file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"},
|
||||
{file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"},
|
||||
{file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"},
|
||||
{file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"},
|
||||
{file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"},
|
||||
{file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"},
|
||||
{file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"},
|
||||
{file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"},
|
||||
{file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"},
|
||||
{file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"},
|
||||
{file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"},
|
||||
{file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"},
|
||||
{file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"},
|
||||
{file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"},
|
||||
{file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"},
|
||||
{file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"},
|
||||
{file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"},
|
||||
{file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"},
|
||||
{file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"},
|
||||
{file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"},
|
||||
{file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"},
|
||||
{file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"},
|
||||
{file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"},
|
||||
{file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"},
|
||||
{file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"},
|
||||
{file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"},
|
||||
{file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"},
|
||||
{file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"},
|
||||
{file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"},
|
||||
{file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"},
|
||||
{file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"},
|
||||
{file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"},
|
||||
{file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"},
|
||||
{file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1274,13 +1274,13 @@ socks = ["socksio (==1.*)"]
|
||||
|
||||
[[package]]
|
||||
name = "huggingface-hub"
|
||||
version = "0.21.4"
|
||||
version = "0.22.2"
|
||||
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
files = [
|
||||
{file = "huggingface_hub-0.21.4-py3-none-any.whl", hash = "sha256:df37c2c37fc6c82163cdd8a67ede261687d80d1e262526d6c0ce73b6b3630a7b"},
|
||||
{file = "huggingface_hub-0.21.4.tar.gz", hash = "sha256:e1f4968c93726565a80edf6dc309763c7b546d0cfe79aa221206034d50155531"},
|
||||
{file = "huggingface_hub-0.22.2-py3-none-any.whl", hash = "sha256:3429e25f38ccb834d310804a3b711e7e4953db5a9e420cc147a5e194ca90fd17"},
|
||||
{file = "huggingface_hub-0.22.2.tar.gz", hash = "sha256:32e9a9a6843c92f253ff9ca16b9985def4d80a93fb357af5353f770ef74a81be"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1293,15 +1293,16 @@ tqdm = ">=4.42.1"
|
||||
typing-extensions = ">=3.7.4.3"
|
||||
|
||||
[package.extras]
|
||||
all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "mypy (==1.5.1)", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.1.3)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
|
||||
all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.3.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
|
||||
cli = ["InquirerPy (==0.3.4)"]
|
||||
dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "mypy (==1.5.1)", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.1.3)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
|
||||
dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.3.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
|
||||
fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"]
|
||||
hf-transfer = ["hf-transfer (>=0.1.4)"]
|
||||
inference = ["aiohttp", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)"]
|
||||
quality = ["mypy (==1.5.1)", "ruff (>=0.1.3)"]
|
||||
inference = ["aiohttp", "minijinja (>=1.0)"]
|
||||
quality = ["mypy (==1.5.1)", "ruff (>=0.3.0)"]
|
||||
tensorflow = ["graphviz", "pydot", "tensorflow"]
|
||||
testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"]
|
||||
tensorflow-testing = ["keras (<3.0)", "tensorflow"]
|
||||
testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "minijinja (>=1.0)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"]
|
||||
torch = ["safetensors", "torch"]
|
||||
typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"]
|
||||
|
||||
@@ -1567,13 +1568,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "locust"
|
||||
version = "2.24.0"
|
||||
version = "2.24.1"
|
||||
description = "Developer friendly load testing framework"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "locust-2.24.0-py3-none-any.whl", hash = "sha256:1b6b878b4fd0108fec956120815e69775d2616c8f4d1e9f365c222a7a5c17d9a"},
|
||||
{file = "locust-2.24.0.tar.gz", hash = "sha256:6cffa378d995244a7472af6be1d6139331f19aee44e907deee73e0281252804d"},
|
||||
{file = "locust-2.24.1-py3-none-any.whl", hash = "sha256:7f6ed4dc289aad66c304582e6d25e4de5d7c3b175b580332442ab2be35b9d916"},
|
||||
{file = "locust-2.24.1.tar.gz", hash = "sha256:094161d44d94839bf1120fd7898b7abb9c143833743ba7c096beb470a236b9a7"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2109,61 +2110,62 @@ numpy = [
|
||||
|
||||
[[package]]
|
||||
name = "orjson"
|
||||
version = "3.9.15"
|
||||
version = "3.10.0"
|
||||
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "orjson-3.9.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d61f7ce4727a9fa7680cd6f3986b0e2c732639f46a5e0156e550e35258aa313a"},
|
||||
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4feeb41882e8aa17634b589533baafdceb387e01e117b1ec65534ec724023d04"},
|
||||
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fbbeb3c9b2edb5fd044b2a070f127a0ac456ffd079cb82746fc84af01ef021a4"},
|
||||
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b66bcc5670e8a6b78f0313bcb74774c8291f6f8aeef10fe70e910b8040f3ab75"},
|
||||
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2973474811db7b35c30248d1129c64fd2bdf40d57d84beed2a9a379a6f57d0ab"},
|
||||
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fe41b6f72f52d3da4db524c8653e46243c8c92df826ab5ffaece2dba9cccd58"},
|
||||
{file = "orjson-3.9.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4228aace81781cc9d05a3ec3a6d2673a1ad0d8725b4e915f1089803e9efd2b99"},
|
||||
{file = "orjson-3.9.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f7b65bfaf69493c73423ce9db66cfe9138b2f9ef62897486417a8fcb0a92bfe"},
|
||||
{file = "orjson-3.9.15-cp310-none-win32.whl", hash = "sha256:2d99e3c4c13a7b0fb3792cc04c2829c9db07838fb6973e578b85c1745e7d0ce7"},
|
||||
{file = "orjson-3.9.15-cp310-none-win_amd64.whl", hash = "sha256:b725da33e6e58e4a5d27958568484aa766e825e93aa20c26c91168be58e08cbb"},
|
||||
{file = "orjson-3.9.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c8e8fe01e435005d4421f183038fc70ca85d2c1e490f51fb972db92af6e047c2"},
|
||||
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87f1097acb569dde17f246faa268759a71a2cb8c96dd392cd25c668b104cad2f"},
|
||||
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff0f9913d82e1d1fadbd976424c316fbc4d9c525c81d047bbdd16bd27dd98cfc"},
|
||||
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8055ec598605b0077e29652ccfe9372247474375e0e3f5775c91d9434e12d6b1"},
|
||||
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6768a327ea1ba44c9114dba5fdda4a214bdb70129065cd0807eb5f010bfcbb5"},
|
||||
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12365576039b1a5a47df01aadb353b68223da413e2e7f98c02403061aad34bde"},
|
||||
{file = "orjson-3.9.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:71c6b009d431b3839d7c14c3af86788b3cfac41e969e3e1c22f8a6ea13139404"},
|
||||
{file = "orjson-3.9.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e18668f1bd39e69b7fed19fa7cd1cd110a121ec25439328b5c89934e6d30d357"},
|
||||
{file = "orjson-3.9.15-cp311-none-win32.whl", hash = "sha256:62482873e0289cf7313461009bf62ac8b2e54bc6f00c6fabcde785709231a5d7"},
|
||||
{file = "orjson-3.9.15-cp311-none-win_amd64.whl", hash = "sha256:b3d336ed75d17c7b1af233a6561cf421dee41d9204aa3cfcc6c9c65cd5bb69a8"},
|
||||
{file = "orjson-3.9.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:82425dd5c7bd3adfe4e94c78e27e2fa02971750c2b7ffba648b0f5d5cc016a73"},
|
||||
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c51378d4a8255b2e7c1e5cc430644f0939539deddfa77f6fac7b56a9784160a"},
|
||||
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6ae4e06be04dc00618247c4ae3f7c3e561d5bc19ab6941427f6d3722a0875ef7"},
|
||||
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcef128f970bb63ecf9a65f7beafd9b55e3aaf0efc271a4154050fc15cdb386e"},
|
||||
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b72758f3ffc36ca566ba98a8e7f4f373b6c17c646ff8ad9b21ad10c29186f00d"},
|
||||
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c57bc7b946cf2efa67ac55766e41764b66d40cbd9489041e637c1304400494"},
|
||||
{file = "orjson-3.9.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:946c3a1ef25338e78107fba746f299f926db408d34553b4754e90a7de1d44068"},
|
||||
{file = "orjson-3.9.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2f256d03957075fcb5923410058982aea85455d035607486ccb847f095442bda"},
|
||||
{file = "orjson-3.9.15-cp312-none-win_amd64.whl", hash = "sha256:5bb399e1b49db120653a31463b4a7b27cf2fbfe60469546baf681d1b39f4edf2"},
|
||||
{file = "orjson-3.9.15-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b17f0f14a9c0ba55ff6279a922d1932e24b13fc218a3e968ecdbf791b3682b25"},
|
||||
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f6cbd8e6e446fb7e4ed5bac4661a29e43f38aeecbf60c4b900b825a353276a1"},
|
||||
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76bc6356d07c1d9f4b782813094d0caf1703b729d876ab6a676f3aaa9a47e37c"},
|
||||
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdfa97090e2d6f73dced247a2f2d8004ac6449df6568f30e7fa1a045767c69a6"},
|
||||
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7413070a3e927e4207d00bd65f42d1b780fb0d32d7b1d951f6dc6ade318e1b5a"},
|
||||
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cf1596680ac1f01839dba32d496136bdd5d8ffb858c280fa82bbfeb173bdd40"},
|
||||
{file = "orjson-3.9.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:809d653c155e2cc4fd39ad69c08fdff7f4016c355ae4b88905219d3579e31eb7"},
|
||||
{file = "orjson-3.9.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:920fa5a0c5175ab14b9c78f6f820b75804fb4984423ee4c4f1e6d748f8b22bc1"},
|
||||
{file = "orjson-3.9.15-cp38-none-win32.whl", hash = "sha256:2b5c0f532905e60cf22a511120e3719b85d9c25d0e1c2a8abb20c4dede3b05a5"},
|
||||
{file = "orjson-3.9.15-cp38-none-win_amd64.whl", hash = "sha256:67384f588f7f8daf040114337d34a5188346e3fae6c38b6a19a2fe8c663a2f9b"},
|
||||
{file = "orjson-3.9.15-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6fc2fe4647927070df3d93f561d7e588a38865ea0040027662e3e541d592811e"},
|
||||
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34cbcd216e7af5270f2ffa63a963346845eb71e174ea530867b7443892d77180"},
|
||||
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f541587f5c558abd93cb0de491ce99a9ef8d1ae29dd6ab4dbb5a13281ae04cbd"},
|
||||
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92255879280ef9c3c0bcb327c5a1b8ed694c290d61a6a532458264f887f052cb"},
|
||||
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:05a1f57fb601c426635fcae9ddbe90dfc1ed42245eb4c75e4960440cac667262"},
|
||||
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ede0bde16cc6e9b96633df1631fbcd66491d1063667f260a4f2386a098393790"},
|
||||
{file = "orjson-3.9.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e88b97ef13910e5f87bcbc4dd7979a7de9ba8702b54d3204ac587e83639c0c2b"},
|
||||
{file = "orjson-3.9.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57d5d8cf9c27f7ef6bc56a5925c7fbc76b61288ab674eb352c26ac780caa5b10"},
|
||||
{file = "orjson-3.9.15-cp39-none-win32.whl", hash = "sha256:001f4eb0ecd8e9ebd295722d0cbedf0748680fb9998d3993abaed2f40587257a"},
|
||||
{file = "orjson-3.9.15-cp39-none-win_amd64.whl", hash = "sha256:ea0b183a5fe6b2b45f3b854b0d19c4e932d6f5934ae1f723b07cf9560edd4ec7"},
|
||||
{file = "orjson-3.9.15.tar.gz", hash = "sha256:95cae920959d772f30ab36d3b25f83bb0f3be671e986c72ce22f8fa700dae061"},
|
||||
{file = "orjson-3.10.0-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:47af5d4b850a2d1328660661f0881b67fdbe712aea905dadd413bdea6f792c33"},
|
||||
{file = "orjson-3.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c90681333619d78360d13840c7235fdaf01b2b129cb3a4f1647783b1971542b6"},
|
||||
{file = "orjson-3.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:400c5b7c4222cb27b5059adf1fb12302eebcabf1978f33d0824aa5277ca899bd"},
|
||||
{file = "orjson-3.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5dcb32e949eae80fb335e63b90e5808b4b0f64e31476b3777707416b41682db5"},
|
||||
{file = "orjson-3.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7d507c7493252c0a0264b5cc7e20fa2f8622b8a83b04d819b5ce32c97cf57b"},
|
||||
{file = "orjson-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e286a51def6626f1e0cc134ba2067dcf14f7f4b9550f6dd4535fd9d79000040b"},
|
||||
{file = "orjson-3.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8acd4b82a5f3a3ec8b1dc83452941d22b4711964c34727eb1e65449eead353ca"},
|
||||
{file = "orjson-3.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:30707e646080dd3c791f22ce7e4a2fc2438765408547c10510f1f690bd336217"},
|
||||
{file = "orjson-3.10.0-cp310-none-win32.whl", hash = "sha256:115498c4ad34188dcb73464e8dc80e490a3e5e88a925907b6fedcf20e545001a"},
|
||||
{file = "orjson-3.10.0-cp310-none-win_amd64.whl", hash = "sha256:6735dd4a5a7b6df00a87d1d7a02b84b54d215fb7adac50dd24da5997ffb4798d"},
|
||||
{file = "orjson-3.10.0-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9587053e0cefc284e4d1cd113c34468b7d3f17666d22b185ea654f0775316a26"},
|
||||
{file = "orjson-3.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bef1050b1bdc9ea6c0d08468e3e61c9386723633b397e50b82fda37b3563d72"},
|
||||
{file = "orjson-3.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d16c6963ddf3b28c0d461641517cd312ad6b3cf303d8b87d5ef3fa59d6844337"},
|
||||
{file = "orjson-3.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4251964db47ef090c462a2d909f16c7c7d5fe68e341dabce6702879ec26d1134"},
|
||||
{file = "orjson-3.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73bbbdc43d520204d9ef0817ac03fa49c103c7f9ea94f410d2950755be2c349c"},
|
||||
{file = "orjson-3.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:414e5293b82373606acf0d66313aecb52d9c8c2404b1900683eb32c3d042dbd7"},
|
||||
{file = "orjson-3.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:feaed5bb09877dc27ed0d37f037ddef6cb76d19aa34b108db270d27d3d2ef747"},
|
||||
{file = "orjson-3.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5127478260db640323cea131ee88541cb1a9fbce051f0b22fa2f0892f44da302"},
|
||||
{file = "orjson-3.10.0-cp311-none-win32.whl", hash = "sha256:b98345529bafe3c06c09996b303fc0a21961820d634409b8639bc16bd4f21b63"},
|
||||
{file = "orjson-3.10.0-cp311-none-win_amd64.whl", hash = "sha256:658ca5cee3379dd3d37dbacd43d42c1b4feee99a29d847ef27a1cb18abdfb23f"},
|
||||
{file = "orjson-3.10.0-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4329c1d24fd130ee377e32a72dc54a3c251e6706fccd9a2ecb91b3606fddd998"},
|
||||
{file = "orjson-3.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef0f19fdfb6553342b1882f438afd53c7cb7aea57894c4490c43e4431739c700"},
|
||||
{file = "orjson-3.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4f60db24161534764277f798ef53b9d3063092f6d23f8f962b4a97edfa997a0"},
|
||||
{file = "orjson-3.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1de3fd5c7b208d836f8ecb4526995f0d5877153a4f6f12f3e9bf11e49357de98"},
|
||||
{file = "orjson-3.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f93e33f67729d460a177ba285002035d3f11425ed3cebac5f6ded4ef36b28344"},
|
||||
{file = "orjson-3.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:237ba922aef472761acd697eef77fef4831ab769a42e83c04ac91e9f9e08fa0e"},
|
||||
{file = "orjson-3.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98c1bfc6a9bec52bc8f0ab9b86cc0874b0299fccef3562b793c1576cf3abb570"},
|
||||
{file = "orjson-3.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:30d795a24be16c03dca0c35ca8f9c8eaaa51e3342f2c162d327bd0225118794a"},
|
||||
{file = "orjson-3.10.0-cp312-none-win32.whl", hash = "sha256:6a3f53dc650bc860eb26ec293dfb489b2f6ae1cbfc409a127b01229980e372f7"},
|
||||
{file = "orjson-3.10.0-cp312-none-win_amd64.whl", hash = "sha256:983db1f87c371dc6ffc52931eb75f9fe17dc621273e43ce67bee407d3e5476e9"},
|
||||
{file = "orjson-3.10.0-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9a667769a96a72ca67237224a36faf57db0c82ab07d09c3aafc6f956196cfa1b"},
|
||||
{file = "orjson-3.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade1e21dfde1d37feee8cf6464c20a2f41fa46c8bcd5251e761903e46102dc6b"},
|
||||
{file = "orjson-3.10.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23c12bb4ced1c3308eff7ba5c63ef8f0edb3e4c43c026440247dd6c1c61cea4b"},
|
||||
{file = "orjson-3.10.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2d014cf8d4dc9f03fc9f870de191a49a03b1bcda51f2a957943fb9fafe55aac"},
|
||||
{file = "orjson-3.10.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eadecaa16d9783affca33597781328e4981b048615c2ddc31c47a51b833d6319"},
|
||||
{file = "orjson-3.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd583341218826f48bd7c6ebf3310b4126216920853cbc471e8dbeaf07b0b80e"},
|
||||
{file = "orjson-3.10.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:90bfc137c75c31d32308fd61951d424424426ddc39a40e367704661a9ee97095"},
|
||||
{file = "orjson-3.10.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13b5d3c795b09a466ec9fcf0bd3ad7b85467d91a60113885df7b8d639a9d374b"},
|
||||
{file = "orjson-3.10.0-cp38-none-win32.whl", hash = "sha256:5d42768db6f2ce0162544845facb7c081e9364a5eb6d2ef06cd17f6050b048d8"},
|
||||
{file = "orjson-3.10.0-cp38-none-win_amd64.whl", hash = "sha256:33e6655a2542195d6fd9f850b428926559dee382f7a862dae92ca97fea03a5ad"},
|
||||
{file = "orjson-3.10.0-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4050920e831a49d8782a1720d3ca2f1c49b150953667eed6e5d63a62e80f46a2"},
|
||||
{file = "orjson-3.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1897aa25a944cec774ce4a0e1c8e98fb50523e97366c637b7d0cddabc42e6643"},
|
||||
{file = "orjson-3.10.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9bf565a69e0082ea348c5657401acec3cbbb31564d89afebaee884614fba36b4"},
|
||||
{file = "orjson-3.10.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6ebc17cfbbf741f5c1a888d1854354536f63d84bee537c9a7c0335791bb9009"},
|
||||
{file = "orjson-3.10.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2817877d0b69f78f146ab305c5975d0618df41acf8811249ee64231f5953fee"},
|
||||
{file = "orjson-3.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57d017863ec8aa4589be30a328dacd13c2dc49de1c170bc8d8c8a98ece0f2925"},
|
||||
{file = "orjson-3.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:22c2f7e377ac757bd3476ecb7480c8ed79d98ef89648f0176deb1da5cd014eb7"},
|
||||
{file = "orjson-3.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e62ba42bfe64c60c1bc84799944f80704e996592c6b9e14789c8e2a303279912"},
|
||||
{file = "orjson-3.10.0-cp39-none-win32.whl", hash = "sha256:60c0b1bdbccd959ebd1575bd0147bd5e10fc76f26216188be4a36b691c937077"},
|
||||
{file = "orjson-3.10.0-cp39-none-win_amd64.whl", hash = "sha256:175a41500ebb2fdf320bf78e8b9a75a1279525b62ba400b2b2444e274c2c8bee"},
|
||||
{file = "orjson-3.10.0.tar.gz", hash = "sha256:ba4d8cac5f2e2cff36bea6b6481cdb92b38c202bcec603d6f5ff91960595a1ed"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2495,13 +2497,13 @@ testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygm
|
||||
|
||||
[[package]]
|
||||
name = "pytest-asyncio"
|
||||
version = "0.23.5.post1"
|
||||
version = "0.23.6"
|
||||
description = "Pytest support for asyncio"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-asyncio-0.23.5.post1.tar.gz", hash = "sha256:b9a8806bea78c21276bc34321bbf234ba1b2ea5b30d9f0ce0f2dea45e4685813"},
|
||||
{file = "pytest_asyncio-0.23.5.post1-py3-none-any.whl", hash = "sha256:30f54d27774e79ac409778889880242b0403d09cabd65b727ce90fe92dd5d80e"},
|
||||
{file = "pytest-asyncio-0.23.6.tar.gz", hash = "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f"},
|
||||
{file = "pytest_asyncio-0.23.6-py3-none-any.whl", hash = "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2531,17 +2533,17 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale
|
||||
|
||||
[[package]]
|
||||
name = "pytest-mock"
|
||||
version = "3.12.0"
|
||||
version = "3.14.0"
|
||||
description = "Thin-wrapper around the mock package for easier use with pytest"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"},
|
||||
{file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"},
|
||||
{file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"},
|
||||
{file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pytest = ">=5.0"
|
||||
pytest = ">=6.2.5"
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "pytest-asyncio", "tox"]
|
||||
@@ -2844,28 +2846,28 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.3.3"
|
||||
version = "0.3.4"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:973a0e388b7bc2e9148c7f9be8b8c6ae7471b9be37e1cc732f8f44a6f6d7720d"},
|
||||
{file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfa60d23269d6e2031129b053fdb4e5a7b0637fc6c9c0586737b962b2f834493"},
|
||||
{file = "ruff-0.3.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eca7ff7a47043cf6ce5c7f45f603b09121a7cc047447744b029d1b719278eb5"},
|
||||
{file = "ruff-0.3.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7d3f6762217c1da954de24b4a1a70515630d29f71e268ec5000afe81377642d"},
|
||||
{file = "ruff-0.3.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b24c19e8598916d9c6f5a5437671f55ee93c212a2c4c569605dc3842b6820386"},
|
||||
{file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5a6cbf216b69c7090f0fe4669501a27326c34e119068c1494f35aaf4cc683778"},
|
||||
{file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352e95ead6964974b234e16ba8a66dad102ec7bf8ac064a23f95371d8b198aab"},
|
||||
{file = "ruff-0.3.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d6ab88c81c4040a817aa432484e838aaddf8bfd7ca70e4e615482757acb64f8"},
|
||||
{file = "ruff-0.3.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79bca3a03a759cc773fca69e0bdeac8abd1c13c31b798d5bb3c9da4a03144a9f"},
|
||||
{file = "ruff-0.3.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2700a804d5336bcffe063fd789ca2c7b02b552d2e323a336700abb8ae9e6a3f8"},
|
||||
{file = "ruff-0.3.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd66469f1a18fdb9d32e22b79f486223052ddf057dc56dea0caaf1a47bdfaf4e"},
|
||||
{file = "ruff-0.3.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45817af234605525cdf6317005923bf532514e1ea3d9270acf61ca2440691376"},
|
||||
{file = "ruff-0.3.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0da458989ce0159555ef224d5b7c24d3d2e4bf4c300b85467b08c3261c6bc6a8"},
|
||||
{file = "ruff-0.3.3-py3-none-win32.whl", hash = "sha256:f2831ec6a580a97f1ea82ea1eda0401c3cdf512cf2045fa3c85e8ef109e87de0"},
|
||||
{file = "ruff-0.3.3-py3-none-win_amd64.whl", hash = "sha256:be90bcae57c24d9f9d023b12d627e958eb55f595428bafcb7fec0791ad25ddfc"},
|
||||
{file = "ruff-0.3.3-py3-none-win_arm64.whl", hash = "sha256:0171aab5fecdc54383993389710a3d1227f2da124d76a2784a7098e818f92d61"},
|
||||
{file = "ruff-0.3.3.tar.gz", hash = "sha256:38671be06f57a2f8aba957d9f701ea889aa5736be806f18c0cd03d6ff0cbca8d"},
|
||||
{file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:60c870a7d46efcbc8385d27ec07fe534ac32f3b251e4fc44b3cbfd9e09609ef4"},
|
||||
{file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6fc14fa742e1d8f24910e1fff0bd5e26d395b0e0e04cc1b15c7c5e5fe5b4af91"},
|
||||
{file = "ruff-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3ee7880f653cc03749a3bfea720cf2a192e4f884925b0cf7eecce82f0ce5854"},
|
||||
{file = "ruff-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf133dd744f2470b347f602452a88e70dadfbe0fcfb5fd46e093d55da65f82f7"},
|
||||
{file = "ruff-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3860057590e810c7ffea75669bdc6927bfd91e29b4baa9258fd48b540a4365"},
|
||||
{file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:986f2377f7cf12efac1f515fc1a5b753c000ed1e0a6de96747cdf2da20a1b369"},
|
||||
{file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fd98e85869603e65f554fdc5cddf0712e352fe6e61d29d5a6fe087ec82b76c"},
|
||||
{file = "ruff-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64abeed785dad51801b423fa51840b1764b35d6c461ea8caef9cf9e5e5ab34d9"},
|
||||
{file = "ruff-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df52972138318bc7546d92348a1ee58449bc3f9eaf0db278906eb511889c4b50"},
|
||||
{file = "ruff-0.3.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:98e98300056445ba2cc27d0b325fd044dc17fcc38e4e4d2c7711585bd0a958ed"},
|
||||
{file = "ruff-0.3.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:519cf6a0ebed244dce1dc8aecd3dc99add7a2ee15bb68cf19588bb5bf58e0488"},
|
||||
{file = "ruff-0.3.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb0acfb921030d00070539c038cd24bb1df73a2981e9f55942514af8b17be94e"},
|
||||
{file = "ruff-0.3.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cf187a7e7098233d0d0c71175375c5162f880126c4c716fa28a8ac418dcf3378"},
|
||||
{file = "ruff-0.3.4-py3-none-win32.whl", hash = "sha256:af27ac187c0a331e8ef91d84bf1c3c6a5dea97e912a7560ac0cef25c526a4102"},
|
||||
{file = "ruff-0.3.4-py3-none-win_amd64.whl", hash = "sha256:de0d5069b165e5a32b3c6ffbb81c350b1e3d3483347196ffdf86dc0ef9e37dd6"},
|
||||
{file = "ruff-0.3.4-py3-none-win_arm64.whl", hash = "sha256:6810563cc08ad0096b57c717bd78aeac888a1bfd38654d9113cb3dc4d3f74232"},
|
||||
{file = "ruff-0.3.4.tar.gz", hash = "sha256:f0f4484c6541a99862b693e13a151435a279b271cff20e37101116a21e2a1ad1"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3289,13 +3291,13 @@ zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.28.0"
|
||||
version = "0.29.0"
|
||||
description = "The lightning-fast ASGI server."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "uvicorn-0.28.0-py3-none-any.whl", hash = "sha256:6623abbbe6176204a4226e67607b4d52cc60ff62cda0ff177613645cefa2ece1"},
|
||||
{file = "uvicorn-0.28.0.tar.gz", hash = "sha256:cab4473b5d1eaeb5a0f6375ac4bc85007ffc75c3cc1768816d9e5d589857b067"},
|
||||
{file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"},
|
||||
{file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "machine-learning"
|
||||
version = "1.100.0"
|
||||
version = "1.101.0"
|
||||
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" => 130,
|
||||
"android.injected.version.name" => "1.100.0",
|
||||
"android.injected.version.code" => 131,
|
||||
"android.injected.version.name" => "1.101.0",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.00024">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000235">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="81.32752">
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="71.774783">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="26.041597">
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="32.283066">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "Arxiu({})",
|
||||
"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": "Automàtic",
|
||||
"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_viewer_settings_title": "Asset Viewer",
|
||||
"backup_album_selection_page_albums_device": "Àlbums al dispositiu ({})",
|
||||
"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.",
|
||||
@@ -186,6 +189,7 @@
|
||||
"exif_bottom_sheet_location": "UBICACIÓ",
|
||||
"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!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"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",
|
||||
"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",
|
||||
@@ -307,6 +315,7 @@
|
||||
"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.",
|
||||
"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.",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "Archív ({})",
|
||||
"asset_action_delete_err_read_only": "Nelze odstranit položky pouze pro čtení, přeskakuji",
|
||||
"asset_action_share_err_offline": "Nelze načíst offline položky, přeskakuji",
|
||||
"asset_list_group_by_sub_title": "Seskupit podle",
|
||||
"asset_list_layout_settings_dynamic_layout_title": "Dynamické rozložení",
|
||||
"asset_list_layout_settings_group_automatically": "Automaticky",
|
||||
"asset_list_layout_settings_group_by": "Seskupit položky podle",
|
||||
"asset_list_layout_settings_group_by_month": "Měsíc",
|
||||
"asset_list_layout_settings_group_by_month_day": "Měsíc + den",
|
||||
"asset_list_layout_sub_title": "Rozložení",
|
||||
"asset_list_settings_subtitle": "Nastavení rozložení mřížky fotografií",
|
||||
"asset_list_settings_title": "Fotografická mřížka",
|
||||
"asset_viewer_settings_title": "Prohlížeč",
|
||||
"backup_album_selection_page_albums_device": "Alba v zařízení ({})",
|
||||
"backup_album_selection_page_albums_tap": "Klepnutím na položku ji zahrnete, dvojím klepnutím ji vyloučíte",
|
||||
"backup_album_selection_page_assets_scatter": "Položky mohou být roztroušeny ve více albech. To umožňuje zahrnout nebo vyloučit alba během procesu zálohování.",
|
||||
@@ -183,9 +186,10 @@
|
||||
"edit_location_dialog_title": "Poloha",
|
||||
"exif_bottom_sheet_description": "Přidat popis...",
|
||||
"exif_bottom_sheet_details": "PODROBNOSTI",
|
||||
"exif_bottom_sheet_location": "LOKALITA",
|
||||
"exif_bottom_sheet_location": "POLOHA",
|
||||
"exif_bottom_sheet_location_add": "Přidat polohu",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_people": "LIDÉ",
|
||||
"exif_bottom_sheet_person_add_person": "Přidat jméno",
|
||||
"experimental_settings_new_asset_list_subtitle": "Zpracovávám",
|
||||
"experimental_settings_new_asset_list_title": "Povolení experimentální mřížky fotografií",
|
||||
"experimental_settings_subtitle": "Používejte na vlastní riziko!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"map_settings_only_show_favorites": "Zobrazit pouze oblíbené",
|
||||
"map_settings_theme_settings": "Motiv mapy",
|
||||
"map_zoom_to_see_photos": "Oddálit pro zobrazení fotografií",
|
||||
"memories_all_caught_up": "To je všechno",
|
||||
"memories_check_back_tomorrow": "Zítra se podívejte na další vzpomínky",
|
||||
"memories_start_over": "Začít znovu",
|
||||
"memories_swipe_to_close": "Přejetím nahoru zavřete",
|
||||
"monthly_title_text_date_format": "LLLL y",
|
||||
"motion_photos_page_title": "Pohyblivé fotky",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "Nelze upravit datum položek pouze pro čtení, přeskakuji",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "Přístup povolen! Vše je připraveno.",
|
||||
"permission_onboarding_permission_limited": "Přístup omezen. Chcete-li používat Immich k zálohování a správě celé vaší kolekce galerií, povolte v nastavení přístup k fotkám a videím.",
|
||||
"permission_onboarding_request": "Immich potřebuje přístup k zobrazení vašich fotek a videí.",
|
||||
"preferences_settings_title": "Předvolby",
|
||||
"profile_drawer_app_logs": "Logy",
|
||||
"profile_drawer_client_out_of_date_major": "Mobilní aplikace je zastaralá. Aktualizujte ji na nejnovější hlavní verzi.",
|
||||
"profile_drawer_client_out_of_date_minor": "Mobilní aplikace je zastaralá. Aktualizujte ji na nejnovější verzi.",
|
||||
@@ -354,7 +363,7 @@
|
||||
"server_info_box_server_url": "URL serveru",
|
||||
"server_info_box_server_version": "Verze serveru",
|
||||
"setting_image_viewer_help": "V prohlížeči detailů se nejprve načte malá miniatura, poté se načte náhled střední velikosti (je-li povolen) a nakonec se načte originál (je-li povolen).",
|
||||
"setting_image_viewer_original_subtitle": "Umožňuje načíst původní obrázek v plném rozlišení (velký!). Zakázat pro snížení využití dat (v síti i v mezipaměti zařízení).",
|
||||
"setting_image_viewer_original_subtitle": "Umožňuje načíst původní obrázek v plném rozlišení (velký!). Zakažte pro snížení využití dat (v síti i v mezipaměti zařízení).",
|
||||
"setting_image_viewer_original_title": "Načíst původní obrázek",
|
||||
"setting_image_viewer_preview_subtitle": "Umožňuje načíst obrázek se středním rozlišením. Zakažte, pokud chcete přímo načíst originál nebo použít pouze miniaturu.",
|
||||
"setting_image_viewer_preview_title": "Načíst náhled obrázku",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "Arkivér ({})",
|
||||
"asset_action_delete_err_read_only": "Kan ikke slette kun læselige elementer. Springer over",
|
||||
"asset_action_share_err_offline": "Kan ikke hente offline element(er). Springer over",
|
||||
"asset_list_group_by_sub_title": "Group by",
|
||||
"asset_list_layout_settings_dynamic_layout_title": "Dynamisk layout",
|
||||
"asset_list_layout_settings_group_automatically": "Automatisk",
|
||||
"asset_list_layout_settings_group_by": "Gruppér elementer pr. ",
|
||||
"asset_list_layout_settings_group_by_month": "Måned",
|
||||
"asset_list_layout_settings_group_by_month_day": "Måned + dag",
|
||||
"asset_list_layout_sub_title": "Layout",
|
||||
"asset_list_settings_subtitle": "Indstillinger for billedgitterlayout",
|
||||
"asset_list_settings_title": "Billedgitter",
|
||||
"asset_viewer_settings_title": "Asset Viewer",
|
||||
"backup_album_selection_page_albums_device": "Albummer på enhed ({})",
|
||||
"backup_album_selection_page_albums_tap": "Tryk en gang for at inkludere, tryk to gange for at ekskludere",
|
||||
"backup_album_selection_page_assets_scatter": "Elementer kan være spredt på tværs af flere albummer. Albummer kan således inkluderes eller udelukkes under sikkerhedskopieringsprocessen.",
|
||||
@@ -142,8 +145,8 @@
|
||||
"control_bottom_app_bar_archive": "Arkiv",
|
||||
"control_bottom_app_bar_create_new_album": "Opret nyt album",
|
||||
"control_bottom_app_bar_delete": "Slet",
|
||||
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
|
||||
"control_bottom_app_bar_delete_from_local": "Delete from device",
|
||||
"control_bottom_app_bar_delete_from_immich": "Slet fra Immich",
|
||||
"control_bottom_app_bar_delete_from_local": "Slet fra enhed",
|
||||
"control_bottom_app_bar_edit_location": "Rediger placering",
|
||||
"control_bottom_app_bar_edit_time": "Rediger tid og dato",
|
||||
"control_bottom_app_bar_favorite": "Favorit",
|
||||
@@ -165,15 +168,15 @@
|
||||
"daily_title_text_date_year": "E, dd MMM, yyyy",
|
||||
"date_format": "E d. LLL y • hh:mm",
|
||||
"delete_dialog_alert": "Disse elementer vil blive slettet permanent fra Immich og din enhed",
|
||||
"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_alert_local": "Disse elementer slettes permanent fra din enhed, men vil stadig være tilgængelige på serveren",
|
||||
"delete_dialog_alert_local_non_backed_up": "Nogle af elementerne har ingen backup på serveren og vil blive slettet permanent fra din enhed",
|
||||
"delete_dialog_alert_remote": "Disse elementer slettes permanent fra serveren",
|
||||
"delete_dialog_cancel": "Annuller",
|
||||
"delete_dialog_ok": "Slet",
|
||||
"delete_dialog_ok_force": "Delete Anyway",
|
||||
"delete_dialog_ok_force": "Slet alligevel",
|
||||
"delete_dialog_title": "Slet permanent",
|
||||
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
|
||||
"delete_local_dialog_ok_force": "Delete Anyway",
|
||||
"delete_local_dialog_ok_backed_up_only": "Slet kun backup",
|
||||
"delete_local_dialog_ok_force": "Slet alligevel",
|
||||
"delete_shared_link_dialog_content": "Er du sikker på, du vil slette dette delte link?",
|
||||
"delete_shared_link_dialog_title": "Slet delt link",
|
||||
"description_input_hint_text": "Tilføj en beskrivelse...",
|
||||
@@ -185,7 +188,8 @@
|
||||
"exif_bottom_sheet_details": "DETALJER",
|
||||
"exif_bottom_sheet_location": "LOKATION",
|
||||
"exif_bottom_sheet_location_add": "Tilføj en placering",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_people": "PERSONER",
|
||||
"exif_bottom_sheet_person_add_person": "Add name",
|
||||
"experimental_settings_new_asset_list_subtitle": "Under udarbejdelse",
|
||||
"experimental_settings_new_asset_list_title": "Aktiver eksperimentelt fotogitter",
|
||||
"experimental_settings_subtitle": "Brug på eget ansvar!",
|
||||
@@ -276,8 +280,12 @@
|
||||
"map_settings_include_show_archived": "Inkluder arkiveret",
|
||||
"map_settings_only_relative_range": "Datointerval",
|
||||
"map_settings_only_show_favorites": "Vis kun favoritter",
|
||||
"map_settings_theme_settings": "Map Theme",
|
||||
"map_settings_theme_settings": "Korttema",
|
||||
"map_zoom_to_see_photos": "Zoom ud for at vise billeder",
|
||||
"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",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "Bevægelsesbilleder",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "Kan ikke redigere datoen på kun læselige elementer. Springer over",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "Tilladelse givet! Du er nu klar.",
|
||||
"permission_onboarding_permission_limited": "Tilladelse begrænset. For at lade Immich lave sikkerhedskopi og styre hele dit galleri, skal der gives tilladelse til billeder og videoer i indstillinger.",
|
||||
"permission_onboarding_request": "Immich kræver tilliadelse til at se dine billeder og videoer.",
|
||||
"preferences_settings_title": "Preferences",
|
||||
"profile_drawer_app_logs": "Log",
|
||||
"profile_drawer_client_out_of_date_major": "Mobilapp er forældet. Opdater venligst til den nyeste større version",
|
||||
"profile_drawer_client_out_of_date_minor": "Mobilapp er forældet. Opdater venligst til den nyeste mindre version",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "Archiv ({})",
|
||||
"asset_action_delete_err_read_only": "Schreibgeschützten Inhalte können nicht gelöscht werden, überspringe",
|
||||
"asset_action_share_err_offline": "Offline-Inhalte konnten nicht gelesen werden, überspringe",
|
||||
"asset_list_group_by_sub_title": "Group by",
|
||||
"asset_list_layout_settings_dynamic_layout_title": "Dynamisches Layout",
|
||||
"asset_list_layout_settings_group_automatically": "Automatisch",
|
||||
"asset_list_layout_settings_group_by": "Gruppiere Elemente nach",
|
||||
"asset_list_layout_settings_group_by_month": "Monat",
|
||||
"asset_list_layout_settings_group_by_month_day": "Monat + Tag",
|
||||
"asset_list_layout_sub_title": "Layout",
|
||||
"asset_list_settings_subtitle": "Einstellungen für das Fotogitter-Layout",
|
||||
"asset_list_settings_title": "Fotogitter",
|
||||
"asset_viewer_settings_title": "Asset Viewer",
|
||||
"backup_album_selection_page_albums_device": "Alben auf dem Gerät ({})",
|
||||
"backup_album_selection_page_albums_tap": "Tippen um einzuschließen, doppelt tippen um zu entfernen",
|
||||
"backup_album_selection_page_assets_scatter": "Elemente können sich über mehrere Alben verteilen. Daher können diese vor der Sicherung eingeschlossen oder ausgeschlossen werden",
|
||||
@@ -186,6 +189,7 @@
|
||||
"exif_bottom_sheet_location": "STANDORT",
|
||||
"exif_bottom_sheet_location_add": "Aufnahmeort hinzufügen",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_person_add_person": "Add name",
|
||||
"experimental_settings_new_asset_list_subtitle": "In Arbeit",
|
||||
"experimental_settings_new_asset_list_title": "Experimentelles Fotogitter aktivieren",
|
||||
"experimental_settings_subtitle": "Benutzung auf eigene Gefahr!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"map_settings_only_show_favorites": "Nur Favoriten anzeigen",
|
||||
"map_settings_theme_settings": "Karten-Theme",
|
||||
"map_zoom_to_see_photos": "Ansicht verkleinern um Fotos zu sehen",
|
||||
"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",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "Live-Fotos",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "Datum und Uhrzeit von schreibgeschützten Inhalten kann nicht geändert werden, überspringe",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "Berechtigung erteilt! Du bist startklar.",
|
||||
"permission_onboarding_permission_limited": "Berechtigungen unzureichend. Um Immich das Sichern von ganzen Sammlungen zu ermöglichen, muss der Zugriff auf alle Fotos und Videos in den Einstellungen erlaubt werden.",
|
||||
"permission_onboarding_request": "Immich benötigt Berechtigung um auf deine Fotos und Videos zuzugreifen.",
|
||||
"preferences_settings_title": "Preferences",
|
||||
"profile_drawer_app_logs": "Logs",
|
||||
"profile_drawer_client_out_of_date_major": "Mobile-App ist veraltet. Bitte aktualisiere auf die neueste Major-Version.",
|
||||
"profile_drawer_client_out_of_date_minor": "Mobile-App ist veraltet. Bitte aktualisiere auf die neueste Minor-Version.",
|
||||
|
||||
@@ -37,17 +37,16 @@
|
||||
"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_settings_subtitle": "Photo grid layout settings",
|
||||
"asset_list_settings_title": "Timeline",
|
||||
"asset_list_group_by_sub_title": "Group by",
|
||||
"asset_list_layout_sub_title": "Layout",
|
||||
"asset_list_settings_subtitle": "Photo grid layout settings",
|
||||
"asset_list_settings_title": "Photo Grid",
|
||||
"asset_viewer_settings_title": "Asset Viewer",
|
||||
"preferences_settings_title": "Preferences",
|
||||
"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.",
|
||||
@@ -168,9 +167,9 @@
|
||||
"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 the Immich server and from your device",
|
||||
"delete_dialog_alert_local": "These items will be permanently deleted 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 deleted from your device",
|
||||
"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",
|
||||
@@ -283,6 +282,10 @@
|
||||
"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",
|
||||
"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",
|
||||
@@ -312,6 +315,7 @@
|
||||
"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.",
|
||||
"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.",
|
||||
@@ -482,4 +486,4 @@
|
||||
"viewer_remove_from_stack": "Remove from Stack",
|
||||
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
||||
"viewer_unstack": "Un-Stack"
|
||||
}
|
||||
}
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "Archivo ({})",
|
||||
"asset_action_delete_err_read_only": "No se pueden borrar los archivos de solo lectura. Saltando.",
|
||||
"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": "Diseño dinámico",
|
||||
"asset_list_layout_settings_group_automatically": "Automatico",
|
||||
"asset_list_layout_settings_group_by": "Agrupar recursos por",
|
||||
"asset_list_layout_settings_group_by_month": "Mes",
|
||||
"asset_list_layout_settings_group_by_month_day": "Mes + día",
|
||||
"asset_list_layout_sub_title": "Layout",
|
||||
"asset_list_settings_subtitle": "Configuraciones del diseño de la cuadrícula de fotos",
|
||||
"asset_list_settings_title": "Cuadrícula de fotos",
|
||||
"asset_viewer_settings_title": "Asset Viewer",
|
||||
"backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})",
|
||||
"backup_album_selection_page_albums_tap": "Toque para incluir, doble toque para excluir",
|
||||
"backup_album_selection_page_assets_scatter": "Los archivos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.",
|
||||
@@ -186,6 +189,7 @@
|
||||
"exif_bottom_sheet_location": "UBICACIÓN",
|
||||
"exif_bottom_sheet_location_add": "Añadir ubicación",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_person_add_person": "Add name",
|
||||
"experimental_settings_new_asset_list_subtitle": "Trabajo en progreso",
|
||||
"experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental",
|
||||
"experimental_settings_subtitle": "Úsalo bajo tu responsabilidad",
|
||||
@@ -278,6 +282,10 @@
|
||||
"map_settings_only_show_favorites": "Mostrar solo favoritas",
|
||||
"map_settings_theme_settings": "Apariencia del Mapa",
|
||||
"map_zoom_to_see_photos": "Alejar para ver fotos",
|
||||
"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",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "Foto en Movimiento",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "No se puede cambiar la fecha de archivos de solo lectura. Saltando.",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "¡Permiso concedido! Todo listo.",
|
||||
"permission_onboarding_permission_limited": "Permiso limitado. Para permitir que Immich haga copia de seguridad y gestione toda tu colección de galería, concede permisos de fotos y videos en Configuración.",
|
||||
"permission_onboarding_request": "Immich requiere permiso para ver tus fotos y videos.",
|
||||
"preferences_settings_title": "Preferences",
|
||||
"profile_drawer_app_logs": "Registros",
|
||||
"profile_drawer_client_out_of_date_major": "La app de móvil está desactualizada. Por favor actualiza a la última versión principal",
|
||||
"profile_drawer_client_out_of_date_minor": "La app de móvil está desactualizada. Por favor actualiza a la última versión menor",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "Archivo ({})",
|
||||
"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": "Diseño dinámico",
|
||||
"asset_list_layout_settings_group_automatically": "Automatico",
|
||||
"asset_list_layout_settings_group_by": "Agrupar recursos por",
|
||||
"asset_list_layout_settings_group_by_month": "Mes",
|
||||
"asset_list_layout_settings_group_by_month_day": "Mes + día",
|
||||
"asset_list_layout_sub_title": "Layout",
|
||||
"asset_list_settings_subtitle": "Configuraciones del diseño de la cuadrícula de fotos",
|
||||
"asset_list_settings_title": "Cuadrícula de fotos",
|
||||
"asset_viewer_settings_title": "Asset Viewer",
|
||||
"backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})",
|
||||
"backup_album_selection_page_albums_tap": "Pulsar para incluir, pulsar dos veces para excluir",
|
||||
"backup_album_selection_page_assets_scatter": "Los archivos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.",
|
||||
@@ -186,6 +189,7 @@
|
||||
"exif_bottom_sheet_location": "UBICACIÓN",
|
||||
"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": "Trabajo en progreso",
|
||||
"experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental",
|
||||
"experimental_settings_subtitle": "Úsalo bajo tu responsabilidad",
|
||||
@@ -278,6 +282,10 @@
|
||||
"map_settings_only_show_favorites": "Mostrar solo favoritas",
|
||||
"map_settings_theme_settings": "Map Theme",
|
||||
"map_zoom_to_see_photos": "Alejar para ver fotos",
|
||||
"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",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "Foto en Movimiento",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "¡Permiso concedido! Todo listo.",
|
||||
"permission_onboarding_permission_limited": "Permiso limitado. Para permitir que Immich haga copia de seguridad y gestione toda tu colección de galería, concede permisos de fotos y videos en Configuración.",
|
||||
"permission_onboarding_request": "Immich requiere permiso para ver tus fotos y videos.",
|
||||
"preferences_settings_title": "Preferences",
|
||||
"profile_drawer_app_logs": "Registros",
|
||||
"profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
|
||||
"profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "Archivo ({})",
|
||||
"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": "Diseño dinámico",
|
||||
"asset_list_layout_settings_group_automatically": "Automatico",
|
||||
"asset_list_layout_settings_group_by": "Agrupar recursos por",
|
||||
"asset_list_layout_settings_group_by_month": "Mes",
|
||||
"asset_list_layout_settings_group_by_month_day": "Mes + día",
|
||||
"asset_list_layout_sub_title": "Layout",
|
||||
"asset_list_settings_subtitle": "Configuraciones del diseño de la cuadrícula de fotos",
|
||||
"asset_list_settings_title": "Cuadrícula de fotos",
|
||||
"asset_viewer_settings_title": "Asset Viewer",
|
||||
"backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})",
|
||||
"backup_album_selection_page_albums_tap": "Pulsar para incluir, pulsar dos veces para excluir",
|
||||
"backup_album_selection_page_assets_scatter": "Los archivos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.",
|
||||
@@ -186,6 +189,7 @@
|
||||
"exif_bottom_sheet_location": "UBICACIÓN",
|
||||
"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": "Trabajo en progreso",
|
||||
"experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental",
|
||||
"experimental_settings_subtitle": "Úsalo bajo tu responsabilidad",
|
||||
@@ -278,6 +282,10 @@
|
||||
"map_settings_only_show_favorites": "Mostrar solo favoritas",
|
||||
"map_settings_theme_settings": "Map Theme",
|
||||
"map_zoom_to_see_photos": "Alejar para ver fotos",
|
||||
"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",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "Foto en Movimiento",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "¡Permiso concedido! Todo listo.",
|
||||
"permission_onboarding_permission_limited": "Permiso limitado. Para permitir que Immich haga copia de seguridad y gestione toda tu colección de galería, concede permisos de fotos y videos en Configuración.",
|
||||
"permission_onboarding_request": "Immich requiere permiso para ver tus fotos y videos.",
|
||||
"preferences_settings_title": "Preferences",
|
||||
"profile_drawer_app_logs": "Registros",
|
||||
"profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
|
||||
"profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "Arkisto ({})",
|
||||
"asset_action_delete_err_read_only": "Vain luku-tilassa olevia kohteita ei voitu poistaa, ohitetaan",
|
||||
"asset_action_share_err_offline": "Verkottomassa tilassa olevia kohteita ei voitu noutaa, ohitetaan",
|
||||
"asset_list_group_by_sub_title": "Ryhmittele",
|
||||
"asset_list_layout_settings_dynamic_layout_title": "Dynaaminen asetelma",
|
||||
"asset_list_layout_settings_group_automatically": "Automaattisesti",
|
||||
"asset_list_layout_settings_group_by": "Ryhmittele",
|
||||
"asset_list_layout_settings_group_by_month": "Kuukauden mukaan",
|
||||
"asset_list_layout_settings_group_by_month_day": "Kuukauden ja päivän mukaan",
|
||||
"asset_list_layout_sub_title": "Asettelu",
|
||||
"asset_list_settings_subtitle": "Kuvaruudukon asettelu",
|
||||
"asset_list_settings_title": "Kuvaruudukko",
|
||||
"asset_viewer_settings_title": "Katselin",
|
||||
"backup_album_selection_page_albums_device": "Laitteen albumit ({})",
|
||||
"backup_album_selection_page_albums_tap": "Napauta sisällyttääksesi, kaksoisnapauta jättääksesi pois",
|
||||
"backup_album_selection_page_assets_scatter": "Kohteet voivat olla hajaantuneina useisiin albumeihin. Albumeita voidaan sisällyttää varmuuskopiointiin tai jättää siitä pois.",
|
||||
@@ -185,7 +188,8 @@
|
||||
"exif_bottom_sheet_details": "TIEDOT",
|
||||
"exif_bottom_sheet_location": "SIJAINTI",
|
||||
"exif_bottom_sheet_location_add": "Lisää sijainti",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_people": "IHMISET",
|
||||
"exif_bottom_sheet_person_add_person": "Add name",
|
||||
"experimental_settings_new_asset_list_subtitle": "Työn alla",
|
||||
"experimental_settings_new_asset_list_title": "Ota käyttöön kokeellinen kuvaruudukko",
|
||||
"experimental_settings_subtitle": "Käyttö omalla vastuulla!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"map_settings_only_show_favorites": "Näytä vain suosikit",
|
||||
"map_settings_theme_settings": "Kartan teema",
|
||||
"map_zoom_to_see_photos": "Tarkenna nähdäksesi kuvat",
|
||||
"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",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "Liikekuvat",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "Vain luku -tilassa olevien kohteiden päivämäärää ei voitu muokata, ohitetaan",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "Käyttöoikeus myönnetty! Kaikki valmista.",
|
||||
"permission_onboarding_permission_limited": "Rajoitettu käyttöoikeus. Salliaksesi Immichin varmuuskopioida ja hallita koko kuvakirjastoasi, myönnä oikeus kuviin ja videoihin asetuksista.",
|
||||
"permission_onboarding_request": "Immich vaatii käyttöoikeuden kuvien ja videoiden käyttämiseen.",
|
||||
"preferences_settings_title": "Asetukset",
|
||||
"profile_drawer_app_logs": "Lokit",
|
||||
"profile_drawer_client_out_of_date_major": "Sovelluksen mobiiliversio on vanhentunut. Päivitä viimeisimpään merkittävään versioon.",
|
||||
"profile_drawer_client_out_of_date_minor": "Sovelluksen mobiiliversio on vanhentunut. Päivitä viimeisimpään versioon.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"action_common_cancel": "Cancel",
|
||||
"action_common_update": "Update",
|
||||
"action_common_cancel": "Annuler",
|
||||
"action_common_update": "Mise à jour",
|
||||
"add_to_album_bottom_sheet_added": "Ajouté à {album}",
|
||||
"add_to_album_bottom_sheet_already_exists": "Déjà dans {album}",
|
||||
"advanced_settings_log_level_title": "Log level: {}",
|
||||
@@ -35,15 +35,18 @@
|
||||
"app_bar_signout_dialog_title": "Se déconnecter",
|
||||
"archive_page_no_archived_assets": "Aucun élément archivé n'a été trouvé",
|
||||
"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_action_delete_err_read_only": "Impossible de supprimer le(s) élément(s) en lecture seule.",
|
||||
"asset_action_share_err_offline": "Impossible de récupérer le(s) élément(s) hors ligne.",
|
||||
"asset_list_group_by_sub_title": "Groupe par",
|
||||
"asset_list_layout_settings_dynamic_layout_title": "Affichage dynamique",
|
||||
"asset_list_layout_settings_group_automatically": "Automatique",
|
||||
"asset_list_layout_settings_group_by": "Grouper les éléments par",
|
||||
"asset_list_layout_settings_group_by_month": "Mois",
|
||||
"asset_list_layout_settings_group_by_month_day": "Mois + jour",
|
||||
"asset_list_layout_sub_title": "Disposition",
|
||||
"asset_list_settings_subtitle": "Paramètres de disposition de la grille de photos",
|
||||
"asset_list_settings_title": "Grille de photos",
|
||||
"asset_viewer_settings_title": "Visualisateur d'éléments",
|
||||
"backup_album_selection_page_albums_device": "Albums sur l'appareil ({})",
|
||||
"backup_album_selection_page_albums_tap": "Tapez pour inclure, tapez deux fois pour exclure",
|
||||
"backup_album_selection_page_assets_scatter": "Les éléments peuvent être répartis sur plusieurs albums. De ce fait, les albums peuvent être inclus ou exclus pendant le processus de sauvegarde.",
|
||||
@@ -142,15 +145,15 @@
|
||||
"control_bottom_app_bar_archive": "Archive",
|
||||
"control_bottom_app_bar_create_new_album": "Créer un nouvel album",
|
||||
"control_bottom_app_bar_delete": "Supprimer",
|
||||
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
|
||||
"control_bottom_app_bar_delete_from_local": "Delete from device",
|
||||
"control_bottom_app_bar_delete_from_immich": "Supprimer de Immich",
|
||||
"control_bottom_app_bar_delete_from_local": "Supprimer de l'appareil",
|
||||
"control_bottom_app_bar_edit_location": "Modifier la localisation",
|
||||
"control_bottom_app_bar_edit_time": "Modifier la date et l'heure",
|
||||
"control_bottom_app_bar_favorite": "Favoris",
|
||||
"control_bottom_app_bar_share": "Partager",
|
||||
"control_bottom_app_bar_share_to": "Partager à",
|
||||
"control_bottom_app_bar_stack": "Empiler",
|
||||
"control_bottom_app_bar_trash_from_immich": "Move to Trash",
|
||||
"control_bottom_app_bar_trash_from_immich": "Déplacer vers la corbeille",
|
||||
"control_bottom_app_bar_unarchive": "Désarchiver",
|
||||
"control_bottom_app_bar_unfavorite": "Enlever des favoris",
|
||||
"control_bottom_app_bar_upload": "Téléverser",
|
||||
@@ -165,27 +168,28 @@
|
||||
"daily_title_text_date_year": "E, dd MMM, yyyy",
|
||||
"date_format": "E, LLL d, y • h:mm a",
|
||||
"delete_dialog_alert": "Ces éléments seront définitivement supprimés de Immich et de votre appareil.",
|
||||
"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_alert_local": "Ces éléments seront définitivement supprimés de votre appareil mais resteront disponibles sur le serveur d'Immich.",
|
||||
"delete_dialog_alert_local_non_backed_up": "Certains éléments ne sont pas sauvegardés sur Immich et seront définitivement supprimés de votre appareil.",
|
||||
"delete_dialog_alert_remote": "Ces éléments seront définitivement supprimés du serveur Immich.",
|
||||
"delete_dialog_cancel": "Annuler",
|
||||
"delete_dialog_ok": "Supprimer",
|
||||
"delete_dialog_ok_force": "Delete Anyway",
|
||||
"delete_dialog_ok_force": "Supprimer tout de même",
|
||||
"delete_dialog_title": "Supprimer définitivement",
|
||||
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
|
||||
"delete_local_dialog_ok_force": "Delete Anyway",
|
||||
"delete_local_dialog_ok_backed_up_only": "Suppression des données sauvegardées uniquement",
|
||||
"delete_local_dialog_ok_force": "Supprimer tout de même",
|
||||
"delete_shared_link_dialog_content": "Êtes-vous sûr de vouloir supprimer ce lien partagé ?",
|
||||
"delete_shared_link_dialog_title": "Supprimer le lien partagé",
|
||||
"description_input_hint_text": "Ajouter une description…",
|
||||
"description_input_submit_error": "Erreur de mise à jour de la description, vérifier le journal pour plus de détails",
|
||||
"edit_date_time_dialog_date_time": "Date and Time",
|
||||
"edit_date_time_dialog_timezone": "Timezone",
|
||||
"edit_location_dialog_title": "Location",
|
||||
"edit_date_time_dialog_date_time": "Date et heure",
|
||||
"edit_date_time_dialog_timezone": "Fuseau horaire",
|
||||
"edit_location_dialog_title": "Localisation",
|
||||
"exif_bottom_sheet_description": "Ajouter une description…",
|
||||
"exif_bottom_sheet_details": "DÉTAILS",
|
||||
"exif_bottom_sheet_location": "LOCALISATION",
|
||||
"exif_bottom_sheet_location_add": "Add a location",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_people": "PERSONNES",
|
||||
"exif_bottom_sheet_person_add_person": "Ajouter un nom",
|
||||
"experimental_settings_new_asset_list_subtitle": "En cours de développement",
|
||||
"experimental_settings_new_asset_list_title": "Activer la grille de photos expérimentale",
|
||||
"experimental_settings_subtitle": "Utilisez à vos dépends !",
|
||||
@@ -195,14 +199,14 @@
|
||||
"home_page_add_to_album_conflicts": "{added} éléments ajoutés à l'album {album}. Les éléments {failed} sont déjà dans l'album.",
|
||||
"home_page_add_to_album_err_local": "Impossible d'ajouter des éléments locaux aux albums pour le moment, étape ignorée",
|
||||
"home_page_add_to_album_success": "{added} éléments ajoutés à l'album {album}.",
|
||||
"home_page_album_err_partner": "Impossible d'ajouter les médias de partenaire à un album pour le moment, cette opération est donc ignorée.",
|
||||
"home_page_album_err_partner": "Il n'est pas encore possible d'ajouter des éléments d'un partenaire à un album.",
|
||||
"home_page_archive_err_local": "Impossible d'archiver les ressources locales pour l'instant, étape ignorée",
|
||||
"home_page_archive_err_partner": "Impossible d'archiver les médias de partenaire à un album pour le moment, cette opération est donc ignorée.",
|
||||
"home_page_archive_err_partner": "Impossible d'archiver les éléments d'un partenaire.",
|
||||
"home_page_building_timeline": "Construction de la chronologie",
|
||||
"home_page_delete_err_partner": "Impossible de supprimer les médias de partenaire à un album pour le moment, cette opération est donc ignorée.",
|
||||
"home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping",
|
||||
"home_page_delete_err_partner": "Ne peut pas supprimer les éléments d'un partenaire.",
|
||||
"home_page_delete_remote_err_local": "Des éléments locaux sont dans la sélection de suppression à distance, ils sont donc ignorés.",
|
||||
"home_page_favorite_err_local": "Impossible d'ajouter les médias locaux aux favoris pour le moment, cette opération est donc ignorée.",
|
||||
"home_page_favorite_err_partner": "Impossible de marquer comme favoris les médias de partenaires pour le moment, cette opération est donc ignorée.",
|
||||
"home_page_favorite_err_partner": "Il n'est pas encore possible de mettre en favori les éléments d'un partenaire.",
|
||||
"home_page_first_time_notice": "Si c'est la première fois que vous utilisez l'application, veillez à choisir un ou plusieurs albums de sauvegarde afin que la chronologie puisse alimenter les photos et les vidéos de cet ou ces albums.",
|
||||
"home_page_share_err_local": "Impossible de partager par lien les médias locaux, cette opération est donc ignorée.",
|
||||
"home_page_upload_err_limit": "Limite de téléchargement de 30 éléments en même temps, demande ignorée",
|
||||
@@ -276,12 +280,16 @@
|
||||
"map_settings_include_show_archived": "Inclure les archives",
|
||||
"map_settings_only_relative_range": "Plage de dates",
|
||||
"map_settings_only_show_favorites": "Afficher uniquement les favoris",
|
||||
"map_settings_theme_settings": "Map Theme",
|
||||
"map_settings_theme_settings": "Thème de la carte",
|
||||
"map_zoom_to_see_photos": "Dézoomer pour voir les photos",
|
||||
"memories_all_caught_up": "Vous avez tout vu",
|
||||
"memories_check_back_tomorrow": "Revenez demain pour d'autres souvenirs",
|
||||
"memories_start_over": "Recommencer",
|
||||
"memories_swipe_to_close": "Balayez vers le haut pour fermer",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "Photos avec mouvement",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "Impossible de modifier l'emplacement de médias en lecture seule, la modification est donc ignorée.",
|
||||
"multiselect_grid_edit_gps_err_read_only": "Impossible de modifier l'emplacement de médias en lecture seule, la modification est donc ignorée.",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "Impossible de modifier la date d'un élément d'actif en lecture seule.",
|
||||
"multiselect_grid_edit_gps_err_read_only": "Impossible de modifier l'emplacement d'un élément en lecture seule.",
|
||||
"notification_permission_dialog_cancel": "Annuler",
|
||||
"notification_permission_dialog_content": "Pour activer les notifications, allez dans Paramètres et sélectionnez Autoriser.",
|
||||
"notification_permission_dialog_settings": "Paramètres",
|
||||
@@ -297,7 +305,7 @@
|
||||
"partner_page_stop_sharing_content": "{} ne pourra plus accéder à vos photos.",
|
||||
"partner_page_stop_sharing_title": "Arrêter de partager vos photos ?",
|
||||
"partner_page_title": "Partenaire",
|
||||
"permission_onboarding_back": "Back",
|
||||
"permission_onboarding_back": "Retour",
|
||||
"permission_onboarding_continue_anyway": "Continuer quand même",
|
||||
"permission_onboarding_get_started": "Commencer",
|
||||
"permission_onboarding_go_to_settings": "Accéder aux paramètres",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "Permission accordée ! Vous êtes prêts.",
|
||||
"permission_onboarding_permission_limited": "Permission limitée. Pour permettre à Immich de sauvegarder et de gérer l'ensemble de votre bibliothèque, accordez l'autorisation pour les photos et vidéos dans les Paramètres.",
|
||||
"permission_onboarding_request": "Immich demande l'autorisation de visionner vos photos et vidéo",
|
||||
"preferences_settings_title": "Préférences",
|
||||
"profile_drawer_app_logs": "Journaux",
|
||||
"profile_drawer_client_out_of_date_major": "L'application mobile est obsolète. Veuillez effectuer la mise à jour vers la dernière version majeure.",
|
||||
"profile_drawer_client_out_of_date_minor": "L'application mobile est obsolète. Veuillez effectuer la mise à jour vers la dernière version mineure.",
|
||||
@@ -414,8 +423,8 @@
|
||||
"shared_link_edit_show_meta": "Afficher les métadonnées",
|
||||
"shared_link_edit_submit_button": "Mettre à jour le lien",
|
||||
"shared_link_empty": "Vous n'avez pas de liens partagés",
|
||||
"shared_link_error_server_url_fetch": "Cannot fetch the server url",
|
||||
"shared_link_expired": "Expired",
|
||||
"shared_link_error_server_url_fetch": "Impossible de récupérer l'url du serveur",
|
||||
"shared_link_expired": "Expiré",
|
||||
"shared_link_expires_day": "Expire dans {} jour",
|
||||
"shared_link_expires_days": "Expire dans {} jours",
|
||||
"shared_link_expires_hour": "Expire dans {} heure",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"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_viewer_settings_title": "Asset Viewer",
|
||||
"backup_album_selection_page_albums_device": "Albums on device ({})",
|
||||
"backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
|
||||
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
|
||||
@@ -186,6 +189,7 @@
|
||||
"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!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"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",
|
||||
"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",
|
||||
@@ -307,6 +315,7 @@
|
||||
"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.",
|
||||
"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.",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "Archívum ({})",
|
||||
"asset_action_delete_err_read_only": "Nem sikerült törölni a csak-olvasható elem(ek)et, így ezeket átugorjuk",
|
||||
"asset_action_share_err_offline": "Nem sikerült betölteni az offline elem(ek)et, így ezeket kihagyjuk",
|
||||
"asset_list_group_by_sub_title": "Group by",
|
||||
"asset_list_layout_settings_dynamic_layout_title": "Dinamikus elrendezés",
|
||||
"asset_list_layout_settings_group_automatically": "Automatikus",
|
||||
"asset_list_layout_settings_group_by": "Elemek csoportosítása",
|
||||
"asset_list_layout_settings_group_by_month": "hónapok szerint",
|
||||
"asset_list_layout_settings_group_by_month_day": "hónap és nap szerint",
|
||||
"asset_list_layout_sub_title": "Layout",
|
||||
"asset_list_settings_subtitle": "Fotórács elrendezése",
|
||||
"asset_list_settings_title": "Fotórács",
|
||||
"asset_viewer_settings_title": "Asset Viewer",
|
||||
"backup_album_selection_page_albums_device": "Ezen az eszközön lévő albumok ({})",
|
||||
"backup_album_selection_page_albums_tap": "Koppincs a hozzáadáshoz, duplán koppincs az eltávolításhoz",
|
||||
"backup_album_selection_page_assets_scatter": "Egy elem több albumban is lehet. Ezért a mentéshez albumokat lehet hozzáadni vagy azokat a mentésből kihagyni.",
|
||||
@@ -186,6 +189,7 @@
|
||||
"exif_bottom_sheet_location": "HELYSZÍN",
|
||||
"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": "Fejlesztés alatt",
|
||||
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
||||
"experimental_settings_subtitle": "Csak saját felelősségre használd",
|
||||
@@ -278,6 +282,10 @@
|
||||
"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",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "Mozgó Fotók",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "Hozzáférés engedélyezve! Minden készen áll.",
|
||||
"permission_onboarding_permission_limited": "Korlátozott hozzáférés. Ha szeretnéd, hogy az Immich a teljes galéria gyűjteményedet mentse és kezelje, akkor a Beállításokban engedélyezd a fotó és videó jogosultságokat.",
|
||||
"permission_onboarding_request": "Engedélyezni kell, hogy az Immich hozzáférjen a képekhez és videókhoz",
|
||||
"preferences_settings_title": "Preferences",
|
||||
"profile_drawer_app_logs": "Naplók",
|
||||
"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.",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"advanced_settings_self_signed_ssl_subtitle": "Salta la verifica dei certificati SSL del server. Richiesto con l'uso di certificati self-signed.",
|
||||
"advanced_settings_self_signed_ssl_title": "Consenti certificati SSL self-signed",
|
||||
"advanced_settings_tile_subtitle": "Impostazioni aggiuntive utenti",
|
||||
"advanced_settings_tile_title": "Avanzato",
|
||||
"advanced_settings_tile_title": "Avanzate",
|
||||
"advanced_settings_troubleshooting_subtitle": "Attiva funzioni addizionali per la risoluzione dei problemi",
|
||||
"advanced_settings_troubleshooting_title": "Risoluzione problemi",
|
||||
"album_info_card_backup_album_excluded": "ESCLUSI",
|
||||
@@ -36,14 +36,17 @@
|
||||
"archive_page_no_archived_assets": "Nessuna oggetto archiviato",
|
||||
"archive_page_title": "Archivia ({})",
|
||||
"asset_action_delete_err_read_only": "Non posso eliminare degli elementi in sola lettura, ignorato",
|
||||
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
|
||||
"asset_action_share_err_offline": "Non è possibile recuperare le risorse offline, ignoro",
|
||||
"asset_list_group_by_sub_title": "Raggruppa per",
|
||||
"asset_list_layout_settings_dynamic_layout_title": "Layout dinamico",
|
||||
"asset_list_layout_settings_group_automatically": "Automatico",
|
||||
"asset_list_layout_settings_group_by": "Raggruppa le immagini per",
|
||||
"asset_list_layout_settings_group_by_month": "Mese",
|
||||
"asset_list_layout_settings_group_by_month_day": "Mese + giorno",
|
||||
"asset_list_layout_sub_title": "Layout",
|
||||
"asset_list_settings_subtitle": "Impostazion del layout della griglia delle foto",
|
||||
"asset_list_settings_title": "Griglia foto",
|
||||
"asset_viewer_settings_title": "Visualizzazione risorse",
|
||||
"backup_album_selection_page_albums_device": "Album sul dispositivo ({})",
|
||||
"backup_album_selection_page_albums_tap": "Tap per includere, doppio tap per escludere.",
|
||||
"backup_album_selection_page_assets_scatter": "Stesse immagini e video possono trovarsi tra più album, così gli album possono essere inclusi o esclusi dal backup.",
|
||||
@@ -147,13 +150,13 @@
|
||||
"control_bottom_app_bar_edit_location": "Modifica posizione",
|
||||
"control_bottom_app_bar_edit_time": "Modifica data e ora",
|
||||
"control_bottom_app_bar_favorite": "Preferiti",
|
||||
"control_bottom_app_bar_share": "Condividi",
|
||||
"control_bottom_app_bar_share_to": "Share To",
|
||||
"control_bottom_app_bar_share": "Condivisione",
|
||||
"control_bottom_app_bar_share_to": "Condividi a",
|
||||
"control_bottom_app_bar_stack": "Stack",
|
||||
"control_bottom_app_bar_trash_from_immich": "Sposta nel cestino",
|
||||
"control_bottom_app_bar_unarchive": "Rimuovi dagli archivi",
|
||||
"control_bottom_app_bar_unfavorite": "Unfavorite",
|
||||
"control_bottom_app_bar_upload": "Upload",
|
||||
"control_bottom_app_bar_unfavorite": "Rimuovi preferito",
|
||||
"control_bottom_app_bar_upload": "Carica",
|
||||
"create_album_page_untitled": "Senza titolo",
|
||||
"create_shared_album_page_create": "Crea",
|
||||
"create_shared_album_page_share": "Condividi",
|
||||
@@ -185,7 +188,8 @@
|
||||
"exif_bottom_sheet_details": "DETTAGLI",
|
||||
"exif_bottom_sheet_location": "POSIZIONE",
|
||||
"exif_bottom_sheet_location_add": "Aggiungi una posizione",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_people": "PERSONE",
|
||||
"exif_bottom_sheet_person_add_person": "Aggiungi nome",
|
||||
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
||||
"experimental_settings_new_asset_list_title": "Attiva griglia di foto sperimentale",
|
||||
"experimental_settings_subtitle": "Usalo a tuo rischio!",
|
||||
@@ -208,7 +212,7 @@
|
||||
"home_page_upload_err_limit": "Puoi caricare al massimo 30 file per volta, ignora quelli in eccesso",
|
||||
"image_viewer_page_state_provider_download_error": "Errore nel Download",
|
||||
"image_viewer_page_state_provider_download_success": "Download con successo",
|
||||
"image_viewer_page_state_provider_share_error": "Share Error",
|
||||
"image_viewer_page_state_provider_share_error": "Errore di condivisione",
|
||||
"library_page_albums": "Album",
|
||||
"library_page_archive": "Archivia",
|
||||
"library_page_device_albums": "Album sul dispositivo",
|
||||
@@ -216,7 +220,7 @@
|
||||
"library_page_new_album": "Nuovo Album",
|
||||
"library_page_sharing": "Condividendo",
|
||||
"library_page_sort_asset_count": "Numero di elementi",
|
||||
"library_page_sort_created": "Creato il più recente",
|
||||
"library_page_sort_created": "Data di creazione",
|
||||
"library_page_sort_last_modified": "Ultima modifica",
|
||||
"library_page_sort_most_oldest_photo": "Foto più vecchia",
|
||||
"library_page_sort_most_recent_photo": "Più recente",
|
||||
@@ -265,19 +269,23 @@
|
||||
"map_no_location_permission_content": "L'accesso alla posizione è necessario per visualizzare gli elementi per la tua posizione attuale. Vuoi consentirlo adesso?",
|
||||
"map_no_location_permission_title": "Location Permission denied",
|
||||
"map_settings_dark_mode": "Modalità scura",
|
||||
"map_settings_date_range_option_all": "All",
|
||||
"map_settings_date_range_option_all": "Tutto",
|
||||
"map_settings_date_range_option_day": "Ultime 24 ore",
|
||||
"map_settings_date_range_option_days": "Ultimi {} giorni",
|
||||
"map_settings_date_range_option_year": "Ultimo anno",
|
||||
"map_settings_date_range_option_years": "Ultimi {} anni",
|
||||
"map_settings_dialog_cancel": "Cancel",
|
||||
"map_settings_dialog_cancel": "Annulla",
|
||||
"map_settings_dialog_save": "Salva",
|
||||
"map_settings_dialog_title": "Map Settings",
|
||||
"map_settings_include_show_archived": "Include Archived",
|
||||
"map_settings_only_relative_range": "Date range",
|
||||
"map_settings_only_show_favorites": "Show Favorite Only",
|
||||
"map_settings_dialog_title": "Impostazioni Mappa",
|
||||
"map_settings_include_show_archived": "Includi Archiviati",
|
||||
"map_settings_only_relative_range": "Intervallo date",
|
||||
"map_settings_only_show_favorites": "Mostra solo preferiti",
|
||||
"map_settings_theme_settings": "Tema della mappa",
|
||||
"map_zoom_to_see_photos": "Zoom out to see photos",
|
||||
"memories_all_caught_up": "All caught up",
|
||||
"memories_check_back_tomorrow": "Torna domani per altri ricordi",
|
||||
"memories_start_over": "Ricomincia",
|
||||
"memories_swipe_to_close": "Scorri sopra per chiudere",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "Motion Foto",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "Non posso modificare la data degli elementi in sola lettura, ignorato",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "Concessi i permessi! Ora sei tutto apposto",
|
||||
"permission_onboarding_permission_limited": "Permessi limitati. Perché Immich possa controllare e fare i backup di tutte le foto, concedere i permessi all'intera galleria dalle impostazioni ",
|
||||
"permission_onboarding_request": "Immich richiede i permessi per vedere le tue foto e video",
|
||||
"preferences_settings_title": "Preferenze",
|
||||
"profile_drawer_app_logs": "Logs",
|
||||
"profile_drawer_client_out_of_date_major": "L'applicazione non è aggiornata. Per favore aggiorna all'ultima versione principale.",
|
||||
"profile_drawer_client_out_of_date_minor": "L'applicazione non è aggiornata. Per favore aggiorna all'ultima versione minore.",
|
||||
@@ -317,7 +326,7 @@
|
||||
"profile_drawer_server_out_of_date_minor": "Il server non è aggiornato. Per favore aggiorna all'ultima versione minore.",
|
||||
"profile_drawer_settings": "Impostazioni ",
|
||||
"profile_drawer_sign_out": "Logout",
|
||||
"profile_drawer_trash": "Trash",
|
||||
"profile_drawer_trash": "Cestino",
|
||||
"recently_added_page_title": "Aggiunti di recente",
|
||||
"scaffold_body_error_occurred": "Si è verificato un errore.",
|
||||
"search_bar_hint": "Cerca le tue foto",
|
||||
@@ -357,7 +366,7 @@
|
||||
"setting_image_viewer_original_subtitle": "Abilita per caricare l'immagine originale a risoluzione massima (grande!). Disabilita per ridurre l'utilizzo di banda (sia sul network che nella cache del dispositivo).",
|
||||
"setting_image_viewer_original_title": "Carica l'immagine originale",
|
||||
"setting_image_viewer_preview_subtitle": "Abilita per caricare un'immagine a risoluzione media.\nDisabilita per caricare direttamente l'immagine originale o usare la thumbnail.",
|
||||
"setting_image_viewer_preview_title": "Carica immagine di preview",
|
||||
"setting_image_viewer_preview_title": "Carica immagine di anteprima",
|
||||
"setting_notifications_notify_failures_grace_period": "Notifica caricamenti falliti in background: {}",
|
||||
"setting_notifications_notify_hours": "{} ore",
|
||||
"setting_notifications_notify_immediately": "immediatamente",
|
||||
@@ -377,10 +386,10 @@
|
||||
"share_add_title": "Aggiungi un titolo ",
|
||||
"share_create_album": "Crea album",
|
||||
"shared_album_activities_input_disable": "I commenti sono disabilitati",
|
||||
"shared_album_activities_input_hint": "Say something",
|
||||
"shared_album_activities_input_hint": "Dici qualcosa",
|
||||
"shared_album_activity_remove_content": "Vuoi eliminare questa attività?",
|
||||
"shared_album_activity_remove_title": "Elimina attività",
|
||||
"shared_album_activity_setting_subtitle": "Let others respond",
|
||||
"shared_album_activity_setting_subtitle": "Permetti agli altri di rispondere",
|
||||
"shared_album_activity_setting_title": "Commenti e Mi piace",
|
||||
"shared_album_section_people_action_error": "Errore durante la rimozione/uscita dell'album",
|
||||
"shared_album_section_people_action_leave": "Rimuovi utente dall'album",
|
||||
@@ -414,7 +423,7 @@
|
||||
"shared_link_edit_show_meta": "Visualizza metadati",
|
||||
"shared_link_edit_submit_button": "Aggiorna link",
|
||||
"shared_link_empty": "Non hai alcun link condiviso",
|
||||
"shared_link_error_server_url_fetch": "Cannot fetch the server url",
|
||||
"shared_link_error_server_url_fetch": "Non è possibile trovare l'indirizzo del server",
|
||||
"shared_link_expired": "Scaduto",
|
||||
"shared_link_expires_day": "Scade tra {} giorno",
|
||||
"shared_link_expires_days": "Scade tra {} giorni",
|
||||
@@ -429,18 +438,18 @@
|
||||
"shared_link_info_chip_metadata": "EXIF",
|
||||
"shared_link_info_chip_upload": "Carica",
|
||||
"shared_link_manage_links": "Gestisci link condivisi",
|
||||
"share_done": "Done",
|
||||
"share_done": "Fatto",
|
||||
"share_invite": "Invita nell'album ",
|
||||
"sharing_page_album": "Album condivisi",
|
||||
"sharing_page_description": "Crea un album condiviso per condividere foto e video con persone sul tuo network",
|
||||
"sharing_page_empty_list": "LISTA VUOTA",
|
||||
"sharing_silver_appbar_create_shared_album": "Crea album condiviso",
|
||||
"sharing_silver_appbar_shared_links": "Link condivisi",
|
||||
"sharing_silver_appbar_share_partner": "Condividi con il partner",
|
||||
"sharing_silver_appbar_share_partner": "Condividi con partner",
|
||||
"tab_controller_nav_library": "Libreria",
|
||||
"tab_controller_nav_photos": "Foto",
|
||||
"tab_controller_nav_search": "Cerca",
|
||||
"tab_controller_nav_sharing": "Condividi",
|
||||
"tab_controller_nav_sharing": "Condivisione",
|
||||
"theme_setting_asset_list_storage_indicator_title": "Mostra indicatore dello storage nei titoli dei contenuti",
|
||||
"theme_setting_asset_list_tiles_per_row_title": "Numero di contenuti per riga ({})",
|
||||
"theme_setting_dark_mode_switch": "Dark mode",
|
||||
@@ -463,7 +472,7 @@
|
||||
"trash_page_restore_all": "Ripristina tutto",
|
||||
"trash_page_select_assets_btn": "Seleziona elemento",
|
||||
"trash_page_select_btn": "Seleziona",
|
||||
"trash_page_title": "Trash ({})",
|
||||
"trash_page_title": "Cestino ({})",
|
||||
"upload_dialog_cancel": "Cancella",
|
||||
"upload_dialog_info": "Vuoi fare il backup sul server di ciò che hai selezionato?",
|
||||
"upload_dialog_ok": "Carica",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "アーカイブ({})",
|
||||
"asset_action_delete_err_read_only": "読み取り専用の項目は削除できません。スキップします",
|
||||
"asset_action_share_err_offline": "オフラインの項目をゲットできません。スキップします",
|
||||
"asset_list_group_by_sub_title": "Group by",
|
||||
"asset_list_layout_settings_dynamic_layout_title": "ダイナミックレイアウト",
|
||||
"asset_list_layout_settings_group_automatically": "自動",
|
||||
"asset_list_layout_settings_group_by": "写真のグループ分け",
|
||||
"asset_list_layout_settings_group_by_month": "月",
|
||||
"asset_list_layout_settings_group_by_month_day": "月 + 日",
|
||||
"asset_list_layout_sub_title": "Layout",
|
||||
"asset_list_settings_subtitle": "グリッドに関する設定",
|
||||
"asset_list_settings_title": "グリッド",
|
||||
"asset_viewer_settings_title": "Asset Viewer",
|
||||
"backup_album_selection_page_albums_device": "端末上のアルバム数: {} ",
|
||||
"backup_album_selection_page_albums_tap": "タップで選択、ダブルタップで除外",
|
||||
"backup_album_selection_page_assets_scatter": "同じ写真が複数のアルバムに登録されていることがあるので、アルバムを選択・除外してバックアップする写真を選べます。",
|
||||
@@ -186,6 +189,7 @@
|
||||
"exif_bottom_sheet_location": "撮影場所",
|
||||
"exif_bottom_sheet_location_add": "位置情報を追加",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_person_add_person": "Add name",
|
||||
"experimental_settings_new_asset_list_subtitle": "製作途中(WIP)",
|
||||
"experimental_settings_new_asset_list_title": "試験的なグリッドを有効化",
|
||||
"experimental_settings_subtitle": "試験的機能につき自己責任で!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"map_settings_only_show_favorites": "お気に入りのみを表示",
|
||||
"map_settings_theme_settings": "マップの見た目",
|
||||
"map_zoom_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",
|
||||
"monthly_title_text_date_format": "yyyy年 MM月",
|
||||
"motion_photos_page_title": "モーションフォト",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "読み取り専用の項目の日付を変更できません",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "写真へのアクセスが許可されました",
|
||||
"permission_onboarding_permission_limited": "写真へのアクセスが制限されています。Immichに写真のバックアップと管理を行わせるにはシステム設定から写真と動画のアクセス権限を変更してください。",
|
||||
"permission_onboarding_request": "Immichは写真へのアクセス許可が必要です",
|
||||
"preferences_settings_title": "Preferences",
|
||||
"profile_drawer_app_logs": "ログ",
|
||||
"profile_drawer_client_out_of_date_major": "アプリが更新されてません。最新のバージョンに更新してください",
|
||||
"profile_drawer_client_out_of_date_minor": "アプリが更新されてません。最新のマイナーバージョンに更新してください",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "보관 ({})",
|
||||
"asset_action_delete_err_read_only": "읽기 전용 미디어를 삭제할 수 없어 건너뜁니다",
|
||||
"asset_action_share_err_offline": "오프라인 미디어를 가져올 수 없어 건너뜁니다",
|
||||
"asset_list_group_by_sub_title": "그룹화 기준",
|
||||
"asset_list_layout_settings_dynamic_layout_title": "다이나믹 레이아웃",
|
||||
"asset_list_layout_settings_group_automatically": "자동",
|
||||
"asset_list_layout_settings_group_by": "다음으로 미디어 그룹화",
|
||||
"asset_list_layout_settings_group_by_month": "월",
|
||||
"asset_list_layout_settings_group_by_month_day": "월 + 일",
|
||||
"asset_list_layout_sub_title": "레이아웃",
|
||||
"asset_list_settings_subtitle": "사진 배열 레이아웃 설정",
|
||||
"asset_list_settings_title": "사진 배열",
|
||||
"asset_viewer_settings_title": "미디어 뷰어",
|
||||
"backup_album_selection_page_albums_device": "기기의 앨범({})",
|
||||
"backup_album_selection_page_albums_tap": "포함하려면 탭하고 제외하려면 두 번 탭하세요",
|
||||
"backup_album_selection_page_assets_scatter": "미디어파일은 여러 앨범에 분산될 수 있습니다. 따라서 백업 프로세스 중에 앨범에서 포함하거나 제외할 수 있습니다.",
|
||||
@@ -113,7 +116,7 @@
|
||||
"cache_settings_clear_cache_button_title": "앱의 캐시를 지웁니다. 이 작업은 캐시가 다시 빌드될 때까지 앱의 성능에 상당한 영향을 미칩니다.",
|
||||
"cache_settings_duplicated_assets_clear_button": "클리어",
|
||||
"cache_settings_duplicated_assets_subtitle": "앱에서 블랙리스트에 오른 사진 및 동영상",
|
||||
"cache_settings_duplicated_assets_title": "중복된 자산 ({})",
|
||||
"cache_settings_duplicated_assets_title": "중복된 미디어 ({})",
|
||||
"cache_settings_image_cache_size": "이미재 캐시 크기 ({} 미디어)",
|
||||
"cache_settings_statistics_album": "라이브러리 썸네일",
|
||||
"cache_settings_statistics_assets": "{} 미디어 ({})",
|
||||
@@ -185,7 +188,8 @@
|
||||
"exif_bottom_sheet_details": "상세정보",
|
||||
"exif_bottom_sheet_location": "위치",
|
||||
"exif_bottom_sheet_location_add": "위치 지정",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_people": "사람들",
|
||||
"exif_bottom_sheet_person_add_person": "이름 추가",
|
||||
"experimental_settings_new_asset_list_subtitle": "진행중",
|
||||
"experimental_settings_new_asset_list_title": "실험적 사진 그리드 적용",
|
||||
"experimental_settings_subtitle": "문제시 책임지지 않습니다!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"map_settings_only_show_favorites": "즐겨찾기에만 표시",
|
||||
"map_settings_theme_settings": "지도 테마",
|
||||
"map_zoom_to_see_photos": "축소하여 사진 보기",
|
||||
"memories_all_caught_up": "모두 따라잡기",
|
||||
"memories_check_back_tomorrow": "더 많은 추억을 위해 내일 다시 확인하세요.",
|
||||
"memories_start_over": "다시 시작",
|
||||
"memories_swipe_to_close": "위로 스와이프하여 닫기",
|
||||
"monthly_title_text_date_format": "y년 M월",
|
||||
"motion_photos_page_title": "모션 사진",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "읽기 전용 미디어의 날짜를 편집할 수 없어 건너뜁니다",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "승인되었습니다! 모든 준비가 완료되었습니다",
|
||||
"permission_onboarding_permission_limited": "권한 제한. Immich가 전체 갤러리 컬렉션을 백업하고 관리하도록 하려면 설정에서 사진 및 동영상 권한을 부여하세요",
|
||||
"permission_onboarding_request": "Immich는 사진과 동영상을 볼 수 있는 권한을 요구합니다",
|
||||
"preferences_settings_title": "기본 설정",
|
||||
"profile_drawer_app_logs": "로그",
|
||||
"profile_drawer_client_out_of_date_major": "모바일 앱이 오래되었습니다. 최신 메이져 버전으로 업데이트하세요.",
|
||||
"profile_drawer_client_out_of_date_minor": "모바일 앱이 구 버전입니다. 최신 마이너 버전으로 업데이트하세요",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "Arhīvs ({})",
|
||||
"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": "Dinamiskais izkārtojums",
|
||||
"asset_list_layout_settings_group_automatically": "Automātiski",
|
||||
"asset_list_layout_settings_group_by": "Grupēt aktīvus pēc",
|
||||
"asset_list_layout_settings_group_by_month": "Mēnesis",
|
||||
"asset_list_layout_settings_group_by_month_day": "Mēnesis + diena",
|
||||
"asset_list_layout_sub_title": "Layout",
|
||||
"asset_list_settings_subtitle": "Fotorežģa izkārtojuma iestatījumi",
|
||||
"asset_list_settings_title": "Fotorežģis",
|
||||
"asset_viewer_settings_title": "Asset Viewer",
|
||||
"backup_album_selection_page_albums_device": "Albumi ierīcē ({})",
|
||||
"backup_album_selection_page_albums_tap": "Pieskarieties, lai iekļautu, veiciet dubultskārienu, lai izslēgtu",
|
||||
"backup_album_selection_page_assets_scatter": "Aktīvi var būt izmētāti pa vairākiem albumiem. Tādējādi dublēšanas procesā albumus var iekļaut vai neiekļaut.",
|
||||
@@ -186,6 +189,7 @@
|
||||
"exif_bottom_sheet_location": "ATRAŠANĀS VIETA",
|
||||
"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": "Izstrādes posmā",
|
||||
"experimental_settings_new_asset_list_title": "Iespējot eksperimentālo fotorežģi",
|
||||
"experimental_settings_subtitle": "Izmanto uzņemoties risku!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"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",
|
||||
"monthly_title_text_date_format": "MMMM g",
|
||||
"motion_photos_page_title": "Kustību Fotoattēli",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "Atļauja piešķirta! Jūs esat gatavi darbam.",
|
||||
"permission_onboarding_permission_limited": "Atļauja ierobežota. Lai atļautu Immich dublēšanu un varētu pārvaldīt visu galeriju kolekciju, sadaļā Iestatījumi piešķiriet fotoattēlu un video atļaujas.",
|
||||
"permission_onboarding_request": "Immich nepieciešama atļauja skatīt jūsu fotoattēlus un videoklipus.",
|
||||
"preferences_settings_title": "Preferences",
|
||||
"profile_drawer_app_logs": "Žurnāli",
|
||||
"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.",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"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_viewer_settings_title": "Asset Viewer",
|
||||
"backup_album_selection_page_albums_device": "Albums on device ({})",
|
||||
"backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
|
||||
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
|
||||
@@ -186,6 +189,7 @@
|
||||
"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!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"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",
|
||||
"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",
|
||||
@@ -307,6 +315,7 @@
|
||||
"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.",
|
||||
"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.",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "Arkiv ({})",
|
||||
"asset_action_delete_err_read_only": "Kan ikke slette objekt(er) med kun lese-rettighet, hopper over",
|
||||
"asset_action_share_err_offline": "Kan ikke hente offline objekt(er), hopper over",
|
||||
"asset_list_group_by_sub_title": "Grupper etter",
|
||||
"asset_list_layout_settings_dynamic_layout_title": "Dynamisk bildeorganisering",
|
||||
"asset_list_layout_settings_group_automatically": "Automatisk",
|
||||
"asset_list_layout_settings_group_by": "Grupper bilder etter",
|
||||
"asset_list_layout_settings_group_by_month": "Måned",
|
||||
"asset_list_layout_settings_group_by_month_day": "Måned + dag",
|
||||
"asset_list_layout_sub_title": "Fordeling",
|
||||
"asset_list_settings_subtitle": "Innstillinger for layout av fotorutenett",
|
||||
"asset_list_settings_title": "Fotorutenett",
|
||||
"asset_viewer_settings_title": "Objektviser",
|
||||
"backup_album_selection_page_albums_device": "Album på enhet ({})",
|
||||
"backup_album_selection_page_albums_tap": "Trykk for å inkludere, dobbelttrykk for å ekskludere",
|
||||
"backup_album_selection_page_assets_scatter": "Objekter kan bli spredd over flere album. Album kan derfor bli inkludert eller ekskludert under sikkerhetskopieringen.",
|
||||
@@ -185,7 +188,8 @@
|
||||
"exif_bottom_sheet_details": "DETALJER",
|
||||
"exif_bottom_sheet_location": "PLASSERING",
|
||||
"exif_bottom_sheet_location_add": "Legg til lokasjon",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_people": "MENNESKER",
|
||||
"exif_bottom_sheet_person_add_person": "Legg til navn",
|
||||
"experimental_settings_new_asset_list_subtitle": "Under utvikling",
|
||||
"experimental_settings_new_asset_list_title": "Aktiver eksperimentell rutenettsvisning",
|
||||
"experimental_settings_subtitle": "Bruk på egen risiko!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"map_settings_only_show_favorites": "Vis kun favoritter",
|
||||
"map_settings_theme_settings": "Karttema",
|
||||
"map_zoom_to_see_photos": "Zoom ut for å se bilder",
|
||||
"memories_all_caught_up": "Alt utført",
|
||||
"memories_check_back_tomorrow": "Sjekk igjen i morgen for flere minner",
|
||||
"memories_start_over": "Start på nytt",
|
||||
"memories_swipe_to_close": "Swipe opp for å lukke",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "Bevegelige bilder",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "Kan ikke endre dato på objekt(er) med kun lese-rettigheter, hopper over",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "Tilgang gitt! Du er i gang.",
|
||||
"permission_onboarding_permission_limited": "Begrenset tilgang. For å la Immich sikkerhetskopiere og håndtere galleriet, tillatt bilde- og video-tilgang i Innstillinger.",
|
||||
"permission_onboarding_request": "Immich trenger tilgang til å se dine bilder og videoer",
|
||||
"preferences_settings_title": "Innstillinger",
|
||||
"profile_drawer_app_logs": "Logg",
|
||||
"profile_drawer_client_out_of_date_major": "Mobilapp er utdatert. Vennligst oppdater til nyeste versjon.",
|
||||
"profile_drawer_client_out_of_date_minor": "Mobilapp er utdatert. Vennligst oppdater til nyeste versjon.",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"album_viewer_appbar_share_remove": "Verwijder uit album",
|
||||
"album_viewer_appbar_share_to": "Delen met",
|
||||
"album_viewer_page_share_add_users": "Gebruikers toevoegen",
|
||||
"all_people_page_title": "Personen",
|
||||
"all_people_page_title": "Mensen",
|
||||
"all_videos_page_title": "Video's",
|
||||
"app_bar_signout_dialog_content": "Weet je zeker dat je wilt uitloggen?",
|
||||
"app_bar_signout_dialog_ok": "Ja",
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "Archief ({})",
|
||||
"asset_action_delete_err_read_only": "Kan alleen-lezen asset(s) niet verwijderen, overslaan",
|
||||
"asset_action_share_err_offline": "Kan offline asset(s) niet ophalen, overslaan",
|
||||
"asset_list_group_by_sub_title": "Groupeer op",
|
||||
"asset_list_layout_settings_dynamic_layout_title": "Dynamische layout",
|
||||
"asset_list_layout_settings_group_automatically": "Automatisch",
|
||||
"asset_list_layout_settings_group_by": "Groupeer assets per",
|
||||
"asset_list_layout_settings_group_by_month": "Maand",
|
||||
"asset_list_layout_settings_group_by_month_day": "Maand + dag",
|
||||
"asset_list_layout_sub_title": "Layout",
|
||||
"asset_list_settings_subtitle": "Fotorasterlayoutinstellingen",
|
||||
"asset_list_settings_title": "Fotoraster",
|
||||
"asset_viewer_settings_title": "Foto weergave",
|
||||
"backup_album_selection_page_albums_device": "Albums op apparaat ({})",
|
||||
"backup_album_selection_page_albums_tap": "Tik om in te voegen, dubbel tik om uit te sluiten",
|
||||
"backup_album_selection_page_assets_scatter": "Assets kunnen over verschillende albums verdeeld zijn, dus albums kunnen ingesloten of uitgesloten zijn van het backup proces.",
|
||||
@@ -186,6 +189,7 @@
|
||||
"exif_bottom_sheet_location": "LOCATIE",
|
||||
"exif_bottom_sheet_location_add": "Locatie toevoegen",
|
||||
"exif_bottom_sheet_people": "MENSEN",
|
||||
"exif_bottom_sheet_person_add_person": "Naam toevoegen",
|
||||
"experimental_settings_new_asset_list_subtitle": "Werk in uitvoering",
|
||||
"experimental_settings_new_asset_list_title": "Experimenteel fotoraster inschakelen",
|
||||
"experimental_settings_subtitle": "Gebruik op eigen risico!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"map_settings_only_show_favorites": "Toon enkel favorieten",
|
||||
"map_settings_theme_settings": "Kaart thema",
|
||||
"map_zoom_to_see_photos": "Zoom uit om foto's te zien",
|
||||
"memories_all_caught_up": "Je bent helemaal bij",
|
||||
"memories_check_back_tomorrow": "Kom morgen terug voor meer herinneringen",
|
||||
"memories_start_over": "Opnieuw beginnen",
|
||||
"memories_swipe_to_close": "Swipe omhoog om te sluiten",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "Bewegende foto's",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "Kan datum van alleen-lezen asset(s) niet wijzigen, overslaan",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "Toestemming verleend. Je bent helemaal klaar.",
|
||||
"permission_onboarding_permission_limited": "Beperkte toestemming. Geef toestemming tot foto's en video's in Instellingen om Immich een back-up te laten maken van je galerij en deze te beheren.",
|
||||
"permission_onboarding_request": "Immich heeft toestemming nodig om je foto's en video's te bekijken.",
|
||||
"preferences_settings_title": "Voorkeuren",
|
||||
"profile_drawer_app_logs": "Logboek",
|
||||
"profile_drawer_client_out_of_date_major": "Mobiele app is verouderd. Werk bij naar de nieuwste hoofdversie.",
|
||||
"profile_drawer_client_out_of_date_minor": "Mobiele app is verouderd. Werk bij naar de nieuwste subversie.",
|
||||
@@ -326,7 +335,7 @@
|
||||
"search_page_motion_photos": "Bewegende foto's",
|
||||
"search_page_no_objects": "Geen objectgegevens beschikbaar",
|
||||
"search_page_no_places": "Geen locatiegegevens beschikbaar",
|
||||
"search_page_people": "Personen",
|
||||
"search_page_people": "Mensen",
|
||||
"search_page_person_add_name_dialog_cancel": "Annuleren",
|
||||
"search_page_person_add_name_dialog_hint": "Naam",
|
||||
"search_page_person_add_name_dialog_save": "Opslaan",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "Archiwum ({})",
|
||||
"asset_action_delete_err_read_only": "Nie można usunąć zasobów tylko do odczytu, pomijam",
|
||||
"asset_action_share_err_offline": "Nie można pobrać zasobów offline, pomijam",
|
||||
"asset_list_group_by_sub_title": "Grupuj według",
|
||||
"asset_list_layout_settings_dynamic_layout_title": "Układ dynamiczny",
|
||||
"asset_list_layout_settings_group_automatically": "Automatyczny",
|
||||
"asset_list_layout_settings_group_by": "Grupuj zasoby według",
|
||||
"asset_list_layout_settings_group_by_month": "Miesiąc",
|
||||
"asset_list_layout_settings_group_by_month_day": "Miesiąc + dzień",
|
||||
"asset_list_layout_sub_title": "Układ",
|
||||
"asset_list_settings_subtitle": "Ustawienia układu siatki zdjęć",
|
||||
"asset_list_settings_title": "Siatka Zdjęć",
|
||||
"asset_viewer_settings_title": "Przeglądarka zasobów",
|
||||
"backup_album_selection_page_albums_device": "Albumy na urządzeniu ({})",
|
||||
"backup_album_selection_page_albums_tap": "Stuknij, aby włączyć, stuknij dwukrotnie, aby wykluczyć",
|
||||
"backup_album_selection_page_assets_scatter": "Pliki mogą być rozproszone w wielu albumach. Dzięki temu albumy mogą być włączane lub wyłączane podczas procesu tworzenia kopii zapasowej.",
|
||||
@@ -185,7 +188,8 @@
|
||||
"exif_bottom_sheet_details": "SZCZEGÓŁY",
|
||||
"exif_bottom_sheet_location": "LOKALIZACJA",
|
||||
"exif_bottom_sheet_location_add": "Dodaj lokalizację",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_people": "LUDZIE",
|
||||
"exif_bottom_sheet_person_add_person": "Dodaj nazwę",
|
||||
"experimental_settings_new_asset_list_subtitle": "Praca w toku",
|
||||
"experimental_settings_new_asset_list_title": "Włącz eksperymentalną układ zdjęć",
|
||||
"experimental_settings_subtitle": "Używaj na własne ryzyko!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"map_settings_only_show_favorites": "Pokaż tylko ulubione",
|
||||
"map_settings_theme_settings": "Map Theme",
|
||||
"map_zoom_to_see_photos": "Pomniejsz, aby zobaczyć zdjęcia",
|
||||
"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",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "Zdjęcia ruchome",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "Nie można edytować daty zasobów tylko do odczytu, pomijanie",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "Pozwolenie udzielone! Wszystko gotowe.",
|
||||
"permission_onboarding_permission_limited": "Pozwolenie ograniczone. Aby umożliwić Immichowi tworzenie kopii zapasowych całej kolekcji galerii i zarządzanie nią, przyznaj uprawnienia do zdjęć i filmów w Ustawieniach.",
|
||||
"permission_onboarding_request": "Immich potrzebuje pozwolenia na przeglądanie Twoich zdjęć i filmów.",
|
||||
"preferences_settings_title": "Ustawienia",
|
||||
"profile_drawer_app_logs": "Logi",
|
||||
"profile_drawer_client_out_of_date_major": "Aplikacja mobilna jest nieaktualna. Zaktualizuj do najnowszej wersji głównej.",
|
||||
"profile_drawer_client_out_of_date_minor": "Aplikacja mobilna jest nieaktualna. Zaktualizuj do najnowszej wersji dodatkowej.",
|
||||
|
||||
@@ -3,22 +3,22 @@
|
||||
"action_common_update": "Atualizar",
|
||||
"add_to_album_bottom_sheet_added": "Adicionar a {album}",
|
||||
"add_to_album_bottom_sheet_already_exists": "Já pertence a {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_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
|
||||
"advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates",
|
||||
"advanced_settings_tile_subtitle": "Advanced user's settings",
|
||||
"advanced_settings_tile_title": "Advanced",
|
||||
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
|
||||
"advanced_settings_troubleshooting_title": "Troubleshooting",
|
||||
"advanced_settings_log_level_title": "Nível de log: {}",
|
||||
"advanced_settings_prefer_remote_subtitle": "Alguns dispositivos são extremamente lentos a carregar miniaturas da memória. Ative esta opção para preferir imagens remotas.",
|
||||
"advanced_settings_prefer_remote_title": "Preferir imagens remotas",
|
||||
"advanced_settings_self_signed_ssl_subtitle": "Salta a verificação do certificado SSL para o endereço do servidor. Necessário para certificados auto-assinados.",
|
||||
"advanced_settings_self_signed_ssl_title": "Permitir certificados SSL auto-assinados",
|
||||
"advanced_settings_tile_subtitle": "Definições avançadas do utilizador",
|
||||
"advanced_settings_tile_title": "Avançado",
|
||||
"advanced_settings_troubleshooting_subtitle": "Ativar funcionalidades adicionais para a resolução de problemas",
|
||||
"advanced_settings_troubleshooting_title": "Resolução de problemas",
|
||||
"album_info_card_backup_album_excluded": "DELETADO",
|
||||
"album_info_card_backup_album_included": "INCLUÍDO",
|
||||
"album_thumbnail_card_item": "1 item",
|
||||
"album_thumbnail_card_items": "{} itens",
|
||||
"album_thumbnail_card_shared": " · Partilhado",
|
||||
"album_thumbnail_owned": "Owned",
|
||||
"album_thumbnail_shared_by": "Shared by {}",
|
||||
"album_thumbnail_owned": "Seu",
|
||||
"album_thumbnail_shared_by": "Partilhado por {}",
|
||||
"album_viewer_appbar_share_delete": "Deletar álbum",
|
||||
"album_viewer_appbar_share_err_delete": "Falha ao deletar álbum",
|
||||
"album_viewer_appbar_share_err_leave": "Falha ao sair do álbum",
|
||||
@@ -26,24 +26,27 @@
|
||||
"album_viewer_appbar_share_err_title": "Falha ao alterar título do álbum",
|
||||
"album_viewer_appbar_share_leave": "Deixar álbum",
|
||||
"album_viewer_appbar_share_remove": "Remover do álbum",
|
||||
"album_viewer_appbar_share_to": "Share To",
|
||||
"album_viewer_appbar_share_to": "Partilhar com",
|
||||
"album_viewer_page_share_add_users": "Adicionar usuários",
|
||||
"all_people_page_title": "People",
|
||||
"all_people_page_title": "Pessoas",
|
||||
"all_videos_page_title": "Vídeos",
|
||||
"app_bar_signout_dialog_content": "Tem a certeza que deseja sair?",
|
||||
"app_bar_signout_dialog_ok": "Yes",
|
||||
"app_bar_signout_dialog_ok": "Sim",
|
||||
"app_bar_signout_dialog_title": "Sair",
|
||||
"archive_page_no_archived_assets": "No archived assets found",
|
||||
"archive_page_title": "Archive ({})",
|
||||
"archive_page_no_archived_assets": "Nenhum recurso arquivado encontrado",
|
||||
"archive_page_title": "Arquivo ({})",
|
||||
"asset_action_delete_err_read_only": "Não é possível eliminar o(s) recurso(s) só de leitura, ignorando",
|
||||
"asset_action_share_err_offline": "Não é possível obter recurso(s) offline, ignorando",
|
||||
"asset_list_group_by_sub_title": "Agrupar por",
|
||||
"asset_list_layout_settings_dynamic_layout_title": "Disposição dinâmica",
|
||||
"asset_list_layout_settings_group_automatically": "Automatic",
|
||||
"asset_list_layout_settings_group_automatically": "Automático",
|
||||
"asset_list_layout_settings_group_by": "Agrupar recursos por",
|
||||
"asset_list_layout_settings_group_by_month": "Mês",
|
||||
"asset_list_layout_settings_group_by_month_day": "Mês + dia",
|
||||
"asset_list_layout_sub_title": "Disposição",
|
||||
"asset_list_settings_subtitle": "Configurações de layout da grelha de fotos",
|
||||
"asset_list_settings_title": "Grelha de fotos",
|
||||
"asset_viewer_settings_title": "Visualizador de recursos",
|
||||
"backup_album_selection_page_albums_device": "Álbuns no dispositivo ({})",
|
||||
"backup_album_selection_page_albums_tap": "Toque para incluir, duplo toque para exluir",
|
||||
"backup_album_selection_page_assets_scatter": "Os itens podem estar espalhados por vários álbuns. Assim, os álbuns podem ser incluídos ou excluídos durante o processo de backup.",
|
||||
@@ -59,9 +62,9 @@
|
||||
"backup_background_service_in_progress_notification": "Fazendo backup de seus itens…",
|
||||
"backup_background_service_upload_failure_notification": "Falha ao carregar {}",
|
||||
"backup_controller_page_albums": "Backup Álbuns",
|
||||
"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_app_refresh_disabled_content": "Active a atualização de aplicações em segundo plano em Definições > Geral > Atualização de aplicações em segundo plano para utilizar a cópia de segurança em segundo plano.",
|
||||
"backup_controller_page_background_app_refresh_disabled_title": "Atualização da app em segundo plano desativada",
|
||||
"backup_controller_page_background_app_refresh_enable_button_text": "Ir para as definições",
|
||||
"backup_controller_page_background_battery_info_link": "Mostre-me como",
|
||||
"backup_controller_page_background_battery_info_message": "Para obter a melhor experiência de backup em segundo plano, desative todas as otimizações de bateria que restrinjam a atividade em segundo plano do Immich.\n\nComo isso é específico do dispositivo, consulte as informações necessárias do fabricante do dispositivo.",
|
||||
"backup_controller_page_background_battery_info_ok": "OK",
|
||||
@@ -103,17 +106,17 @@
|
||||
"backup_controller_page_uploading_file_info": "Carregando informações do arquivo",
|
||||
"backup_err_only_album": "Não é possível remover apenas o álbum",
|
||||
"backup_info_card_assets": "itens",
|
||||
"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_manual_cancelled": "Cancelado",
|
||||
"backup_manual_failed": "Falhou",
|
||||
"backup_manual_in_progress": "Carregamento em curso. Tente depois de algum tempo",
|
||||
"backup_manual_success": "Sucesso",
|
||||
"backup_manual_title": "Estado do carregamento",
|
||||
"cache_settings_album_thumbnails": "Miniaturas da página da biblioteca ({} itens)",
|
||||
"cache_settings_clear_cache_button": "Limpar cache",
|
||||
"cache_settings_clear_cache_button_title": "Limpa o cache do aplicativo. Isso afetará significativamente o desempenho do aplicativo até que o cache seja reconstruído.",
|
||||
"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_duplicated_assets_clear_button": "LIMPAR",
|
||||
"cache_settings_duplicated_assets_subtitle": "Fotografias e vídeos que estão na lista negra da aplicação",
|
||||
"cache_settings_duplicated_assets_title": "Recursos duplicados ({})",
|
||||
"cache_settings_image_cache_size": "Tamanho do cache de imagem ({} itens)",
|
||||
"cache_settings_statistics_album": "Miniaturas da biblioteca",
|
||||
"cache_settings_statistics_assets": "{} itens ({})",
|
||||
@@ -135,25 +138,25 @@
|
||||
"common_change_password": "Mudar a senha",
|
||||
"common_create_new_album": "Criar novo álbum",
|
||||
"common_server_error": "Verifique a sua ligação de rede, certifique-se de que o servidor está acessível e de que as versões da aplicação/servidor são compatíveis.",
|
||||
"common_shared": "Shared",
|
||||
"common_shared": "Partilhado",
|
||||
"control_bottom_app_bar_add_to_album": "Adicionar ao álbum",
|
||||
"control_bottom_app_bar_album_info": "{} itens",
|
||||
"control_bottom_app_bar_album_info_shared": "{} itens · Partilhado",
|
||||
"control_bottom_app_bar_archive": "Archive",
|
||||
"control_bottom_app_bar_archive": "Arquivo",
|
||||
"control_bottom_app_bar_create_new_album": "Criar novo álbum",
|
||||
"control_bottom_app_bar_delete": "Deletar",
|
||||
"control_bottom_app_bar_delete_from_immich": "Apagar do Immich",
|
||||
"control_bottom_app_bar_delete_from_local": "Apagar do dispositivo",
|
||||
"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_edit_location": "Editar Localização",
|
||||
"control_bottom_app_bar_edit_time": "Editar Data & Hora",
|
||||
"control_bottom_app_bar_favorite": "Favorito",
|
||||
"control_bottom_app_bar_share": "Partilhar",
|
||||
"control_bottom_app_bar_share_to": "Share To",
|
||||
"control_bottom_app_bar_stack": "Stack",
|
||||
"control_bottom_app_bar_share_to": "Partilhar com",
|
||||
"control_bottom_app_bar_stack": "Empilhar",
|
||||
"control_bottom_app_bar_trash_from_immich": "Mover para o lixo",
|
||||
"control_bottom_app_bar_unarchive": "Unarchive",
|
||||
"control_bottom_app_bar_unfavorite": "Unfavorite",
|
||||
"control_bottom_app_bar_upload": "Upload",
|
||||
"control_bottom_app_bar_unarchive": "Desarquivar",
|
||||
"control_bottom_app_bar_unfavorite": "Remover favorito",
|
||||
"control_bottom_app_bar_upload": "Carregar",
|
||||
"create_album_page_untitled": "Sem título",
|
||||
"create_shared_album_page_create": "Criar",
|
||||
"create_shared_album_page_share": "Partilhar",
|
||||
@@ -174,61 +177,62 @@
|
||||
"delete_dialog_title": "Deletar Permanentemente",
|
||||
"delete_local_dialog_ok_backed_up_only": "Eliminar apenas existentes na cópia de segurança",
|
||||
"delete_local_dialog_ok_force": "Apagar de qualquer forma",
|
||||
"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",
|
||||
"delete_shared_link_dialog_content": "Tem a certeza de que pretende apagar esta ligação partilhada?",
|
||||
"delete_shared_link_dialog_title": "Apagar link de partilha",
|
||||
"description_input_hint_text": "Adicionar descrição...",
|
||||
"description_input_submit_error": "Erro ao atualizar a descrição, verifique o registo para obter mais detalhes",
|
||||
"edit_date_time_dialog_date_time": "Data e Hora",
|
||||
"edit_date_time_dialog_timezone": "Fuso horário",
|
||||
"edit_location_dialog_title": "Location",
|
||||
"edit_location_dialog_title": "Localização",
|
||||
"exif_bottom_sheet_description": "Adicionar Descrição...",
|
||||
"exif_bottom_sheet_details": "DETALHES",
|
||||
"exif_bottom_sheet_location": "LOCALIZAÇÃO",
|
||||
"exif_bottom_sheet_location_add": "Add a location",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_location_add": "Adicionar uma localização",
|
||||
"exif_bottom_sheet_people": "Pessoas",
|
||||
"exif_bottom_sheet_person_add_person": "Adicionar nome",
|
||||
"experimental_settings_new_asset_list_subtitle": "Trabalho em andamento",
|
||||
"experimental_settings_new_asset_list_title": "Ativar visualização de grelha experimental",
|
||||
"experimental_settings_subtitle": "Use por sua conta e risco!",
|
||||
"experimental_settings_title": "Experimental",
|
||||
"favorites_page_no_favorites": "No favorite assets found",
|
||||
"favorites_page_no_favorites": "Nenhum recurso favorito encontrado",
|
||||
"favorites_page_title": "Favoritos",
|
||||
"home_page_add_to_album_conflicts": "Ativos {added} adicionados ao álbum {album}. {failed} ativos já estão no álbum.",
|
||||
"home_page_add_to_album_err_local": "Ainda não é possível adicionar recursos locais aos álbuns, ignorando",
|
||||
"home_page_add_to_album_success": "Ativos {added} adicionados ao álbum {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_album_err_partner": "Ainda não é possível adicionar recursos do parceiro a um álbum, ignorando",
|
||||
"home_page_archive_err_local": "Ainda não é possível arquivar recursos locais, ignorando",
|
||||
"home_page_archive_err_partner": "Não é possível arquivar recursos do parceiro, ignorando",
|
||||
"home_page_building_timeline": "A construir a timeline",
|
||||
"home_page_delete_err_partner": "Can not delete partner assets, skipping",
|
||||
"home_page_delete_err_partner": "Não é possível apagar recursos do parceiro, ignorando",
|
||||
"home_page_delete_remote_err_local": "Recursos locais na seleção remota de eliminação, ignorando",
|
||||
"home_page_favorite_err_local": "Ainda não é possível adicionar recursos locais favoritos, ignorando",
|
||||
"home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping",
|
||||
"home_page_favorite_err_partner": "Ainda não é possível marcar como favoritos recursos do parceiro, ignorando",
|
||||
"home_page_first_time_notice": "Se for a primeira vez que utiliza a aplicação, certifique-se de que escolhe um álbum ou álbuns de cópia de segurança, para que a linha cronológica possa preencher as fotografias e os vídeos no(s) álbum(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",
|
||||
"image_viewer_page_state_provider_download_error": "Download Error",
|
||||
"image_viewer_page_state_provider_download_success": "Download Success",
|
||||
"image_viewer_page_state_provider_share_error": "Share Error",
|
||||
"home_page_share_err_local": "Não é possível partilhar recursos locais via link, ignorando",
|
||||
"home_page_upload_err_limit": "Só é possível carregar 30 recursos de cada vez, a ignorar",
|
||||
"image_viewer_page_state_provider_download_error": "Erro ao descarregar",
|
||||
"image_viewer_page_state_provider_download_success": "Descarregado",
|
||||
"image_viewer_page_state_provider_share_error": "Erro ao partilhar",
|
||||
"library_page_albums": "Álbuns",
|
||||
"library_page_archive": "Archive",
|
||||
"library_page_archive": "Arquivo",
|
||||
"library_page_device_albums": "Álbuns no dispositivo",
|
||||
"library_page_favorites": "Favoritos",
|
||||
"library_page_new_album": "Novo álbum",
|
||||
"library_page_sharing": "Partilhar",
|
||||
"library_page_sort_asset_count": "Número de recursos",
|
||||
"library_page_sort_created": "Data de criação",
|
||||
"library_page_sort_last_modified": "Last modified",
|
||||
"library_page_sort_last_modified": "Última modificação",
|
||||
"library_page_sort_most_oldest_photo": "Foto mais antiga",
|
||||
"library_page_sort_most_recent_photo": "Most recent photo",
|
||||
"library_page_sort_most_recent_photo": "Foto mais recente",
|
||||
"library_page_sort_title": "Título do álbum",
|
||||
"location_picker_choose_on_map": "Choose on map",
|
||||
"location_picker_choose_on_map": "Escolha no mapa",
|
||||
"location_picker_latitude": "Latitude",
|
||||
"location_picker_latitude_error": "Enter a valid latitude",
|
||||
"location_picker_latitude_hint": "Enter your latitude here",
|
||||
"location_picker_latitude_error": "Introduza uma latitude válida",
|
||||
"location_picker_latitude_hint": "Introduza aqui a latitude",
|
||||
"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",
|
||||
"location_picker_longitude_error": "Introduza uma longitude válida",
|
||||
"location_picker_longitude_hint": "Introduza aqui a longitude",
|
||||
"login_disabled": "Login desativado",
|
||||
"login_form_api_exception": "Excepção de API. Verifique o URL do servidor e tente novamente.",
|
||||
"login_form_back_button_text": "Voltar",
|
||||
"login_form_button_text": "Login",
|
||||
@@ -243,7 +247,7 @@
|
||||
"login_form_failed_get_oauth_server_config": "Erro ao registrar usando OAuth, verifique o URL do servidor",
|
||||
"login_form_failed_get_oauth_server_disable": "O recurso OAuth não está disponível neste servidor",
|
||||
"login_form_failed_login": "Erro ao fazer login, verifique a url do servidor, email e senha",
|
||||
"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_handshake_exception": "Ocorreu um erro ao ligar ao servidor. Ative o suporte para certificados auto-assinados nas definições se estiver a utilizar um certificado auto-assinado.",
|
||||
"login_form_label_email": "Email",
|
||||
"login_form_label_password": "Senha",
|
||||
"login_form_next_button": "Avançar",
|
||||
@@ -251,82 +255,87 @@
|
||||
"login_form_save_login": "Permanecer logado",
|
||||
"login_form_server_empty": "Introduzir um URL de servidor.",
|
||||
"login_form_server_error": "Não foi possível ligar ao servidor.",
|
||||
"login_password_changed_error": "There was an error updating your password",
|
||||
"login_password_changed_success": "Password updated successfully",
|
||||
"map_assets_in_bound": "{} photo",
|
||||
"login_password_changed_error": "Erro ao atualizar a sua password",
|
||||
"login_password_changed_success": "Password atualizada com sucesso",
|
||||
"map_assets_in_bound": "{} foto",
|
||||
"map_assets_in_bounds": "{} fotos",
|
||||
"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_cannot_get_user_location": "Impossível obter a sua localização",
|
||||
"map_location_dialog_cancel": "Cancelar",
|
||||
"map_location_dialog_yes": "Sim",
|
||||
"map_location_picker_page_use_location": "Utilizar esta localização",
|
||||
"map_location_service_disabled_content": "Serviço de localização precisa de estar ativado para mostrar recursos da localização atual. Pretende ativar agora?",
|
||||
"map_location_service_disabled_title": "Serviço de localização desativado",
|
||||
"map_no_assets_in_bounds": "Não há fotos nesta área",
|
||||
"map_no_location_permission_content": "A permissão de localização é necessária para apresentar recursos da sua localização atual. Pretende autorizá-la agora?\n",
|
||||
"map_no_location_permission_title": "Permissão de localização negada",
|
||||
"map_settings_dark_mode": "Modo escuro",
|
||||
"map_settings_date_range_option_all": "Tudo",
|
||||
"map_settings_date_range_option_day": "Últimas 24 horas",
|
||||
"map_settings_date_range_option_days": "Últimos {} dias",
|
||||
"map_settings_date_range_option_year": "Último ano",
|
||||
"map_settings_date_range_option_years": "Últimos {} anos",
|
||||
"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_only_relative_range": "Date range",
|
||||
"map_settings_only_show_favorites": "Show Favorite Only",
|
||||
"map_settings_dialog_cancel": "Cancelar",
|
||||
"map_settings_dialog_save": "Guardar",
|
||||
"map_settings_dialog_title": "Definições do mapa",
|
||||
"map_settings_include_show_archived": "Incluir arquivados",
|
||||
"map_settings_only_relative_range": "Intervalo de datas",
|
||||
"map_settings_only_show_favorites": "Mostrar apenas favoritos",
|
||||
"map_settings_theme_settings": "Tema do mapa",
|
||||
"map_zoom_to_see_photos": "Zoom out to see photos",
|
||||
"map_zoom_to_see_photos": "Reduzir zoom para ver fotos",
|
||||
"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",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "Fotos com movimento",
|
||||
"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",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "Não é possível editar a data de recurso(s) só de leitura, ignorando",
|
||||
"multiselect_grid_edit_gps_err_read_only": "Não é possível editar a localização de recurso(s) só de leitura, ignorando",
|
||||
"notification_permission_dialog_cancel": "Cancelar",
|
||||
"notification_permission_dialog_content": "Para ativar as notificações, vá a Definições e selecione permitir.",
|
||||
"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_dialog_settings": "Definições",
|
||||
"notification_permission_list_tile_content": "Dar permissões para ativar notificações",
|
||||
"notification_permission_list_tile_enable_button": "Ativar notificações",
|
||||
"notification_permission_list_tile_title": "Permissão de notificações",
|
||||
"partner_page_add_partner": "Adicionar parceiro",
|
||||
"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_empty_message": "As suas fotografias ainda não foram partilhadas com nenhum parceiro.",
|
||||
"partner_page_no_more_users": "Não há mais utilizadores para adicionar",
|
||||
"partner_page_partner_add_failed": "Falha ao adicionar parceiro",
|
||||
"partner_page_select_partner": "Selecionar parceiro",
|
||||
"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",
|
||||
"permission_onboarding_back": "Back",
|
||||
"partner_page_shared_to_title": "Partilhado com",
|
||||
"partner_page_stop_sharing_content": "{} deixará de poder aceder às suas fotografias.",
|
||||
"partner_page_stop_sharing_title": "Parar de partilhar as suas fotos?",
|
||||
"partner_page_title": "Parceiro",
|
||||
"permission_onboarding_back": "Voltar",
|
||||
"permission_onboarding_continue_anyway": "Continuar de qualquer maneira",
|
||||
"permission_onboarding_get_started": "Get started",
|
||||
"permission_onboarding_go_to_settings": "Go to settings",
|
||||
"permission_onboarding_grant_permission": "Grant permission",
|
||||
"permission_onboarding_get_started": "Começar",
|
||||
"permission_onboarding_go_to_settings": "Ir para as definições",
|
||||
"permission_onboarding_grant_permission": "Dar permissão",
|
||||
"permission_onboarding_log_out": "Sair",
|
||||
"permission_onboarding_permission_denied": "Permissão negada. Para utilizar o Immich, conceda permissões de fotografia e vídeo nas Definições.",
|
||||
"permission_onboarding_permission_granted": "Autorização concedida! Está tudo pronto.",
|
||||
"permission_onboarding_permission_limited": "Permissão limitada. Para permitir que o Immich faça cópias de segurança e gira toda a sua coleção de galerias, conceda permissões para fotografias e vídeos nas Definições.",
|
||||
"permission_onboarding_request": "O Immich requer autorização para ver as suas fotografias e vídeos.",
|
||||
"preferences_settings_title": "Preferências",
|
||||
"profile_drawer_app_logs": "Logs",
|
||||
"profile_drawer_client_out_of_date_major": "A aplicação móvel está desatualizada. Atualize para a versão principal mais recente.",
|
||||
"profile_drawer_client_out_of_date_minor": "A aplicação móvel está desatualizada. Por favor, atualize para a versão mais recente.",
|
||||
"profile_drawer_client_server_up_to_date": "Cliente e Servidor atualizados",
|
||||
"profile_drawer_documentation": "Documentation",
|
||||
"profile_drawer_documentation": "Documentação",
|
||||
"profile_drawer_github": "GitHub",
|
||||
"profile_drawer_server_out_of_date_major": "O servidor está desatualizado. Atualize para a versão principal mais recente.",
|
||||
"profile_drawer_server_out_of_date_minor": "O servidor está desatualizado. Atualize para a versão mais recente.",
|
||||
"profile_drawer_settings": "Configurações",
|
||||
"profile_drawer_sign_out": "Sair",
|
||||
"profile_drawer_trash": "Trash",
|
||||
"profile_drawer_trash": "Lixo",
|
||||
"recently_added_page_title": "Adicionado recentemente",
|
||||
"scaffold_body_error_occurred": "Ocorreu um erro",
|
||||
"search_bar_hint": "Busque suas fotos",
|
||||
"search_page_categories": "Categories",
|
||||
"search_page_categories": "Categorias",
|
||||
"search_page_favorites": "Favoritos",
|
||||
"search_page_motion_photos": "Fotos com movimento",
|
||||
"search_page_no_objects": "Nenhuma informação de objeto disponível",
|
||||
"search_page_no_places": "Nenhuma informação de sítios disponível",
|
||||
"search_page_people": "People",
|
||||
"search_page_people": "Pessoas",
|
||||
"search_page_person_add_name_dialog_cancel": "Cancelar",
|
||||
"search_page_person_add_name_dialog_hint": "Nome",
|
||||
"search_page_person_add_name_dialog_save": "Guardar",
|
||||
@@ -336,22 +345,22 @@
|
||||
"search_page_person_edit_name": "Editar nome",
|
||||
"search_page_places": "Sítios",
|
||||
"search_page_recently_added": "Adicionado recentemente",
|
||||
"search_page_screenshots": "Screenshots",
|
||||
"search_page_screenshots": "Capturas de ecrã",
|
||||
"search_page_selfies": "Selfies",
|
||||
"search_page_things": "Objetos",
|
||||
"search_page_videos": "Vídeos",
|
||||
"search_page_view_all_button": "Ver tudo",
|
||||
"search_page_your_activity": "A sua atividade",
|
||||
"search_page_your_map": "Your Map",
|
||||
"search_page_your_map": "O seu mapa",
|
||||
"search_result_page_new_search_hint": "Nova Busca",
|
||||
"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",
|
||||
"search_suggestion_list_smart_search_hint_1": "A pesquisa inteligente está activada por predefinição. Para pesquisar metadados, utilize a sintaxe ",
|
||||
"search_suggestion_list_smart_search_hint_2": "m:a-sua-pesquisa",
|
||||
"select_additional_user_for_sharing_page_suggestions": "Sugestões",
|
||||
"select_user_for_sharing_page_err_album": "Falha ao criar o álbum",
|
||||
"select_user_for_sharing_page_share_suggestions": "Sugestões",
|
||||
"server_info_box_app_version": "Versão da app",
|
||||
"server_info_box_latest_release": "Última versão",
|
||||
"server_info_box_server_url": "Server URL",
|
||||
"server_info_box_server_url": "URL do servidor",
|
||||
"server_info_box_server_version": "Versão do servidor",
|
||||
"setting_image_viewer_help": "O visualizador de detalhes carrega primeiro a miniatura pequena, depois carrega a visualização de tamanho médio (se ativado) e, finalmente, carrega o original (se ativado).",
|
||||
"setting_image_viewer_original_subtitle": "Ative para carregar a imagem original em resolução total (grande!). Desative para reduzir o uso de dados (na rede e no cache do dispositivo).",
|
||||
@@ -376,66 +385,66 @@
|
||||
"share_add_photos": "Adicionar fotos",
|
||||
"share_add_title": "Adicione um título",
|
||||
"share_create_album": "Criar álbum",
|
||||
"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_activities_input_disable": "Comentários desativados",
|
||||
"shared_album_activities_input_hint": "Dizer alguma coisa",
|
||||
"shared_album_activity_remove_content": "Deseja eliminar esta atividade?",
|
||||
"shared_album_activity_remove_title": "Apagar atividade",
|
||||
"shared_album_activity_setting_subtitle": "Permitir a outros responder",
|
||||
"shared_album_activity_setting_title": "Comentários e gostos",
|
||||
"shared_album_section_people_action_error": "Erro ao sair/remover do álbum",
|
||||
"shared_album_section_people_action_leave": "Remover utilizador do álbum",
|
||||
"shared_album_section_people_action_remove_user": "Remover utilizador do álbum",
|
||||
"shared_album_section_people_owner_label": "Owner",
|
||||
"shared_album_section_people_title": "PEOPLE",
|
||||
"shared_album_section_people_owner_label": "Dono",
|
||||
"shared_album_section_people_title": "PESSOAS",
|
||||
"share_dialog_preparing": "Preparando...",
|
||||
"shared_link_app_bar_title": "Links partilhados",
|
||||
"shared_link_clipboard_copied_massage": "Copied to clipboard",
|
||||
"shared_link_clipboard_copied_massage": "Copiado para a área de transferência",
|
||||
"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_create_app_bar_title": "Criar link para partilhar",
|
||||
"shared_link_create_error": "Erro ao criar o link partilhado",
|
||||
"shared_link_create_info": "Deixar qualquer pessoa com o link ver a(s) foto(s) selecionada(s)",
|
||||
"shared_link_create_submit_button": "Criar link",
|
||||
"shared_link_edit_allow_download": "Permitir que um utilizador público descarregue",
|
||||
"shared_link_edit_allow_upload": "Permitir que um utilizador público carregue",
|
||||
"shared_link_edit_app_bar_title": "Edit link",
|
||||
"shared_link_edit_app_bar_title": "Editar link",
|
||||
"shared_link_edit_change_expiry": "Alterar o prazo de validade",
|
||||
"shared_link_edit_description": "Description",
|
||||
"shared_link_edit_description_hint": "Enter the share description",
|
||||
"shared_link_edit_expire_after": "Expire after",
|
||||
"shared_link_edit_description": "Descrição",
|
||||
"shared_link_edit_description_hint": "Introduzir a descrição da partilha",
|
||||
"shared_link_edit_expire_after": "Expira depois",
|
||||
"shared_link_edit_expire_after_option_day": "1 dia",
|
||||
"shared_link_edit_expire_after_option_days": "{} dias",
|
||||
"shared_link_edit_expire_after_option_hour": "1 hora",
|
||||
"shared_link_edit_expire_after_option_hours": "{} horas",
|
||||
"shared_link_edit_expire_after_option_minute": "1 minuto",
|
||||
"shared_link_edit_expire_after_option_minutes": "{} minutos",
|
||||
"shared_link_edit_expire_after_option_never": "Never",
|
||||
"shared_link_edit_expire_after_option_never": "Nunca",
|
||||
"shared_link_edit_password": "Password",
|
||||
"shared_link_edit_password_hint": "Enter the share password",
|
||||
"shared_link_edit_password_hint": "Introduza a password da partilha",
|
||||
"shared_link_edit_show_meta": "Mostrar metadados",
|
||||
"shared_link_edit_submit_button": "Atualizar link",
|
||||
"shared_link_empty": "Não tem links partilhados",
|
||||
"shared_link_error_server_url_fetch": "Cannot fetch the server url",
|
||||
"shared_link_expired": "Expired",
|
||||
"shared_link_error_server_url_fetch": "Erro ao abrir o url do servidor",
|
||||
"shared_link_expired": "Expirou",
|
||||
"shared_link_expires_day": "Expira em {} dia",
|
||||
"shared_link_expires_days": "Expira em {} dias",
|
||||
"shared_link_expires_hour": "Expira em {} hora",
|
||||
"shared_link_expires_hours": "Expira em {} horas",
|
||||
"shared_link_expires_minute": "Expira em {} minuto",
|
||||
"shared_link_expires_minutes": "Expires in {} minutes",
|
||||
"shared_link_expires_never": "Expires ∞",
|
||||
"shared_link_expires_minutes": "Expira em {} minutos",
|
||||
"shared_link_expires_never": "Expira ∞",
|
||||
"shared_link_expires_second": "Expira em {} segundo",
|
||||
"shared_link_expires_seconds": "Expires in {} seconds",
|
||||
"shared_link_expires_seconds": "Expira em {} segundos",
|
||||
"shared_link_info_chip_download": "Descarregar",
|
||||
"shared_link_info_chip_metadata": "EXIF",
|
||||
"shared_link_info_chip_upload": "Upload",
|
||||
"shared_link_info_chip_upload": "Carregar",
|
||||
"shared_link_manage_links": "Gerir links partilhados",
|
||||
"share_done": "Done",
|
||||
"share_done": "Feito",
|
||||
"share_invite": "Convidar para álbum",
|
||||
"sharing_page_album": "Álbuns partilhados",
|
||||
"sharing_page_description": "Crie álbuns partilhados para partilhar fotografias e vídeos com pessoas da sua rede.",
|
||||
"sharing_page_empty_list": "LISTA VAZIA",
|
||||
"sharing_silver_appbar_create_shared_album": "Criar álbum partilhado",
|
||||
"sharing_silver_appbar_shared_links": "Shared links",
|
||||
"sharing_silver_appbar_shared_links": "Links partilhados",
|
||||
"sharing_silver_appbar_share_partner": "Partilhar com parceiro",
|
||||
"tab_controller_nav_library": "Biblioteca",
|
||||
"tab_controller_nav_photos": "Fotos",
|
||||
@@ -451,30 +460,30 @@
|
||||
"theme_setting_theme_title": "Tema",
|
||||
"theme_setting_three_stage_loading_subtitle": "O carregamento em três estágios pode aumentar o desempenho do carregamento, mas causa uma carga de rede significativamente maior",
|
||||
"theme_setting_three_stage_loading_title": "Habilitar carregamento em três estágios",
|
||||
"translated_text_options": "Options",
|
||||
"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",
|
||||
"translated_text_options": "Opções",
|
||||
"trash_page_delete": "Apagar",
|
||||
"trash_page_delete_all": "Apagar tudo",
|
||||
"trash_page_empty_trash_btn": "Esvaziar lixo",
|
||||
"trash_page_empty_trash_dialog_content": "Deseja esvaziar os recursos no lixo? Estes recursos serão eliminados de forma permanente do 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_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",
|
||||
"trash_page_info": "Recursos no lixo são apagados de forma permanente depois de {} dias",
|
||||
"trash_page_no_assets": "Não existem recursos no lixo",
|
||||
"trash_page_restore": "Restaurar",
|
||||
"trash_page_restore_all": "Restaurar tudo",
|
||||
"trash_page_select_assets_btn": "Selecionar recursos",
|
||||
"trash_page_select_btn": "Selecionar",
|
||||
"trash_page_title": "Lixo ({})",
|
||||
"upload_dialog_cancel": "Cancelar",
|
||||
"upload_dialog_info": "Pretende efetuar a cópia de segurança do(s) recurso(s) selecionado(s) para o servidor?",
|
||||
"upload_dialog_ok": "Carregar",
|
||||
"upload_dialog_title": "Carregar recurso",
|
||||
"version_announcement_overlay_ack": "Aceitar",
|
||||
"version_announcement_overlay_release_notes": "notas de lançamento",
|
||||
"version_announcement_overlay_text_1": "Olá, há um novo lançamento de",
|
||||
"version_announcement_overlay_text_2": "por favor, tome o seu tempo para visitar o",
|
||||
"version_announcement_overlay_text_3": "e certifique-se de que a configuração do docker-compose e do .env estejam atualizadas para evitar configurações incorretas, especialmente se usar o WatchTower ou qualquer mecanismo que lide com a atualização automática do servidor.",
|
||||
"version_announcement_overlay_title": "Nova versão do servidor disponível \uD83C\uDF89",
|
||||
"viewer_remove_from_stack": "Remove from Stack",
|
||||
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
||||
"viewer_unstack": "Un-Stack"
|
||||
"viewer_remove_from_stack": "Remover da pilha",
|
||||
"viewer_stack_use_as_main_asset": "Usar como recurso principal",
|
||||
"viewer_unstack": "Desenpilhar"
|
||||
}
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "Архив ({})",
|
||||
"asset_action_delete_err_read_only": "Невозможно удалить объект(ы) только для чтения, пропуск...",
|
||||
"asset_action_share_err_offline": "Невозможно получить оффлайн-объект(ы), пропуск...",
|
||||
"asset_list_group_by_sub_title": "Group by",
|
||||
"asset_list_layout_settings_dynamic_layout_title": "Динамическое расположение",
|
||||
"asset_list_layout_settings_group_automatically": "Автоматически",
|
||||
"asset_list_layout_settings_group_by": "Группировать объекты по:",
|
||||
"asset_list_layout_settings_group_by_month": "Месяцу",
|
||||
"asset_list_layout_settings_group_by_month_day": "Месяцу и дню",
|
||||
"asset_list_layout_sub_title": "Layout",
|
||||
"asset_list_settings_subtitle": "Настройка макета сетки фотографий",
|
||||
"asset_list_settings_title": "Сетка фотографий",
|
||||
"asset_viewer_settings_title": "Asset Viewer",
|
||||
"backup_album_selection_page_albums_device": "Альбомов на устройстве ({})",
|
||||
"backup_album_selection_page_albums_tap": "Нажмите, чтобы включить, нажмите дважды, чтобы исключить",
|
||||
"backup_album_selection_page_assets_scatter": "Объекты могут быть разбросаны по нескольким альбомам. Таким образом, альбомы могут быть включены или исключены из процесса резервного копирования.",
|
||||
@@ -164,9 +167,9 @@
|
||||
"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": "Эти объекты будут безвозвратно удалены с сервера Immich и вашего устройства.",
|
||||
"delete_dialog_alert_local": "Эти объекты будут безвозвратно удалены с вашего устройства, но по-прежнему будут доступны на сервере Immich",
|
||||
"delete_dialog_alert_local_non_backed_up": "Некоторые объекты не были скопированы на сервера Immich и будут безвозвратно удалены с вашего устройства",
|
||||
"delete_dialog_alert": "Эти элементы будут безвозвратно удалены из приложения, а также с вашего устройства",
|
||||
"delete_dialog_alert_local": "Эти объекты будут безвозвратно удалены с Вашего устройства, но по-прежнему будут доступны на сервере Immich",
|
||||
"delete_dialog_alert_local_non_backed_up": "Резервные копии некоторых объектов не были загружены в Immich и будут безвозвратно удалены с Вашего устройства",
|
||||
"delete_dialog_alert_remote": "Эти объекты будут безвозвратно удалены с сервера Immich",
|
||||
"delete_dialog_cancel": "Отменить",
|
||||
"delete_dialog_ok": "Удалить",
|
||||
@@ -186,6 +189,7 @@
|
||||
"exif_bottom_sheet_location": "Местоположение",
|
||||
"exif_bottom_sheet_location_add": "Добавить местоположение",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_person_add_person": "Add name",
|
||||
"experimental_settings_new_asset_list_subtitle": "Ведутся работы",
|
||||
"experimental_settings_new_asset_list_title": "Включить экспериментальную сетку фотографий",
|
||||
"experimental_settings_subtitle": "Используйте на свой страх и риск!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"map_settings_only_show_favorites": "Показать только избранное",
|
||||
"map_settings_theme_settings": "Тема карты",
|
||||
"map_zoom_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",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "Динамические фото",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "Невозможно редактировать дату объектов только для чтения, пропуск...",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "Доступ получен! Всё готово.",
|
||||
"permission_onboarding_permission_limited": "Доступ к файлам ограничен. Чтобы Immich мог создавать резервные копии и управлять вашей галереей, пожалуйста, предоставьте приложению разрешение на доступ к \"Фото и видео\" в Настройках.",
|
||||
"permission_onboarding_request": "Immich просит вас предоставить разрешение на доступ к вашим фото и видео",
|
||||
"preferences_settings_title": "Preferences",
|
||||
"profile_drawer_app_logs": "Журнал",
|
||||
"profile_drawer_client_out_of_date_major": "Версия мобильного приложения устарела. Пожалуйста, обновитесь до последней основной версии.",
|
||||
"profile_drawer_client_out_of_date_minor": "Версия мобильного приложения устарела. Пожалуйста, обновитесь до последней вспомогательной версии.",
|
||||
@@ -457,7 +466,7 @@
|
||||
"trash_page_empty_trash_btn": "Очистить корзину",
|
||||
"trash_page_empty_trash_dialog_content": "Вы хотите очистить свою корзину? Эти объекты будут навсегда удалены из Immich.",
|
||||
"trash_page_empty_trash_dialog_ok": "ОК",
|
||||
"trash_page_info": "Удаленные объекты будут окончательно удалены через {} дней",
|
||||
"trash_page_info": "Удаленные элементы будут окончательно удалены через {} дней",
|
||||
"trash_page_no_assets": "Удаленные объекты отсутсвуют",
|
||||
"trash_page_restore": "Восстановить",
|
||||
"trash_page_restore_all": "Восстановить все",
|
||||
@@ -477,4 +486,4 @@
|
||||
"viewer_remove_from_stack": "Удалить из стека",
|
||||
"viewer_stack_use_as_main_asset": "Использовать в качестве основного объекта",
|
||||
"viewer_unstack": "Разобрать стек"
|
||||
}
|
||||
}
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "Archív ({})",
|
||||
"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": "Dynamické rozloženie",
|
||||
"asset_list_layout_settings_group_automatically": "Automaticky",
|
||||
"asset_list_layout_settings_group_by": "Zoskupiť položky podľa",
|
||||
"asset_list_layout_settings_group_by_month": "Mesiac",
|
||||
"asset_list_layout_settings_group_by_month_day": "Mesiac + deň",
|
||||
"asset_list_layout_sub_title": "Layout",
|
||||
"asset_list_settings_subtitle": "Nastavenia rozloženia mriežky fotografií",
|
||||
"asset_list_settings_title": "Fotografická mriežka",
|
||||
"asset_viewer_settings_title": "Asset Viewer",
|
||||
"backup_album_selection_page_albums_device": "Albumy v zariadení ({})",
|
||||
"backup_album_selection_page_albums_tap": "Ťuknutím na položku ju zahrniete, dvojitým ťuknutím ju vylúčite",
|
||||
"backup_album_selection_page_assets_scatter": "Súbory môžu byť roztrúsené vo viacerých albumoch. To umožňuje zahrnúť alebo vylúčiť albumy počas procesu zálohovania.",
|
||||
@@ -186,6 +189,7 @@
|
||||
"exif_bottom_sheet_location": "LOKALITA",
|
||||
"exif_bottom_sheet_location_add": "Nastaviť polohu",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_person_add_person": "Add name",
|
||||
"experimental_settings_new_asset_list_subtitle": "Prebiehajúca práca",
|
||||
"experimental_settings_new_asset_list_title": "Povolenie experimentálnej mriežky fotografií",
|
||||
"experimental_settings_subtitle": "Používajte na vlastné riziko!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"map_settings_only_show_favorites": "Zobraziť iba obľúbené",
|
||||
"map_settings_theme_settings": "Map Theme",
|
||||
"map_zoom_to_see_photos": "Oddiaľte priblíženie aby ste videli fotky",
|
||||
"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",
|
||||
"monthly_title_text_date_format": "LLLL y",
|
||||
"motion_photos_page_title": "Pohyblivé fotky",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "Povolenie udelené! Všetko je nastavené.",
|
||||
"permission_onboarding_permission_limited": "Povolenie obmedzené. Ak chcete, aby Immich zálohoval a spravoval celú vašu zbierku galérie, udeľte v Nastaveniach povolenia na fotografie a videá.",
|
||||
"permission_onboarding_request": "Immich vyžaduje povolenie na prezeranie vašich fotografií a videí.",
|
||||
"preferences_settings_title": "Preferences",
|
||||
"profile_drawer_app_logs": "Logy",
|
||||
"profile_drawer_client_out_of_date_major": "Mobilná aplikácia je zastaralá. Prosím aktualizujte na najnovšiu verziu.",
|
||||
"profile_drawer_client_out_of_date_minor": "Mobilná aplikácia je zastaralá. Prosím aktualizujte na najnovšiu verziu.",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"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_viewer_settings_title": "Asset Viewer",
|
||||
"backup_album_selection_page_albums_device": "Albums on device ({})",
|
||||
"backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
|
||||
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
|
||||
@@ -186,6 +189,7 @@
|
||||
"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!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"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",
|
||||
"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",
|
||||
@@ -307,6 +315,7 @@
|
||||
"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.",
|
||||
"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.",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"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": "Dinamični raspored",
|
||||
"asset_list_layout_settings_group_automatically": "Automatic",
|
||||
"asset_list_layout_settings_group_by": "Grupiši zapise po",
|
||||
"asset_list_layout_settings_group_by_month": "Mesec",
|
||||
"asset_list_layout_settings_group_by_month_day": "Mesec + Dan",
|
||||
"asset_list_layout_sub_title": "Layout",
|
||||
"asset_list_settings_subtitle": "Opcije za mrežni prikaz fotografija",
|
||||
"asset_list_settings_title": "Mrežni prikaz fotografija",
|
||||
"asset_viewer_settings_title": "Asset Viewer",
|
||||
"backup_album_selection_page_albums_device": "Albuma na uređaju ({})",
|
||||
"backup_album_selection_page_albums_tap": "Dodirni da uključiš, dodirni dvaput da isključiš",
|
||||
"backup_album_selection_page_assets_scatter": "Zapisi se mogu naći u više različitih albuma. Odatle albumi se mogu uključiti ili isključiti tokom procesa pravljenja pozadinskih kopija.",
|
||||
@@ -186,6 +189,7 @@
|
||||
"exif_bottom_sheet_location": "LOKACIJA",
|
||||
"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": "U izradi",
|
||||
"experimental_settings_new_asset_list_title": "Aktiviraj eksperimentalni mrežni prikaz fotografija",
|
||||
"experimental_settings_subtitle": "Koristiti na sopstvenu odgovornost!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"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",
|
||||
"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",
|
||||
@@ -307,6 +315,7 @@
|
||||
"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.",
|
||||
"preferences_settings_title": "Preferences",
|
||||
"profile_drawer_app_logs": "Evidencija",
|
||||
"profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
|
||||
"profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"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_viewer_settings_title": "Asset Viewer",
|
||||
"backup_album_selection_page_albums_device": "Albums on device ({})",
|
||||
"backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
|
||||
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
|
||||
@@ -186,6 +189,7 @@
|
||||
"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!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"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",
|
||||
"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",
|
||||
@@ -307,6 +315,7 @@
|
||||
"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.",
|
||||
"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.",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "Arkivera ({})",
|
||||
"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": "Dynamisk layout",
|
||||
"asset_list_layout_settings_group_automatically": "Automatic",
|
||||
"asset_list_layout_settings_group_by": "Gruppera bilder efter",
|
||||
"asset_list_layout_settings_group_by_month": "Månad",
|
||||
"asset_list_layout_settings_group_by_month_day": "Månad + dag",
|
||||
"asset_list_layout_sub_title": "Layout",
|
||||
"asset_list_settings_subtitle": "Layoutinställningar för bildrutnät",
|
||||
"asset_list_settings_title": "Bildrutnät",
|
||||
"asset_viewer_settings_title": "Asset Viewer",
|
||||
"backup_album_selection_page_albums_device": "Album på enhet ({})",
|
||||
"backup_album_selection_page_albums_tap": "Tryck en gång för att inkludera, tryck två gånger för att exkludera",
|
||||
"backup_album_selection_page_assets_scatter": "Objekt kan vara utspridda över flera album. Därför kan album inkluderas eller exkluderas under säkerhetskopieringsprocessen",
|
||||
@@ -186,6 +189,7 @@
|
||||
"exif_bottom_sheet_location": "PLATS",
|
||||
"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": "Under uppbyggnad",
|
||||
"experimental_settings_new_asset_list_title": "Aktivera experimentellt fotorutnät",
|
||||
"experimental_settings_subtitle": "Använd på egen risk!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"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",
|
||||
"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",
|
||||
@@ -307,6 +315,7 @@
|
||||
"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 kräver tillstånd för att se dina foton och videor.",
|
||||
"preferences_settings_title": "Preferences",
|
||||
"profile_drawer_app_logs": "Loggar",
|
||||
"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.",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "เก็บถาวร ({})",
|
||||
"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": "แผนผังปรับตัว",
|
||||
"asset_list_layout_settings_group_automatically": "อัตโนมัติ",
|
||||
"asset_list_layout_settings_group_by": "จัดกลุ่มทรัพยากรโดย",
|
||||
"asset_list_layout_settings_group_by_month": "เดือน",
|
||||
"asset_list_layout_settings_group_by_month_day": "เดือน + วัน",
|
||||
"asset_list_layout_sub_title": "Layout",
|
||||
"asset_list_settings_subtitle": "Photo grid layout settings",
|
||||
"asset_list_settings_title": "Photo Grid",
|
||||
"asset_viewer_settings_title": "Asset Viewer",
|
||||
"backup_album_selection_page_albums_device": "อัลบั้มบนเครื่อง ({})",
|
||||
"backup_album_selection_page_albums_tap": "กดเพื่อรวม กดสองครั้งเพื่อยกเว้น",
|
||||
"backup_album_selection_page_assets_scatter": "ทรัพยาการสามารถกระจายไปในหลายอัลบั้ม ดังนั้นอัลบั้มสามารถถูกรวมหรือยกเว้นในกระบวนการสำรองข้อมูล",
|
||||
@@ -186,6 +189,7 @@
|
||||
"exif_bottom_sheet_location": "ตำแหน่ง",
|
||||
"exif_bottom_sheet_location_add": "เพิ่มตำแหน่ง",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_person_add_person": "Add name",
|
||||
"experimental_settings_new_asset_list_subtitle": "กำลังพัฒนา",
|
||||
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
||||
"experimental_settings_subtitle": "Use at your own risk!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"map_settings_only_show_favorites": "แสดงรายการโปรดเท่านั้น",
|
||||
"map_settings_theme_settings": "Map Theme",
|
||||
"map_zoom_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",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "ภาพเคลื่อนไหว",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "ให้สิทธิ์สำเร็จ คุณพร้อมใช้งานแล้ว",
|
||||
"permission_onboarding_permission_limited": "สิทธ์จำกัด เพื่อให้ Immich สำรองข้อมูลและบริหารคลังรูปภาพได้ ตั้งค่าสิทธิเข้าถึงรูปภาพและวิดีโอ",
|
||||
"permission_onboarding_request": "Immich จำเป็นจะต้องได้รับสิทธิ์ดูรูปภาพและวิดีโอ",
|
||||
"preferences_settings_title": "Preferences",
|
||||
"profile_drawer_app_logs": "Log",
|
||||
"profile_drawer_client_out_of_date_major": "แอปพลิเคชันมีอัพเดต โปรดอัปเดตเป็นเวอร์ชันหลักล่าสุด",
|
||||
"profile_drawer_client_out_of_date_minor": "แอปพลิเคชันมีอัพเดต โปรดอัปเดตเป็นเวอร์ชันรองล่าสุด",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "Архів ({})",
|
||||
"asset_action_delete_err_read_only": "Неможливо видалити елемент(и) лише для читання, пропущено",
|
||||
"asset_action_share_err_offline": "Неможливо отримати оффлайн-елемент(и), пропущено",
|
||||
"asset_list_group_by_sub_title": "Group by",
|
||||
"asset_list_layout_settings_dynamic_layout_title": "Динамічне компонування",
|
||||
"asset_list_layout_settings_group_automatically": "Автоматично",
|
||||
"asset_list_layout_settings_group_by": "Групувати елементи по",
|
||||
"asset_list_layout_settings_group_by_month": "Місяць",
|
||||
"asset_list_layout_settings_group_by_month_day": "Місяць + день",
|
||||
"asset_list_layout_sub_title": "Layout",
|
||||
"asset_list_settings_subtitle": "Налаштування компонування знімків",
|
||||
"asset_list_settings_title": "Фото-сітка",
|
||||
"asset_viewer_settings_title": "Asset Viewer",
|
||||
"backup_album_selection_page_albums_device": "Альбоми на пристрої ({})",
|
||||
"backup_album_selection_page_albums_tap": "Доторк — для включення, подвійний доторк — для вилучення ",
|
||||
"backup_album_selection_page_assets_scatter": "Елементи можуть належати до кількох альбомів водночас. Таким чином, альбоми можуть бути включені або вилучені під час резервного копіювання.",
|
||||
@@ -186,6 +189,7 @@
|
||||
"exif_bottom_sheet_location": "МІСЦЕ",
|
||||
"exif_bottom_sheet_location_add": "Додати місцезнаходження",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_person_add_person": "Add name",
|
||||
"experimental_settings_new_asset_list_subtitle": "В розробці",
|
||||
"experimental_settings_new_asset_list_title": "Експериментальний макет знімків",
|
||||
"experimental_settings_subtitle": "На власний ризик!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"map_settings_only_show_favorites": "Лише улюбені",
|
||||
"map_settings_theme_settings": "Тема карти",
|
||||
"map_zoom_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",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "Рухомі Знімки",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "Неможливо редагувати дату елементів лише для читання, пропущено",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "Доступ надано! Все готово.",
|
||||
"permission_onboarding_permission_limited": "Обмежений доступ. Аби дозволити Immich резервне копіювання та керування вашою галереєю, надайте доступ до знімків та відео у Налаштуваннях",
|
||||
"permission_onboarding_request": "Immich потребує доступу до ваших знімків та відео.",
|
||||
"preferences_settings_title": "Preferences",
|
||||
"profile_drawer_app_logs": "Журнал",
|
||||
"profile_drawer_client_out_of_date_major": "Мобільний додаток застарів. Будь ласка, оновіть до останньої мажорної версії.",
|
||||
"profile_drawer_client_out_of_date_minor": "Мобільний додаток застарів. Будь ласка, оновіть до останньої мінорної версії.",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "Kho lưu trữ ({})",
|
||||
"asset_action_delete_err_read_only": "Không thể xoá ảnh chỉ có quyền đọc, bỏ qua",
|
||||
"asset_action_share_err_offline": "Không thể tải ảnh ngoại tuyến, bỏ qua",
|
||||
"asset_list_group_by_sub_title": "Group by",
|
||||
"asset_list_layout_settings_dynamic_layout_title": "Bố cục động",
|
||||
"asset_list_layout_settings_group_automatically": "Tự động",
|
||||
"asset_list_layout_settings_group_by": " Nhóm ảnh theo",
|
||||
"asset_list_layout_settings_group_by_month": "Tháng",
|
||||
"asset_list_layout_settings_group_by_month_day": "Tháng + ngày",
|
||||
"asset_list_layout_sub_title": "Layout",
|
||||
"asset_list_settings_subtitle": "Cài đặt bố cục lưới ảnh",
|
||||
"asset_list_settings_title": "Lưới ảnh",
|
||||
"asset_viewer_settings_title": "Asset Viewer",
|
||||
"backup_album_selection_page_albums_device": "Album trên thiết bị ({})",
|
||||
"backup_album_selection_page_albums_tap": "Nhấn để chọn, nhấn đúp để bỏ qua",
|
||||
"backup_album_selection_page_assets_scatter": "Ảnh có thể có trong nhiều album khác nhau. Trong quá trình sao lưu, bạn có thể chọn để sao lưu tất cả các album hoặc chỉ một số album nhất định.",
|
||||
@@ -150,7 +153,7 @@
|
||||
"control_bottom_app_bar_share": "Chia sẻ",
|
||||
"control_bottom_app_bar_share_to": "Chia sẻ với",
|
||||
"control_bottom_app_bar_stack": "Xếp nhóm",
|
||||
"control_bottom_app_bar_trash_from_immich": "Di chuyển tới thùng rác",
|
||||
"control_bottom_app_bar_trash_from_immich": "Chuyển tới thùng rác",
|
||||
"control_bottom_app_bar_unarchive": "Huỷ lưu trữ",
|
||||
"control_bottom_app_bar_unfavorite": "Bỏ yêu thích",
|
||||
"control_bottom_app_bar_upload": "Tải lên",
|
||||
@@ -185,7 +188,8 @@
|
||||
"exif_bottom_sheet_details": "CHI TIẾT",
|
||||
"exif_bottom_sheet_location": "VỊ TRÍ",
|
||||
"exif_bottom_sheet_location_add": "Thêm vị trí",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_people": "MỌI NGƯỜI",
|
||||
"exif_bottom_sheet_person_add_person": "Add name",
|
||||
"experimental_settings_new_asset_list_subtitle": "Đang trong quá trình phát triển",
|
||||
"experimental_settings_new_asset_list_title": "Bật lưới ảnh thử nghiệm",
|
||||
"experimental_settings_subtitle": "Sử dụng có thể rủi ro!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"map_settings_only_show_favorites": "Chỉ hiển thị mục yêu thích",
|
||||
"map_settings_theme_settings": "Giao diện bản đồ",
|
||||
"map_zoom_to_see_photos": "Thu nhỏ để xem ảnh",
|
||||
"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",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "Ảnh động",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "Không thể chỉnh sửa ngày của ảnh chỉ có quyền đọc, bỏ qua",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "Cấp quyền hoàn tất!",
|
||||
"permission_onboarding_permission_limited": "Quyền truy cập vào ảnh của bạn bị hạn chế. Để Immich sao lưu và quản lý toàn bộ thư viện ảnh của bạn, hãy cấp quyền truy cập toàn bộ ảnh trong Cài đặt.",
|
||||
"permission_onboarding_request": "Immich cần quyền để xem ảnh và video của bạn",
|
||||
"preferences_settings_title": "Preferences",
|
||||
"profile_drawer_app_logs": "Nhật ký",
|
||||
"profile_drawer_client_out_of_date_major": "Ứng dụng đã lỗi thời. Vui lòng cập nhật lên phiên bản chính mới nhất.",
|
||||
"profile_drawer_client_out_of_date_minor": "Ứng dụng đã lỗi thời. Vui lòng cập nhật lên phiên bản phụ mới nhất.",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "归档({})",
|
||||
"asset_action_delete_err_read_only": "无法删除只读项目,跳过",
|
||||
"asset_action_share_err_offline": "无法获取离线项目,跳过",
|
||||
"asset_list_group_by_sub_title": "分组方式",
|
||||
"asset_list_layout_settings_dynamic_layout_title": "动态布局",
|
||||
"asset_list_layout_settings_group_automatically": "自动",
|
||||
"asset_list_layout_settings_group_by": "项目分组方式",
|
||||
"asset_list_layout_settings_group_by_month": "月",
|
||||
"asset_list_layout_settings_group_by_month_day": "月和日",
|
||||
"asset_list_layout_sub_title": "布局",
|
||||
"asset_list_settings_subtitle": "照片网格布局设置",
|
||||
"asset_list_settings_title": "照片网格",
|
||||
"asset_viewer_settings_title": "资源查看器",
|
||||
"backup_album_selection_page_albums_device": "设备上的相册({})",
|
||||
"backup_album_selection_page_albums_tap": "单击选中,双击取消",
|
||||
"backup_album_selection_page_assets_scatter": "项目会分散在多个相册中。因此,可以在备份过程中包含或排除相册。",
|
||||
@@ -185,7 +188,8 @@
|
||||
"exif_bottom_sheet_details": "详情",
|
||||
"exif_bottom_sheet_location": "位置",
|
||||
"exif_bottom_sheet_location_add": "添加位置信息",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_people": "人物",
|
||||
"exif_bottom_sheet_person_add_person": "添加姓名",
|
||||
"experimental_settings_new_asset_list_subtitle": "正在处理",
|
||||
"experimental_settings_new_asset_list_title": "启用实验性照片网格",
|
||||
"experimental_settings_subtitle": "使用风险自负!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"map_settings_only_show_favorites": "仅显示收藏的项目",
|
||||
"map_settings_theme_settings": "地图主题",
|
||||
"map_zoom_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",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "动图",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "无法编辑只读项目的日期,跳过",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "已授权!一切就绪。",
|
||||
"permission_onboarding_permission_limited": "权限受限:要让 Immich 备份和管理您的整个图库收藏,请在“设置”中授予照片和视频权限。",
|
||||
"permission_onboarding_request": "Immich 需要权限才能查看您的照片和视频。",
|
||||
"preferences_settings_title": "偏好设置",
|
||||
"profile_drawer_app_logs": "日志",
|
||||
"profile_drawer_client_out_of_date_major": "客户端有大版本升级,请尽快升级至最新版。",
|
||||
"profile_drawer_client_out_of_date_minor": "客户端有小版本升级,请尽快升级至最新版。",
|
||||
|
||||
@@ -37,13 +37,16 @@
|
||||
"archive_page_title": "归档({})",
|
||||
"asset_action_delete_err_read_only": "无法删除只读项目,跳过",
|
||||
"asset_action_share_err_offline": "无法获取离线项目,跳过",
|
||||
"asset_list_group_by_sub_title": "分组方式",
|
||||
"asset_list_layout_settings_dynamic_layout_title": "动态布局",
|
||||
"asset_list_layout_settings_group_automatically": "自动",
|
||||
"asset_list_layout_settings_group_by": "项目分组方式",
|
||||
"asset_list_layout_settings_group_by_month": "月",
|
||||
"asset_list_layout_settings_group_by_month_day": "月和日",
|
||||
"asset_list_layout_sub_title": "布局",
|
||||
"asset_list_settings_subtitle": "照片网格布局设置",
|
||||
"asset_list_settings_title": "照片网格",
|
||||
"asset_viewer_settings_title": "资源查看器",
|
||||
"backup_album_selection_page_albums_device": "设备上的相册({})",
|
||||
"backup_album_selection_page_albums_tap": "单击选中,双击取消",
|
||||
"backup_album_selection_page_assets_scatter": "项目会分散在多个相册中。因此,可以在备份过程中包含或排除相册。",
|
||||
@@ -185,7 +188,8 @@
|
||||
"exif_bottom_sheet_details": "详情",
|
||||
"exif_bottom_sheet_location": "位置",
|
||||
"exif_bottom_sheet_location_add": "添加位置信息",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_people": "人物",
|
||||
"exif_bottom_sheet_person_add_person": "添加姓名",
|
||||
"experimental_settings_new_asset_list_subtitle": "正在处理",
|
||||
"experimental_settings_new_asset_list_title": "启用实验性照片网格",
|
||||
"experimental_settings_subtitle": "使用风险自负!",
|
||||
@@ -278,6 +282,10 @@
|
||||
"map_settings_only_show_favorites": "仅显示收藏的项目",
|
||||
"map_settings_theme_settings": "地图主题",
|
||||
"map_zoom_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",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "动图",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "无法编辑只读项目的日期,跳过",
|
||||
@@ -307,6 +315,7 @@
|
||||
"permission_onboarding_permission_granted": "已授权!一切就绪。",
|
||||
"permission_onboarding_permission_limited": "权限受限:要让 Immich 备份和管理您的整个图库收藏,请在“设置”中授予照片和视频权限。",
|
||||
"permission_onboarding_request": "Immich 需要权限才能查看您的照片和视频。",
|
||||
"preferences_settings_title": "偏好设置",
|
||||
"profile_drawer_app_logs": "日志",
|
||||
"profile_drawer_client_out_of_date_major": "客户端有大版本升级,请尽快升级至最新版。",
|
||||
"profile_drawer_client_out_of_date_minor": "客户端有小版本升级,请尽快升级至最新版。",
|
||||
|
||||
@@ -383,7 +383,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 145;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -525,7 +525,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 145;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -553,7 +553,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 145;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@@ -55,11 +55,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.99.0</string>
|
||||
<string>1.100.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>145</string>
|
||||
<string>146</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Beta"
|
||||
lane :beta do
|
||||
increment_version_number(
|
||||
version_number: "1.100.0"
|
||||
version_number: "1.101.0"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -5,32 +5,32 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000246">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000226">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.175843">
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.183824">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="6.871371">
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="3.799845">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.189451">
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.185425">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="142.078248">
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="111.245268">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="78.774821">
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="72.572736">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -243,12 +243,7 @@ class AlbumService {
|
||||
}
|
||||
}
|
||||
|
||||
await _db.writeTxn(() async {
|
||||
await album.assets.update(link: successAssets);
|
||||
final a = await _db.albums.get(album.id);
|
||||
// trigger watcher
|
||||
await _db.albums.put(a!);
|
||||
});
|
||||
await _updateAssets(album.id, add: successAssets);
|
||||
|
||||
return AddAssetsResponse(
|
||||
alreadyInAlbum: duplicatedAssets,
|
||||
@@ -257,11 +252,28 @@ class AlbumService {
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _updateAssets(
|
||||
int albumId, {
|
||||
Iterable<Asset> add = const [],
|
||||
Iterable<Asset> remove = const [],
|
||||
}) {
|
||||
return _db.writeTxn(() async {
|
||||
final album = await _db.albums.get(albumId);
|
||||
if (album == null) return;
|
||||
await album.assets.update(link: add, unlink: remove);
|
||||
album.startDate =
|
||||
await album.assets.filter().fileCreatedAtProperty().min();
|
||||
album.endDate = await album.assets.filter().fileCreatedAtProperty().max();
|
||||
album.lastModifiedAssetTimestamp =
|
||||
await album.assets.filter().updatedAtProperty().max();
|
||||
await _db.albums.put(album);
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> addAdditionalUserToAlbum(
|
||||
List<String> sharedUserIds,
|
||||
Album album,
|
||||
@@ -342,7 +354,7 @@ class AlbumService {
|
||||
await _apiService.albumApi.removeUserFromAlbum(album.remoteId!, "me");
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint("Error deleteAlbum ${e.toString()}");
|
||||
debugPrint("Error leaveAlbum ${e.toString()}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -352,24 +364,25 @@ class AlbumService {
|
||||
Iterable<Asset> assets,
|
||||
) async {
|
||||
try {
|
||||
await _apiService.albumApi.removeAssetFromAlbum(
|
||||
final response = await _apiService.albumApi.removeAssetFromAlbum(
|
||||
album.remoteId!,
|
||||
BulkIdsDto(
|
||||
ids: assets.map((asset) => asset.remoteId!).toList(),
|
||||
),
|
||||
);
|
||||
await _db.writeTxn(() async {
|
||||
await album.assets.update(unlink: assets);
|
||||
final a = await _db.albums.get(album.id);
|
||||
// trigger watcher
|
||||
await _db.albums.put(a!);
|
||||
});
|
||||
|
||||
return true;
|
||||
if (response != null) {
|
||||
final toRemove = response.every((e) => e.success)
|
||||
? assets
|
||||
: response
|
||||
.where((e) => e.success)
|
||||
.map((e) => assets.firstWhere((a) => a.remoteId == e.id));
|
||||
await _updateAssets(album.id, remove: toRemove);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error deleteAlbum ${e.toString()}");
|
||||
return false;
|
||||
debugPrint("Error removeAssetFromAlbum ${e.toString()}");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> removeUserFromAlbum(
|
||||
@@ -413,7 +426,7 @@ class AlbumService {
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint("Error deleteAlbum ${e.toString()}");
|
||||
debugPrint("Error changeTitleAlbum ${e.toString()}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
||||
this.assetsPerRow,
|
||||
this.showStorageIndicator,
|
||||
this.listener,
|
||||
this.margin = 5.0,
|
||||
this.margin = 2.0,
|
||||
this.selectionActive = false,
|
||||
this.preselectedAssets,
|
||||
this.canDeselect = true,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/constants/immich_colors.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
@@ -55,27 +56,27 @@ class _MemoryEpilogueState extends State<MemoryEpilogue>
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Text(
|
||||
'All caught up',
|
||||
"memories_all_caught_up",
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
).tr(),
|
||||
const SizedBox(height: 16.0),
|
||||
Text(
|
||||
'Check back tomorrow for more memories',
|
||||
"memories_check_back_tomorrow",
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
).tr(),
|
||||
const SizedBox(height: 16.0),
|
||||
TextButton(
|
||||
onPressed: widget.onStartOver,
|
||||
child: Text(
|
||||
'Start Over',
|
||||
"memories_start_over",
|
||||
style: context.textTheme.displayMedium?.copyWith(
|
||||
color: immichDarkThemePrimaryColor,
|
||||
),
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -106,11 +107,11 @@ class _MemoryEpilogueState extends State<MemoryEpilogue>
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Swipe up to close',
|
||||
"memories_swipe_to_close",
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,15 +1,60 @@
|
||||
/// A wrapper for [CuratedLocationsResponseDto] objects
|
||||
/// and [CuratedObjectsResponseDto] to be displayed in
|
||||
/// a view
|
||||
class CuratedContent {
|
||||
/// The label to show associated with this curated object
|
||||
final String label;
|
||||
|
||||
/// The id to lookup the asset from the server
|
||||
final String id;
|
||||
|
||||
CuratedContent({
|
||||
required this.id,
|
||||
required this.label,
|
||||
});
|
||||
}
|
||||
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||
import 'dart:convert';
|
||||
|
||||
/// A wrapper for [CuratedLocationsResponseDto] objects
|
||||
/// and [CuratedObjectsResponseDto] to be displayed in
|
||||
/// a view
|
||||
class CuratedContent {
|
||||
/// The label to show associated with this curated object
|
||||
final String label;
|
||||
|
||||
/// The id to lookup the asset from the server
|
||||
final String id;
|
||||
|
||||
CuratedContent({
|
||||
required this.label,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
CuratedContent copyWith({
|
||||
String? label,
|
||||
String? id,
|
||||
}) {
|
||||
return CuratedContent(
|
||||
label: label ?? this.label,
|
||||
id: id ?? this.id,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'label': label,
|
||||
'id': id,
|
||||
};
|
||||
}
|
||||
|
||||
factory CuratedContent.fromMap(Map<String, dynamic> map) {
|
||||
return CuratedContent(
|
||||
label: map['label'] as String,
|
||||
id: map['id'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory CuratedContent.fromJson(String source) =>
|
||||
CuratedContent.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
|
||||
@override
|
||||
String toString() => 'CuratedContent(label: $label, id: $id)';
|
||||
|
||||
@override
|
||||
bool operator ==(covariant CuratedContent other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.label == label && other.id == id;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => label.hashCode ^ id.hashCode;
|
||||
}
|
||||
|
||||
310
mobile/lib/modules/search/models/search_filter.dart
Normal file
310
mobile/lib/modules/search/models/search_filter.dart
Normal file
@@ -0,0 +1,310 @@
|
||||
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
class SearchLocationFilter {
|
||||
String? country;
|
||||
String? state;
|
||||
String? city;
|
||||
SearchLocationFilter({
|
||||
this.country,
|
||||
this.state,
|
||||
this.city,
|
||||
});
|
||||
|
||||
SearchLocationFilter copyWith({
|
||||
String? country,
|
||||
String? state,
|
||||
String? city,
|
||||
}) {
|
||||
return SearchLocationFilter(
|
||||
country: country ?? this.country,
|
||||
state: state ?? this.state,
|
||||
city: city ?? this.city,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'country': country,
|
||||
'state': state,
|
||||
'city': city,
|
||||
};
|
||||
}
|
||||
|
||||
factory SearchLocationFilter.fromMap(Map<String, dynamic> map) {
|
||||
return SearchLocationFilter(
|
||||
country: map['country'] != null ? map['country'] as String : null,
|
||||
state: map['state'] != null ? map['state'] as String : null,
|
||||
city: map['city'] != null ? map['city'] as String : null,
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory SearchLocationFilter.fromJson(String source) =>
|
||||
SearchLocationFilter.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'SearchLocationFilter(country: $country, state: $state, city: $city)';
|
||||
|
||||
@override
|
||||
bool operator ==(covariant SearchLocationFilter other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.country == country &&
|
||||
other.state == state &&
|
||||
other.city == city;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => country.hashCode ^ state.hashCode ^ city.hashCode;
|
||||
}
|
||||
|
||||
class SearchCameraFilter {
|
||||
String? make;
|
||||
String? model;
|
||||
SearchCameraFilter({
|
||||
this.make,
|
||||
this.model,
|
||||
});
|
||||
|
||||
SearchCameraFilter copyWith({
|
||||
String? make,
|
||||
String? model,
|
||||
}) {
|
||||
return SearchCameraFilter(
|
||||
make: make ?? this.make,
|
||||
model: model ?? this.model,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'make': make,
|
||||
'model': model,
|
||||
};
|
||||
}
|
||||
|
||||
factory SearchCameraFilter.fromMap(Map<String, dynamic> map) {
|
||||
return SearchCameraFilter(
|
||||
make: map['make'] != null ? map['make'] as String : null,
|
||||
model: map['model'] != null ? map['model'] as String : null,
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory SearchCameraFilter.fromJson(String source) =>
|
||||
SearchCameraFilter.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
|
||||
@override
|
||||
String toString() => 'SearchCameraFilter(make: $make, model: $model)';
|
||||
|
||||
@override
|
||||
bool operator ==(covariant SearchCameraFilter other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.make == make && other.model == model;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => make.hashCode ^ model.hashCode;
|
||||
}
|
||||
|
||||
class SearchDateFilter {
|
||||
DateTime? takenBefore;
|
||||
DateTime? takenAfter;
|
||||
SearchDateFilter({
|
||||
this.takenBefore,
|
||||
this.takenAfter,
|
||||
});
|
||||
|
||||
SearchDateFilter copyWith({
|
||||
DateTime? takenBefore,
|
||||
DateTime? takenAfter,
|
||||
}) {
|
||||
return SearchDateFilter(
|
||||
takenBefore: takenBefore ?? this.takenBefore,
|
||||
takenAfter: takenAfter ?? this.takenAfter,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'takenBefore': takenBefore?.millisecondsSinceEpoch,
|
||||
'takenAfter': takenAfter?.millisecondsSinceEpoch,
|
||||
};
|
||||
}
|
||||
|
||||
factory SearchDateFilter.fromMap(Map<String, dynamic> map) {
|
||||
return SearchDateFilter(
|
||||
takenBefore: map['takenBefore'] != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(map['takenBefore'] as int)
|
||||
: null,
|
||||
takenAfter: map['takenAfter'] != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(map['takenAfter'] as int)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory SearchDateFilter.fromJson(String source) =>
|
||||
SearchDateFilter.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'SearchDateFilter(takenBefore: $takenBefore, takenAfter: $takenAfter)';
|
||||
|
||||
@override
|
||||
bool operator ==(covariant SearchDateFilter other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.takenBefore == takenBefore && other.takenAfter == takenAfter;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => takenBefore.hashCode ^ takenAfter.hashCode;
|
||||
}
|
||||
|
||||
class SearchDisplayFilters {
|
||||
bool isNotInAlbum = false;
|
||||
bool isArchive = false;
|
||||
bool isFavorite = false;
|
||||
SearchDisplayFilters({
|
||||
required this.isNotInAlbum,
|
||||
required this.isArchive,
|
||||
required this.isFavorite,
|
||||
});
|
||||
|
||||
SearchDisplayFilters copyWith({
|
||||
bool? isNotInAlbum,
|
||||
bool? isArchive,
|
||||
bool? isFavorite,
|
||||
}) {
|
||||
return SearchDisplayFilters(
|
||||
isNotInAlbum: isNotInAlbum ?? this.isNotInAlbum,
|
||||
isArchive: isArchive ?? this.isArchive,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'isNotInAlbum': isNotInAlbum,
|
||||
'isArchive': isArchive,
|
||||
'isFavorite': isFavorite,
|
||||
};
|
||||
}
|
||||
|
||||
factory SearchDisplayFilters.fromMap(Map<String, dynamic> map) {
|
||||
return SearchDisplayFilters(
|
||||
isNotInAlbum: map['isNotInAlbum'] as bool,
|
||||
isArchive: map['isArchive'] as bool,
|
||||
isFavorite: map['isFavorite'] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory SearchDisplayFilters.fromJson(String source) =>
|
||||
SearchDisplayFilters.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'SearchDisplayFilters(isNotInAlbum: $isNotInAlbum, isArchive: $isArchive, isFavorite: $isFavorite)';
|
||||
|
||||
@override
|
||||
bool operator ==(covariant SearchDisplayFilters other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.isNotInAlbum == isNotInAlbum &&
|
||||
other.isArchive == isArchive &&
|
||||
other.isFavorite == isFavorite;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
isNotInAlbum.hashCode ^ isArchive.hashCode ^ isFavorite.hashCode;
|
||||
}
|
||||
|
||||
class SearchFilter {
|
||||
String? context;
|
||||
String? filename;
|
||||
Set<PersonResponseDto> people;
|
||||
SearchLocationFilter location;
|
||||
SearchCameraFilter camera;
|
||||
SearchDateFilter date;
|
||||
SearchDisplayFilters display;
|
||||
|
||||
// Enum
|
||||
AssetType mediaType;
|
||||
|
||||
SearchFilter({
|
||||
this.context,
|
||||
this.filename,
|
||||
required this.people,
|
||||
required this.location,
|
||||
required this.camera,
|
||||
required this.date,
|
||||
required this.display,
|
||||
required this.mediaType,
|
||||
});
|
||||
|
||||
SearchFilter copyWith({
|
||||
String? context,
|
||||
String? filename,
|
||||
Set<PersonResponseDto>? people,
|
||||
SearchLocationFilter? location,
|
||||
SearchCameraFilter? camera,
|
||||
SearchDateFilter? date,
|
||||
SearchDisplayFilters? display,
|
||||
AssetType? mediaType,
|
||||
}) {
|
||||
return SearchFilter(
|
||||
context: context ?? this.context,
|
||||
filename: filename ?? this.filename,
|
||||
people: people ?? this.people,
|
||||
location: location ?? this.location,
|
||||
camera: camera ?? this.camera,
|
||||
date: date ?? this.date,
|
||||
display: display ?? this.display,
|
||||
mediaType: mediaType ?? this.mediaType,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SearchFilter(context: $context, filename: $filename, people: $people, location: $location, camera: $camera, date: $date, display: $display, mediaType: $mediaType)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant SearchFilter other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.context == context &&
|
||||
other.filename == filename &&
|
||||
other.people == people &&
|
||||
other.location == location &&
|
||||
other.camera == camera &&
|
||||
other.date == date &&
|
||||
other.display == display &&
|
||||
other.mediaType == mediaType;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return context.hashCode ^
|
||||
filename.hashCode ^
|
||||
people.hashCode ^
|
||||
location.hashCode ^
|
||||
camera.hashCode ^
|
||||
date.hashCode ^
|
||||
display.hashCode ^
|
||||
mediaType.hashCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/modules/search/models/search_filter.dart';
|
||||
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'paginated_search.provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class PaginatedSearch extends _$PaginatedSearch {
|
||||
Future<List<Asset>?> _search(SearchFilter filter, int page) async {
|
||||
final service = ref.read(searchServiceProvider);
|
||||
final result = await service.search(filter, page);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Asset>> build() async {
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<List<Asset>> getNextPage(SearchFilter filter, int nextPage) async {
|
||||
state = const AsyncValue.loading();
|
||||
|
||||
final newState = await AsyncValue.guard(() async {
|
||||
final assets = await _search(filter, nextPage);
|
||||
|
||||
if (assets != null) {
|
||||
return [...?state.value, ...assets];
|
||||
}
|
||||
});
|
||||
|
||||
state = newState.valueOrNull == null
|
||||
? const AsyncValue.data([])
|
||||
: AsyncValue.data(newState.value!);
|
||||
|
||||
return newState.valueOrNull ?? [];
|
||||
}
|
||||
|
||||
clear() {
|
||||
state = const AsyncValue.data([]);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
AsyncValue<RenderList> paginatedSearchRenderList(
|
||||
PaginatedSearchRenderListRef ref,
|
||||
) {
|
||||
final assets = ref.watch(paginatedSearchProvider).value;
|
||||
|
||||
if (assets != null) {
|
||||
return ref.watch(
|
||||
renderListProviderWithGrouping(
|
||||
(assets, GroupAssetsBy.none),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const AsyncValue.loading();
|
||||
}
|
||||
}
|
||||
44
mobile/lib/modules/search/providers/paginated_search.provider.g.dart
generated
Normal file
44
mobile/lib/modules/search/providers/paginated_search.provider.g.dart
generated
Normal file
@@ -0,0 +1,44 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'paginated_search.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$paginatedSearchRenderListHash() =>
|
||||
r'c2cc2381ee6ea8f8e08d6d4c1289bbf0c6b9647e';
|
||||
|
||||
/// See also [paginatedSearchRenderList].
|
||||
@ProviderFor(paginatedSearchRenderList)
|
||||
final paginatedSearchRenderListProvider =
|
||||
AutoDisposeProvider<AsyncValue<RenderList>>.internal(
|
||||
paginatedSearchRenderList,
|
||||
name: r'paginatedSearchRenderListProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$paginatedSearchRenderListHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef PaginatedSearchRenderListRef
|
||||
= AutoDisposeProviderRef<AsyncValue<RenderList>>;
|
||||
String _$paginatedSearchHash() => r'8312f358261368cf2b5572b839fdd8f8fbe9a62e';
|
||||
|
||||
/// See also [PaginatedSearch].
|
||||
@ProviderFor(PaginatedSearch)
|
||||
final paginatedSearchProvider =
|
||||
AutoDisposeAsyncNotifierProvider<PaginatedSearch, List<Asset>>.internal(
|
||||
PaginatedSearch.new,
|
||||
name: r'paginatedSearchProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$paginatedSearchHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$PaginatedSearch = AutoDisposeAsyncNotifier<List<Asset>>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
@@ -1,51 +1,49 @@
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/modules/search/models/curated_content.dart';
|
||||
import 'package:immich_mobile/modules/search/services/person.service.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'people.provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<CuratedContent>> getCuratedPeople(
|
||||
GetCuratedPeopleRef ref,
|
||||
) async {
|
||||
final PersonService personService = ref.read(personServiceProvider);
|
||||
|
||||
final curatedPeople = await personService.getCuratedPeople();
|
||||
|
||||
return curatedPeople
|
||||
.map((p) => CuratedContent(id: p.id, label: p.name))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<RenderList> personAssets(PersonAssetsRef ref, String personId) async {
|
||||
final PersonService personService = ref.read(personServiceProvider);
|
||||
final assets = await personService.getPersonAssets(personId);
|
||||
if (assets == null) {
|
||||
return RenderList.empty();
|
||||
}
|
||||
|
||||
final settings = ref.read(appSettingsServiceProvider);
|
||||
final groupBy =
|
||||
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
|
||||
return await RenderList.fromAssets(assets, groupBy);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<bool> updatePersonName(
|
||||
UpdatePersonNameRef ref,
|
||||
String personId,
|
||||
String updatedName,
|
||||
) async {
|
||||
final PersonService personService = ref.read(personServiceProvider);
|
||||
final person = await personService.updateName(personId, updatedName);
|
||||
|
||||
if (person != null && person.name == updatedName) {
|
||||
ref.invalidate(getCuratedPeopleProvider);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/modules/search/services/person.service.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'people.provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<PersonResponseDto>> getAllPeople(
|
||||
GetAllPeopleRef ref,
|
||||
) async {
|
||||
final PersonService personService = ref.read(personServiceProvider);
|
||||
|
||||
final people = await personService.getAllPeople();
|
||||
|
||||
return people;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<RenderList> personAssets(PersonAssetsRef ref, String personId) async {
|
||||
final PersonService personService = ref.read(personServiceProvider);
|
||||
final assets = await personService.getPersonAssets(personId);
|
||||
if (assets == null) {
|
||||
return RenderList.empty();
|
||||
}
|
||||
|
||||
final settings = ref.read(appSettingsServiceProvider);
|
||||
final groupBy =
|
||||
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
|
||||
return await RenderList.fromAssets(assets, groupBy);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<bool> updatePersonName(
|
||||
UpdatePersonNameRef ref,
|
||||
String personId,
|
||||
String updatedName,
|
||||
) async {
|
||||
final PersonService personService = ref.read(personServiceProvider);
|
||||
final person = await personService.updateName(personId, updatedName);
|
||||
|
||||
if (person != null && person.name == updatedName) {
|
||||
ref.invalidate(getAllPeopleProvider);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -6,23 +6,21 @@ part of 'people.provider.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$getCuratedPeopleHash() => r'2a534553812abe69abce2c2e41aa62b8de16e9d0';
|
||||
String _$getAllPeopleHash() => r'4eff6666be5a74710d1e8587e01d8154310d85bd';
|
||||
|
||||
/// See also [getCuratedPeople].
|
||||
@ProviderFor(getCuratedPeople)
|
||||
final getCuratedPeopleProvider =
|
||||
AutoDisposeFutureProvider<List<CuratedContent>>.internal(
|
||||
getCuratedPeople,
|
||||
name: r'getCuratedPeopleProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$getCuratedPeopleHash,
|
||||
/// See also [getAllPeople].
|
||||
@ProviderFor(getAllPeople)
|
||||
final getAllPeopleProvider =
|
||||
AutoDisposeFutureProvider<List<PersonResponseDto>>.internal(
|
||||
getAllPeople,
|
||||
name: r'getAllPeopleProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$getAllPeopleHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef GetCuratedPeopleRef
|
||||
= AutoDisposeFutureProviderRef<List<CuratedContent>>;
|
||||
typedef GetAllPeopleRef = AutoDisposeFutureProviderRef<List<PersonResponseDto>>;
|
||||
String _$personAssetsHash() => r'1d6eff5ca3aa630b58c4dad9516193b21896984d';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
@@ -172,7 +170,7 @@ class _PersonAssetsProviderElement
|
||||
String get personId => (origin as PersonAssetsProvider).personId;
|
||||
}
|
||||
|
||||
String _$updatePersonNameHash() => r'c7179a7cc558669c3b30b03fbca7782a42f2b6fd';
|
||||
String _$updatePersonNameHash() => r'7145aaaf6fc38fdafe3a283ebf3d3f4fd0774cd2';
|
||||
|
||||
/// See also [updatePersonName].
|
||||
@ProviderFor(updatePersonName)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'search_filter.provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<String>> getSearchSuggestions(
|
||||
GetSearchSuggestionsRef ref,
|
||||
SearchSuggestionType type, {
|
||||
String? locationCountry,
|
||||
String? locationState,
|
||||
String? make,
|
||||
String? model,
|
||||
}) async {
|
||||
final SearchService service = ref.read(searchServiceProvider);
|
||||
|
||||
final suggestions = await service.getSearchSuggestions(
|
||||
type,
|
||||
country: locationCountry,
|
||||
state: locationState,
|
||||
make: make,
|
||||
model: model,
|
||||
);
|
||||
|
||||
return suggestions ?? [];
|
||||
}
|
||||
229
mobile/lib/modules/search/providers/search_filter.provider.g.dart
generated
Normal file
229
mobile/lib/modules/search/providers/search_filter.provider.g.dart
generated
Normal file
@@ -0,0 +1,229 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'search_filter.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$getSearchSuggestionsHash() =>
|
||||
r'bc1e9a1a060868f14e6eb970d2251dbfe39c6866';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
/// See also [getSearchSuggestions].
|
||||
@ProviderFor(getSearchSuggestions)
|
||||
const getSearchSuggestionsProvider = GetSearchSuggestionsFamily();
|
||||
|
||||
/// See also [getSearchSuggestions].
|
||||
class GetSearchSuggestionsFamily extends Family<AsyncValue<List<String>>> {
|
||||
/// See also [getSearchSuggestions].
|
||||
const GetSearchSuggestionsFamily();
|
||||
|
||||
/// See also [getSearchSuggestions].
|
||||
GetSearchSuggestionsProvider call(
|
||||
SearchSuggestionType type, {
|
||||
String? locationCountry,
|
||||
String? locationState,
|
||||
String? make,
|
||||
String? model,
|
||||
}) {
|
||||
return GetSearchSuggestionsProvider(
|
||||
type,
|
||||
locationCountry: locationCountry,
|
||||
locationState: locationState,
|
||||
make: make,
|
||||
model: model,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
GetSearchSuggestionsProvider getProviderOverride(
|
||||
covariant GetSearchSuggestionsProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.type,
|
||||
locationCountry: provider.locationCountry,
|
||||
locationState: provider.locationState,
|
||||
make: provider.make,
|
||||
model: provider.model,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'getSearchSuggestionsProvider';
|
||||
}
|
||||
|
||||
/// See also [getSearchSuggestions].
|
||||
class GetSearchSuggestionsProvider
|
||||
extends AutoDisposeFutureProvider<List<String>> {
|
||||
/// See also [getSearchSuggestions].
|
||||
GetSearchSuggestionsProvider(
|
||||
SearchSuggestionType type, {
|
||||
String? locationCountry,
|
||||
String? locationState,
|
||||
String? make,
|
||||
String? model,
|
||||
}) : this._internal(
|
||||
(ref) => getSearchSuggestions(
|
||||
ref as GetSearchSuggestionsRef,
|
||||
type,
|
||||
locationCountry: locationCountry,
|
||||
locationState: locationState,
|
||||
make: make,
|
||||
model: model,
|
||||
),
|
||||
from: getSearchSuggestionsProvider,
|
||||
name: r'getSearchSuggestionsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$getSearchSuggestionsHash,
|
||||
dependencies: GetSearchSuggestionsFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
GetSearchSuggestionsFamily._allTransitiveDependencies,
|
||||
type: type,
|
||||
locationCountry: locationCountry,
|
||||
locationState: locationState,
|
||||
make: make,
|
||||
model: model,
|
||||
);
|
||||
|
||||
GetSearchSuggestionsProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.type,
|
||||
required this.locationCountry,
|
||||
required this.locationState,
|
||||
required this.make,
|
||||
required this.model,
|
||||
}) : super.internal();
|
||||
|
||||
final SearchSuggestionType type;
|
||||
final String? locationCountry;
|
||||
final String? locationState;
|
||||
final String? make;
|
||||
final String? model;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<List<String>> Function(GetSearchSuggestionsRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: GetSearchSuggestionsProvider._internal(
|
||||
(ref) => create(ref as GetSearchSuggestionsRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
type: type,
|
||||
locationCountry: locationCountry,
|
||||
locationState: locationState,
|
||||
make: make,
|
||||
model: model,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<List<String>> createElement() {
|
||||
return _GetSearchSuggestionsProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is GetSearchSuggestionsProvider &&
|
||||
other.type == type &&
|
||||
other.locationCountry == locationCountry &&
|
||||
other.locationState == locationState &&
|
||||
other.make == make &&
|
||||
other.model == model;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, type.hashCode);
|
||||
hash = _SystemHash.combine(hash, locationCountry.hashCode);
|
||||
hash = _SystemHash.combine(hash, locationState.hashCode);
|
||||
hash = _SystemHash.combine(hash, make.hashCode);
|
||||
hash = _SystemHash.combine(hash, model.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin GetSearchSuggestionsRef on AutoDisposeFutureProviderRef<List<String>> {
|
||||
/// The parameter `type` of this provider.
|
||||
SearchSuggestionType get type;
|
||||
|
||||
/// The parameter `locationCountry` of this provider.
|
||||
String? get locationCountry;
|
||||
|
||||
/// The parameter `locationState` of this provider.
|
||||
String? get locationState;
|
||||
|
||||
/// The parameter `make` of this provider.
|
||||
String? get make;
|
||||
|
||||
/// The parameter `model` of this provider.
|
||||
String? get model;
|
||||
}
|
||||
|
||||
class _GetSearchSuggestionsProviderElement
|
||||
extends AutoDisposeFutureProviderElement<List<String>>
|
||||
with GetSearchSuggestionsRef {
|
||||
_GetSearchSuggestionsProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
SearchSuggestionType get type =>
|
||||
(origin as GetSearchSuggestionsProvider).type;
|
||||
@override
|
||||
String? get locationCountry =>
|
||||
(origin as GetSearchSuggestionsProvider).locationCountry;
|
||||
@override
|
||||
String? get locationState =>
|
||||
(origin as GetSearchSuggestionsProvider).locationState;
|
||||
@override
|
||||
String? get make => (origin as GetSearchSuggestionsProvider).make;
|
||||
@override
|
||||
String? get model => (origin as GetSearchSuggestionsProvider).model;
|
||||
}
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
@@ -1,67 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart';
|
||||
|
||||
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
|
||||
class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
||||
SearchResultPageNotifier(this._searchService)
|
||||
: super(
|
||||
SearchResultPageState(
|
||||
searchResult: [],
|
||||
isError: false,
|
||||
isLoading: true,
|
||||
isSuccess: false,
|
||||
isSmart: false,
|
||||
),
|
||||
);
|
||||
|
||||
final SearchService _searchService;
|
||||
|
||||
Future<void> search(String searchTerm, {bool smartSearch = true}) async {
|
||||
state = state.copyWith(
|
||||
searchResult: [],
|
||||
isError: false,
|
||||
isLoading: true,
|
||||
isSuccess: false,
|
||||
);
|
||||
|
||||
List<Asset>? assets =
|
||||
await _searchService.searchAsset(searchTerm, smartSearch: smartSearch);
|
||||
|
||||
if (assets != null) {
|
||||
state = state.copyWith(
|
||||
searchResult: assets,
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isSmart: smartSearch,
|
||||
);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
searchResult: [],
|
||||
isError: true,
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isSmart: smartSearch,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final searchResultPageProvider =
|
||||
StateNotifierProvider<SearchResultPageNotifier, SearchResultPageState>(
|
||||
(ref) {
|
||||
return SearchResultPageNotifier(ref.watch(searchServiceProvider));
|
||||
});
|
||||
|
||||
final searchRenderListProvider = Provider((ref) {
|
||||
final result = ref.watch(searchResultPageProvider);
|
||||
return ref.watch(
|
||||
renderListProviderWithGrouping(
|
||||
(result.searchResult, result.isSmart ? GroupAssetsBy.none : null),
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -20,7 +20,7 @@ class PersonService {
|
||||
|
||||
PersonService(this._apiService, this._db);
|
||||
|
||||
Future<List<PersonResponseDto>> getCuratedPeople() async {
|
||||
Future<List<PersonResponseDto>> getAllPeople() async {
|
||||
try {
|
||||
final peopleResponseDto = await _apiService.personApi.getAllPeople();
|
||||
return peopleResponseDto?.people ?? [];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/search/models/search_filter.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
@@ -29,25 +30,92 @@ class SearchService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Asset>?> searchAsset(
|
||||
String searchTerm, {
|
||||
bool smartSearch = true,
|
||||
Future<List<String>?> getSearchSuggestions(
|
||||
SearchSuggestionType type, {
|
||||
String? country,
|
||||
String? state,
|
||||
String? make,
|
||||
String? model,
|
||||
}) async {
|
||||
// TODO search in local DB: 1. when offline, 2. to find local assets
|
||||
try {
|
||||
final SearchResponseDto? results = await _apiService.searchApi.search(
|
||||
query: searchTerm,
|
||||
smart: smartSearch,
|
||||
return await _apiService.searchApi.getSearchSuggestions(
|
||||
type,
|
||||
country: country,
|
||||
state: state,
|
||||
make: make,
|
||||
model: model,
|
||||
);
|
||||
if (results == null) {
|
||||
} catch (e) {
|
||||
debugPrint("[ERROR] [getSearchSuggestions] ${e.toString()}");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Asset>?> search(SearchFilter filter, int page) async {
|
||||
try {
|
||||
SearchResponseDto? response;
|
||||
AssetTypeEnum? type;
|
||||
if (filter.mediaType == AssetType.image) {
|
||||
type = AssetTypeEnum.IMAGE;
|
||||
} else if (filter.mediaType == AssetType.video) {
|
||||
type = AssetTypeEnum.VIDEO;
|
||||
}
|
||||
|
||||
if (filter.context != null && filter.context!.isNotEmpty) {
|
||||
response = await _apiService.searchApi.searchSmart(
|
||||
SmartSearchDto(
|
||||
query: filter.context!,
|
||||
country: filter.location.country,
|
||||
state: filter.location.state,
|
||||
city: filter.location.city,
|
||||
make: filter.camera.make,
|
||||
model: filter.camera.model,
|
||||
takenAfter: filter.date.takenAfter,
|
||||
takenBefore: filter.date.takenBefore,
|
||||
isArchived: filter.display.isArchive,
|
||||
isFavorite: filter.display.isFavorite,
|
||||
isNotInAlbum: filter.display.isNotInAlbum,
|
||||
personIds: filter.people.map((e) => e.id).toList(),
|
||||
type: type,
|
||||
page: page,
|
||||
size: 1000,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
response = await _apiService.searchApi.searchMetadata(
|
||||
MetadataSearchDto(
|
||||
originalFileName:
|
||||
filter.filename != null && filter.filename!.isNotEmpty
|
||||
? filter.filename
|
||||
: null,
|
||||
country: filter.location.country,
|
||||
state: filter.location.state,
|
||||
city: filter.location.city,
|
||||
make: filter.camera.make,
|
||||
model: filter.camera.model,
|
||||
takenAfter: filter.date.takenAfter,
|
||||
takenBefore: filter.date.takenBefore,
|
||||
isArchived: filter.display.isArchive,
|
||||
isFavorite: filter.display.isFavorite,
|
||||
isNotInAlbum: filter.display.isNotInAlbum,
|
||||
personIds: filter.people.map((e) => e.id).toList(),
|
||||
type: type,
|
||||
page: page,
|
||||
size: 1000,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (response == null) {
|
||||
return null;
|
||||
}
|
||||
// TODO local DB might be out of date; add assets not yet in DB?
|
||||
return _db.assets.getAllByRemoteId(results.assets.items.map((e) => e.id));
|
||||
} catch (e) {
|
||||
debugPrint("[ERROR] [searchAsset] ${e.toString()}");
|
||||
return null;
|
||||
|
||||
return _db.assets
|
||||
.getAllByRemoteId(response.assets.items.map((e) => e.id));
|
||||
} catch (error) {
|
||||
debugPrint("Error [search] $error");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<CuratedLocationsResponseDto>?> getCuratedLocation() async {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/modules/search/models/curated_content.dart';
|
||||
import 'package:immich_mobile/modules/search/models/search_filter.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
|
||||
@@ -57,7 +59,22 @@ class ExploreGrid extends StatelessWidget {
|
||||
),
|
||||
)
|
||||
: context.pushRoute(
|
||||
SearchResultRoute(searchTerm: 'm:${content.label}'),
|
||||
SearchInputRoute(
|
||||
prefilter: SearchFilter(
|
||||
people: {},
|
||||
location: SearchLocationFilter(
|
||||
city: content.label,
|
||||
),
|
||||
camera: SearchCameraFilter(),
|
||||
date: SearchDateFilter(),
|
||||
display: SearchDisplayFilters(
|
||||
isNotInAlbum: false,
|
||||
isArchive: false,
|
||||
isFavorite: false,
|
||||
),
|
||||
mediaType: AssetType.other,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
||||
|
||||
class ImmichSearchBar extends HookConsumerWidget
|
||||
implements PreferredSizeWidget {
|
||||
const ImmichSearchBar({
|
||||
super.key,
|
||||
required this.searchFocusNode,
|
||||
required this.onSubmitted,
|
||||
});
|
||||
|
||||
final FocusNode searchFocusNode;
|
||||
final Function(String) onSubmitted;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final searchTermController = useTextEditingController(text: "");
|
||||
final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
|
||||
|
||||
focusSearch() {
|
||||
searchTermController.clear();
|
||||
ref.watch(searchPageStateProvider.notifier).getSuggestedSearchTerms();
|
||||
ref.watch(searchPageStateProvider.notifier).enableSearch();
|
||||
ref.watch(searchPageStateProvider.notifier).setSearchTerm("");
|
||||
|
||||
searchFocusNode.requestFocus();
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
searchFocusNotifier.addListener(focusSearch);
|
||||
return () {
|
||||
searchFocusNotifier.removeListener(focusSearch);
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
leading: isSearchEnabled
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
searchFocusNode.unfocus();
|
||||
ref.watch(searchPageStateProvider.notifier).disableSearch();
|
||||
searchTermController.clear();
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.search_rounded,
|
||||
size: 20,
|
||||
),
|
||||
title: TextField(
|
||||
controller: searchTermController,
|
||||
focusNode: searchFocusNode,
|
||||
autofocus: false,
|
||||
onTap: focusSearch,
|
||||
onSubmitted: (searchTerm) {
|
||||
onSubmitted(searchTerm);
|
||||
searchTermController.clear();
|
||||
ref.watch(searchPageStateProvider.notifier).setSearchTerm("");
|
||||
},
|
||||
onChanged: (value) {
|
||||
ref.watch(searchPageStateProvider.notifier).setSearchTerm(value);
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'search_bar_hint'.tr(),
|
||||
hintStyle: context.textTheme.bodyLarge?.copyWith(
|
||||
color: context.themeData.colorScheme.onSurface.withOpacity(0.75),
|
||||
),
|
||||
enabledBorder: const UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent),
|
||||
),
|
||||
focusedBorder: const UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
}
|
||||
|
||||
// Used to focus search from outside this widget.
|
||||
// For example when double pressing the search nav icon.
|
||||
final searchFocusNotifier = SearchFocusNotifier();
|
||||
|
||||
class SearchFocusNotifier with ChangeNotifier {
|
||||
void requestFocus() {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
120
mobile/lib/modules/search/ui/search_filter/camera_picker.dart
Normal file
120
mobile/lib/modules/search/ui/search_filter/camera_picker.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/search/models/search_filter.dart';
|
||||
import 'package:immich_mobile/modules/search/providers/search_filter.provider.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
class CameraPicker extends HookConsumerWidget {
|
||||
const CameraPicker({super.key, required this.onSelect, this.filter});
|
||||
|
||||
final Function(Map<String, String?>) onSelect;
|
||||
final SearchCameraFilter? filter;
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final makeTextController = useTextEditingController(text: filter?.make);
|
||||
final modelTextController = useTextEditingController(text: filter?.model);
|
||||
final selectedMake = useState<String?>(filter?.make);
|
||||
final selectedModel = useState<String?>(filter?.model);
|
||||
|
||||
final make = ref.watch(
|
||||
getSearchSuggestionsProvider(
|
||||
SearchSuggestionType.cameraMake,
|
||||
),
|
||||
);
|
||||
|
||||
final models = ref.watch(
|
||||
getSearchSuggestionsProvider(
|
||||
SearchSuggestionType.cameraModel,
|
||||
make: selectedMake.value,
|
||||
),
|
||||
);
|
||||
|
||||
final inputDecorationTheme = InputDecorationTheme(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
contentPadding: const EdgeInsets.only(left: 16),
|
||||
);
|
||||
|
||||
final menuStyle = MenuStyle(
|
||||
shape: MaterialStatePropertyAll<OutlinedBorder>(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(
|
||||
// bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
DropdownMenu(
|
||||
dropdownMenuEntries: switch (make) {
|
||||
AsyncError() => [],
|
||||
AsyncData(:final value) => value
|
||||
.map(
|
||||
(e) => DropdownMenuEntry(
|
||||
value: e,
|
||||
label: e,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
_ => [],
|
||||
},
|
||||
width: context.width * 0.45,
|
||||
menuHeight: 400,
|
||||
label: const Text('Make'),
|
||||
inputDecorationTheme: inputDecorationTheme,
|
||||
controller: makeTextController,
|
||||
menuStyle: menuStyle,
|
||||
leadingIcon: const Icon(Icons.photo_camera_rounded),
|
||||
trailingIcon: const Icon(Icons.arrow_drop_down_rounded),
|
||||
selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded),
|
||||
onSelected: (value) {
|
||||
selectedMake.value = value.toString();
|
||||
onSelect({
|
||||
'make': selectedMake.value,
|
||||
'model': selectedModel.value,
|
||||
});
|
||||
},
|
||||
),
|
||||
DropdownMenu(
|
||||
dropdownMenuEntries: switch (models) {
|
||||
AsyncError() => [],
|
||||
AsyncData(:final value) => value
|
||||
.map(
|
||||
(e) => DropdownMenuEntry(
|
||||
value: e,
|
||||
label: e,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
_ => [],
|
||||
},
|
||||
width: context.width * 0.45,
|
||||
menuHeight: 400,
|
||||
label: const Text('Model'),
|
||||
inputDecorationTheme: inputDecorationTheme,
|
||||
controller: modelTextController,
|
||||
menuStyle: menuStyle,
|
||||
leadingIcon: const Icon(Icons.camera),
|
||||
trailingIcon: const Icon(Icons.arrow_drop_down_rounded),
|
||||
selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded),
|
||||
onSelected: (value) {
|
||||
selectedModel.value = value.toString();
|
||||
onSelect({
|
||||
'make': selectedMake.value,
|
||||
'model': selectedModel.value,
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/modules/search/models/search_filter.dart';
|
||||
|
||||
enum DisplayOption {
|
||||
notInAlbum,
|
||||
favorite,
|
||||
archive,
|
||||
}
|
||||
|
||||
class DisplayOptionPicker extends HookWidget {
|
||||
const DisplayOptionPicker({
|
||||
super.key,
|
||||
required this.onSelect,
|
||||
this.filter,
|
||||
});
|
||||
|
||||
final Function(Map<DisplayOption, bool>) onSelect;
|
||||
final SearchDisplayFilters? filter;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final options = useState<Map<DisplayOption, bool>>({
|
||||
DisplayOption.notInAlbum: filter?.isNotInAlbum ?? false,
|
||||
DisplayOption.favorite: filter?.isFavorite ?? false,
|
||||
DisplayOption.archive: filter?.isArchive ?? false,
|
||||
});
|
||||
|
||||
return ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
CheckboxListTile(
|
||||
title: const Text('Not in album'),
|
||||
value: options.value[DisplayOption.notInAlbum],
|
||||
onChanged: (bool? value) {
|
||||
options.value = {
|
||||
...options.value,
|
||||
DisplayOption.notInAlbum: value!,
|
||||
};
|
||||
onSelect(options.value);
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: const Text('Favorite'),
|
||||
value: options.value[DisplayOption.favorite],
|
||||
onChanged: (value) {
|
||||
options.value = {
|
||||
...options.value,
|
||||
DisplayOption.favorite: value!,
|
||||
};
|
||||
onSelect(options.value);
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: const Text('Archive'),
|
||||
value: options.value[DisplayOption.archive],
|
||||
onChanged: (value) {
|
||||
options.value = {
|
||||
...options.value,
|
||||
DisplayOption.archive: value!,
|
||||
};
|
||||
onSelect(options.value);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
|
||||
class FilterBottomSheetScaffold extends StatelessWidget {
|
||||
const FilterBottomSheetScaffold({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.onSearch,
|
||||
required this.onClear,
|
||||
required this.title,
|
||||
this.expanded,
|
||||
});
|
||||
|
||||
final bool? expanded;
|
||||
final String title;
|
||||
final Widget child;
|
||||
final Function() onSearch;
|
||||
final Function() onClear;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
buildChildWidget() {
|
||||
if (expanded != null && expanded == true) {
|
||||
return Expanded(child: child);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
title,
|
||||
style: context.textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
buildChildWidget(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
onClear();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Clear'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
onSearch();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Apply filter'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
166
mobile/lib/modules/search/ui/search_filter/location_picker.dart
Normal file
166
mobile/lib/modules/search/ui/search_filter/location_picker.dart
Normal file
@@ -0,0 +1,166 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/search/models/search_filter.dart';
|
||||
import 'package:immich_mobile/modules/search/providers/search_filter.provider.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
class LocationPicker extends HookConsumerWidget {
|
||||
const LocationPicker({super.key, required this.onSelected, this.filter});
|
||||
|
||||
final Function(Map<String, String?>) onSelected;
|
||||
final SearchLocationFilter? filter;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final countryTextController =
|
||||
useTextEditingController(text: filter?.country);
|
||||
final stateTextController = useTextEditingController(text: filter?.state);
|
||||
final cityTextController = useTextEditingController(text: filter?.city);
|
||||
|
||||
final selectedCountry = useState<String?>(filter?.country);
|
||||
final selectedState = useState<String?>(filter?.state);
|
||||
final selectedCity = useState<String?>(filter?.city);
|
||||
|
||||
final countries = ref.watch(
|
||||
getSearchSuggestionsProvider(
|
||||
SearchSuggestionType.country,
|
||||
locationCountry: selectedCountry.value,
|
||||
locationState: selectedState.value,
|
||||
),
|
||||
);
|
||||
|
||||
final states = ref.watch(
|
||||
getSearchSuggestionsProvider(
|
||||
SearchSuggestionType.state,
|
||||
locationCountry: selectedCountry.value,
|
||||
locationState: selectedState.value,
|
||||
),
|
||||
);
|
||||
|
||||
final cities = ref.watch(
|
||||
getSearchSuggestionsProvider(
|
||||
SearchSuggestionType.city,
|
||||
locationCountry: selectedCountry.value,
|
||||
locationState: selectedState.value,
|
||||
),
|
||||
);
|
||||
|
||||
final inputDecorationTheme = InputDecorationTheme(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
contentPadding: const EdgeInsets.only(left: 16),
|
||||
);
|
||||
|
||||
final menuStyle = MenuStyle(
|
||||
shape: MaterialStatePropertyAll<OutlinedBorder>(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
DropdownMenu(
|
||||
dropdownMenuEntries: switch (countries) {
|
||||
AsyncError() => [],
|
||||
AsyncData(:final value) => value
|
||||
.map(
|
||||
(e) => DropdownMenuEntry(
|
||||
value: e,
|
||||
label: e,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
_ => [],
|
||||
},
|
||||
menuHeight: 400,
|
||||
width: context.width * 0.9,
|
||||
label: const Text('Country'),
|
||||
inputDecorationTheme: inputDecorationTheme,
|
||||
menuStyle: menuStyle,
|
||||
controller: countryTextController,
|
||||
trailingIcon: const Icon(Icons.arrow_drop_down_rounded),
|
||||
selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded),
|
||||
onSelected: (value) {
|
||||
selectedCountry.value = value.toString();
|
||||
onSelected({
|
||||
'country': selectedCountry.value,
|
||||
'state': selectedState.value,
|
||||
'city': selectedCity.value,
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
DropdownMenu(
|
||||
dropdownMenuEntries: switch (states) {
|
||||
AsyncError() => [],
|
||||
AsyncData(:final value) => value
|
||||
.map(
|
||||
(e) => DropdownMenuEntry(
|
||||
value: e,
|
||||
label: e,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
_ => [],
|
||||
},
|
||||
menuHeight: 400,
|
||||
width: context.width * 0.9,
|
||||
label: const Text('State'),
|
||||
inputDecorationTheme: inputDecorationTheme,
|
||||
menuStyle: menuStyle,
|
||||
controller: stateTextController,
|
||||
trailingIcon: const Icon(Icons.arrow_drop_down_rounded),
|
||||
selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded),
|
||||
onSelected: (value) {
|
||||
selectedState.value = value.toString();
|
||||
onSelected({
|
||||
'country': selectedCountry.value,
|
||||
'state': selectedState.value,
|
||||
'city': selectedCity.value,
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
DropdownMenu(
|
||||
dropdownMenuEntries: switch (cities) {
|
||||
AsyncError() => [],
|
||||
AsyncData(:final value) => value
|
||||
.map(
|
||||
(e) => DropdownMenuEntry(
|
||||
value: e,
|
||||
label: e,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
_ => [],
|
||||
},
|
||||
menuHeight: 400,
|
||||
width: context.width * 0.9,
|
||||
label: const Text('City'),
|
||||
inputDecorationTheme: inputDecorationTheme,
|
||||
menuStyle: menuStyle,
|
||||
controller: cityTextController,
|
||||
trailingIcon: const Icon(Icons.arrow_drop_down_rounded),
|
||||
selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded),
|
||||
onSelected: (value) {
|
||||
selectedCity.value = value.toString();
|
||||
onSelected({
|
||||
'country': selectedCountry.value,
|
||||
'state': selectedState.value,
|
||||
'city': selectedCity.value,
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
|
||||
class MediaTypePicker extends HookWidget {
|
||||
const MediaTypePicker({super.key, required this.onSelect, this.filter});
|
||||
|
||||
final Function(AssetType) onSelect;
|
||||
final AssetType? filter;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedMediaType = useState(filter ?? AssetType.other);
|
||||
|
||||
return ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
RadioListTile(
|
||||
title: const Text("All"),
|
||||
value: AssetType.other,
|
||||
onChanged: (value) {
|
||||
selectedMediaType.value = value!;
|
||||
onSelect(value);
|
||||
},
|
||||
groupValue: selectedMediaType.value,
|
||||
),
|
||||
RadioListTile(
|
||||
title: const Text("Image"),
|
||||
value: AssetType.image,
|
||||
onChanged: (value) {
|
||||
selectedMediaType.value = value!;
|
||||
onSelect(value);
|
||||
},
|
||||
groupValue: selectedMediaType.value,
|
||||
),
|
||||
RadioListTile(
|
||||
title: const Text("Video"),
|
||||
value: AssetType.video,
|
||||
onChanged: (value) {
|
||||
selectedMediaType.value = value!;
|
||||
onSelect(value);
|
||||
},
|
||||
groupValue: selectedMediaType.value,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart' as local_store;
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
class PeoplePicker extends HookConsumerWidget {
|
||||
const PeoplePicker({super.key, required this.onSelect, this.filter});
|
||||
|
||||
final Function(Set<PersonResponseDto>) onSelect;
|
||||
final Set<PersonResponseDto>? filter;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
var imageSize = 45.0;
|
||||
final people = ref.watch(getAllPeopleProvider);
|
||||
final headers = {
|
||||
"x-immich-user-token":
|
||||
local_store.Store.get(local_store.StoreKey.accessToken),
|
||||
};
|
||||
final selectedPeople = useState<Set<PersonResponseDto>>(filter ?? {});
|
||||
|
||||
return people.widgetWhen(
|
||||
onData: (people) {
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: people.length,
|
||||
padding: const EdgeInsets.all(8),
|
||||
itemBuilder: (context, index) {
|
||||
final person = people[index];
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(15)),
|
||||
),
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
person.name,
|
||||
style: context.textTheme.bodyLarge,
|
||||
),
|
||||
leading: SizedBox(
|
||||
height: imageSize,
|
||||
child: Material(
|
||||
shape: const CircleBorder(side: BorderSide.none),
|
||||
elevation: 3,
|
||||
child: CircleAvatar(
|
||||
maxRadius: imageSize / 2,
|
||||
backgroundImage: NetworkImage(
|
||||
getFaceThumbnailUrl(person.id),
|
||||
headers: headers,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (selectedPeople.value.contains(person)) {
|
||||
selectedPeople.value.remove(person);
|
||||
} else {
|
||||
selectedPeople.value.add(person);
|
||||
}
|
||||
|
||||
selectedPeople.value = {...selectedPeople.value};
|
||||
onSelect(selectedPeople.value);
|
||||
},
|
||||
selected: selectedPeople.value.contains(person),
|
||||
selectedTileColor: context.primaryColor.withOpacity(0.2),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(15)),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
|
||||
class SearchFilterChip extends StatelessWidget {
|
||||
final String label;
|
||||
final Function() onTap;
|
||||
final Widget? currentFilter;
|
||||
final IconData icon;
|
||||
|
||||
const SearchFilterChip({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
required this.icon,
|
||||
this.currentFilter,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (currentFilter != null) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
color: context.primaryColor.withAlpha(25),
|
||||
shape: StadiumBorder(
|
||||
side: BorderSide(color: context.primaryColor),
|
||||
),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 2.0, horizontal: 14.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
currentFilter!,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape:
|
||||
StadiumBorder(side: BorderSide(color: Colors.grey.withAlpha(100))),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 14.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
Text(label),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Future<T> showFilterBottomSheet<T>({
|
||||
required BuildContext context,
|
||||
required Widget child,
|
||||
bool isScrollControlled = false,
|
||||
bool isDismissible = true,
|
||||
}) async {
|
||||
return await showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: isScrollControlled,
|
||||
useSafeArea: false,
|
||||
isDismissible: isDismissible,
|
||||
showDragHandle: isDismissible,
|
||||
builder: (BuildContext context) {
|
||||
return child;
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
||||
|
||||
class SearchSuggestionList extends ConsumerWidget {
|
||||
const SearchSuggestionList({super.key, required this.onSubmitted});
|
||||
|
||||
final Function(String) onSubmitted;
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final searchTerm = ref.watch(searchPageStateProvider).searchTerm;
|
||||
final searchSuggestion =
|
||||
ref.watch(searchPageStateProvider).searchSuggestion;
|
||||
|
||||
return Container(
|
||||
color: searchTerm.isEmpty
|
||||
? Colors.black.withOpacity(0.5)
|
||||
: context.scaffoldBackgroundColor,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
color: context.isDarkTheme ? Colors.grey[800] : Colors.grey[100],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'search_suggestion_list_smart_search_hint_1'.tr(),
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
TextSpan(
|
||||
text: 'search_suggestion_list_smart_search_hint_2'.tr(),
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: true,
|
||||
child: ListView.builder(
|
||||
itemBuilder: ((context, index) {
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
onSubmitted("m:${searchSuggestion[index]}");
|
||||
},
|
||||
title: Text(searchSuggestion[index]),
|
||||
);
|
||||
}),
|
||||
itemCount: searchSuggestion.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,38 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/explore_grid.dart';
|
||||
|
||||
@RoutePage()
|
||||
class AllPeoplePage extends HookConsumerWidget {
|
||||
const AllPeoplePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final curatedPeople = ref.watch(getCuratedPeopleProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'all_people_page_title',
|
||||
).tr(),
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
),
|
||||
body: curatedPeople.widgetWhen(
|
||||
onData: (people) => ExploreGrid(
|
||||
isPeople: true,
|
||||
curatedContent: people,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/modules/search/models/curated_content.dart';
|
||||
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/explore_grid.dart';
|
||||
|
||||
@RoutePage()
|
||||
class AllPeoplePage extends HookConsumerWidget {
|
||||
const AllPeoplePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final curatedPeople = ref.watch(getAllPeopleProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'all_people_page_title',
|
||||
).tr(),
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
),
|
||||
body: curatedPeople.widgetWhen(
|
||||
onData: (people) => ExploreGrid(
|
||||
isPeople: true,
|
||||
curatedContent: people
|
||||
.map((e) => CuratedContent(label: e.name, id: e.id))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
563
mobile/lib/modules/search/views/search_input_page.dart
Normal file
563
mobile/lib/modules/search/views/search_input_page.dart
Normal file
@@ -0,0 +1,563 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/search/models/search_filter.dart';
|
||||
import 'package:immich_mobile/modules/search/providers/paginated_search.provider.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/search_filter/camera_picker.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/search_filter/display_option_picker.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/search_filter/filter_bottom_sheet_scaffold.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/search_filter/location_picker.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/search_filter/media_type_picker.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/search_filter/people_picker.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/search_filter/search_filter_chip.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/search_filter/search_filter_utils.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
@RoutePage()
|
||||
class SearchInputPage extends HookConsumerWidget {
|
||||
const SearchInputPage({super.key, this.prefilter});
|
||||
|
||||
final SearchFilter? prefilter;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isContextualSearch = useState(true);
|
||||
final textSearchController = useTextEditingController();
|
||||
final filter = useState<SearchFilter>(
|
||||
SearchFilter(
|
||||
people: prefilter?.people ?? {},
|
||||
location: prefilter?.location ?? SearchLocationFilter(),
|
||||
camera: prefilter?.camera ?? SearchCameraFilter(),
|
||||
date: prefilter?.date ?? SearchDateFilter(),
|
||||
display: prefilter?.display ??
|
||||
SearchDisplayFilters(
|
||||
isNotInAlbum: false,
|
||||
isArchive: false,
|
||||
isFavorite: false,
|
||||
),
|
||||
mediaType: prefilter?.mediaType ?? AssetType.other,
|
||||
),
|
||||
);
|
||||
|
||||
final previousFilter = useState(filter.value);
|
||||
|
||||
final peopleCurrentFilterWidget = useState<Widget?>(null);
|
||||
final dateRangeCurrentFilterWidget = useState<Widget?>(null);
|
||||
final cameraCurrentFilterWidget = useState<Widget?>(null);
|
||||
final locationCurrentFilterWidget = useState<Widget?>(null);
|
||||
final mediaTypeCurrentFilterWidget = useState<Widget?>(null);
|
||||
final displayOptionCurrentFilterWidget = useState<Widget?>(null);
|
||||
|
||||
final currentPage = useState(1);
|
||||
final searchProvider = ref.watch(paginatedSearchProvider);
|
||||
final searchResultCount = useState(0);
|
||||
|
||||
search() async {
|
||||
if (prefilter == null && filter.value == previousFilter.value) return;
|
||||
|
||||
ref.watch(paginatedSearchProvider.notifier).clear();
|
||||
|
||||
currentPage.value = 1;
|
||||
|
||||
final searchResult = await ref
|
||||
.watch(paginatedSearchProvider.notifier)
|
||||
.getNextPage(filter.value, currentPage.value);
|
||||
previousFilter.value = filter.value;
|
||||
|
||||
searchResultCount.value = searchResult.length;
|
||||
}
|
||||
|
||||
searchPrefilter() {
|
||||
if (prefilter != null) {
|
||||
Future.delayed(
|
||||
Duration.zero,
|
||||
() {
|
||||
search();
|
||||
|
||||
if (prefilter!.location.city != null) {
|
||||
locationCurrentFilterWidget.value = Text(
|
||||
prefilter!.location.city!,
|
||||
style: context.textTheme.labelLarge,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
searchPrefilter();
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
loadMoreSearchResult() async {
|
||||
currentPage.value += 1;
|
||||
final searchResult = await ref
|
||||
.watch(paginatedSearchProvider.notifier)
|
||||
.getNextPage(filter.value, currentPage.value);
|
||||
searchResultCount.value = searchResult.length;
|
||||
}
|
||||
|
||||
showPeoplePicker() {
|
||||
handleOnSelect(Set<PersonResponseDto> value) {
|
||||
filter.value = filter.value.copyWith(
|
||||
people: value,
|
||||
);
|
||||
|
||||
peopleCurrentFilterWidget.value = Text(
|
||||
value.map((e) => e.name != '' ? e.name : "No name").join(', '),
|
||||
style: context.textTheme.labelLarge,
|
||||
);
|
||||
}
|
||||
|
||||
handleClear() {
|
||||
filter.value = filter.value.copyWith(
|
||||
people: {},
|
||||
);
|
||||
|
||||
peopleCurrentFilterWidget.value = null;
|
||||
search();
|
||||
}
|
||||
|
||||
showFilterBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
child: FractionallySizedBox(
|
||||
heightFactor: 0.8,
|
||||
child: FilterBottomSheetScaffold(
|
||||
title: 'Select people',
|
||||
expanded: true,
|
||||
onSearch: search,
|
||||
onClear: handleClear,
|
||||
child: PeoplePicker(
|
||||
onSelect: handleOnSelect,
|
||||
filter: filter.value.people,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
showLocationPicker() {
|
||||
handleOnSelect(Map<String, String?> value) {
|
||||
filter.value = filter.value.copyWith(
|
||||
location: SearchLocationFilter(
|
||||
country: value['country'],
|
||||
city: value['city'],
|
||||
state: value['state'],
|
||||
),
|
||||
);
|
||||
|
||||
final locationText = <String>[];
|
||||
if (value['country'] != null) {
|
||||
locationText.add(value['country']!);
|
||||
}
|
||||
|
||||
if (value['state'] != null) {
|
||||
locationText.add(value['state']!);
|
||||
}
|
||||
|
||||
if (value['city'] != null) {
|
||||
locationText.add(value['city']!);
|
||||
}
|
||||
|
||||
locationCurrentFilterWidget.value = Text(
|
||||
locationText.join(', '),
|
||||
style: context.textTheme.labelLarge,
|
||||
);
|
||||
}
|
||||
|
||||
handleClear() {
|
||||
filter.value = filter.value.copyWith(
|
||||
location: SearchLocationFilter(),
|
||||
);
|
||||
|
||||
locationCurrentFilterWidget.value = null;
|
||||
search();
|
||||
}
|
||||
|
||||
showFilterBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
isDismissible: false,
|
||||
child: FilterBottomSheetScaffold(
|
||||
title: 'Select location',
|
||||
onSearch: search,
|
||||
onClear: handleClear,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: LocationPicker(
|
||||
onSelected: handleOnSelect,
|
||||
filter: filter.value.location,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
showCameraPicker() {
|
||||
handleOnSelect(Map<String, String?> value) {
|
||||
filter.value = filter.value.copyWith(
|
||||
camera: SearchCameraFilter(
|
||||
make: value['make'],
|
||||
model: value['model'],
|
||||
),
|
||||
);
|
||||
|
||||
cameraCurrentFilterWidget.value = Text(
|
||||
'${value['make'] ?? ''} ${value['model'] ?? ''}',
|
||||
style: context.textTheme.labelLarge,
|
||||
);
|
||||
}
|
||||
|
||||
handleClear() {
|
||||
filter.value = filter.value.copyWith(
|
||||
camera: SearchCameraFilter(),
|
||||
);
|
||||
|
||||
cameraCurrentFilterWidget.value = null;
|
||||
search();
|
||||
}
|
||||
|
||||
showFilterBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
isDismissible: false,
|
||||
child: FilterBottomSheetScaffold(
|
||||
title: 'Select camera type',
|
||||
onSearch: search,
|
||||
onClear: handleClear,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: CameraPicker(
|
||||
onSelect: handleOnSelect,
|
||||
filter: filter.value.camera,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
showDatePicker() async {
|
||||
final firstDate = DateTime(1900);
|
||||
final lastDate = DateTime.now();
|
||||
|
||||
final date = await showDateRangePicker(
|
||||
context: context,
|
||||
firstDate: firstDate,
|
||||
lastDate: lastDate,
|
||||
currentDate: DateTime.now(),
|
||||
initialDateRange: DateTimeRange(
|
||||
start: filter.value.date.takenAfter ?? lastDate,
|
||||
end: filter.value.date.takenBefore ?? lastDate,
|
||||
),
|
||||
helpText: 'Select a date range',
|
||||
cancelText: 'Cancel',
|
||||
confirmText: 'Select',
|
||||
saveText: 'Save',
|
||||
errorFormatText: 'Invalid date format',
|
||||
errorInvalidText: 'Invalid date',
|
||||
fieldStartHintText: 'Start date',
|
||||
fieldEndHintText: 'End date',
|
||||
initialEntryMode: DatePickerEntryMode.input,
|
||||
);
|
||||
|
||||
if (date == null) {
|
||||
filter.value = filter.value.copyWith(
|
||||
date: SearchDateFilter(),
|
||||
);
|
||||
|
||||
dateRangeCurrentFilterWidget.value = null;
|
||||
search();
|
||||
return;
|
||||
}
|
||||
|
||||
filter.value = filter.value.copyWith(
|
||||
date: SearchDateFilter(
|
||||
takenAfter: date.start,
|
||||
takenBefore: date.end.add(
|
||||
const Duration(
|
||||
hours: 23,
|
||||
minutes: 59,
|
||||
seconds: 59,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// If date range is less than 24 hours, set the end date to the end of the day
|
||||
if (date.end.difference(date.start).inHours < 24) {
|
||||
dateRangeCurrentFilterWidget.value = Text(
|
||||
date.start.toLocal().toIso8601String().split('T').first,
|
||||
style: context.textTheme.labelLarge,
|
||||
);
|
||||
} else {
|
||||
dateRangeCurrentFilterWidget.value = Text(
|
||||
'${date.start.toLocal().toIso8601String().split('T').first} to ${date.end.toLocal().toIso8601String().split('T').first}',
|
||||
style: context.textTheme.labelLarge,
|
||||
);
|
||||
}
|
||||
|
||||
search();
|
||||
}
|
||||
|
||||
// MEDIA PICKER
|
||||
showMediaTypePicker() {
|
||||
handleOnSelected(AssetType assetType) {
|
||||
filter.value = filter.value.copyWith(
|
||||
mediaType: assetType,
|
||||
);
|
||||
|
||||
mediaTypeCurrentFilterWidget.value = Text(
|
||||
assetType == AssetType.image ? 'Image' : 'Video',
|
||||
style: context.textTheme.labelLarge,
|
||||
);
|
||||
}
|
||||
|
||||
handleClear() {
|
||||
filter.value = filter.value.copyWith(
|
||||
mediaType: AssetType.other,
|
||||
);
|
||||
|
||||
mediaTypeCurrentFilterWidget.value = null;
|
||||
search();
|
||||
}
|
||||
|
||||
showFilterBottomSheet(
|
||||
context: context,
|
||||
child: FilterBottomSheetScaffold(
|
||||
title: 'Select media type',
|
||||
onSearch: search,
|
||||
onClear: handleClear,
|
||||
child: MediaTypePicker(
|
||||
onSelect: handleOnSelected,
|
||||
filter: filter.value.mediaType,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// DISPLAY OPTION
|
||||
showDisplayOptionPicker() {
|
||||
handleOnSelect(Map<DisplayOption, bool> value) {
|
||||
final filterText = <String>[];
|
||||
|
||||
value.forEach((key, value) {
|
||||
switch (key) {
|
||||
case DisplayOption.notInAlbum:
|
||||
filter.value = filter.value.copyWith(
|
||||
display: filter.value.display.copyWith(
|
||||
isNotInAlbum: value,
|
||||
),
|
||||
);
|
||||
if (value) filterText.add('Not in album');
|
||||
break;
|
||||
case DisplayOption.archive:
|
||||
filter.value = filter.value.copyWith(
|
||||
display: filter.value.display.copyWith(
|
||||
isArchive: value,
|
||||
),
|
||||
);
|
||||
if (value) filterText.add('Archive');
|
||||
break;
|
||||
case DisplayOption.favorite:
|
||||
filter.value = filter.value.copyWith(
|
||||
display: filter.value.display.copyWith(
|
||||
isFavorite: value,
|
||||
),
|
||||
);
|
||||
if (value) filterText.add('Favorite');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
displayOptionCurrentFilterWidget.value = Text(
|
||||
filterText.join(', '),
|
||||
style: context.textTheme.labelLarge,
|
||||
);
|
||||
}
|
||||
|
||||
handleClear() {
|
||||
filter.value = filter.value.copyWith(
|
||||
display: SearchDisplayFilters(
|
||||
isNotInAlbum: false,
|
||||
isArchive: false,
|
||||
isFavorite: false,
|
||||
),
|
||||
);
|
||||
|
||||
displayOptionCurrentFilterWidget.value = null;
|
||||
search();
|
||||
}
|
||||
|
||||
showFilterBottomSheet(
|
||||
context: context,
|
||||
child: FilterBottomSheetScaffold(
|
||||
title: 'Display options',
|
||||
onSearch: search,
|
||||
onClear: handleClear,
|
||||
child: DisplayOptionPicker(
|
||||
onSelect: handleOnSelect,
|
||||
filter: filter.value.display,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
handleTextSubmitted(String value) {
|
||||
if (isContextualSearch.value) {
|
||||
filter.value = filter.value.copyWith(
|
||||
context: value,
|
||||
filename: null,
|
||||
);
|
||||
} else {
|
||||
filter.value = filter.value.copyWith(filename: value, context: null);
|
||||
}
|
||||
|
||||
search();
|
||||
}
|
||||
|
||||
buildSearchResult() {
|
||||
return switch (searchProvider) {
|
||||
AsyncData() => Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: NotificationListener<ScrollEndNotification>(
|
||||
onNotification: (notification) {
|
||||
final metrics = notification.metrics;
|
||||
final shouldLoadMore = searchResultCount.value > 75;
|
||||
if (metrics.pixels >= metrics.maxScrollExtent &&
|
||||
shouldLoadMore) {
|
||||
loadMoreSearchResult();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: MultiselectGrid(
|
||||
renderListProvider: paginatedSearchRenderListProvider,
|
||||
archiveEnabled: true,
|
||||
deleteEnabled: true,
|
||||
editEnabled: true,
|
||||
favoriteEnabled: true,
|
||||
stackEnabled: false,
|
||||
emptyIndicator: const SizedBox(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
AsyncError(:final error) => Text('Error: $error'),
|
||||
_ => const Expanded(child: Center(child: CircularProgressIndicator())),
|
||||
};
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: isContextualSearch.value
|
||||
? const Icon(Icons.abc_rounded)
|
||||
: const Icon(Icons.image_search_rounded),
|
||||
onPressed: () {
|
||||
isContextualSearch.value = !isContextualSearch.value;
|
||||
textSearchController.clear();
|
||||
},
|
||||
),
|
||||
],
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
||||
onPressed: () {
|
||||
context.router.pop();
|
||||
},
|
||||
),
|
||||
title: TextField(
|
||||
controller: textSearchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: isContextualSearch.value
|
||||
? 'Sunrise on the beach'
|
||||
: 'File name or extension',
|
||||
hintStyle: context.textTheme.bodyLarge?.copyWith(
|
||||
color: context.themeData.colorScheme.onSurface.withOpacity(0.75),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
enabledBorder: const UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent),
|
||||
),
|
||||
focusedBorder: const UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
onSubmitted: handleTextSubmitted,
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12.0),
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
children: [
|
||||
SearchFilterChip(
|
||||
icon: Icons.people_alt_rounded,
|
||||
onTap: showPeoplePicker,
|
||||
label: 'People',
|
||||
currentFilter: peopleCurrentFilterWidget.value,
|
||||
),
|
||||
SearchFilterChip(
|
||||
icon: Icons.location_pin,
|
||||
onTap: showLocationPicker,
|
||||
label: 'Location',
|
||||
currentFilter: locationCurrentFilterWidget.value,
|
||||
),
|
||||
SearchFilterChip(
|
||||
icon: Icons.camera_alt_rounded,
|
||||
onTap: showCameraPicker,
|
||||
label: 'Camera',
|
||||
currentFilter: cameraCurrentFilterWidget.value,
|
||||
),
|
||||
SearchFilterChip(
|
||||
icon: Icons.date_range_rounded,
|
||||
onTap: showDatePicker,
|
||||
label: 'Date',
|
||||
currentFilter: dateRangeCurrentFilterWidget.value,
|
||||
),
|
||||
SearchFilterChip(
|
||||
icon: Icons.video_collection_outlined,
|
||||
onTap: showMediaTypePicker,
|
||||
label: 'Media Type',
|
||||
currentFilter: mediaTypeCurrentFilterWidget.value,
|
||||
),
|
||||
SearchFilterChip(
|
||||
icon: Icons.display_settings_outlined,
|
||||
onTap: showDisplayOptionPicker,
|
||||
label: 'Display Options',
|
||||
currentFilter: displayOptionCurrentFilterWidget.value,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
buildSearchResult(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,279 +1,274 @@
|
||||
import 'dart:math' as math;
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/search/models/curated_content.dart';
|
||||
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
|
||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/curated_people_row.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/curated_places_row.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/immich_search_bar.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/search_row_title.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/scaffold_error_body.dart';
|
||||
|
||||
@RoutePage()
|
||||
// ignore: must_be_immutable
|
||||
class SearchPage extends HookConsumerWidget {
|
||||
SearchPage({super.key});
|
||||
|
||||
FocusNode searchFocusNode = FocusNode();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
|
||||
final curatedLocation = ref.watch(getCuratedLocationProvider);
|
||||
final curatedPeople = ref.watch(getCuratedPeopleProvider);
|
||||
final isMapEnabled =
|
||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.map));
|
||||
double imageSize = math.min(context.width / 3, 150);
|
||||
|
||||
TextStyle categoryTitleStyle = const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 15.0,
|
||||
);
|
||||
|
||||
Color categoryIconColor = context.isDarkTheme ? Colors.white : Colors.black;
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
searchFocusNode = FocusNode();
|
||||
return () => searchFocusNode.dispose();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
onSearchSubmitted(String searchTerm) async {
|
||||
searchFocusNode.unfocus();
|
||||
ref.watch(searchPageStateProvider.notifier).disableSearch();
|
||||
|
||||
context.pushRoute(
|
||||
SearchResultRoute(
|
||||
searchTerm: searchTerm,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
showNameEditModel(
|
||||
String personId,
|
||||
String personName,
|
||||
) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return PersonNameEditForm(personId: personId, personName: personName);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
buildPeople() {
|
||||
return SizedBox(
|
||||
height: imageSize,
|
||||
child: curatedPeople.widgetWhen(
|
||||
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
|
||||
onData: (people) => Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 8,
|
||||
),
|
||||
child: CuratedPeopleRow(
|
||||
content: people.take(12).toList(),
|
||||
onTap: (content, index) {
|
||||
context.pushRoute(
|
||||
PersonResultRoute(
|
||||
personId: content.id,
|
||||
personName: content.label,
|
||||
),
|
||||
);
|
||||
},
|
||||
onNameTap: (person, index) => {
|
||||
showNameEditModel(person.id, person.label),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buildPlaces() {
|
||||
return SizedBox(
|
||||
height: imageSize,
|
||||
child: curatedLocation.widgetWhen(
|
||||
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
|
||||
onData: (locations) => CuratedPlacesRow(
|
||||
isMapEnabled: isMapEnabled,
|
||||
content: locations
|
||||
.map(
|
||||
(o) => CuratedContent(
|
||||
id: o.id,
|
||||
label: o.city,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
imageSize: imageSize,
|
||||
onTap: (content, index) {
|
||||
context.pushRoute(
|
||||
SearchResultRoute(
|
||||
searchTerm: 'm:${content.label}',
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: ImmichSearchBar(
|
||||
searchFocusNode: searchFocusNode,
|
||||
onSubmitted: onSearchSubmitted,
|
||||
),
|
||||
body: GestureDetector(
|
||||
onTap: () {
|
||||
searchFocusNode.unfocus();
|
||||
ref.watch(searchPageStateProvider.notifier).disableSearch();
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
ListView(
|
||||
children: [
|
||||
SearchRowTitle(
|
||||
title: "search_page_people".tr(),
|
||||
onViewAllPressed: () =>
|
||||
context.pushRoute(const AllPeopleRoute()),
|
||||
),
|
||||
buildPeople(),
|
||||
SearchRowTitle(
|
||||
title: "search_page_places".tr(),
|
||||
onViewAllPressed: () =>
|
||||
context.pushRoute(const CuratedLocationRoute()),
|
||||
top: 0,
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
buildPlaces(),
|
||||
const SizedBox(height: 24.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'search_page_your_activity',
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.favorite_border_rounded,
|
||||
color: categoryIconColor,
|
||||
),
|
||||
title:
|
||||
Text('search_page_favorites', style: categoryTitleStyle)
|
||||
.tr(),
|
||||
onTap: () => context.pushRoute(const FavoritesRoute()),
|
||||
),
|
||||
const CategoryDivider(),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.schedule_outlined,
|
||||
color: categoryIconColor,
|
||||
),
|
||||
title: Text(
|
||||
'search_page_recently_added',
|
||||
style: categoryTitleStyle,
|
||||
).tr(),
|
||||
onTap: () => context.pushRoute(const RecentlyAddedRoute()),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Text(
|
||||
'search_page_categories',
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
ListTile(
|
||||
title:
|
||||
Text('search_page_screenshots', style: categoryTitleStyle)
|
||||
.tr(),
|
||||
leading: Icon(
|
||||
Icons.screenshot,
|
||||
color: categoryIconColor,
|
||||
),
|
||||
onTap: () => context.pushRoute(
|
||||
SearchResultRoute(
|
||||
searchTerm: 'screenshots',
|
||||
),
|
||||
),
|
||||
),
|
||||
const CategoryDivider(),
|
||||
ListTile(
|
||||
title: Text('search_page_selfies', style: categoryTitleStyle)
|
||||
.tr(),
|
||||
leading: Icon(
|
||||
Icons.photo_camera_front_outlined,
|
||||
color: categoryIconColor,
|
||||
),
|
||||
onTap: () => context.pushRoute(
|
||||
SearchResultRoute(
|
||||
searchTerm: 'selfies',
|
||||
),
|
||||
),
|
||||
),
|
||||
const CategoryDivider(),
|
||||
ListTile(
|
||||
title: Text('search_page_videos', style: categoryTitleStyle)
|
||||
.tr(),
|
||||
leading: Icon(
|
||||
Icons.play_circle_outline,
|
||||
color: categoryIconColor,
|
||||
),
|
||||
onTap: () => context.pushRoute(const AllVideosRoute()),
|
||||
),
|
||||
const CategoryDivider(),
|
||||
ListTile(
|
||||
title: Text(
|
||||
'search_page_motion_photos',
|
||||
style: categoryTitleStyle,
|
||||
).tr(),
|
||||
leading: Icon(
|
||||
Icons.motion_photos_on_outlined,
|
||||
color: categoryIconColor,
|
||||
),
|
||||
onTap: () => context.pushRoute(const AllMotionPhotosRoute()),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isSearchEnabled)
|
||||
SearchSuggestionList(onSubmitted: onSearchSubmitted),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CategoryDivider extends StatelessWidget {
|
||||
const CategoryDivider({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 56,
|
||||
right: 16,
|
||||
),
|
||||
child: Divider(
|
||||
height: 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/search/models/curated_content.dart';
|
||||
import 'package:immich_mobile/modules/search/models/search_filter.dart';
|
||||
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
|
||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/curated_people_row.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/curated_places_row.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/search_row_title.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_app_bar.dart';
|
||||
import 'package:immich_mobile/shared/ui/scaffold_error_body.dart';
|
||||
|
||||
@RoutePage()
|
||||
// ignore: must_be_immutable
|
||||
class SearchPage extends HookConsumerWidget {
|
||||
const SearchPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final curatedLocation = ref.watch(getCuratedLocationProvider);
|
||||
final curatedPeople = ref.watch(getAllPeopleProvider);
|
||||
final isMapEnabled =
|
||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.map));
|
||||
double imageSize = math.min(context.width / 3, 150);
|
||||
|
||||
TextStyle categoryTitleStyle = const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 15.0,
|
||||
);
|
||||
|
||||
Color categoryIconColor = context.isDarkTheme ? Colors.white : Colors.black;
|
||||
|
||||
showNameEditModel(
|
||||
String personId,
|
||||
String personName,
|
||||
) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return PersonNameEditForm(personId: personId, personName: personName);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
buildPeople() {
|
||||
return SizedBox(
|
||||
height: imageSize,
|
||||
child: curatedPeople.widgetWhen(
|
||||
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
|
||||
onData: (people) => Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 8,
|
||||
),
|
||||
child: CuratedPeopleRow(
|
||||
content: people
|
||||
.map((e) => CuratedContent(label: e.name, id: e.id))
|
||||
.take(12)
|
||||
.toList(),
|
||||
onTap: (content, index) {
|
||||
context.pushRoute(
|
||||
PersonResultRoute(
|
||||
personId: content.id,
|
||||
personName: content.label,
|
||||
),
|
||||
);
|
||||
},
|
||||
onNameTap: (person, index) => {
|
||||
showNameEditModel(person.id, person.label),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buildPlaces() {
|
||||
return SizedBox(
|
||||
height: imageSize,
|
||||
child: curatedLocation.widgetWhen(
|
||||
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
|
||||
onData: (locations) => CuratedPlacesRow(
|
||||
isMapEnabled: isMapEnabled,
|
||||
content: locations
|
||||
.map(
|
||||
(o) => CuratedContent(
|
||||
id: o.id,
|
||||
label: o.city,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
imageSize: imageSize,
|
||||
onTap: (content, index) {
|
||||
context.pushRoute(
|
||||
SearchInputRoute(
|
||||
prefilter: SearchFilter(
|
||||
people: {},
|
||||
location: SearchLocationFilter(
|
||||
city: content.label,
|
||||
),
|
||||
camera: SearchCameraFilter(),
|
||||
date: SearchDateFilter(),
|
||||
display: SearchDisplayFilters(
|
||||
isNotInAlbum: false,
|
||||
isArchive: false,
|
||||
isFavorite: false,
|
||||
),
|
||||
mediaType: AssetType.other,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buildSearchButton() {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.pushRoute(SearchInputRoute());
|
||||
},
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
side: BorderSide(
|
||||
color: context.isDarkTheme
|
||||
? Colors.grey[800]!
|
||||
: const Color.fromARGB(255, 225, 225, 225),
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 12.0,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.search, color: context.primaryColor),
|
||||
const SizedBox(width: 16.0),
|
||||
Text(
|
||||
"Search your photos",
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
color:
|
||||
context.isDarkTheme ? Colors.white70 : Colors.black54,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: const ImmichAppBar(),
|
||||
body: Stack(
|
||||
children: [
|
||||
ListView(
|
||||
children: [
|
||||
buildSearchButton(),
|
||||
SearchRowTitle(
|
||||
title: "search_page_people".tr(),
|
||||
onViewAllPressed: () =>
|
||||
context.pushRoute(const AllPeopleRoute()),
|
||||
),
|
||||
buildPeople(),
|
||||
SearchRowTitle(
|
||||
title: "search_page_places".tr(),
|
||||
onViewAllPressed: () =>
|
||||
context.pushRoute(const CuratedLocationRoute()),
|
||||
top: 0,
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
buildPlaces(),
|
||||
const SizedBox(height: 24.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'search_page_your_activity',
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.favorite_border_rounded,
|
||||
color: categoryIconColor,
|
||||
),
|
||||
title: Text('search_page_favorites', style: categoryTitleStyle)
|
||||
.tr(),
|
||||
onTap: () => context.pushRoute(const FavoritesRoute()),
|
||||
),
|
||||
const CategoryDivider(),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.schedule_outlined,
|
||||
color: categoryIconColor,
|
||||
),
|
||||
title: Text(
|
||||
'search_page_recently_added',
|
||||
style: categoryTitleStyle,
|
||||
).tr(),
|
||||
onTap: () => context.pushRoute(const RecentlyAddedRoute()),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Text(
|
||||
'search_page_categories',
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
ListTile(
|
||||
title:
|
||||
Text('search_page_videos', style: categoryTitleStyle).tr(),
|
||||
leading: Icon(
|
||||
Icons.play_circle_outline,
|
||||
color: categoryIconColor,
|
||||
),
|
||||
onTap: () => context.pushRoute(const AllVideosRoute()),
|
||||
),
|
||||
const CategoryDivider(),
|
||||
ListTile(
|
||||
title: Text(
|
||||
'search_page_motion_photos',
|
||||
style: categoryTitleStyle,
|
||||
).tr(),
|
||||
leading: Icon(
|
||||
Icons.motion_photos_on_outlined,
|
||||
color: categoryIconColor,
|
||||
),
|
||||
onTap: () => context.pushRoute(const AllMotionPhotosRoute()),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CategoryDivider extends StatelessWidget {
|
||||
const CategoryDivider({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 56,
|
||||
right: 16,
|
||||
),
|
||||
child: Divider(
|
||||
height: 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
||||
import 'package:immich_mobile/modules/search/providers/search_result_page.provider.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
||||
import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
|
||||
class SearchType {
|
||||
SearchType({required this.isSmart, required this.searchTerm});
|
||||
|
||||
final bool isSmart;
|
||||
final String searchTerm;
|
||||
}
|
||||
|
||||
SearchType _getSearchType(String searchTerm) {
|
||||
if (searchTerm.startsWith('m:')) {
|
||||
return SearchType(isSmart: false, searchTerm: searchTerm.substring(2));
|
||||
} else {
|
||||
return SearchType(isSmart: true, searchTerm: searchTerm);
|
||||
}
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class SearchResultPage extends HookConsumerWidget {
|
||||
const SearchResultPage({
|
||||
super.key,
|
||||
required this.searchTerm,
|
||||
});
|
||||
|
||||
final String searchTerm;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final searchTermController = useTextEditingController(text: "");
|
||||
final isNewSearch = useState(false);
|
||||
final currentSearchTerm = useState(searchTerm);
|
||||
|
||||
FocusNode? searchFocusNode;
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
searchFocusNode = FocusNode();
|
||||
|
||||
var searchType = _getSearchType(searchTerm);
|
||||
|
||||
Future.delayed(
|
||||
Duration.zero,
|
||||
() => ref
|
||||
.read(searchResultPageProvider.notifier)
|
||||
.search(searchType.searchTerm, smartSearch: searchType.isSmart),
|
||||
);
|
||||
return () => searchFocusNode?.dispose();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
Future<void> onSearchSubmitted(String newSearchTerm) {
|
||||
debugPrint("Re-Search with $newSearchTerm");
|
||||
searchFocusNode?.unfocus();
|
||||
isNewSearch.value = false;
|
||||
currentSearchTerm.value = newSearchTerm;
|
||||
var searchType = _getSearchType(newSearchTerm);
|
||||
return ref
|
||||
.watch(searchResultPageProvider.notifier)
|
||||
.search(searchType.searchTerm, smartSearch: searchType.isSmart);
|
||||
}
|
||||
|
||||
buildTextField() {
|
||||
return TextField(
|
||||
controller: searchTermController,
|
||||
focusNode: searchFocusNode,
|
||||
autofocus: false,
|
||||
onTap: () {
|
||||
searchTermController.clear();
|
||||
ref.watch(searchPageStateProvider.notifier).setSearchTerm("");
|
||||
searchFocusNode?.requestFocus();
|
||||
},
|
||||
textInputAction: TextInputAction.search,
|
||||
onSubmitted: (searchTerm) {
|
||||
if (searchTerm.isNotEmpty) {
|
||||
searchTermController.clear();
|
||||
onSearchSubmitted(searchTerm);
|
||||
} else {
|
||||
isNewSearch.value = false;
|
||||
}
|
||||
},
|
||||
onChanged: (value) {
|
||||
ref.watch(searchPageStateProvider.notifier).setSearchTerm(value);
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'search_result_page_new_search_hint'.tr(),
|
||||
enabledBorder: const UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent),
|
||||
),
|
||||
focusedBorder: const UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent),
|
||||
),
|
||||
hintStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16.0,
|
||||
color: context.isDarkTheme
|
||||
? Colors.grey[500]
|
||||
: Colors.black.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buildChip() {
|
||||
return Chip(
|
||||
label: Wrap(
|
||||
spacing: 5,
|
||||
runAlignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
currentSearchTerm.value,
|
||||
style: TextStyle(
|
||||
color: context.primaryColor,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
Icon(
|
||||
Icons.close_rounded,
|
||||
color: context.primaryColor,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: context.primaryColor.withAlpha(50),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> refresh() async => onSearchSubmitted(currentSearchTerm.value);
|
||||
|
||||
buildSearchResult() {
|
||||
final searchResultPageState = ref.watch(searchResultPageProvider);
|
||||
|
||||
if (searchResultPageState.isError) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: const Text("common_server_error").tr(),
|
||||
);
|
||||
}
|
||||
|
||||
if (searchResultPageState.isLoading) {
|
||||
return const Center(child: ImmichLoadingIndicator());
|
||||
}
|
||||
|
||||
if (searchResultPageState.isSuccess) {
|
||||
return MultiselectGrid(
|
||||
renderListProvider: searchRenderListProvider,
|
||||
archiveEnabled: true,
|
||||
deleteEnabled: true,
|
||||
editEnabled: true,
|
||||
favoriteEnabled: true,
|
||||
stackEnabled: false,
|
||||
onRefresh: refresh,
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
splashRadius: 20,
|
||||
onPressed: () {
|
||||
if (isNewSearch.value) {
|
||||
isNewSearch.value = false;
|
||||
} else {
|
||||
context.popRoute(true);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
title: GestureDetector(
|
||||
onTap: () {
|
||||
isNewSearch.value = true;
|
||||
searchFocusNode?.requestFocus();
|
||||
},
|
||||
child: isNewSearch.value ? buildTextField() : buildChip(),
|
||||
),
|
||||
centerTitle: false,
|
||||
),
|
||||
body: GestureDetector(
|
||||
onTap: () {
|
||||
if (searchFocusNode != null) {
|
||||
searchFocusNode?.unfocus();
|
||||
}
|
||||
|
||||
ref.watch(searchPageStateProvider.notifier).disableSearch();
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
buildSearchResult(),
|
||||
if (isNewSearch.value)
|
||||
SearchSuggestionList(onSubmitted: onSearchSubmitted),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,9 @@ import 'package:immich_mobile/modules/login/views/change_password_page.dart';
|
||||
import 'package:immich_mobile/modules/login/views/login_page.dart';
|
||||
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/modules/onboarding/views/permission_onboarding_page.dart';
|
||||
import 'package:immich_mobile/modules/search/models/search_filter.dart';
|
||||
import 'package:immich_mobile/modules/settings/views/settings_sub_page.dart';
|
||||
import 'package:immich_mobile/modules/search/views/search_input_page.dart';
|
||||
import 'package:immich_mobile/modules/shared_link/models/shared_link.dart';
|
||||
import 'package:immich_mobile/modules/shared_link/views/shared_link_edit_page.dart';
|
||||
import 'package:immich_mobile/modules/shared_link/views/shared_link_page.dart';
|
||||
@@ -43,7 +45,6 @@ import 'package:immich_mobile/modules/search/views/curated_location_page.dart';
|
||||
import 'package:immich_mobile/modules/search/views/person_result_page.dart';
|
||||
import 'package:immich_mobile/modules/search/views/recently_added_page.dart';
|
||||
import 'package:immich_mobile/modules/search/views/search_page.dart';
|
||||
import 'package:immich_mobile/modules/search/views/search_result_page.dart';
|
||||
import 'package:immich_mobile/modules/settings/views/settings_page.dart';
|
||||
import 'package:immich_mobile/routing/auth_guard.dart';
|
||||
import 'package:immich_mobile/routing/custom_transition_builders.dart';
|
||||
@@ -125,10 +126,6 @@ class AppRouter extends _$AppRouter {
|
||||
page: BackupControllerRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard, _backupPermissionGuard],
|
||||
),
|
||||
AutoRoute(
|
||||
page: SearchResultRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
),
|
||||
AutoRoute(
|
||||
page: CuratedLocationRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
@@ -223,6 +220,11 @@ class AppRouter extends _$AppRouter {
|
||||
page: BackupOptionsRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
),
|
||||
CustomRoute(
|
||||
page: SearchInputRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
transitionsBuilder: TransitionsBuilders.noTransition,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -255,22 +255,21 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
child: const RecentlyAddedPage(),
|
||||
);
|
||||
},
|
||||
SearchRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<SearchRouteArgs>(
|
||||
orElse: () => const SearchRouteArgs());
|
||||
SearchInputRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<SearchInputRouteArgs>(
|
||||
orElse: () => const SearchInputRouteArgs());
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: SearchPage(key: args.key),
|
||||
child: SearchInputPage(
|
||||
key: args.key,
|
||||
prefilter: args.prefilter,
|
||||
),
|
||||
);
|
||||
},
|
||||
SearchResultRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<SearchResultRouteArgs>();
|
||||
SearchRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: SearchResultPage(
|
||||
key: args.key,
|
||||
searchTerm: args.searchTerm,
|
||||
),
|
||||
child: const SearchPage(),
|
||||
);
|
||||
},
|
||||
SelectAdditionalUserForSharingRoute.name: (routeData) {
|
||||
@@ -1113,69 +1112,55 @@ class RecentlyAddedRoute extends PageRouteInfo<void> {
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [SearchPage]
|
||||
class SearchRoute extends PageRouteInfo<SearchRouteArgs> {
|
||||
SearchRoute({
|
||||
/// [SearchInputPage]
|
||||
class SearchInputRoute extends PageRouteInfo<SearchInputRouteArgs> {
|
||||
SearchInputRoute({
|
||||
Key? key,
|
||||
SearchFilter? prefilter,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
SearchInputRoute.name,
|
||||
args: SearchInputRouteArgs(
|
||||
key: key,
|
||||
prefilter: prefilter,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'SearchInputRoute';
|
||||
|
||||
static const PageInfo<SearchInputRouteArgs> page =
|
||||
PageInfo<SearchInputRouteArgs>(name);
|
||||
}
|
||||
|
||||
class SearchInputRouteArgs {
|
||||
const SearchInputRouteArgs({
|
||||
this.key,
|
||||
this.prefilter,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final SearchFilter? prefilter;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SearchInputRouteArgs{key: $key, prefilter: $prefilter}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [SearchPage]
|
||||
class SearchRoute extends PageRouteInfo<void> {
|
||||
const SearchRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
SearchRoute.name,
|
||||
args: SearchRouteArgs(key: key),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'SearchRoute';
|
||||
|
||||
static const PageInfo<SearchRouteArgs> page = PageInfo<SearchRouteArgs>(name);
|
||||
}
|
||||
|
||||
class SearchRouteArgs {
|
||||
const SearchRouteArgs({this.key});
|
||||
|
||||
final Key? key;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SearchRouteArgs{key: $key}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [SearchResultPage]
|
||||
class SearchResultRoute extends PageRouteInfo<SearchResultRouteArgs> {
|
||||
SearchResultRoute({
|
||||
Key? key,
|
||||
required String searchTerm,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
SearchResultRoute.name,
|
||||
args: SearchResultRouteArgs(
|
||||
key: key,
|
||||
searchTerm: searchTerm,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'SearchResultRoute';
|
||||
|
||||
static const PageInfo<SearchResultRouteArgs> page =
|
||||
PageInfo<SearchResultRouteArgs>(name);
|
||||
}
|
||||
|
||||
class SearchResultRouteArgs {
|
||||
const SearchResultRouteArgs({
|
||||
this.key,
|
||||
required this.searchTerm,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final String searchTerm;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SearchResultRouteArgs{key: $key, searchTerm: $searchTerm}';
|
||||
}
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
|
||||
@@ -38,7 +38,7 @@ class TabNavigationObserver extends AutoRouterObserver {
|
||||
if (route.name == 'SearchRoute') {
|
||||
// Refresh Location State
|
||||
ref.invalidate(getCuratedLocationProvider);
|
||||
ref.invalidate(getCuratedPeopleProvider);
|
||||
ref.invalidate(getAllPeopleProvider);
|
||||
}
|
||||
|
||||
if (route.name == 'SharingRoute') {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:immich_mobile/utils/datetime_comparison.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
@@ -72,21 +73,18 @@ class Album {
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
if (other is! Album) return false;
|
||||
|
||||
final lastModifiedAssetTimestampIsSetAndEqual =
|
||||
lastModifiedAssetTimestamp != null &&
|
||||
other.lastModifiedAssetTimestamp != null
|
||||
? lastModifiedAssetTimestamp!
|
||||
.isAtSameMomentAs(other.lastModifiedAssetTimestamp!)
|
||||
: true;
|
||||
|
||||
return id == other.id &&
|
||||
remoteId == other.remoteId &&
|
||||
localId == other.localId &&
|
||||
name == other.name &&
|
||||
createdAt.isAtSameMomentAs(other.createdAt) &&
|
||||
modifiedAt.isAtSameMomentAs(other.modifiedAt) &&
|
||||
lastModifiedAssetTimestampIsSetAndEqual &&
|
||||
isAtSameMomentAs(startDate, other.startDate) &&
|
||||
isAtSameMomentAs(endDate, other.endDate) &&
|
||||
isAtSameMomentAs(
|
||||
lastModifiedAssetTimestamp,
|
||||
other.lastModifiedAssetTimestamp,
|
||||
) &&
|
||||
shared == other.shared &&
|
||||
activityEnabled == other.activityEnabled &&
|
||||
owner.value == other.owner.value &&
|
||||
@@ -104,6 +102,8 @@ class Album {
|
||||
name.hashCode ^
|
||||
createdAt.hashCode ^
|
||||
modifiedAt.hashCode ^
|
||||
startDate.hashCode ^
|
||||
endDate.hashCode ^
|
||||
lastModifiedAssetTimestamp.hashCode ^
|
||||
shared.hashCode ^
|
||||
activityEnabled.hashCode ^
|
||||
|
||||
@@ -56,8 +56,16 @@ class HashService {
|
||||
}
|
||||
final file = await assetEntities[i].originFile;
|
||||
if (file == null) {
|
||||
final fileName = await assetEntities[i].titleAsync.catchError((error) {
|
||||
_log.warning(
|
||||
"Failed to get title for asset ${assetEntities[i].id}",
|
||||
);
|
||||
|
||||
return "";
|
||||
});
|
||||
|
||||
_log.warning(
|
||||
"Failed to get file for asset ${assetEntities[i].id}, skipping",
|
||||
"Failed to get file for asset ${assetEntities[i].id}, name: $fileName, created on: ${assetEntities[i].createDateTime}, skipping",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user