Compare commits

..

1 Commits

Author SHA1 Message Date
Zack Pollard
2c7a29b2f4 fix: panning interrupted while moving around the map 2025-06-19 12:05:40 +01:00
152 changed files with 928 additions and 690 deletions

View File

@@ -55,7 +55,7 @@
"userEnvProbe": "loginInteractiveShell",
"remoteEnv": {
// The location where your uploaded files are stored
"UPLOAD_LOCATION": "${localEnv:UPLOAD_LOCATION:./library}",
"UPLOAD_LOCATION": "${localEnv:UPLOAD_LOCATION:upload-devcontainer-volume}",
// Connection secret for postgres. You should change it to a random password
// Please use only the characters `A-Za-z0-9`, without special characters or spaces
"DB_PASSWORD": "${localEnv:DB_PASSWORD:postgres}",

View File

@@ -51,19 +51,14 @@ fix_permissions() {
run_cmd sudo find "${IMMICH_WORKSPACE}/server/upload" -not -path "${IMMICH_WORKSPACE}/server/upload/postgres/*" -not -path "${IMMICH_WORKSPACE}/server/upload/postgres" -exec chown node {} +
# Change ownership for directories that exist
for dir in "${IMMICH_WORKSPACE}/.vscode" \
run_cmd sudo chown node -R "${IMMICH_WORKSPACE}/.vscode" \
"${IMMICH_WORKSPACE}/cli/node_modules" \
"${IMMICH_WORKSPACE}/e2e/node_modules" \
"${IMMICH_WORKSPACE}/open-api/typescript-sdk/node_modules" \
"${IMMICH_WORKSPACE}/server/node_modules" \
"${IMMICH_WORKSPACE}/server/dist" \
"${IMMICH_WORKSPACE}/web/node_modules" \
"${IMMICH_WORKSPACE}/web/dist"; do
if [ -d "$dir" ]; then
run_cmd sudo chown node -R "$dir"
fi
done
"${IMMICH_WORKSPACE}/web/dist"
log ""
}

View File

@@ -12,8 +12,8 @@ services:
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
- server_node_modules:/workspaces/immich/server/node_modules
- web_node_modules:/workspaces/immich/web/node_modules
- ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/workspaces/immich/server/upload
- ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/workspaces/immich/server/upload/upload
- ${UPLOAD_LOCATION-./Library}/photos:/workspaces/immich/server/upload
- ${UPLOAD_LOCATION-./Library}/photos/upload:/workspaces/immich/server/upload/upload
- /etc/localtime:/etc/localtime:ro
immich-web:
@@ -29,9 +29,8 @@ services:
POSTGRES_USER: ${DB_USERNAME-postgres}
POSTGRES_DB: ${DB_DATABASE_NAME-immich}
POSTGRES_INITDB_ARGS: '--data-checksums'
POSTGRES_HOST_AUTH_METHOD: md5
volumes:
- ${UPLOAD_LOCATION:-postgres-devcontainer-volume}${UPLOAD_LOCATION:+/postgres}:/var/lib/postgresql/data
volumes:
- ${UPLOAD_LOCATION-./Library}/postgres:/var/lib/postgresql/data
redis:
env_file: !reset []
@@ -43,6 +42,4 @@ volumes:
open_api_node_modules:
server_node_modules:
web_node_modules:
upload1-devcontainer-volume:
upload2-devcontainer-volume:
postgres-devcontainer-volume:
upload-devcontainer-volume:

View File

@@ -644,7 +644,7 @@ jobs:
contents: read
services:
postgres:
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.1
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres

6
cli/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@immich/cli",
"version": "2.2.72",
"version": "2.2.69",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/cli",
"version": "2.2.72",
"version": "2.2.69",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"chokidar": "^4.0.3",
@@ -54,7 +54,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.135.3",
"version": "1.135.0",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.2.72",
"version": "2.2.69",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",

View File

@@ -16,7 +16,7 @@ name: immich-dev
services:
immich-server:
container_name: immich_server
command: ['/usr/src/app/bin/immich-dev']
command: [ '/usr/src/app/bin/immich-dev' ]
image: immich-server-dev:latest
# extends:
# file: hwaccel.transcoding.yml
@@ -70,7 +70,7 @@ services:
# user: 0:0
build:
context: ../web
command: ['/usr/src/app/bin/immich-web']
command: [ '/usr/src/app/bin/immich-web' ]
env_file:
- .env
ports:
@@ -122,7 +122,7 @@ services:
database:
container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.1-pgvectors0.2.0
env_file:
- .env
environment:

View File

@@ -63,7 +63,7 @@ services:
database:
container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.1-pgvectors0.2.0
env_file:
- .env
environment:
@@ -91,7 +91,7 @@ services:
# add data source for http://immich-prometheus:9090 to get started
immich-grafana:
container_name: immich_grafana
command: ['./run.sh', '-disable-reporting']
command: [ './run.sh', '-disable-reporting' ]
ports:
- 3000:3000
image: grafana/grafana:12.0.1-ubuntu@sha256:65575bb9c761335e2ff30e364f21d38632e3b2e75f5f81d83cc92f44b9bbc055

View File

@@ -56,7 +56,7 @@ services:
database:
container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.1-pgvectors0.2.0
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}

View File

@@ -64,13 +64,7 @@ COMMIT;
### Updating VectorChord
When installing a new version of VectorChord, you will need to manually update the extension and reindex by connecting to the Immich database and running:
```
ALTER EXTENSION vchord UPDATE;
REINDEX INDEX face_index;
REINDEX INDEX clip_index;
```
When installing a new version of VectorChord, you will need to manually update the extension by connecting to the Immich database and running `ALTER EXTENSION vchord UPDATE;`.
## Migrating to VectorChord
@@ -82,8 +76,6 @@ Support for pgvecto.rs will be dropped in a later release, hence we recommend al
The easiest option is to have both extensions installed during the migration:
<details>
<summary>Migration steps (automatic)</summary>
1. Ensure you still have pgvecto.rs installed
2. Install `pgvector` (`>= 0.7.0, < 1.0.0`). The easiest way to do this is on Debian/Ubuntu by adding the [PostgreSQL Apt repository][pg-apt] and then running `apt install postgresql-NN-pgvector`, where `NN` is your Postgres version (e.g., `16`)
3. [Install VectorChord][vchord-install]
@@ -97,12 +89,8 @@ The easiest option is to have both extensions installed during the migration:
11. Restart the Postgres database
12. Uninstall pgvecto.rs (e.g. `apt-get purge vectors-pg14` on Debian-based environments, replacing `pg14` as appropriate). `pgvector` must remain installed as it provides the data types used by `vchord`
</details>
If it is not possible to have both VectorChord and pgvecto.rs installed at the same time, you can perform the migration with more manual steps:
<details>
<summary>Migration steps (manual)</summary>
1. While pgvecto.rs is still installed, run the following SQL command using psql or your choice of database client. Take note of the number outputted by this command as you will need it later
```sql
@@ -135,20 +123,14 @@ ALTER TABLE face_search ALTER COLUMN embedding SET DATA TYPE vector(512);
5. Start Immich and let it create new indices using VectorChord
</details>
### Migrating from pgvector
<details>
<summary>Migration steps</summary>
1. Ensure you have at least 0.7.0 of pgvector installed. If it is below that, please upgrade it and run the SQL command `ALTER EXTENSION vector UPDATE;` using psql or your choice of database client
2. Follow the Prerequisites to install VectorChord
3. If Immich does not have superuser permissions, run the SQL command `CREATE EXTENSION vchord CASCADE;`
4. Remove the `DB_VECTOR_EXTENSION=pgvector` environmental variable as it will make Immich still use pgvector if set
5. Start Immich and let it create new indices using VectorChord
</details>
Note that VectorChord itself uses pgvector types, so you should not uninstall pgvector after following these steps.
[vchord-install]: https://docs.vectorchord.ai/vectorchord/getting-started/installation.html

View File

@@ -1,16 +1,4 @@
[
{
"label": "v1.135.3",
"url": "https://v1.135.3.archive.immich.app"
},
{
"label": "v1.135.2",
"url": "https://v1.135.2.archive.immich.app"
},
{
"label": "v1.135.1",
"url": "https://v1.135.1.archive.immich.app"
},
{
"label": "v1.135.0",
"url": "https://v1.135.0.archive.immich.app"

8
e2e/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich-e2e",
"version": "1.135.3",
"version": "1.135.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-e2e",
"version": "1.135.3",
"version": "1.135.0",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
@@ -44,7 +44,7 @@
},
"../cli": {
"name": "@immich/cli",
"version": "2.2.72",
"version": "2.2.69",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@@ -93,7 +93,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.135.3",
"version": "1.135.0",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.135.3",
"version": "1.135.0",
"description": "",
"main": "index.js",
"type": "module",

View File

@@ -14,11 +14,7 @@ describe('/people', () => {
let nameAlicePerson: PersonResponseDto;
let nameBobPerson: PersonResponseDto;
let nameCharliePerson: PersonResponseDto;
let nameNullPerson4Assets: PersonResponseDto;
let nameNullPerson3Assets: PersonResponseDto;
let nameNullPerson1Asset: PersonResponseDto;
let nameBillPersonFavourite: PersonResponseDto;
let nameFreddyPersonFavourite: PersonResponseDto;
let nameNullPerson: PersonResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
@@ -31,11 +27,7 @@ describe('/people', () => {
nameCharliePerson,
nameBobPerson,
nameAlicePerson,
nameNullPerson4Assets,
nameNullPerson3Assets,
nameNullPerson1Asset,
nameBillPersonFavourite,
nameFreddyPersonFavourite,
nameNullPerson,
] = await Promise.all([
utils.createPerson(admin.accessToken, {
name: 'visible_person',
@@ -60,26 +52,11 @@ describe('/people', () => {
utils.createPerson(admin.accessToken, {
name: '',
}),
utils.createPerson(admin.accessToken, {
name: '',
}),
utils.createPerson(admin.accessToken, {
name: '',
}),
utils.createPerson(admin.accessToken, {
name: 'Bill',
isFavorite: true,
}),
utils.createPerson(admin.accessToken, {
name: 'Freddy',
isFavorite: true,
}),
]);
const asset1 = await utils.createAsset(admin.accessToken);
const asset2 = await utils.createAsset(admin.accessToken);
const asset3 = await utils.createAsset(admin.accessToken);
const asset4 = await utils.createAsset(admin.accessToken);
await Promise.all([
utils.createFace({ assetId: asset1.id, personId: visiblePerson.id }),
@@ -87,27 +64,15 @@ describe('/people', () => {
utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }),
utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }),
utils.createFace({ assetId: asset2.id, personId: multipleAssetsPerson.id }),
utils.createFace({ assetId: asset3.id, personId: multipleAssetsPerson.id }), // 4 assets
utils.createFace({ assetId: asset3.id, personId: multipleAssetsPerson.id }),
// Named persons
utils.createFace({ assetId: asset1.id, personId: nameCharliePerson.id }), // 1 asset
utils.createFace({ assetId: asset1.id, personId: nameBobPerson.id }),
utils.createFace({ assetId: asset2.id, personId: nameBobPerson.id }), // 2 assets
utils.createFace({ assetId: asset1.id, personId: nameAlicePerson.id }), // 1 asset
// Null-named person 4 assets
utils.createFace({ assetId: asset1.id, personId: nameNullPerson4Assets.id }),
utils.createFace({ assetId: asset2.id, personId: nameNullPerson4Assets.id }),
utils.createFace({ assetId: asset3.id, personId: nameNullPerson4Assets.id }),
utils.createFace({ assetId: asset4.id, personId: nameNullPerson4Assets.id }), // 4 assets
// Null-named person 3 assets
utils.createFace({ assetId: asset1.id, personId: nameNullPerson3Assets.id }),
utils.createFace({ assetId: asset2.id, personId: nameNullPerson3Assets.id }),
utils.createFace({ assetId: asset3.id, personId: nameNullPerson3Assets.id }), // 3 assets
// Null-named person 1 asset
utils.createFace({ assetId: asset3.id, personId: nameNullPerson1Asset.id }),
// Favourite People
utils.createFace({ assetId: asset1.id, personId: nameFreddyPersonFavourite.id }),
utils.createFace({ assetId: asset2.id, personId: nameFreddyPersonFavourite.id }),
utils.createFace({ assetId: asset1.id, personId: nameBillPersonFavourite.id }),
// Null-named person
utils.createFace({ assetId: asset1.id, personId: nameNullPerson.id }),
utils.createFace({ assetId: asset2.id, personId: nameNullPerson.id }), // 2 assets
]);
});
@@ -122,19 +87,15 @@ describe('/people', () => {
expect(status).toBe(200);
expect(body).toEqual({
hasNextPage: false,
total: 11,
total: 7,
hidden: 1,
people: [
expect.objectContaining({ name: 'Freddy' }),
expect.objectContaining({ name: 'Bill' }),
expect.objectContaining({ name: 'multiple_assets_person' }),
expect.objectContaining({ name: 'Bob' }),
expect.objectContaining({ name: 'Alice' }),
expect.objectContaining({ name: 'Charlie' }),
expect.objectContaining({ name: 'visible_person' }),
expect.objectContaining({ id: nameNullPerson4Assets.id, name: '' }),
expect.objectContaining({ id: nameNullPerson3Assets.id, name: '' }),
expect.objectContaining({ name: 'hidden_person' }), // Should really be before the null names
expect.objectContaining({ name: 'hidden_person' }),
],
});
});
@@ -144,21 +105,17 @@ describe('/people', () => {
expect(status).toBe(200);
expect(body.hasNextPage).toBe(false);
expect(body.total).toBe(11); // All persons
expect(body.total).toBe(7); // All persons
expect(body.hidden).toBe(1); // 'hidden_person'
const people = body.people as PersonResponseDto[];
expect(people.map((p) => p.id)).toEqual([
nameFreddyPersonFavourite.id, // name: 'Freddy', count: 2
nameBillPersonFavourite.id, // name: 'Bill', count: 1
multipleAssetsPerson.id, // name: 'multiple_assets_person', count: 3
nameBobPerson.id, // name: 'Bob', count: 2
nameAlicePerson.id, // name: 'Alice', count: 1
nameCharliePerson.id, // name: 'Charlie', count: 1
visiblePerson.id, // name: 'visible_person', count: 1
nameNullPerson4Assets.id, // name: '', count: 4
nameNullPerson3Assets.id, // name: '', count: 3
]);
expect(people.some((p) => p.id === hiddenPerson.id)).toBe(false);
@@ -170,18 +127,14 @@ describe('/people', () => {
expect(status).toBe(200);
expect(body).toEqual({
hasNextPage: false,
total: 11,
total: 7,
hidden: 1,
people: [
expect.objectContaining({ name: 'Freddy' }),
expect.objectContaining({ name: 'Bill' }),
expect.objectContaining({ name: 'multiple_assets_person' }),
expect.objectContaining({ name: 'Bob' }),
expect.objectContaining({ name: 'Alice' }),
expect.objectContaining({ name: 'Charlie' }),
expect.objectContaining({ name: 'visible_person' }),
expect.objectContaining({ id: nameNullPerson4Assets.id, name: '' }),
expect.objectContaining({ id: nameNullPerson3Assets.id, name: '' }),
],
});
});
@@ -195,9 +148,9 @@ describe('/people', () => {
expect(status).toBe(200);
expect(body).toEqual({
hasNextPage: true,
total: 11,
total: 7,
hidden: 1,
people: [expect.objectContaining({ name: 'Alice' })],
people: [expect.objectContaining({ name: 'visible_person' })],
});
});
});

View File

@@ -244,6 +244,7 @@ async def load(model: InferenceModel) -> InferenceModel:
async def idle_shutdown_task() -> None:
while True:
log.debug("Checking for inactivity...")
if (
last_called is not None
and not active_requests

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 204,
"android.injected.version.name" => "1.135.3",
"android.injected.version.code" => 201,
"android.injected.version.name" => "1.135.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')

View File

@@ -346,7 +346,6 @@
};
F0B57D372DF764BD00DC5BCC = {
CreatedOnToolsVersion = 16.4;
ProvisioningStyle = Automatic;
};
FAC6F88F2D287C890078CB2F = {
CreatedOnToolsVersion = 16.0;
@@ -649,7 +648,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 208;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -793,7 +792,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 208;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -823,7 +822,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 208;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -857,7 +856,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -865,7 +864,7 @@
INFOPLIST_FILE = WidgetExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Widget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -900,7 +899,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -908,7 +907,7 @@
INFOPLIST_FILE = WidgetExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Widget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -940,7 +939,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -948,7 +947,7 @@
INFOPLIST_FILE = WidgetExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Widget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -979,7 +978,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 208;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1023,7 +1022,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 208;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1064,7 +1063,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 208;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;

View File

@@ -78,7 +78,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.135.1</string>
<string>1.134.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -93,7 +93,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>210</string>
<string>208</string>
<key>FLTEnableImpeller</key>
<true />
<key>ITSAppUsesNonExemptEncryption</key>
@@ -115,8 +115,8 @@
</dict>
<key>NSBonjourServices</key>
<array>
<string>_googlecast._tcp</string>
<string>_CC1AD845._googlecast._tcp</string>
<string>_googlecast._tcp</string>
<string>_CC1AD845._googlecast._tcp</string>
</array>
<key>NSCameraUsageDescription</key>
<string>We need to access the camera to let you take beautiful video using this app</string>
@@ -168,8 +168,5 @@
<true />
<key>NSFaceIDUsageDescription</key>
<string>We need to use FaceID to allow access to your locked folder</string>
<key>NSLocalNetworkUsageDescription</key>
<string>We need local network permission to connect to the local server using IP address and
allow the casting feature to work</string>
</dict>
</plist>
</plist>

View File

@@ -43,7 +43,7 @@ struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
"Choose an album to show images from"
}
@Parameter(title: "Album")
@Parameter(title: "Album", default: NO_ALBUM)
var album: Album?
@Parameter(title: "Show Album Name", default: false)

View File

@@ -22,7 +22,7 @@ platform :ios do
path: "./Runner.xcodeproj",
)
increment_version_number(
version_number: "1.135.3"
version_number: "1.135.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -10,5 +10,3 @@ enum TextSearchType {
}
enum AssetVisibilityEnum { timeline, hidden, archive, locked }
enum SortUserBy { id }

View File

@@ -0,0 +1,18 @@
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:openapi/api.dart';
abstract interface class ISyncStreamRepository implements IDatabaseRepository {
Future<void> updateUsersV1(Iterable<SyncUserV1> data);
Future<void> deleteUsersV1(Iterable<SyncUserDeleteV1> data);
Future<void> updatePartnerV1(Iterable<SyncPartnerV1> data);
Future<void> deletePartnerV1(Iterable<SyncPartnerDeleteV1> data);
Future<void> updateAssetsV1(Iterable<SyncAssetV1> data);
Future<void> deleteAssetsV1(Iterable<SyncAssetDeleteV1> data);
Future<void> updateAssetsExifV1(Iterable<SyncAssetExifV1> data);
Future<void> updatePartnerAssetsV1(Iterable<SyncAssetV1> data);
Future<void> deletePartnerAssetsV1(Iterable<SyncAssetDeleteV1> data);
Future<void> updatePartnerAssetsExifV1(Iterable<SyncAssetExifV1> data);
}

View File

@@ -0,0 +1,22 @@
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
abstract interface class IUserRepository implements IDatabaseRepository {
Future<bool> insert(UserDto user);
Future<UserDto?> getByUserId(String id);
Future<List<UserDto?>> getByUserIds(List<String> ids);
Future<List<UserDto>> getAll({SortUserBy? sortBy});
Future<bool> updateAll(List<UserDto> users);
Future<UserDto> update(UserDto user);
Future<void> delete(List<String> ids);
Future<void> deleteAll();
}
enum SortUserBy { id }

View File

@@ -0,0 +1,15 @@
import 'dart:typed_data';
import 'package:immich_mobile/domain/models/user.model.dart';
abstract interface class IUserApiRepository {
Future<UserDto?> getMyUser();
Future<List<UserDto>> getAll();
/// Saves the [data] in the server and uses it as the current users profile image
Future<String> createProfileImage({
required String name,
required Uint8List data,
});
}

View File

@@ -1,8 +1,8 @@
import 'dart:async';
import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart';
import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
@@ -10,12 +10,12 @@ class SyncStreamService {
final Logger _logger = Logger('SyncStreamService');
final ISyncApiRepository _syncApiRepository;
final SyncStreamRepository _syncStreamRepository;
final ISyncStreamRepository _syncStreamRepository;
final bool Function()? _cancelChecker;
SyncStreamService({
required ISyncApiRepository syncApiRepository,
required SyncStreamRepository syncStreamRepository,
required ISyncStreamRepository syncStreamRepository,
bool Function()? cancelChecker,
}) : _syncApiRepository = syncApiRepository,
_syncStreamRepository = syncStreamRepository,

View File

@@ -1,24 +1,24 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/interfaces/user_api.interface.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
import 'package:logging/logging.dart';
class UserService {
final Logger _log = Logger("UserService");
final IsarUserRepository _isarUserRepository;
final UserApiRepository _userApiRepository;
final IUserRepository _userRepository;
final IUserApiRepository _userApiRepository;
final StoreService _storeService;
UserService({
required IsarUserRepository isarUserRepository,
required UserApiRepository userApiRepository,
required IUserRepository userRepository,
required IUserApiRepository userApiRepository,
required StoreService storeService,
}) : _isarUserRepository = isarUserRepository,
}) : _userRepository = userRepository,
_userApiRepository = userApiRepository,
_storeService = storeService;
@@ -38,7 +38,7 @@ class UserService {
final user = await _userApiRepository.getMyUser();
if (user == null) return null;
await _storeService.put(StoreKey.currentUser, user);
await _isarUserRepository.update(user);
await _userRepository.update(user);
return user;
}
@@ -50,7 +50,7 @@ class UserService {
);
final updatedUser = getMyUser().copyWith(profileImagePath: path);
await _storeService.put(StoreKey.currentUser, updatedUser);
await _isarUserRepository.update(updatedUser);
await _userRepository.update(updatedUser);
return path;
} catch (e) {
_log.warning("Failed to upload profile image", e);
@@ -59,10 +59,10 @@ class UserService {
}
Future<List<UserDto>> getAll() async {
return await _isarUserRepository.getAll();
return await _userRepository.getAll();
}
Future<void> deleteAll() {
return _isarUserRepository.deleteAll();
return _userRepository.deleteAll();
}
}

View File

@@ -1,4 +1,5 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
@@ -9,12 +10,14 @@ import 'package:logging/logging.dart';
import 'package:openapi/api.dart' as api show AssetVisibility;
import 'package:openapi/api.dart' hide AssetVisibility;
class SyncStreamRepository extends DriftDatabaseRepository {
class DriftSyncStreamRepository extends DriftDatabaseRepository
implements ISyncStreamRepository {
final Logger _logger = Logger('DriftSyncStreamRepository');
final Drift _db;
SyncStreamRepository(super.db) : _db = db;
DriftSyncStreamRepository(super.db) : _db = db;
@override
Future<void> deleteUsersV1(Iterable<SyncUserDeleteV1> data) async {
try {
await _db.batch((batch) {
@@ -31,6 +34,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> updateUsersV1(Iterable<SyncUserV1> data) async {
try {
await _db.batch((batch) {
@@ -53,6 +57,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> deletePartnerV1(Iterable<SyncPartnerDeleteV1> data) async {
try {
await _db.batch((batch) {
@@ -72,6 +77,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> updatePartnerV1(Iterable<SyncPartnerV1> data) async {
try {
await _db.batch((batch) {
@@ -95,6 +101,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> deleteAssetsV1(Iterable<SyncAssetDeleteV1> data) async {
try {
await _deleteAssetsV1(data);
@@ -104,6 +111,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> updateAssetsV1(Iterable<SyncAssetV1> data) async {
try {
await _updateAssetsV1(data);
@@ -113,6 +121,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> deletePartnerAssetsV1(Iterable<SyncAssetDeleteV1> data) async {
try {
await _deleteAssetsV1(data);
@@ -122,6 +131,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> updatePartnerAssetsV1(Iterable<SyncAssetV1> data) async {
try {
await _updateAssetsV1(data);
@@ -131,6 +141,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> updateAssetsExifV1(Iterable<SyncAssetExifV1> data) async {
try {
await _updateAssetExifV1(data);
@@ -140,6 +151,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> updatePartnerAssetsExifV1(Iterable<SyncAssetExifV1> data) async {
try {
await _updateAssetExifV1(data);

View File

@@ -1,26 +1,30 @@
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
as entity;
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:isar/isar.dart';
class IsarUserRepository extends IsarDatabaseRepository {
class IsarUserRepository extends IsarDatabaseRepository
implements IUserRepository {
final Isar _db;
const IsarUserRepository(super.db) : _db = db;
@override
Future<void> delete(List<String> ids) async {
await transaction(() async {
await _db.users.deleteAllById(ids);
});
}
@override
Future<void> deleteAll() async {
await transaction(() async {
await _db.users.clear();
});
}
@override
Future<List<UserDto>> getAll({SortUserBy? sortBy}) async {
return (await _db.users
.where()
@@ -35,14 +39,17 @@ class IsarUserRepository extends IsarDatabaseRepository {
.toList();
}
@override
Future<UserDto?> getByUserId(String id) async {
return (await _db.users.getById(id))?.toDto();
}
@override
Future<List<UserDto?>> getByUserIds(List<String> ids) async {
return (await _db.users.getAllById(ids)).map((u) => u?.toDto()).toList();
}
@override
Future<bool> insert(UserDto user) async {
await transaction(() async {
await _db.users.put(entity.User.fromDto(user));
@@ -50,6 +57,7 @@ class IsarUserRepository extends IsarDatabaseRepository {
return true;
}
@override
Future<UserDto> update(UserDto user) async {
await transaction(() async {
await _db.users.put(entity.User.fromDto(user));
@@ -57,6 +65,7 @@ class IsarUserRepository extends IsarDatabaseRepository {
return user;
}
@override
Future<bool> updateAll(List<UserDto> users) async {
await transaction(() async {
await _db.users.putAll(users.map(entity.User.fromDto).toList());

View File

@@ -1,15 +1,17 @@
import 'dart:typed_data';
import 'package:http/http.dart';
import 'package:immich_mobile/domain/interfaces/user_api.interface.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/repositories/api.repository.dart';
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
import 'package:openapi/api.dart';
class UserApiRepository extends ApiRepository {
class UserApiRepository extends ApiRepository implements IUserApiRepository {
final UsersApi _api;
const UserApiRepository(this._api);
@override
Future<UserDto?> getMyUser() async {
final (adminDto, preferenceDto) =
await (_api.getMyUser(), _api.getMyPreferences()).wait;
@@ -18,6 +20,7 @@ class UserApiRepository extends ApiRepository {
return UserConverter.fromAdminDto(adminDto, preferenceDto);
}
@override
Future<String> createProfileImage({
required String name,
required Uint8List data,
@@ -30,6 +33,7 @@ class UserApiRepository extends ApiRepository {
return res.profileImagePath;
}
@override
Future<List<UserDto>> getAll() async {
final dto = await checkNull(_api.searchUsers());
return dto.map(UserConverter.fromSimpleUserDto).toList();

View File

@@ -0,0 +1,16 @@
import 'package:immich_mobile/models/activities/activity.model.dart';
abstract interface class IActivityApiRepository {
Future<List<Activity>> getAll(
String albumId, {
String? assetId,
});
Future<Activity> create(
String albumId,
ActivityType type, {
String? assetId,
String? comment,
});
Future<void> delete(String id);
Future<ActivityStats> getStats(String albumId, {String? assetId});
}

View File

@@ -0,0 +1,42 @@
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/entities/album.entity.dart';
abstract interface class IAlbumApiRepository {
Future<Album> get(String id);
Future<List<Album>> getAll({bool? shared});
Future<Album> create(
String name, {
required Iterable<String> assetIds,
Iterable<String> sharedUserIds = const [],
});
Future<Album> update(
String albumId, {
String? name,
String? thumbnailAssetId,
String? description,
bool? activityEnabled,
SortOrder? sortOrder,
});
Future<void> delete(String albumId);
Future<({List<String> added, List<String> duplicates})> addAssets(
String albumId,
Iterable<String> assetIds,
);
Future<({List<String> removed, List<String> failed})> removeAssets(
String albumId,
Iterable<String> assetIds,
);
Future<Album> addUsers(
String albumId,
Iterable<String> userIds,
);
Future<void> removeUser(String albumId, {required String userId});
}

View File

@@ -0,0 +1,21 @@
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
abstract interface class IAlbumMediaRepository {
Future<List<Album>> getAll();
Future<List<String>> getAssetIds(String albumId);
Future<int> getAssetCount(String albumId);
Future<List<Asset>> getAssets(
String albumId, {
int start = 0,
int end = 0x7fffffffffffffff,
DateTime? modifiedFrom,
DateTime? modifiedUntil,
bool orderByModificationDate = false,
});
Future<Album> get(String id);
}

View File

@@ -0,0 +1,6 @@
import 'package:immich_mobile/models/auth/biometric_status.model.dart';
abstract interface class IBiometricRepository {
Future<BiometricStatus> getStatus();
Future<bool> authenticate(String? message);
}

View File

@@ -0,0 +1,15 @@
import 'package:background_downloader/background_downloader.dart';
abstract interface class IDownloadRepository {
void Function(TaskStatusUpdate)? onImageDownloadStatus;
void Function(TaskStatusUpdate)? onVideoDownloadStatus;
void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus;
void Function(TaskProgressUpdate)? onTaskProgress;
Future<List<TaskRecord>> getLiveVideoTasks();
Future<List<bool>> downloadAll(List<DownloadTask> tasks);
Future<bool> cancel(String id);
Future<void> deleteAllTrackingRecords();
Future<void> deleteRecordsWithIds(List<String> id);
}

View File

@@ -0,0 +1,4 @@
abstract interface class INetworkRepository {
Future<String?> getWifiName();
Future<String?> getWifiIp();
}

View File

@@ -0,0 +1,8 @@
import 'package:immich_mobile/domain/models/user.model.dart';
abstract class IPartnerRepository {
Future<List<UserDto>> getSharedWith();
Future<List<UserDto>> getSharedBy();
Stream<List<UserDto>> watchSharedWith();
Stream<List<UserDto>> watchSharedBy();
}

View File

@@ -0,0 +1,13 @@
import 'package:immich_mobile/domain/models/user.model.dart';
abstract interface class IPartnerApiRepository {
Future<List<UserDto>> getAll(Direction direction);
Future<UserDto> create(String id);
Future<UserDto> update(String id, {required bool inTimeline});
Future<void> delete(String id);
}
enum Direction {
sharedWithMe,
sharedByMe,
}

View File

@@ -1,5 +1,11 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
abstract interface class IPersonApiRepository {
Future<List<Person>> getAll();
Future<Person> update(String id, {String? name});
}
class Person {
Person({
required this.id,

View File

@@ -0,0 +1,5 @@
abstract interface class ISecureStorageRepository {
Future<String?> read(String key);
Future<void> write(String key, String value);
Future<void> delete(String key);
}

View File

@@ -0,0 +1,9 @@
import 'package:immich_mobile/models/sessions/session_create_response.model.dart';
abstract interface class ISessionAPIRepository {
Future<SessionCreateResponse> createSession(
String deviceName,
String deviceOS, {
int? duration,
});
}

View File

@@ -0,0 +1,7 @@
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
abstract interface class IShareHandlerRepository {
void Function(List<ShareIntentAttachment>)? onSharedMedia;
Future<void> init();
}

View File

@@ -0,0 +1,39 @@
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
abstract class ITimelineRepository {
Future<List<String>> getTimelineUserIds(String id);
Stream<List<String>> watchTimelineUsers(String id);
Stream<RenderList> watchArchiveTimeline(String userId);
Stream<RenderList> watchFavoriteTimeline(String userId);
Stream<RenderList> watchTrashTimeline(String userId);
Stream<RenderList> watchAlbumTimeline(
Album album,
GroupAssetsBy groupAssetsBy,
);
Stream<RenderList> watchAllVideosTimeline(String userId);
Stream<RenderList> watchHomeTimeline(
String userId,
GroupAssetsBy groupAssetsBy,
);
Stream<RenderList> watchMultiUsersTimeline(
List<String> userIds,
GroupAssetsBy groupAssetsBy,
);
Future<RenderList> getTimelineFromAssets(
List<Asset> assets,
GroupAssetsBy getGroupByOption,
);
Stream<RenderList> watchAssetSelectionTimeline(String userId);
Stream<RenderList> watchLockedTimeline(
String userId,
GroupAssetsBy groupAssetsBy,
);
}

View File

@@ -1,8 +1,8 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
import 'package:immich_mobile/domain/models/person.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
class SearchLocationFilter {
String? country;

View File

@@ -6,9 +6,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/person.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/providers/search/paginated_search.provider.dart';
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';

View File

@@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/interfaces/backup_album.interface.dart';
import 'package:immich_mobile/interfaces/file_media.interface.dart';
import 'package:immich_mobile/models/auth/auth_state.model.dart';
@@ -107,7 +108,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
final AuthState _authState;
final BackgroundService _backgroundService;
final GalleryPermissionNotifier _galleryPermissionNotifier;
final AlbumMediaRepository _albumMediaRepository;
final IAlbumMediaRepository _albumMediaRepository;
final IFileMediaRepository _fileMediaRepository;
final BackupAlbumService _backupAlbumService;
final Ref ref;

View File

@@ -26,7 +26,7 @@ final syncApiRepositoryProvider = Provider(
);
final syncStreamRepositoryProvider = Provider(
(ref) => SyncStreamRepository(ref.watch(driftProvider)),
(ref) => DriftSyncStreamRepository(ref.watch(driftProvider)),
);
final localSyncServiceProvider = Provider(

View File

@@ -1,4 +1,6 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/interfaces/user_api.interface.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
@@ -10,16 +12,16 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'user.provider.g.dart';
@Riverpod(keepAlive: true)
IsarUserRepository userRepository(Ref ref) =>
IUserRepository userRepository(Ref ref) =>
IsarUserRepository(ref.watch(isarProvider));
@Riverpod(keepAlive: true)
UserApiRepository userApiRepository(Ref ref) =>
IUserApiRepository userApiRepository(Ref ref) =>
UserApiRepository(ref.watch(apiServiceProvider).usersApi);
@Riverpod(keepAlive: true)
UserService userService(Ref ref) => UserService(
isarUserRepository: ref.watch(userRepositoryProvider),
userRepository: ref.watch(userRepositoryProvider),
userApiRepository: ref.watch(userApiRepositoryProvider),
storeService: ref.watch(storeServiceProvider),
);

View File

@@ -6,11 +6,11 @@ part of 'user.provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$userRepositoryHash() => r'538791a4ad126ed086c9db682c67fc5c654d54f3';
String _$userRepositoryHash() => r'1a2ac726bcc44397dcaecf449084fefd336696d4';
/// See also [userRepository].
@ProviderFor(userRepository)
final userRepositoryProvider = Provider<IsarUserRepository>.internal(
final userRepositoryProvider = Provider<IUserRepository>.internal(
userRepository,
name: r'userRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
@@ -22,12 +22,12 @@ final userRepositoryProvider = Provider<IsarUserRepository>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef UserRepositoryRef = ProviderRef<IsarUserRepository>;
String _$userApiRepositoryHash() => r'8a7340ca4544c8c6b20225c65bff2abb9e96baa2';
typedef UserRepositoryRef = ProviderRef<IUserRepository>;
String _$userApiRepositoryHash() => r'6b19f2c99fb83162a5ceb91adb8589eaae01bc92';
/// See also [userApiRepository].
@ProviderFor(userApiRepository)
final userApiRepositoryProvider = Provider<UserApiRepository>.internal(
final userApiRepositoryProvider = Provider<IUserApiRepository>.internal(
userApiRepository,
name: r'userApiRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
@@ -39,8 +39,8 @@ final userApiRepositoryProvider = Provider<UserApiRepository>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef UserApiRepositoryRef = ProviderRef<UserApiRepository>;
String _$userServiceHash() => r'181414dddc7891be6237e13d568c287a804228d1';
typedef UserApiRepositoryRef = ProviderRef<IUserApiRepository>;
String _$userServiceHash() => r'4a0873357b7115b4d6bfa8e89b847c0b74ce0d93';
/// See also [userService].
@ProviderFor(userService)

View File

@@ -1,5 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/person.model.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/services/person.service.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';

View File

@@ -1,5 +1,6 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
import 'package:immich_mobile/interfaces/activity_api.interface.dart';
import 'package:immich_mobile/models/activities/activity.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
@@ -9,17 +10,20 @@ final activityApiRepositoryProvider = Provider(
(ref) => ActivityApiRepository(ref.watch(apiServiceProvider).activitiesApi),
);
class ActivityApiRepository extends ApiRepository {
class ActivityApiRepository extends ApiRepository
implements IActivityApiRepository {
final ActivitiesApi _api;
ActivityApiRepository(this._api);
@override
Future<List<Activity>> getAll(String albumId, {String? assetId}) async {
final response =
await checkNull(_api.getActivities(albumId, assetId: assetId));
return response.map(_toActivity).toList();
}
@override
Future<Activity> create(
String albumId,
ActivityType type, {
@@ -38,10 +42,12 @@ class ActivityApiRepository extends ApiRepository {
return _toActivity(response);
}
@override
Future<void> delete(String id) {
return checkNull(_api.deleteActivity(id));
}
@override
Future<ActivityStats> getStats(String albumId, {String? assetId}) async {
final response =
await checkNull(_api.getActivityStatistics(albumId, assetId: assetId));

View File

@@ -5,6 +5,7 @@ import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
as entity;
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
import 'package:immich_mobile/interfaces/album_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:openapi/api.dart';
@@ -13,21 +14,24 @@ final albumApiRepositoryProvider = Provider(
(ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi),
);
class AlbumApiRepository extends ApiRepository {
class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
final AlbumsApi _api;
AlbumApiRepository(this._api);
@override
Future<Album> get(String id) async {
final dto = await checkNull(_api.getAlbumInfo(id));
return _toAlbum(dto);
}
@override
Future<List<Album>> getAll({bool? shared}) async {
final dtos = await checkNull(_api.getAllAlbums(shared: shared));
return dtos.map(_toAlbum).toList();
}
@override
Future<Album> create(
String name, {
required Iterable<String> assetIds,
@@ -50,6 +54,7 @@ class AlbumApiRepository extends ApiRepository {
return _toAlbum(responseDto);
}
@override
Future<Album> update(
String albumId, {
String? name,
@@ -79,10 +84,12 @@ class AlbumApiRepository extends ApiRepository {
return _toAlbum(response);
}
@override
Future<void> delete(String albumId) {
return _api.deleteAlbum(albumId);
}
@override
Future<({List<String> added, List<String> duplicates})> addAssets(
String albumId,
Iterable<String> assetIds,
@@ -107,6 +114,7 @@ class AlbumApiRepository extends ApiRepository {
return (added: added, duplicates: duplicates);
}
@override
Future<({List<String> removed, List<String> failed})> removeAssets(
String albumId,
Iterable<String> assetIds,
@@ -128,6 +136,7 @@ class AlbumApiRepository extends ApiRepository {
return (removed: removed, failed: failed);
}
@override
Future<Album> addUsers(String albumId, Iterable<String> userIds) async {
final albumUsers =
userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList();
@@ -140,6 +149,7 @@ class AlbumApiRepository extends ApiRepository {
return _toAlbum(response);
}
@override
Future<void> removeUser(String albumId, {required String userId}) {
return _api.removeUserFromAlbum(albumId, userId);
}

View File

@@ -4,13 +4,14 @@ import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:photo_manager/photo_manager.dart' hide AssetType;
final albumMediaRepositoryProvider =
Provider((ref) => const AlbumMediaRepository());
class AlbumMediaRepository {
class AlbumMediaRepository implements IAlbumMediaRepository {
const AlbumMediaRepository();
bool get useCustomFilter =>
@@ -40,6 +41,7 @@ class AlbumMediaRepository {
)
: null;
@override
Future<List<Album>> getAll() async {
final filter = useCustomFilter
? CustomFilter.sql(where: '${CustomColumns.base.width} > 0')
@@ -50,6 +52,7 @@ class AlbumMediaRepository {
return assetPathEntities.map(_toAlbum).toList();
}
@override
Future<List<String>> getAssetIds(String albumId) async {
final album =
await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter());
@@ -58,12 +61,14 @@ class AlbumMediaRepository {
return assets.map((e) => e.id).toList();
}
@override
Future<int> getAssetCount(String albumId) async {
final album =
await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter());
return album.assetCountAsync;
}
@override
Future<List<Asset>> getAssets(
String albumId, {
int start = 0,
@@ -92,7 +97,12 @@ class AlbumMediaRepository {
return assets.map(AssetMediaRepository.toAsset).toList().cast();
}
Future<Album> get(String id) async {
@override
Future<Album> get(
String id, {
DateTime? modifiedFrom,
DateTime? modifiedUntil,
}) async {
final assetPathEntity = await AssetPathEntity.fromId(
id,
filterOption: _getAlbumFilter(containsPathModified: true),

View File

@@ -1,16 +1,18 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/biometric.interface.dart';
import 'package:immich_mobile/models/auth/biometric_status.model.dart';
import 'package:local_auth/local_auth.dart';
final biometricRepositoryProvider =
Provider((ref) => BiometricRepository(LocalAuthentication()));
class BiometricRepository {
class BiometricRepository implements IBiometricRepository {
final LocalAuthentication _localAuth;
BiometricRepository(this._localAuth);
@override
Future<BiometricStatus> getStatus() async {
final bool canAuthenticateWithBiometrics =
await _localAuth.canCheckBiometrics;
@@ -24,6 +26,7 @@ class BiometricRepository {
);
}
@override
Future<bool> authenticate(String? message) async {
return _localAuth.authenticate(
localizedReason: message ?? 'please_auth_to_access'.tr(),

View File

@@ -1,16 +1,21 @@
import 'package:background_downloader/background_downloader.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/download.interface.dart';
import 'package:immich_mobile/utils/download.dart';
final downloadRepositoryProvider = Provider((ref) => DownloadRepository());
class DownloadRepository {
class DownloadRepository implements IDownloadRepository {
@override
void Function(TaskStatusUpdate)? onImageDownloadStatus;
@override
void Function(TaskStatusUpdate)? onVideoDownloadStatus;
@override
void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus;
@override
void Function(TaskProgressUpdate)? onTaskProgress;
DownloadRepository() {
@@ -33,18 +38,22 @@ class DownloadRepository {
);
}
@override
Future<List<bool>> downloadAll(List<DownloadTask> tasks) {
return FileDownloader().enqueueAll(tasks);
}
@override
Future<void> deleteAllTrackingRecords() {
return FileDownloader().database.deleteAllRecords();
}
@override
Future<bool> cancel(String id) {
return FileDownloader().cancelTaskWithId(id);
}
@override
Future<List<TaskRecord>> getLiveVideoTasks() {
return FileDownloader().database.allRecordsWithStatus(
TaskStatus.complete,
@@ -52,6 +61,7 @@ class DownloadRepository {
);
}
@override
Future<void> deleteRecordsWithIds(List<String> ids) {
return FileDownloader().database.deleteRecordsWithIds(ids);
}

View File

@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/network.interface.dart';
import 'package:network_info_plus/network_info_plus.dart';
final networkRepositoryProvider = Provider((_) {
@@ -9,11 +10,12 @@ final networkRepositoryProvider = Provider((_) {
return NetworkRepository(networkInfo);
});
class NetworkRepository {
class NetworkRepository implements INetworkRepository {
final NetworkInfo _networkInfo;
NetworkRepository(this._networkInfo);
@override
Future<String?> getWifiName() {
if (Platform.isAndroid) {
// remove quote around the return value on Android
@@ -28,6 +30,7 @@ class NetworkRepository {
return _networkInfo.getWifiName();
}
@override
Future<String?> getWifiIp() {
return _networkInfo.getWifiIP();
}

View File

@@ -2,6 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
as entity;
import 'package:immich_mobile/interfaces/partner.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
import 'package:isar/isar.dart';
@@ -10,9 +11,11 @@ final partnerRepositoryProvider = Provider(
(ref) => PartnerRepository(ref.watch(dbProvider)),
);
class PartnerRepository extends DatabaseRepository {
class PartnerRepository extends DatabaseRepository
implements IPartnerRepository {
PartnerRepository(super.db);
@override
Future<List<UserDto>> getSharedBy() async {
return (await db.users
.filter()
@@ -23,6 +26,7 @@ class PartnerRepository extends DatabaseRepository {
.toList();
}
@override
Future<List<UserDto>> getSharedWith() async {
return (await db.users
.filter()
@@ -33,11 +37,13 @@ class PartnerRepository extends DatabaseRepository {
.toList();
}
@override
Stream<List<UserDto>> watchSharedBy() {
return (db.users.filter().isPartnerSharedByEqualTo(true).sortById().watch())
.map((users) => users.map((u) => u.toDto()).toList());
}
@override
Stream<List<UserDto>> watchSharedWith() {
return (db.users
.filter()

View File

@@ -1,26 +1,24 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:openapi/api.dart';
enum Direction {
sharedWithMe,
sharedByMe,
}
final partnerApiRepositoryProvider = Provider(
(ref) => PartnerApiRepository(
ref.watch(apiServiceProvider).partnersApi,
),
);
class PartnerApiRepository extends ApiRepository {
class PartnerApiRepository extends ApiRepository
implements IPartnerApiRepository {
final PartnersApi _api;
PartnerApiRepository(this._api);
@override
Future<List<UserDto>> getAll(Direction direction) async {
final response = await checkNull(
_api.getPartners(
@@ -32,13 +30,16 @@ class PartnerApiRepository extends ApiRepository {
return response.map(UserConverter.fromPartnerDto).toList();
}
@override
Future<UserDto> create(String id) async {
final dto = await checkNull(_api.createPartner(id));
return UserConverter.fromPartnerDto(dto);
}
@override
Future<void> delete(String id) => _api.removePartner(id);
@override
Future<UserDto> update(String id, {required bool inTimeline}) async {
final dto = await checkNull(
_api.updatePartner(

View File

@@ -1,5 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/person.model.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:openapi/api.dart';
@@ -8,16 +8,19 @@ final personApiRepositoryProvider = Provider(
(ref) => PersonApiRepository(ref.watch(apiServiceProvider).peopleApi),
);
class PersonApiRepository extends ApiRepository {
class PersonApiRepository extends ApiRepository
implements IPersonApiRepository {
final PeopleApi _api;
PersonApiRepository(this._api);
@override
Future<List<Person>> getAll() async {
final dto = await checkNull(_api.getAllPeople());
return dto.people.map(_toPerson).toList();
}
@override
Future<Person> update(String id, {String? name}) async {
final dto = await checkNull(
_api.updatePerson(id, PersonUpdateDto(name: name)),

View File

@@ -1,22 +1,26 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/secure_storage.interface.dart';
final secureStorageRepositoryProvider =
Provider((ref) => SecureStorageRepository(const FlutterSecureStorage()));
class SecureStorageRepository {
class SecureStorageRepository implements ISecureStorageRepository {
final FlutterSecureStorage _secureStorage;
SecureStorageRepository(this._secureStorage);
@override
Future<String?> read(String key) {
return _secureStorage.read(key: key);
}
@override
Future<void> write(String key, String value) {
return _secureStorage.write(key: key, value: value);
}
@override
Future<void> delete(String key) {
return _secureStorage.delete(key: key);
}

View File

@@ -1,4 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/sessions_api.interface.dart';
import 'package:immich_mobile/models/sessions/session_create_response.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
@@ -10,11 +11,13 @@ final sessionsAPIRepositoryProvider = Provider(
),
);
class SessionsAPIRepository extends ApiRepository {
class SessionsAPIRepository extends ApiRepository
implements ISessionAPIRepository {
final SessionsApi _api;
SessionsAPIRepository(this._api);
@override
Future<SessionCreateResponse> createSession(
String deviceType,
String deviceOS, {

View File

@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/share_handler.interface.dart';
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
import 'package:share_handler/share_handler.dart';
@@ -8,11 +9,13 @@ final shareHandlerRepositoryProvider = Provider(
(ref) => ShareHandlerRepository(),
);
class ShareHandlerRepository {
class ShareHandlerRepository implements IShareHandlerRepository {
ShareHandlerRepository();
@override
void Function(List<ShareIntentAttachment> attachments)? onSharedMedia;
@override
Future<void> init() async {
final handler = ShareHandlerPlatform.instance;
final media = await handler.getInitialSharedMedia();

View File

@@ -3,6 +3,7 @@ import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/timeline.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
import 'package:immich_mobile/utils/hash.dart';
@@ -12,9 +13,11 @@ import 'package:isar/isar.dart';
final timelineRepositoryProvider =
Provider((ref) => TimelineRepository(ref.watch(dbProvider)));
class TimelineRepository extends DatabaseRepository {
class TimelineRepository extends DatabaseRepository
implements ITimelineRepository {
TimelineRepository(super.db);
@override
Future<List<String>> getTimelineUserIds(String id) {
return db.users
.filter()
@@ -25,6 +28,7 @@ class TimelineRepository extends DatabaseRepository {
.findAll();
}
@override
Stream<List<String>> watchTimelineUsers(String id) {
return db.users
.filter()
@@ -35,6 +39,7 @@ class TimelineRepository extends DatabaseRepository {
.watch();
}
@override
Stream<RenderList> watchArchiveTimeline(String userId) {
final query = db.assets
.where()
@@ -47,6 +52,7 @@ class TimelineRepository extends DatabaseRepository {
return _watchRenderList(query, GroupAssetsBy.none);
}
@override
Stream<RenderList> watchFavoriteTimeline(String userId) {
final query = db.assets
.where()
@@ -61,6 +67,7 @@ class TimelineRepository extends DatabaseRepository {
return _watchRenderList(query, GroupAssetsBy.none);
}
@override
Stream<RenderList> watchAlbumTimeline(
Album album,
GroupAssetsBy groupAssetByOption,
@@ -79,6 +86,7 @@ class TimelineRepository extends DatabaseRepository {
return _watchRenderList(withSortedOption, groupAssetByOption);
}
@override
Stream<RenderList> watchTrashTimeline(String userId) {
final query = db.assets
.filter()
@@ -89,6 +97,7 @@ class TimelineRepository extends DatabaseRepository {
return _watchRenderList(query, GroupAssetsBy.none);
}
@override
Stream<RenderList> watchAllVideosTimeline(String userId) {
final query = db.assets
.where()
@@ -102,6 +111,7 @@ class TimelineRepository extends DatabaseRepository {
return _watchRenderList(query, GroupAssetsBy.none);
}
@override
Stream<RenderList> watchHomeTimeline(
String userId,
GroupAssetsBy groupAssetByOption,
@@ -118,6 +128,7 @@ class TimelineRepository extends DatabaseRepository {
return _watchRenderList(query, groupAssetByOption);
}
@override
Stream<RenderList> watchMultiUsersTimeline(
List<String> userIds,
GroupAssetsBy groupAssetByOption,
@@ -134,6 +145,7 @@ class TimelineRepository extends DatabaseRepository {
return _watchRenderList(query, groupAssetByOption);
}
@override
Future<RenderList> getTimelineFromAssets(
List<Asset> assets,
GroupAssetsBy getGroupByOption,
@@ -141,6 +153,7 @@ class TimelineRepository extends DatabaseRepository {
return RenderList.fromAssets(assets, getGroupByOption);
}
@override
Stream<RenderList> watchAssetSelectionTimeline(String userId) {
final query = db.assets
.where()
@@ -155,6 +168,7 @@ class TimelineRepository extends DatabaseRepository {
return _watchRenderList(query, GroupAssetsBy.none);
}
@override
Stream<RenderList> watchLockedTimeline(
String userId,
GroupAssetsBy getGroupByOption,

View File

@@ -1,10 +1,10 @@
import 'package:immich_mobile/interfaces/activity_api.interface.dart';
import 'package:immich_mobile/mixins/error_logger.mixin.dart';
import 'package:immich_mobile/models/activities/activity.model.dart';
import 'package:immich_mobile/repositories/activity_api.repository.dart';
import 'package:logging/logging.dart';
class ActivityService with ErrorLoggerMixin {
final ActivityApiRepository _activityApiRepository;
final IActivityApiRepository _activityApiRepository;
@override
final Logger logger = Logger("ActivityService");

View File

@@ -14,6 +14,8 @@ import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
as entity;
import 'package:immich_mobile/interfaces/album.interface.dart';
import 'package:immich_mobile/interfaces/album_api.interface.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/backup_album.interface.dart';
import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart';
@@ -49,8 +51,8 @@ class AlbumService {
final IAlbumRepository _albumRepository;
final IAssetRepository _assetRepository;
final IBackupAlbumRepository _backupAlbumRepository;
final AlbumMediaRepository _albumMediaRepository;
final AlbumApiRepository _albumApiRepository;
final IAlbumMediaRepository _albumMediaRepository;
final IAlbumApiRepository _albumApiRepository;
final Logger _log = Logger('AlbumService');
Completer<bool> _localCompleter = Completer()..complete(false);
Completer<bool> _remoteCompleter = Completer()..complete(false);

View File

@@ -5,11 +5,11 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
@@ -53,7 +53,7 @@ class AssetService {
final IAssetApiRepository _assetApiRepository;
final IAssetRepository _assetRepository;
final IExifInfoRepository _exifInfoRepository;
final IsarUserRepository _isarUserRepository;
final IUserRepository _userRepository;
final IETagRepository _etagRepository;
final IBackupAlbumRepository _backupRepository;
final ApiService _apiService;
@@ -68,7 +68,7 @@ class AssetService {
this._assetApiRepository,
this._assetRepository,
this._exifInfoRepository,
this._isarUserRepository,
this._userRepository,
this._etagRepository,
this._backupRepository,
this._apiService,
@@ -85,9 +85,7 @@ class AssetService {
final syncedUserIds = await _etagRepository.getAllIds();
final List<UserDto> syncedUsers = syncedUserIds.isEmpty
? []
: (await _isarUserRepository.getByUserIds(syncedUserIds))
.nonNulls
.toList();
: (await _userRepository.getByUserIds(syncedUserIds)).nonNulls.toList();
final Stopwatch sw = Stopwatch()..start();
final bool changes = await _syncService.syncRemoteAssetsToDb(
users: syncedUsers,

View File

@@ -11,6 +11,7 @@ import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
import 'package:immich_mobile/interfaces/file_media.interface.dart';
@@ -51,7 +52,7 @@ class BackupService {
final Logger _log = Logger("BackupService");
final AppSettingsService _appSetting;
final AlbumService _albumService;
final AlbumMediaRepository _albumMediaRepository;
final IAlbumMediaRepository _albumMediaRepository;
final IFileMediaRepository _fileMediaRepository;
final IAssetRepository _assetRepository;
final IAssetMediaRepository _assetMediaRepository;

View File

@@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/interfaces/download.interface.dart';
import 'package:immich_mobile/interfaces/file_media.interface.dart';
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
import 'package:immich_mobile/repositories/download.repository.dart';
@@ -22,7 +23,7 @@ final downloadServiceProvider = Provider(
);
class DownloadService {
final DownloadRepository _downloadRepository;
final IDownloadRepository _downloadRepository;
final IFileMediaRepository _fileMediaRepository;
final Logger _log = Logger("DownloadService");
void Function(TaskStatusUpdate)? onImageDownloadStatus;

View File

@@ -1,24 +1,24 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
class EntityService {
final IAssetRepository _assetRepository;
final IsarUserRepository _isarUserRepository;
final IUserRepository _userRepository;
EntityService(
this._assetRepository,
this._isarUserRepository,
this._userRepository,
);
Future<Album> fillAlbumWithDatabaseEntities(Album album) async {
final ownerId = album.ownerId;
if (ownerId != null) {
// replace owner with user from database
final user = await _isarUserRepository.getByUserId(ownerId);
final user = await _userRepository.getByUserId(ownerId);
album.owner.value = user == null ? null : User.fromDto(user);
}
final thumbnailAssetId =
@@ -30,7 +30,7 @@ class EntityService {
}
if (album.remoteUsers.isNotEmpty) {
// replace all users with users from database
final users = await _isarUserRepository
final users = await _userRepository
.getByUserIds(album.remoteUsers.map((user) => user.id).toList());
album.sharedUsers.clear();
album.sharedUsers.addAll(users.nonNulls.map(User.fromDto));

View File

@@ -1,4 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/biometric.interface.dart';
import 'package:immich_mobile/models/auth/biometric_status.model.dart';
import 'package:immich_mobile/repositories/biometric.repository.dart';
@@ -9,7 +10,9 @@ final localAuthServiceProvider = Provider(
);
class LocalAuthService {
final BiometricRepository _biometricRepository;
// final _log = Logger("LocalAuthService");
final IBiometricRepository _biometricRepository;
LocalAuthService(this._biometricRepository);

View File

@@ -1,4 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/network.interface.dart';
import 'package:immich_mobile/repositories/network.repository.dart';
import 'package:immich_mobile/repositories/permission.repository.dart';
@@ -10,7 +11,7 @@ final networkServiceProvider = Provider((ref) {
});
class NetworkService {
final NetworkRepository _repository;
final INetworkRepository _repository;
final IPermissionRepository _permissionRepository;
NetworkService(this._repository, this._permissionRepository);

View File

@@ -1,6 +1,8 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/interfaces/partner.interface.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/repositories/partner.repository.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
@@ -15,14 +17,14 @@ final partnerServiceProvider = Provider(
);
class PartnerService {
final PartnerApiRepository _partnerApiRepository;
final PartnerRepository _partnerRepository;
final IsarUserRepository _isarUserRepository;
final IPartnerApiRepository _partnerApiRepository;
final IPartnerRepository _partnerRepository;
final IUserRepository _userRepository;
final Logger _log = Logger("PartnerService");
PartnerService(
this._partnerApiRepository,
this._isarUserRepository,
this._userRepository,
this._partnerRepository,
);
@@ -45,8 +47,7 @@ class PartnerService {
Future<bool> removePartner(UserDto partner) async {
try {
await _partnerApiRepository.delete(partner.id);
await _isarUserRepository
.update(partner.copyWith(isPartnerSharedBy: false));
await _userRepository.update(partner.copyWith(isPartnerSharedBy: false));
} catch (e) {
_log.warning("Failed to remove partner ${partner.id}", e);
return false;
@@ -57,8 +58,7 @@ class PartnerService {
Future<bool> addPartner(UserDto partner) async {
try {
await _partnerApiRepository.create(partner.id);
await _isarUserRepository
.update(partner.copyWith(isPartnerSharedBy: true));
await _userRepository.update(partner.copyWith(isPartnerSharedBy: true));
return true;
} catch (e) {
_log.warning("Failed to add partner ${partner.id}", e);
@@ -75,7 +75,7 @@ class PartnerService {
partner.id,
inTimeline: inTimeline,
);
await _isarUserRepository
await _userRepository
.update(partner.copyWith(inTimeline: dto.inTimeline));
return true;
} catch (e) {

View File

@@ -1,8 +1,8 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/person.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/repositories/person_api.repository.dart';
@@ -20,7 +20,7 @@ PersonService personService(Ref ref) => PersonService(
class PersonService {
final Logger _log = Logger("PersonService");
final PersonApiRepository _personApiRepository;
final IPersonApiRepository _personApiRepository;
final IAssetApiRepository _assetApiRepository;
final IAssetRepository _assetRepository;

View File

@@ -1,4 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/secure_storage.interface.dart';
import 'package:immich_mobile/repositories/secure_storage.repository.dart';
final secureStorageServiceProvider = Provider(
@@ -8,7 +9,9 @@ final secureStorageServiceProvider = Provider(
);
class SecureStorageService {
final SecureStorageRepository _secureStorageRepository;
// final _log = Logger("LocalAuthService");
final ISecureStorageRepository _secureStorageRepository;
SecureStorageService(this._secureStorageRepository);

View File

@@ -3,20 +3,23 @@ import 'dart:io';
import 'package:collection/collection.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/interfaces/user_api.interface.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/extensions/collection_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
import 'package:immich_mobile/interfaces/album.interface.dart';
import 'package:immich_mobile/interfaces/album_api.interface.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/etag.interface.dart';
import 'package:immich_mobile/interfaces/local_files_manager.interface.dart';
import 'package:immich_mobile/interfaces/partner.interface.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/exif.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
@@ -60,17 +63,17 @@ final syncServiceProvider = Provider(
class SyncService {
final HashService _hashService;
final EntityService _entityService;
final AlbumMediaRepository _albumMediaRepository;
final AlbumApiRepository _albumApiRepository;
final IAlbumMediaRepository _albumMediaRepository;
final IAlbumApiRepository _albumApiRepository;
final IAlbumRepository _albumRepository;
final IAssetRepository _assetRepository;
final IExifInfoRepository _exifInfoRepository;
final IsarUserRepository _isarUserRepository;
final IUserRepository _userRepository;
final UserService _userService;
final PartnerRepository _partnerRepository;
final IPartnerRepository _partnerRepository;
final IETagRepository _eTagRepository;
final PartnerApiRepository _partnerApiRepository;
final UserApiRepository _userApiRepository;
final IPartnerApiRepository _partnerApiRepository;
final IUserApiRepository _userApiRepository;
final AsyncMutex _lock = AsyncMutex();
final Logger _log = Logger('SyncService');
final AppSettingsService _appSettingsService;
@@ -85,7 +88,7 @@ class SyncService {
this._assetRepository,
this._exifInfoRepository,
this._partnerRepository,
this._isarUserRepository,
this._userRepository,
this._userService,
this._eTagRepository,
this._appSettingsService,
@@ -162,7 +165,7 @@ class SyncService {
/// Returns `true`if there were any changes
Future<bool> _syncUsersFromServer(List<UserDto> users) async {
users.sortBy((u) => u.id);
final dbUsers = await _isarUserRepository.getAll(sortBy: SortUserBy.id);
final dbUsers = await _userRepository.getAll(sortBy: SortUserBy.id);
final List<String> toDelete = [];
final List<UserDto> toUpsert = [];
final changes = diffSortedListsSync(
@@ -183,9 +186,9 @@ class SyncService {
onlySecond: (UserDto b) => toDelete.add(b.id),
);
if (changes) {
await _isarUserRepository.transaction(() async {
await _isarUserRepository.delete(toDelete);
await _isarUserRepository.updateAll(toUpsert);
await _userRepository.transaction(() async {
await _userRepository.delete(toDelete);
await _userRepository.updateAll(toUpsert);
});
}
return changes;
@@ -445,7 +448,7 @@ class SyncService {
final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd);
await upsertAssetsWithExif(updated);
final assetsToLink = existingInDb + updated;
final usersToLink = await _isarUserRepository.getByUserIds(userIdsToAdd);
final usersToLink = await _userRepository.getByUserIds(userIdsToAdd);
album.name = dto.name;
album.description = dto.description;

View File

@@ -2,6 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/timeline.interface.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/repositories/timeline.repository.dart';
@@ -17,7 +18,7 @@ final timelineServiceProvider = Provider<TimelineService>((ref) {
});
class TimelineService {
final TimelineRepository _timelineRepository;
final ITimelineRepository _timelineRepository;
final AppSettingsService _appSettingsService;
final UserService _userService;

View File

@@ -57,13 +57,7 @@ Future<void> migrateDatabaseIfNeeded(Isar db) async {
}
final shouldTruncate = version < 8 || version < targetVersion;
if (shouldTruncate) {
if (targetVersion == 12) {
await Store.put(StoreKey.version, targetVersion);
return;
}
await _migrateTo(db, targetVersion);
}
}

View File

@@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/person.model.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
import 'package:immich_mobile/providers/search/people.provider.dart';
import 'package:immich_mobile/services/api.service.dart';

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.135.3
- API version: 1.135.0
- Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
version: 1.135.3+204
version: 1.135.0+201
environment:
sdk: '>=3.3.0 <4.0.0'

View File

@@ -4,9 +4,9 @@ import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart';
import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/domain/services/sync_stream.service.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:mocktail/mocktail.dart';
import '../../fixtures/sync_stream.stub.dart';
@@ -30,7 +30,7 @@ class _MockCancellationWrapper extends Mock implements _CancellationWrapper {}
void main() {
late SyncStreamService sut;
late SyncStreamRepository mockSyncStreamRepo;
late ISyncStreamRepository mockSyncStreamRepo;
late ISyncApiRepository mockSyncApiRepo;
late Function(List<SyncEvent>, Function()) handleEventsCallback;
late _MockAbortCallbackWrapper mockAbortCallbackWrapper;

View File

@@ -1,11 +1,11 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/interfaces/user_api.interface.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
import 'package:mocktail/mocktail.dart';
import '../../fixtures/user.stub.dart';
@@ -14,16 +14,16 @@ import '../service.mock.dart';
void main() {
late UserService sut;
late IsarUserRepository mockUserRepo;
late UserApiRepository mockUserApiRepo;
late IUserRepository mockUserRepo;
late IUserApiRepository mockUserApiRepo;
late StoreService mockStoreService;
setUp(() {
mockUserRepo = MockIsarUserRepository();
mockUserRepo = MockUserRepository();
mockUserApiRepo = MockUserApiRepository();
mockStoreService = MockStoreService();
sut = UserService(
isarUserRepository: mockUserRepo,
userRepository: mockUserRepo,
userApiRepository: mockUserApiRepo,
storeService: mockStoreService,
);

View File

@@ -5,21 +5,21 @@ import 'package:immich_mobile/domain/interfaces/log.interface.dart';
import 'package:immich_mobile/domain/interfaces/storage.interface.dart';
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/interfaces/user_api.interface.dart';
import 'package:mocktail/mocktail.dart';
class MockStoreRepository extends Mock implements IStoreRepository {}
class MockLogRepository extends Mock implements ILogRepository {}
class MockIsarUserRepository extends Mock implements IsarUserRepository {}
class MockUserRepository extends Mock implements IUserRepository {}
class MockDeviceAssetRepository extends Mock
implements IDeviceAssetRepository {}
class MockSyncStreamRepository extends Mock implements SyncStreamRepository {}
class MockSyncStreamRepository extends Mock implements ISyncStreamRepository {}
class MockLocalAlbumRepository extends Mock implements ILocalAlbumRepository {}
@@ -28,6 +28,6 @@ class MockLocalAssetRepository extends Mock implements ILocalAssetRepository {}
class MockStorageRepository extends Mock implements IStorageRepository {}
// API Repos
class MockUserApiRepository extends Mock implements UserApiRepository {}
class MockUserApiRepository extends Mock implements IUserApiRepository {}
class MockSyncApiRepository extends Mock implements ISyncApiRepository {}

View File

@@ -1,6 +1,6 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/log.service.dart';
@@ -11,7 +11,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:mocktail/mocktail.dart';
@@ -55,7 +55,7 @@ void main() {
final MockAlbumRepository albumRepository = MockAlbumRepository();
final MockAssetRepository assetRepository = MockAssetRepository();
final MockExifInfoRepository exifInfoRepository = MockExifInfoRepository();
final MockIsarUserRepository userRepository = MockIsarUserRepository();
final MockUserRepository userRepository = MockUserRepository();
final MockETagRepository eTagRepository = MockETagRepository();
final MockAlbumMediaRepository albumMediaRepository =
MockAlbumMediaRepository();

View File

@@ -1,5 +1,7 @@
import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
import 'package:immich_mobile/interfaces/album.interface.dart';
import 'package:immich_mobile/interfaces/album_api.interface.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
@@ -9,10 +11,8 @@ import 'package:immich_mobile/interfaces/backup_album.interface.dart';
import 'package:immich_mobile/interfaces/etag.interface.dart';
import 'package:immich_mobile/interfaces/file_media.interface.dart';
import 'package:immich_mobile/interfaces/local_files_manager.interface.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/repositories/album_media.repository.dart';
import 'package:immich_mobile/repositories/album_api.repository.dart';
import 'package:immich_mobile/repositories/partner.repository.dart';
import 'package:immich_mobile/interfaces/partner.interface.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:mocktail/mocktail.dart';
class MockAlbumRepository extends Mock implements IAlbumRepository {}
@@ -25,7 +25,7 @@ class MockExifInfoRepository extends Mock implements IExifInfoRepository {}
class MockETagRepository extends Mock implements IETagRepository {}
class MockAlbumMediaRepository extends Mock implements AlbumMediaRepository {}
class MockAlbumMediaRepository extends Mock implements IAlbumMediaRepository {}
class MockBackupAlbumRepository extends Mock
implements IBackupAlbumRepository {}
@@ -36,15 +36,15 @@ class MockAssetMediaRepository extends Mock implements IAssetMediaRepository {}
class MockFileMediaRepository extends Mock implements IFileMediaRepository {}
class MockAlbumApiRepository extends Mock implements AlbumApiRepository {}
class MockAlbumApiRepository extends Mock implements IAlbumApiRepository {}
class MockAuthApiRepository extends Mock implements IAuthApiRepository {}
class MockAuthRepository extends Mock implements IAuthRepository {}
class MockPartnerRepository extends Mock implements PartnerRepository {}
class MockPartnerRepository extends Mock implements IPartnerRepository {}
class MockPartnerApiRepository extends Mock implements PartnerApiRepository {}
class MockPartnerApiRepository extends Mock implements IPartnerApiRepository {}
class MockLocalFilesManagerRepository extends Mock
implements ILocalFilesManager {}

View File

@@ -22,7 +22,7 @@ void main() {
late MockExifInfoRepository exifInfoRepository;
late MockETagRepository eTagRepository;
late MockBackupAlbumRepository backupAlbumRepository;
late MockIsarUserRepository userRepository;
late MockUserRepository userRepository;
late MockAssetMediaRepository assetMediaRepository;
late MockApiService apiService;
@@ -35,7 +35,7 @@ void main() {
assetRepository = MockAssetRepository();
assetApiRepository = MockAssetApiRepository();
exifInfoRepository = MockExifInfoRepository();
userRepository = MockIsarUserRepository();
userRepository = MockUserRepository();
eTagRepository = MockETagRepository();
backupAlbumRepository = MockBackupAlbumRepository();
apiService = MockApiService();

View File

@@ -12,11 +12,11 @@ import '../repository.mocks.dart';
void main() {
late EntityService sut;
late MockAssetRepository assetRepository;
late MockIsarUserRepository userRepository;
late MockUserRepository userRepository;
setUp(() {
assetRepository = MockAssetRepository();
userRepository = MockIsarUserRepository();
userRepository = MockUserRepository();
sut = EntityService(assetRepository, userRepository);
});

View File

@@ -8503,7 +8503,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.135.3",
"version": "1.135.0",
"contact": {}
},
"tags": [],

View File

@@ -1,12 +1,12 @@
{
"name": "@immich/sdk",
"version": "1.135.3",
"version": "1.135.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/sdk",
"version": "1.135.3",
"version": "1.135.0",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/sdk",
"version": "1.135.3",
"version": "1.135.0",
"description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module",
"main": "./build/index.js",

View File

@@ -1,6 +1,6 @@
/**
* Immich
* 1.135.3
* 1.135.0
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/

View File

@@ -1,12 +1,12 @@
{
"name": "immich",
"version": "1.135.3",
"version": "1.135.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich",
"version": "1.135.3",
"version": "1.135.0",
"hasInstallScript": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@@ -42,6 +42,7 @@
"handlebars": "^4.7.8",
"i18n-iso-countries": "^7.6.0",
"ioredis": "^5.3.2",
"joi": "^17.10.0",
"js-yaml": "^4.1.0",
"kysely": "^0.28.0",
"kysely-postgres-js": "^2.0.0",
@@ -1348,6 +1349,21 @@
"node": ">=6"
}
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
"license": "BSD-3-Clause"
},
"node_modules/@hapi/topo": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -5036,6 +5052,27 @@
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/@sideway/address": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
"integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@sideway/formula": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
"license": "BSD-3-Clause"
},
"node_modules/@sideway/pinpoint": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
"license": "BSD-3-Clause"
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
@@ -11296,6 +11333,19 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/joi": {
"version": "17.13.3",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
"integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^9.3.0",
"@hapi/topo": "^5.1.0",
"@sideway/address": "^4.1.5",
"@sideway/formula": "^3.0.1",
"@sideway/pinpoint": "^2.0.0"
}
},
"node_modules/jose": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.0.10.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.135.3",
"version": "1.135.0",
"description": "",
"author": "",
"private": true,
@@ -68,6 +68,7 @@
"handlebars": "^4.7.8",
"i18n-iso-countries": "^7.6.0",
"ioredis": "^5.3.2",
"joi": "^17.10.0",
"js-yaml": "^4.1.0",
"kysely": "^0.28.0",
"kysely-postgres-js": "^2.0.0",

View File

@@ -224,7 +224,7 @@ limit
with
"assets" as (
select
date_trunc('MONTH', "localDateTime" AT TIME ZONE 'UTC') AT TIME ZONE 'UTC' as "timeBucket"
date_trunc('MONTH', "localDateTime" at time zone 'UTC') at time zone 'UTC' as "timeBucket"
from
"assets"
where
@@ -232,7 +232,7 @@ with
and "assets"."visibility" in ('archive', 'timeline')
)
select
("timeBucket" AT TIME ZONE 'UTC')::date::text as "timeBucket",
"timeBucket"::date::text as "timeBucket",
count(*) as "count"
from
"assets"
@@ -300,7 +300,7 @@ with
where
"assets"."deletedAt" is null
and "assets"."visibility" in ('archive', 'timeline')
and date_trunc('MONTH', "localDateTime" AT TIME ZONE 'UTC') AT TIME ZONE 'UTC' = $2
and date_trunc('MONTH', "localDateTime" at time zone 'UTC') at time zone 'UTC' = $2
and not exists (
select
from

View File

@@ -12,38 +12,6 @@ delete from "person"
where
"person"."id" in ($1)
-- PersonRepository.getAllForUser
select
"person".*
from
"person"
inner join "asset_faces" on "asset_faces"."personId" = "person"."id"
inner join "assets" on "asset_faces"."assetId" = "assets"."id"
and "assets"."visibility" = 'timeline'
and "assets"."deletedAt" is null
where
"person"."ownerId" = $1
and "asset_faces"."deletedAt" is null
and "person"."isHidden" = $2
group by
"person"."id"
having
(
"person"."name" != $3
or count("asset_faces"."assetId") >= $4
)
order by
"person"."isHidden" asc,
"person"."isFavorite" desc,
NULLIF(person.name, '') is null asc,
count("asset_faces"."assetId") desc,
NULLIF(person.name, '') asc nulls last,
"person"."createdAt"
limit
$5
offset
$6
-- PersonRepository.getAllWithoutFaces
select
"person".*

View File

@@ -130,7 +130,7 @@ from
where
"ownerId" = $1
and "updatedAt" < now() - interval '1 millisecond'
and "updateId" <= $2
and "updateId" < $2
and "updateId" >= $3
order by
"updateId" asc
@@ -274,7 +274,7 @@ from
where
"assets"."ownerId" = $1
and "exif"."updatedAt" < now() - interval '1 millisecond'
and "exif"."updateId" <= $2
and "exif"."updateId" < $2
and "exif"."updateId" >= $3
order by
"exif"."updateId" asc
@@ -418,7 +418,7 @@ from
where
"albumsId" = $1
and "updatedAt" < now() - interval '1 millisecond'
and "updateId" <= $2
and "updateId" < $2
and "updateId" >= $3
order by
"updateId" asc

View File

@@ -42,6 +42,11 @@ interface LivePhotoSearchOptions {
type: AssetType;
}
export enum TimeBucketSize {
DAY = 'DAY',
MONTH = 'MONTH',
}
interface AssetBuilderOptions {
isFavorite?: boolean;
isTrashed?: boolean;
@@ -485,13 +490,13 @@ export class AssetRepository {
.execute();
}
@GenerateSql({ params: [{}] })
@GenerateSql({ params: [{ size: TimeBucketSize.MONTH }] })
async getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]> {
return this.db
.with('assets', (qb) =>
qb
.selectFrom('assets')
.select(truncatedDate<Date>().as('timeBucket'))
.select(truncatedDate<Date>(TimeBucketSize.MONTH).as('timeBucket'))
.$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
.$if(options.visibility === undefined, withDefaultVisibility)
@@ -520,7 +525,7 @@ export class AssetRepository {
.$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!)),
)
.selectFrom('assets')
.select(sql<string>`("timeBucket" AT TIME ZONE 'UTC')::date::text`.as('timeBucket'))
.select(sql<string>`"timeBucket"::date::text`.as('timeBucket'))
.select((eb) => eb.fn.countAll<number>().as('count'))
.groupBy('timeBucket')
.orderBy('timeBucket', options.order ?? 'desc')
@@ -571,7 +576,7 @@ export class AssetRepository {
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
.$if(options.visibility == undefined, withDefaultVisibility)
.$if(!!options.visibility, (qb) => qb.where('assets.visibility', '=', options.visibility!))
.where(truncatedDate(), '=', timeBucket.replace(/^[+-]/, ''))
.where(truncatedDate(TimeBucketSize.MONTH), '=', timeBucket.replace(/^[+-]/, ''))
.$if(!!options.albumId, (qb) =>
qb.where((eb) =>
eb.exists(

View File

@@ -119,6 +119,8 @@ export class DatabaseRepository {
await sql`CREATE EXTENSION IF NOT EXISTS ${sql.raw(extension)} CASCADE`.execute(this.db);
if (extension === DatabaseExtension.VECTORCHORD) {
const dbName = sql.id(await this.getDatabaseName());
await sql`ALTER DATABASE ${dbName} SET vchordrq.prewarm_dim = '512,640,768,1024,1152,1536'`.execute(this.db);
await sql`SET vchordrq.prewarm_dim = '512,640,768,1024,1152,1536'`.execute(this.db);
await sql`ALTER DATABASE ${dbName} SET vchordrq.probes = 1`.execute(this.db);
await sql`SET vchordrq.probes = 1`.execute(this.db);
}
@@ -140,29 +142,21 @@ export class DatabaseRepository {
}
targetVersion ??= availableVersion;
const isVectors = extension === DatabaseExtension.VECTORS;
let restartRequired = false;
const diff = semver.diff(installedVersion, targetVersion);
if (!diff) {
return { restartRequired: false };
}
await Promise.all([
this.db.schema.dropIndex(VectorIndex.CLIP).ifExists().execute(),
this.db.schema.dropIndex(VectorIndex.FACE).ifExists().execute(),
]);
await this.db.transaction().execute(async (tx) => {
await this.setSearchPath(tx);
await sql`ALTER EXTENSION ${sql.raw(extension)} UPDATE TO ${sql.lit(targetVersion)}`.execute(tx);
if (extension === DatabaseExtension.VECTORS && (diff === 'major' || diff === 'minor')) {
if (isVectors && (diff === 'major' || diff === 'minor')) {
await sql`SELECT pgvectors_upgrade()`.execute(tx);
restartRequired = true;
}
});
if (!restartRequired) {
if (diff && !restartRequired) {
await Promise.all([this.reindexVectors(VectorIndex.CLIP), this.reindexVectors(VectorIndex.FACE)]);
}

View File

@@ -138,7 +138,6 @@ export class PersonRepository {
.stream();
}
@GenerateSql({ params: [{ take: 1, skip: 0 }, DummyValue.UUID] })
async getAllForUser(pagination: PaginationOptions, userId: string, options?: PersonSearchOptions) {
const items = await this.db
.selectFrom('person')
@@ -180,9 +179,8 @@ export class PersonRepository {
)
.$if(!options?.closestFaceAssetId, (qb) =>
qb
.orderBy(sql`NULLIF(person.name, '') is null`, 'asc')
.orderBy((eb) => eb.fn.count('asset_faces.assetId'), 'desc')
.orderBy(sql`NULLIF(person.name, '')`, (om) => om.asc().nullsLast())
.orderBy(sql`NULLIF(person.name, '') asc nulls last`)
.orderBy('person.createdAt'),
)
.$if(!options?.withHidden, (qb) => qb.where('person.isHidden', '=', false))

View File

@@ -111,7 +111,7 @@ export class SyncRepository {
.select(columns.syncAsset)
.where('ownerId', '=', partnerId)
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
.where('updateId', '<=', beforeUpdateId)
.where('updateId', '<', beforeUpdateId)
.$if(!!afterUpdateId, (eb) => eb.where('updateId', '>=', afterUpdateId!))
.orderBy('updateId', 'asc')
.stream();
@@ -169,7 +169,7 @@ export class SyncRepository {
.innerJoin('assets', 'assets.id', 'exif.assetId')
.where('assets.ownerId', '=', partnerId)
.where('exif.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
.where('exif.updateId', '<=', beforeUpdateId)
.where('exif.updateId', '<', beforeUpdateId)
.$if(!!afterUpdateId, (eb) => eb.where('exif.updateId', '>=', afterUpdateId!))
.orderBy('exif.updateId', 'asc')
.stream();
@@ -273,7 +273,7 @@ export class SyncRepository {
.select(columns.syncAlbumUser)
.where('albumsId', '=', albumId)
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
.where('updateId', '<=', beforeUpdateId)
.where('updateId', '<', beforeUpdateId)
.$if(!!afterUpdateId, (eb) => eb.where('updateId', '>=', afterUpdateId!))
.orderBy('updateId', 'asc')
.stream();

View File

@@ -1,21 +0,0 @@
import { Kysely, sql } from 'kysely';
export async function up(qb: Kysely<any>): Promise<void> {
type Conf = { db: string; guc: string[] };
const res = await sql<Conf>`select current_database() db, to_json(setconfig) guc from pg_db_role_setting`.execute(qb);
if (res.rows.length === 0) {
return;
}
const { db, guc } = res.rows[0];
await sql.raw(`alter database "${db}" reset all;`).execute(qb);
for (const parameter of guc) {
const [key, value] = parameter.split('=');
if (key === 'vchordrq.prewarm_dim') {
continue;
}
await sql.raw(`alter database "${db}" set ${key} to ${value};`).execute(qb);
}
}
export async function down(): Promise<void> {}

Some files were not shown because too many files have changed in this diff Show More