Merge branch 'main' into refactor/nonnegative-rating

This commit is contained in:
Mees Frensel 2026-04-29 16:14:20 +02:00
commit 8c02edcdb7
851 changed files with 39426 additions and 10740 deletions

6
.gitattributes vendored
View File

@ -6,6 +6,12 @@ mobile/openapi/**/*.dart linguist-generated=true
mobile/lib/**/*.g.dart -diff -merge
mobile/lib/**/*.g.dart linguist-generated=true
mobile/android/**/*.g.kt -diff -merge
mobile/android/**/*.g.kt linguist-generated=true
mobile/ios/**/*.g.swift -diff -merge
mobile/ios/**/*.g.swift linguist-generated=true
mobile/lib/**/*.drift.dart -diff -merge
mobile/lib/**/*.drift.dart linguist-generated=true

2
.github/.nvmrc vendored
View File

@ -1 +1 @@
24.14.1
24.15.0

View File

@ -103,7 +103,7 @@ jobs:
- name: Restore Gradle Cache
id: cache-gradle-restore
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.gradle/caches
@ -160,7 +160,7 @@ jobs:
- name: Save Gradle Cache
id: cache-gradle-save
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
if: github.ref == 'refs/heads/main'
with:
path: |
@ -210,7 +210,7 @@ jobs:
working-directory: ./mobile
- name: Setup Ruby
uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0
uses: ruby/setup-ruby@7372622e62b60b3cb750dcd2b9e32c247ffec26a # v1.302.0
with:
ruby-version: '3.3'
bundler-cache: true

View File

@ -24,7 +24,7 @@ jobs:
persist-credentials: false
- name: Check for breaking API changes
uses: oasdiff/oasdiff-action/breaking@e6faebce24cf20ac38653d0d2c7f4aa80aaafc79 # v0.0.38
uses: oasdiff/oasdiff-action/breaking@f8cb9308b42121e793f835bd14c0b8090420430c # v0.0.39
with:
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
revision: open-api/immich-openapi-specs.json

View File

@ -57,7 +57,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -70,7 +70,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@ -83,6 +83,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
with:
category: '/language:${{matrix.language}}'

View File

@ -211,7 +211,7 @@ jobs:
run: 'mise run //deployment:tf apply'
- name: Comment
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
uses: actions-cool/maintain-one-comment@909842216bc8e8658364c572ec52100f4c2cc50a # v3.3.0
if: ${{ steps.parameters.outputs.event == 'pr' }}
with:
token: ${{ steps.token.outputs.token }}

View File

@ -42,7 +42,7 @@ jobs:
run: 'mise run //deployment:tf destroy -- -refresh=false'
- name: Comment
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
uses: actions-cool/maintain-one-comment@909842216bc8e8658364c572ec52100f4c2cc50a # v3.3.0
with:
token: ${{ steps.token.outputs.token }}
number: ${{ github.event.number }}

View File

@ -142,7 +142,7 @@ jobs:
github-token: ${{ steps.generate-token.outputs.token }}
- name: Create draft release
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
with:
draft: true
tag_name: ${{ needs.bump_version.outputs.version }}

3
.gitmodules vendored
View File

@ -1,6 +1,3 @@
[submodule "mobile/.isar"]
path = mobile/.isar
url = https://github.com/isar/isar
[submodule "e2e/test-assets"]
path = e2e/test-assets
url = https://github.com/immich-app/test-assets

12
.vscode/settings.json vendored
View File

@ -13,10 +13,6 @@
"editor.wordBasedSuggestions": "off"
},
"[javascript]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.removeUnusedImports": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
@ -29,18 +25,10 @@
"editor.formatOnSave": true
},
"[svelte]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.removeUnusedImports": "explicit"
},
"editor.defaultFormatter": "svelte.svelte-vscode",
"editor.formatOnSave": true
},
"[typescript]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.removeUnusedImports": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},

View File

@ -1 +1 @@
24.14.1
24.15.0

View File

@ -68,6 +68,6 @@
"micromatch": "^4.0.8"
},
"volta": {
"node": "24.14.1"
"node": "24.15.0"
}
}

View File

@ -1,6 +1,6 @@
[tools]
terragrunt = "1.0.0"
opentofu = "1.11.5"
terragrunt = "1.0.2"
opentofu = "1.11.6"
[tasks."tg:fmt"]
run = "terragrunt hclfmt"

View File

@ -85,7 +85,7 @@ services:
container_name: immich_prometheus
ports:
- 9090:9090
image: prom/prometheus@sha256:dda13e28bf95a5e5ca5b8ed56852006094c1c8e8871d9c9dbeed30aa6e55271f
image: prom/prometheus@sha256:e4254400b85610324913f0dc4acf92603d9984e7519414c5a12811aa6146acc3
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus

View File

@ -1 +1 @@
24.14.1
24.15.0

View File

@ -210,7 +210,7 @@ The provided restore process ensures your database is never in a broken state by
## Filesystem
Immich stores two types of content in the filesystem: (a) original, unmodified assets (photos and videos), and (b) generated content. We recommend backing up the entire contents of `UPLOAD_LOCATION`, but only the original content is critical, which is stored in the following folders:
Immich does not handle filesystem backups for you. You have to arrange these yourself! Immich stores two types of content in the filesystem: (a) original, unmodified assets (photos and videos), and (b) generated content. We recommend backing up the entire contents of `UPLOAD_LOCATION`, but only the original content is critical, which is stored in the following folders:
1. `UPLOAD_LOCATION/library`
2. `UPLOAD_LOCATION/upload`

View File

@ -50,6 +50,10 @@ Before enabling OAuth in Immich, a new client application needs to be configured
- `https://immich.example.com/auth/login`
- `https://immich.example.com/user-settings`
3. Configure Backchannel logout URL
If the authentication server supports it, the **Backchannel logout URL** can be specified, and it is of the form: `http://DOMAIN:PORT/api/oauth/backchannel-logout`.
## Enable OAuth
Once you have a new OAuth client application configured, Immich can be configured using the Administration Settings page, available on the web (Administration -> Settings).
@ -63,6 +67,8 @@ Once you have a new OAuth client application configured, Immich can be configure
| `scope` | string | openid email profile | Full list of scopes to send with the request (space delimited) |
| `id_token_signed_response_alg` | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
| `userinfo_signed_response_alg` | string | none | The algorithm used to sign the userinfo response (examples: RS256, HS256) |
| `prompt` | string | (empty) | Prompt parameter for authorization url (examples: select_account, login, consent) |
| `end_session_endpoint` | URL | (empty) | Http(s) alternative end session endpoint (logout URI) |
| Request timeout | string | 30,000 (30 seconds) | Number of milliseconds to wait for http requests to complete before giving up |
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** |
| Role Claim | string | immich_role | Claim mapping for the user's role. (should return "user" or "admin")**¹** |
@ -181,6 +187,7 @@ Configuration of OAuth in Immich System Settings
| Scope | openid email profile immich_scope |
| ID Token Signed Response Algorithm | RS256 |
| Userinfo Signed Response Algorithm | RS256 |
| End Session Endpoint | https://auth.example.com/logout?rd=https://immich.example.com/ |
| Storage Label Claim | uid |
| Storage Quota Claim | immich_quota |
| Default Storage Quota (GiB) | 0 (empty for unlimited quota) |

View File

@ -18,6 +18,7 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
| `JPEG 2000` | `.jp2` | :white_check_mark: | |
| `JPEG` | `.jpeg` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
| `JPEG XL` | `.jxl` | :white_check_mark: | |
| `MPO` | `.mpo` | :white_check_mark: | Multi-Picture |
| `PNG` | `.png` | :white_check_mark: | |
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
| `RAW` | `.raw` | :white_check_mark: | |

View File

@ -193,6 +193,7 @@ The default configuration looks like this:
"defaultStorageQuota": null,
"enabled": false,
"issuerUrl": "",
"endSessionEndpoint": "",
"mobileOverrideEnabled": false,
"mobileRedirectUri": "",
"profileSigningAlgorithm": "none",

View File

@ -37,7 +37,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
| `IMMICH_LOG_FORMAT` | Log output format (`console`, `json`) | `console` | server | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `/data` | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `IMMICH_HELMET_FILE` | Path to a json file with [helmet](https://www.npmjs.com/package/helmet) options. Set to `false` to disable. Set to `true` to use `server/helmet.json`. | `false` | server | api, microservices |
| `IMMICH_HELMET_FILE` | Path to a json file with [helmet](https://www.npmjs.com/package/helmet) options. Set to `false` to disable. Set to `true` to use `server/helmet.json`. | `false` | server | api |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | |
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |

View File

@ -17,10 +17,10 @@
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "~3.9.0",
"@docusaurus/preset-classic": "~3.9.0",
"@docusaurus/theme-common": "~3.9.0",
"@docusaurus/theme-mermaid": "~3.9.0",
"@docusaurus/core": "~3.10.0",
"@docusaurus/preset-classic": "~3.10.0",
"@docusaurus/theme-common": "~3.10.0",
"@docusaurus/theme-mermaid": "~3.10.0",
"@mdi/js": "^7.3.67",
"@mdi/react": "^1.6.1",
"@mdx-js/react": "^3.0.0",
@ -36,9 +36,9 @@
"url": "^0.11.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "~3.9.0",
"@docusaurus/module-type-aliases": "~3.10.0",
"@docusaurus/tsconfig": "^3.10.0",
"@docusaurus/types": "^3.7.0",
"@docusaurus/types": "^3.10.0",
"prettier": "^3.7.4",
"typescript": "^6.0.0"
},
@ -58,6 +58,6 @@
"node": ">=20"
},
"volta": {
"node": "24.14.1"
"node": "24.15.0"
}
}

View File

@ -1,5 +1,12 @@
import { exportJWK, generateKeyPair } from 'jose';
import {
calculateJwkThumbprint,
exportJWK,
importPKCS8,
importSPKI,
SignJWT,
} from 'jose';
import Provider from 'oidc-provider';
import { PRIVATE_KEY_PEM, PUBLIC_KEY_PEM } from './test-keys';
export enum OAuthClient {
DEFAULT = 'client-default',
@ -44,6 +51,29 @@ const claims = [
},
];
const privateKey = await importPKCS8(PRIVATE_KEY_PEM, 'RS256', {
extractable: true,
});
const publicKey = await importSPKI(PUBLIC_KEY_PEM, 'RS256', {
extractable: true,
});
const kid = await calculateJwkThumbprint(await exportJWK(publicKey));
export async function generateLogoutToken(iss: string, sub: string) {
return await new SignJWT({
iss: iss,
aud: OAuthClient.DEFAULT,
iat: Math.floor(Date.now() / 1000),
jti: crypto.randomUUID(),
sub: sub,
events: {
'http://schemas.openid.net/event/backchannel-logout': {},
},
})
.setProtectedHeader({ alg: 'RS256', typ: 'logout+jwt', kid: kid })
.sign(privateKey);
}
const withDefaultClaims = (sub: string) => ({
sub,
email: `${sub}@immich.app`,
@ -66,10 +96,6 @@ const getClaims = (sub: string, use?: string) => {
};
const setup = async () => {
const { privateKey, publicKey } = await generateKeyPair('RS256', {
extractable: true,
});
const redirectUris = [
'http://127.0.0.1:2285/auth/login',
'https://photos.immich.app/oauth/mobile-redirect',

View File

@ -0,0 +1,38 @@
export const PRIVATE_KEY_PEM = `-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCVj5C7hzN3E2HO
TcJ+DN/e2NSTQFj4rPylz4J8xjm8Es7l0k2kK5EEGvUNVGZbw7s055c+6kwP9eqg
B5XFE7+26Fcq1sou6Tbm310kU4dnMW5l2CgwrhaGyb1pNysao0AMLT60dFYqtUwn
ha9ceCsa+ZU1JrknVf3rONtppBvhWoI7CO9XX1keVQ0unHPzCWUjpXTzC8OGEbmB
2w7ZIUf8OfJkd5RZ4OtIpML71W9n13aDxT50x2/EW/pFLFtQ/oaleOKHpvlRXDRX
W86G4moUJym3gHMXMUj2aOcFG2UJnpLruKz3i5qZwYiTRlBP6O9EIQNCVtYxchuN
V1CCcBU1AgMBAAECggEAJLfXMu8Nx89ynPVyyUMMaFfoEpHC9iR0L5obQVpiPMYK
VRqVVLecdftPS9s7eQ58BNBRzdC0ZVu841aRYs3HLNbsZZhPkYZQpAxU//Dg5okY
fzj7Hv5yidt4HN9+Pd8z/3lRMnj4WapifLaBt8xJ2ujJBMBRxzJBsXDnT0+Kx7+y
bYDeuVfyUTEikaK3QZTbuRF3D3eiuN16GG+hv8UqTF2eYbPxdiLjYpTSHa4mH88C
qfJz2Xt4SEzmyeo3G+MO17wDFOwtEe8ojlJfULHnHJSFdUwTfYIFM1bg5/fJ9MOS
/fO3TSG+wkQqjQa6eoGssAzP87fL2XNLzlDtGY/7uQKBgQDHuJHOtf1EjOvNYiP7
EN+8QGs41ghzt9CQRQxWbHpusR3IW3P83KMXwYmrlG70oOUXBRGSB/ESXUofXc5W
pu5+Y55S44aUnu/a9yOBttYW0dtHZSL0zFT+PlVASwUzFZ2zcH1KXlUkSpfL5OAD
PyDDTnBZ2AWh45fRO9wLo6PPuQKBgQC/tI03RqU3mOjqukKbquYeIpXHfRU5Z0DM
u9ru1THYEl6fmkMXycxo/mvW3awyFuyKy/VodqIgKnFgumEqCHZh6OAMm/LC7TfA
l9tjFSs/MyOqQVD4kbX+z6Oq4c4GccDoXfsQ3gzECoBapegi/F+6/25y+/C8ghXb
J/Jg1GQXXQKBgQDFgWbfzuVZZyrBfu4qGLPJDMN7/114YizknwPma3xf/tN/EcGQ
K/k1QvWMMkvPq1UiAKcxjJ0AFjV482FcG9T6NDWbrtmmG88C8Sex3Ue2ZW2+GuwI
vhDHJIlV/Vp0/Elp7DJa2xLDwuh+gCZvz3vs6KL+ljxrrhCyn8mp0PfsMQKBgFFZ
KnuETOO0zVGdzFoGQTQUdP58A5+iQwsdxB+I9Ge+E80iRso3ZbhADj7VPhbbR3D2
b6LuhImluQrUzBpsEOAnU7vGCVPSGdBuIDiBaSKebsn2gYeZPWNtdQQ0YZq2dqek
Cb/0mfIuipzsvf7qnSza62F7q4IyqVegMegI+Jg5AoGATM3NMy7JZeKzSkm+3ohU
3xZOwgqKV9SH+0OeYWpuBxT7D7FlrKKI4NJ3XN3hg2f/DJAF6dH11CPe7pk94yol
HMbh+PQUQ6GYvAzxIOvagWboQ3lzeyubNMpyFjfOrIE/WOQCUBZ9tIwCHIarIuyi
QRuNOj3+U8T/n1Ww352HBdw=
-----END PRIVATE KEY-----`;
export const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlY+Qu4czdxNhzk3Cfgzf
3tjUk0BY+Kz8pc+CfMY5vBLO5dJNpCuRBBr1DVRmW8O7NOeXPupMD/XqoAeVxRO/
tuhXKtbKLuk25t9dJFOHZzFuZdgoMK4Whsm9aTcrGqNADC0+tHRWKrVMJ4WvXHgr
GvmVNSa5J1X96zjbaaQb4VqCOwjvV19ZHlUNLpxz8wllI6V08wvDhhG5gdsO2SFH
/DnyZHeUWeDrSKTC+9VvZ9d2g8U+dMdvxFv6RSxbUP6GpXjih6b5UVw0V1vOhuJq
FCcpt4BzFzFI9mjnBRtlCZ6S67is94uamcGIk0ZQT+jvRCEDQlbWMXIbjVdQgnAV
NQIDAQAB
-----END PUBLIC KEY-----`;

View File

@ -1 +1 @@
24.14.1
24.15.0

View File

@ -58,6 +58,6 @@
"vitest": "^4.0.0"
},
"volta": {
"node": "24.14.1"
"node": "24.15.0"
}
}

View File

@ -2,82 +2,43 @@ import { expect } from 'vitest';
export const errorDto = {
unauthorized: {
error: 'Unauthorized',
statusCode: 401,
message: 'Authentication required',
correlationId: expect.any(String),
},
unauthorizedWithMessage: (message: string) => ({
error: 'Unauthorized',
statusCode: 401,
message,
correlationId: expect.any(String),
}),
forbidden: {
error: 'Forbidden',
statusCode: 403,
message: expect.any(String),
correlationId: expect.any(String),
},
missingPermission: (permission: string) => ({
error: 'Forbidden',
statusCode: 403,
message: `Missing required permission: ${permission}`,
correlationId: expect.any(String),
}),
wrongPassword: {
error: 'Bad Request',
statusCode: 400,
message: 'Wrong password',
correlationId: expect.any(String),
},
invalidToken: {
error: 'Unauthorized',
statusCode: 401,
message: 'Invalid user token',
correlationId: expect.any(String),
},
invalidShareKey: {
error: 'Unauthorized',
statusCode: 401,
message: 'Invalid share key',
correlationId: expect.any(String),
},
passwordRequired: {
error: 'Unauthorized',
statusCode: 401,
message: 'Password required',
correlationId: expect.any(String),
},
badRequest: (message: any = null) => ({
error: 'Bad Request',
statusCode: 400,
message: message ?? expect.anything(),
correlationId: expect.any(String),
}),
noPermission: {
error: 'Bad Request',
statusCode: 400,
message: expect.stringContaining('Not found or no'),
correlationId: expect.any(String),
},
incorrectLogin: {
error: 'Unauthorized',
statusCode: 401,
message: 'Incorrect email or password',
correlationId: expect.any(String),
},
alreadyHasAdmin: {
error: 'Bad Request',
statusCode: 400,
message: 'The server already has an admin',
correlationId: expect.any(String),
},
invalidEmail: {
error: 'Bad Request',
statusCode: 400,
message: ['email must be an email'],
correlationId: expect.any(String),
},
};

View File

@ -154,23 +154,31 @@ describe('/albums', () => {
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedLink,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedEditorUser,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedViewerUser,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: true,
}),
expect.objectContaining({
ownerId: user2.userId,
albumName: user2SharedUser,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
]),
shared: true,
}),
]),
@ -184,23 +192,31 @@ describe('/albums', () => {
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedEditorUser,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedViewerUser,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedLink,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1NotShared,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: false,
}),
]),
@ -216,23 +232,31 @@ describe('/albums', () => {
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedEditorUser,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedViewerUser,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedLink,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: true,
}),
expect.objectContaining({
ownerId: user2.userId,
albumName: user2SharedUser,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
]),
shared: true,
}),
]),
@ -248,8 +272,10 @@ describe('/albums', () => {
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
ownerId: user1.userId,
albumName: user1NotShared,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: false,
}),
]),
@ -286,13 +312,17 @@ describe('/albums', () => {
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
ownerId: user4.userId,
albumName: user4DeletedAsset,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user4.userId }) },
]),
shared: false,
}),
expect.objectContaining({
ownerId: user4.userId,
albumName: user4Empty,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user4.userId }) },
]),
shared: false,
}),
]),
@ -362,16 +392,17 @@ describe('/albums', () => {
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...user2Albums[0],
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
assetCount: 1,
lastModifiedAssetTimestamp: expect.any(String),
endDate: expect.any(String),
startDate: expect.any(String),
albumUsers: expect.any(Array),
shared: true,
});
expect(body).toEqual(
expect.objectContaining({
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
assetCount: 1,
lastModifiedAssetTimestamp: expect.any(String),
endDate: expect.any(String),
startDate: expect.any(String),
albumUsers: expect.any(Array),
shared: true,
}),
);
});
});
@ -397,15 +428,13 @@ describe('/albums', () => {
id: expect.any(String),
createdAt: expect.any(String),
updatedAt: expect.any(String),
ownerId: user1.userId,
albumName: 'New album',
description: '',
albumThumbnailAssetId: null,
shared: false,
albumUsers: [],
albumUsers: [{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) }],
hasSharedLink: false,
assetCount: 0,
owner: expect.objectContaining({ email: user1.userEmail }),
isActivityEnabled: true,
order: AssetOrder.Desc,
});
@ -621,11 +650,11 @@ describe('/albums', () => {
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
albumUsers: [
albumUsers: expect.arrayContaining([
expect.objectContaining({
user: expect.objectContaining({ id: user2.userId }),
}),
],
]),
}),
);
});
@ -637,7 +666,7 @@ describe('/albums', () => {
.send({ albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Cannot be shared with owner'));
expect(body).toEqual(errorDto.badRequest('User already added'));
});
it('should not be able to add existing user to shared album', async () => {
@ -663,7 +692,7 @@ describe('/albums', () => {
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
});
expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer);
expect(album.albumUsers[1].role).toEqual(AlbumUserRole.Viewer);
const { status } = await request(app)
.put(`/albums/${album.id}/user/${user2.userId}`)
@ -678,7 +707,10 @@ describe('/albums', () => {
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(body).toEqual(
expect.objectContaining({
albumUsers: [expect.objectContaining({ role: AlbumUserRole.Editor })],
albumUsers: [
expect.objectContaining({ role: AlbumUserRole.Owner }),
expect.objectContaining({ role: AlbumUserRole.Editor }),
],
}),
);
});
@ -689,7 +721,7 @@ describe('/albums', () => {
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
});
expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer);
expect(album.albumUsers[1].role).toEqual(AlbumUserRole.Viewer);
const { status, body } = await request(app)
.put(`/albums/${album.id}/user/${user2.userId}`)

View File

@ -1,9 +1,10 @@
import { OAuthClient, OAuthUser } from '@immich/e2e-auth-server';
import { OAuthClient, OAuthUser, generateLogoutToken } from '@immich/e2e-auth-server';
import {
LoginResponseDto,
SystemConfigOAuthDto,
getConfigDefaults,
getMyUser,
getSessions,
startOAuth,
updateConfig,
} from '@immich/sdk';
@ -88,17 +89,19 @@ describe(`/oauth`, () => {
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
await setupOAuth(admin.accessToken, {
enabled: true,
clientId: OAuthClient.DEFAULT,
clientSecret: OAuthClient.DEFAULT,
buttonText: 'Login with Immich',
storageLabelClaim: 'immich_username',
});
});
describe('POST /oauth/authorize', () => {
beforeAll(async () => {
await setupOAuth(admin.accessToken, {
enabled: true,
clientId: OAuthClient.DEFAULT,
clientSecret: OAuthClient.DEFAULT,
buttonText: 'Login with Immich',
storageLabelClaim: 'immich_username',
});
});
it(`should throw an error if a redirect uri is not provided`, async () => {
const { status, body } = await request(app).post('/oauth/authorize').send({});
expect(status).toBe(400);
@ -118,9 +121,46 @@ describe(`/oauth`, () => {
expect(params.get('redirect_uri')).toBe('http://127.0.0.1:2285/auth/login');
expect(params.get('state')).toBeDefined();
});
it('should not include the prompt parameter when not configured', async () => {
const { status, body } = await request(app)
.post('/oauth/authorize')
.send({ redirectUri: 'http://127.0.0.1:2285/auth/login' });
expect(status).toBe(201);
const params = new URL(body.url).searchParams;
expect(params.get('prompt')).toBeNull();
});
it('should include the prompt parameter when configured', async () => {
await setupOAuth(admin.accessToken, {
enabled: true,
clientId: OAuthClient.DEFAULT,
clientSecret: OAuthClient.DEFAULT,
prompt: 'select_account',
});
const { status, body } = await request(app)
.post('/oauth/authorize')
.send({ redirectUri: 'http://127.0.0.1:2285/auth/login' });
expect(status).toBe(201);
const params = new URL(body.url).searchParams;
expect(params.get('prompt')).toBe('select_account');
});
});
describe('POST /oauth/callback', () => {
beforeAll(async () => {
await setupOAuth(admin.accessToken, {
enabled: true,
clientId: OAuthClient.DEFAULT,
clientSecret: OAuthClient.DEFAULT,
buttonText: 'Login with Immich',
storageLabelClaim: 'immich_username',
});
});
it(`should throw an error if a url is not provided`, async () => {
const { status, body } = await request(app).post('/oauth/callback').send({});
expect(status).toBe(400);
@ -159,10 +199,9 @@ describe(`/oauth`, () => {
it(`should throw an error if the codeVerifier doesn't match the challenge`, async () => {
const callbackParams = await loginWithOAuth('oauth-auto-register');
const { codeVerifier } = await loginWithOAuth('oauth-auto-register');
const { status, body } = await request(app)
const { status } = await request(app)
.post('/oauth/callback')
.send({ ...callbackParams, codeVerifier });
console.log(body);
expect(status).toBeGreaterThanOrEqual(400);
});
@ -293,9 +332,7 @@ describe(`/oauth`, () => {
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
expect(status).toBe(500);
expect(body).toMatchObject({
error: 'Internal Server Error',
message: 'Failed to finish oauth',
statusCode: 500,
});
});
@ -334,6 +371,50 @@ describe(`/oauth`, () => {
});
});
describe(`POST /oauth/backchannel-logout`, () => {
it(`should throw an error if the logout_token is not provided`, async () => {
const { status, body } = await request(app).post('/oauth/backchannel-logout').send({});
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['[logout_token] Invalid input: expected string, received undefined']));
});
it(`should throw an error if an invalid logout token is provided`, async () => {
const { status, body } = await request(app)
.post('/oauth/backchannel-logout')
.send({ logout_token: 'invalid token' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Error backchannel logout: token validation failed'));
});
it(`should logout user if a valid logout token is provided`, async () => {
await setupOAuth(admin.accessToken, {
enabled: true,
clientId: OAuthClient.DEFAULT,
clientSecret: OAuthClient.DEFAULT,
autoRegister: true,
signingAlgorithm: 'RS256',
buttonText: 'Login with Immich',
});
const callbackParams = await loginWithOAuth('backchannel-logout-user');
const { status: callbackStatus, body: callbackBody } = await request(app)
.post('/oauth/callback')
.send(callbackParams);
expect(callbackStatus).toBe(201);
await expect(getSessions({ headers: asBearerAuth(callbackBody.accessToken) })).resolves.toHaveLength(1);
const logoutToken = await generateLogoutToken('http://0.0.0.0:2286', 'backchannel-logout-user');
const { status, body } = await request(app).post('/oauth/backchannel-logout').send({ logout_token: logoutToken });
expect(status).toBe(200);
expect(body).toMatchObject({});
await expect(getSessions({ headers: asBearerAuth(callbackBody.accessToken) })).rejects.toMatchObject({
status: 401,
});
});
});
describe('mobile redirect override', () => {
beforeAll(async () => {
await setupOAuth(admin.accessToken, {
@ -412,11 +493,10 @@ describe(`/oauth`, () => {
});
it('should reject OAuth discovery over HTTP', async () => {
const { status, body } = await request(app)
const { status } = await request(app)
.post('/oauth/authorize')
.send({ redirectUri: 'http://127.0.0.1:2285/auth/login' });
expect(status).toBe(500);
expect(body).toMatchObject({ statusCode: 500 });
});
});
});

View File

@ -207,16 +207,6 @@ describe('/server', () => {
});
});
describe('GET /server/theme', () => {
it('should respond with the server theme', async () => {
const { status, body } = await request(app).get('/server/theme');
expect(status).toBe(200);
expect(body).toEqual({
customCss: '',
});
});
});
describe('GET /server/license', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/server/license');

View File

@ -1,7 +1,9 @@
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
import { expect, test } from '@playwright/test';
import { readFile } from 'node:fs/promises';
import { basename, join } from 'node:path';
import type { Socket } from 'socket.io-client';
import { utils } from 'src/utils';
import { testAssetDir, utils } from 'src/utils';
test.describe('Detail Panel', () => {
let admin: LoginResponseDto;
@ -83,4 +85,42 @@ test.describe('Detail Panel', () => {
await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id });
await expect(textarea).toHaveValue('new description');
});
test.describe('Date editor', () => {
test('displays inferred asset timezone', async ({ context, page }) => {
const test = {
filepath: 'metadata/dates/datetimeoriginal-gps.jpg',
expected: {
dateTime: '2025-12-01T11:30',
// Test with a timezone which is NOT the first among timezones with the same offset
// This is to check that the editor does not simply fall back to the first available timezone with that offset
// America/Denver (-07:00) is not the first among timezones with offset -07:00
timeZoneWithOffset: 'America/Denver (-07:00)',
},
};
const asset = await utils.createAsset(admin.accessToken, {
assetData: {
bytes: await readFile(join(testAssetDir, test.filepath)),
filename: basename(test.filepath),
},
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
// asset viewer -> detail panel -> date editor
await utils.setAuthCookies(context, admin.accessToken);
await page.goto(`/photos/${asset.id}`);
await page.waitForSelector('#immich-asset-viewer');
await page.getByRole('button', { name: 'Info' }).click();
await page.getByTestId('detail-panel-edit-date-button').click();
await page.waitForSelector('[role="dialog"]');
const datetime = page.locator('#datetime');
await expect(datetime).toHaveValue(test.expected.dateTime);
const timezone = page.getByRole('combobox', { name: 'Timezone' });
await expect(timezone).toHaveValue(test.expected.timeZoneWithOffset);
});
});
});

View File

@ -3,6 +3,7 @@
*/
import {
AlbumUserRole,
AssetTypeEnum,
AssetVisibility,
UserAvatarColor,
@ -420,9 +421,7 @@ export function getAlbum(
albumThumbnailAssetId: album.thumbnailAssetId,
createdAt: album.createdAt,
updatedAt: album.updatedAt,
ownerId: albumOwner.id,
owner: albumOwner,
albumUsers: [], // Empty array for non-shared album
albumUsers: [{ user: albumOwner, role: AlbumUserRole.Owner }],
shared: false,
hasSharedLink: false,
isActivityEnabled: true,

View File

@ -223,6 +223,7 @@ export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserI
'.jp2',
'.jpe',
'.jxl',
'.mpo',
'.svg',
'.tif',
'.tiff',

View File

@ -349,7 +349,7 @@ test.describe('Timeline', () => {
expect(visibleMockAssetsYearMonths).toContain(month);
}
});
test('Deep link to last photo, scroll up', async ({ page }) => {
test.skip('Deep link to last photo, scroll up', async ({ page }) => {
const lastAsset = assets.at(-1)!;
await pageUtils.deepLinkPhotosPage(page, lastAsset.id);
@ -361,7 +361,7 @@ test.describe('Timeline', () => {
await thumbnailUtils.expectInViewport(page, '14e5901f-fd7f-40c0-b186-4d7e7fc67968');
});
test('Deep link to first bucket, scroll down', async ({ page }) => {
test.skip('Deep link to first bucket, scroll down', async ({ page }) => {
const lastAsset = assets.at(0)!;
await pageUtils.deepLinkPhotosPage(page, lastAsset.id);
await timelineUtils.locator(page).hover();
@ -440,7 +440,7 @@ test.describe('Timeline', () => {
await thumbnailUtils.expectInViewport(page, asset.id);
await thumbnailUtils.expectSelectedDisabled(page, asset.id);
});
test('Add photos to album', async ({ page }) => {
test.skip('Add photos to album', async ({ page }) => {
const album = timelineRestData.album;
await pageUtils.openAlbumPage(page, album.id);
await page.locator('nav button[aria-label="Add photos"]').click();
@ -752,7 +752,7 @@ test.describe('Timeline', () => {
await page.getByText('Photos', { exact: true }).click();
await thumbnailUtils.expectInViewport(page, assetToFavorite.id);
});
test('open /favorites, archive photo, unarchive photo', async ({ page }) => {
test.skip('open /favorites, archive photo, unarchive photo', async ({ page }) => {
await pageUtils.openFavorites(page);
const assetToArchive = getAsset(timelineRestData, 'ad31e29f-2069-4574-b9a9-ad86523c92cb')!;
await thumbnailUtils.withAssetId(page, assetToArchive.id).hover();

@ -1 +1 @@
Subproject commit 163c251744e0a35d7ecfd02682452043f149fc2b
Subproject commit 0eac5a37384c151be88381b41f9e28d8d59a4466

View File

@ -276,9 +276,11 @@
"oauth_button_text": "Button text",
"oauth_client_secret_description": "Required for confidential client, or if PKCE (Proof Key for Code Exchange) is not supported for public client.",
"oauth_enable_description": "Login with OAuth",
"oauth_end_session_url_description": "Redirect the user to this URI when they log out.",
"oauth_mobile_redirect_uri": "Mobile redirect URI",
"oauth_mobile_redirect_uri_override": "Mobile redirect URI override",
"oauth_mobile_redirect_uri_override_description": "Enable when OAuth provider does not allow a mobile URI, like ''{callback}''",
"oauth_prompt_description": "Prompt parameter (e.g. select_account, login, consent)",
"oauth_role_claim": "Role Claim",
"oauth_role_claim_description": "Automatically grant admin access based on the presence of this claim. The claim may have either 'user' or 'admin'.",
"oauth_settings": "OAuth",

View File

@ -48,14 +48,14 @@ FROM python:3.13-slim-trixie@sha256:d168b8d9eb761f4d3fe305ebd04aeb7e7f2de0297cec
RUN apt-get update && \
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.28.4/intel-igc-core-2_2.28.4+20760_amd64.deb && \
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.28.4/intel-igc-opencl-2_2.28.4+20760_amd64.deb && \
wget -nv https://github.com/intel/compute-runtime/releases/download/26.05.37020.3/intel-opencl-icd_26.05.37020.3-0_amd64.deb && \
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.32.7/intel-igc-core-2_2.32.7+21184_amd64.deb && \
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.32.7/intel-igc-opencl-2_2.32.7+21184_amd64.deb && \
wget -nv https://github.com/intel/compute-runtime/releases/download/26.14.37833.4/intel-opencl-icd_26.14.37833.4-0_amd64.deb && \
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-core_1.0.17537.24_amd64.deb && \
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-opencl_1.0.17537.24_amd64.deb && \
wget -nv https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-opencl-icd-legacy1_24.35.30872.36_amd64.deb && \
# TODO: Figure out how to get renovate to manage this differently versioned libigdgmm file
wget -nv https://github.com/intel/compute-runtime/releases/download/26.05.37020.3/libigdgmm12_22.9.0_amd64.deb && \
wget -nv https://github.com/intel/compute-runtime/releases/download/26.14.37833.4/libigdgmm12_22.9.0_amd64.deb && \
dpkg -i *.deb && \
rm *.deb && \
apt-get remove wget -yqq && \

View File

@ -183,7 +183,10 @@ async def predict(
text: str | None = Form(default=None),
) -> Any:
if image is not None:
inputs: Image | str = await run(lambda: decode_pil(image))
decoded = await run(lambda: decode_pil(image))
if decoded.width == 0 or decoded.height == 0:
raise HTTPException(400, "Image has zero width or height")
inputs: Image | str = decoded
elif text is not None:
inputs = text
else:

View File

@ -9,12 +9,12 @@ dependencies = [
"aiocache>=0.12.1,<1.0",
"fastapi>=0.95.2,<1.0",
"gunicorn>=21.1.0",
"huggingface-hub>=0.20.1,<1.0",
"huggingface-hub>=1.0,<2.0",
"insightface>=0.7.3,<1.0",
"numpy<2.4.0",
"opencv-python-headless>=4.7.0.72,<5.0",
"orjson>=3.9.5",
"pillow>=12.2,<12.3",
"pillow>=12.2,<13",
"pydantic>=2.0.0,<3",
"pydantic-settings>=2.5.2,<3",
"python-multipart>=0.0.6,<1.0",

View File

@ -1198,6 +1198,19 @@ class TestLoad:
mock_model.model_format = ModelFormat.ONNX
@pytest.mark.parametrize("size", [(0, 100), (100, 0), (0, 0)])
def test_predict_rejects_empty_image(size: tuple[int, int], deployed_app: TestClient) -> None:
with mock.patch("immich_ml.main.decode_pil", return_value=Image.new("RGB", size)):
response = deployed_app.post(
"http://localhost:3003/predict",
data={"entries": json.dumps({"clip": {"visual": {"modelName": "ViT-B-32__openai"}}})},
files={"image": b"fake image bytes"},
)
assert response.status_code == 400
assert "zero" in response.json()["detail"].lower()
def test_root_endpoint(deployed_app: TestClient) -> None:
response = deployed_app.get("http://localhost:3003")

View File

@ -14,11 +14,11 @@ config_roots = [
]
[tools]
node = "24.14.1"
flutter = "3.35.7"
pnpm = "10.33.0"
terragrunt = "1.0.0"
opentofu = "1.11.5"
node = "24.15.0"
flutter = "3.41.6"
pnpm = "10.33.1"
terragrunt = "1.0.2"
opentofu = "1.11.6"
java = "21.0.2"
[tools."github:CQLabs/homebrew-dcm"]

View File

@ -1,5 +1,5 @@
{
"dart.flutterSdkPath": ".fvm/versions/3.41.6",
"dart.flutterSdkPath": ".fvm/versions/3.41.7",
"dart.lineLength": 120,
"[dart]": {
"editor.rulers": [

View File

@ -43,8 +43,8 @@ class BackgroundEngineLock(context: Context) : BackgroundWorkerLockApi, ImmichPl
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
super.onAttachedToEngine(binding)
checkAndEnforceBackgroundLock(binding.applicationContext)
engineCount.incrementAndGet()
checkAndEnforceBackgroundLock(binding.applicationContext)
Log.i(TAG, "Flutter engine attached. Attached Engines count: $engineCount")
}

View File

@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
@ -37,36 +37,150 @@ private object BackgroundWorkerPigeonUtils {
)
}
}
fun doubleEquals(a: Double, b: Double): Boolean {
// Normalize -0.0 to 0.0 and handle NaN equality.
return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN())
}
fun floatEquals(a: Float, b: Float): Boolean {
// Normalize -0.0 to 0.0 and handle NaN equality.
return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN())
}
fun doubleHash(d: Double): Int {
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
val normalized = if (d == 0.0) 0.0 else d
val bits = java.lang.Double.doubleToLongBits(normalized)
return (bits xor (bits ushr 32)).toInt()
}
fun floatHash(f: Float): Int {
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
val normalized = if (f == 0.0f) 0.0f else f
return java.lang.Float.floatToIntBits(normalized)
}
fun deepEquals(a: Any?, b: Any?): Boolean {
if (a === b) {
return true
}
if (a == null || b == null) {
return false
}
if (a is ByteArray && b is ByteArray) {
return a.contentEquals(b)
return a.contentEquals(b)
}
if (a is IntArray && b is IntArray) {
return a.contentEquals(b)
return a.contentEquals(b)
}
if (a is LongArray && b is LongArray) {
return a.contentEquals(b)
return a.contentEquals(b)
}
if (a is DoubleArray && b is DoubleArray) {
return a.contentEquals(b)
if (a.size != b.size) return false
for (i in a.indices) {
if (!doubleEquals(a[i], b[i])) return false
}
return true
}
if (a is FloatArray && b is FloatArray) {
if (a.size != b.size) return false
for (i in a.indices) {
if (!floatEquals(a[i], b[i])) return false
}
return true
}
if (a is Array<*> && b is Array<*>) {
return a.size == b.size &&
a.indices.all{ deepEquals(a[it], b[it]) }
if (a.size != b.size) return false
for (i in a.indices) {
if (!deepEquals(a[i], b[i])) return false
}
return true
}
if (a is List<*> && b is List<*>) {
return a.size == b.size &&
a.indices.all{ deepEquals(a[it], b[it]) }
if (a.size != b.size) return false
val iterA = a.iterator()
val iterB = b.iterator()
while (iterA.hasNext() && iterB.hasNext()) {
if (!deepEquals(iterA.next(), iterB.next())) return false
}
return true
}
if (a is Map<*, *> && b is Map<*, *>) {
return a.size == b.size && a.all {
(b as Map<Any?, Any?>).containsKey(it.key) &&
deepEquals(it.value, b[it.key])
if (a.size != b.size) return false
for (entry in a) {
val key = entry.key
var found = false
for (bEntry in b) {
if (deepEquals(key, bEntry.key)) {
if (deepEquals(entry.value, bEntry.value)) {
found = true
break
} else {
return false
}
}
}
if (!found) return false
}
return true
}
if (a is Double && b is Double) {
return doubleEquals(a, b)
}
if (a is Float && b is Float) {
return floatEquals(a, b)
}
return a == b
}
fun deepHash(value: Any?): Int {
return when (value) {
null -> 0
is ByteArray -> value.contentHashCode()
is IntArray -> value.contentHashCode()
is LongArray -> value.contentHashCode()
is DoubleArray -> {
var result = 1
for (item in value) {
result = 31 * result + doubleHash(item)
}
result
}
is FloatArray -> {
var result = 1
for (item in value) {
result = 31 * result + floatHash(item)
}
result
}
is Array<*> -> {
var result = 1
for (item in value) {
result = 31 * result + deepHash(item)
}
result
}
is List<*> -> {
var result = 1
for (item in value) {
result = 31 * result + deepHash(item)
}
result
}
is Map<*, *> -> {
var result = 0
for (entry in value) {
result += ((deepHash(entry.key) * 31) xor deepHash(entry.value))
}
result
}
is Double -> doubleHash(value)
is Float -> floatHash(value)
else -> value.hashCode()
}
}
}
/**
@ -79,7 +193,7 @@ class FlutterError (
val code: String,
override val message: String? = null,
val details: Any? = null
) : Throwable()
) : RuntimeException()
/** Generated class from Pigeon that represents data sent in messages. */
data class BackgroundWorkerSettings (
@ -101,15 +215,22 @@ data class BackgroundWorkerSettings (
)
}
override fun equals(other: Any?): Boolean {
if (other !is BackgroundWorkerSettings) {
if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
return BackgroundWorkerPigeonUtils.deepEquals(toList(), other.toList()) }
val other = other as BackgroundWorkerSettings
return BackgroundWorkerPigeonUtils.deepEquals(this.requiresCharging, other.requiresCharging) && BackgroundWorkerPigeonUtils.deepEquals(this.minimumDelaySeconds, other.minimumDelaySeconds)
}
override fun hashCode(): Int = toList().hashCode()
override fun hashCode(): Int {
var result = javaClass.hashCode()
result = 31 * result + BackgroundWorkerPigeonUtils.deepHash(this.requiresCharging)
result = 31 * result + BackgroundWorkerPigeonUtils.deepHash(this.minimumDelaySeconds)
return result
}
}
private open class BackgroundWorkerPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {

View File

@ -5,8 +5,10 @@ import android.provider.MediaStore
import android.util.Log
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import io.flutter.embedding.engine.FlutterEngineCache
import java.util.concurrent.TimeUnit
@ -18,6 +20,7 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
override fun enable() {
enqueueMediaObserver(ctx)
enqueuePeriodicWorker(ctx)
}
override fun saveNotificationMessage(title: String, body: String) {
@ -27,12 +30,14 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
override fun configure(settings: BackgroundWorkerSettings) {
BackgroundWorkerPreferences(ctx).updateSettings(settings)
enqueueMediaObserver(ctx)
enqueuePeriodicWorker(ctx)
}
override fun disable() {
WorkManager.getInstance(ctx).apply {
cancelUniqueWork(OBSERVER_WORKER_NAME)
cancelUniqueWork(BACKGROUND_WORKER_NAME)
cancelUniqueWork(PERIODIC_WORKER_NAME)
}
Log.i(TAG, "Cancelled background upload tasks")
}
@ -40,6 +45,7 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
companion object {
private const val BACKGROUND_WORKER_NAME = "immich/BackgroundWorkerV1"
private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1"
private const val PERIODIC_WORKER_NAME = "immich/PeriodicBackgroundWorkerV1"
const val ENGINE_CACHE_KEY = "immich::background_worker::engine"
@ -55,7 +61,7 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
setRequiresCharging(settings.requiresCharging)
}.build()
val work = OneTimeWorkRequest.Builder(MediaObserver::class.java)
val work = OneTimeWorkRequestBuilder<MediaObserver>()
.setConstraints(constraints)
.build()
WorkManager.getInstance(ctx)
@ -67,10 +73,30 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
)
}
fun enqueuePeriodicWorker(ctx: Context) {
val settings = BackgroundWorkerPreferences(ctx).getSettings()
val constraints = Constraints.Builder().apply {
setRequiresCharging(settings.requiresCharging)
}.build()
val work =
PeriodicWorkRequestBuilder<PeriodicWorker>(
1,
TimeUnit.HOURS,
15,
TimeUnit.MINUTES
).setConstraints(constraints)
.build()
WorkManager.getInstance(ctx)
.enqueueUniquePeriodicWork(PERIODIC_WORKER_NAME, ExistingPeriodicWorkPolicy.UPDATE, work)
Log.i(TAG, "Enqueued periodic background worker with name: $PERIODIC_WORKER_NAME")
}
fun enqueueBackgroundWorker(ctx: Context) {
val constraints = Constraints.Builder().setRequiresBatteryNotLow(true).build()
val work = OneTimeWorkRequest.Builder(BackgroundWorker::class.java)
val work = OneTimeWorkRequestBuilder<BackgroundWorker>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
.build()

View File

@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")

View File

@ -0,0 +1,16 @@
package app.alextran.immich.background
import android.content.Context
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
class PeriodicWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
private val ctx: Context = context.applicationContext
override fun doWork(): Result {
Log.i("PeriodicWorker", "Periodic worker triggered, starting background worker")
BackgroundWorkerApiImpl.enqueueBackgroundWorker(ctx)
return Result.success()
}
}

View File

@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
@ -46,7 +46,7 @@ class FlutterError (
val code: String,
override val message: String? = null,
val details: Any? = null
) : Throwable()
) : RuntimeException()
enum class NetworkCapability(val raw: Int) {
CELLULAR(0),
@ -75,7 +75,7 @@ private open class ConnectivityPigeonCodec : StandardMessageCodec() {
when (value) {
is NetworkCapability -> {
stream.write(129)
writeValue(stream, value.raw)
writeValue(stream, value.raw.toLong())
}
else -> super.writeValue(stream, value)
}

View File

@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
@ -34,36 +34,150 @@ private object NetworkPigeonUtils {
)
}
}
fun doubleEquals(a: Double, b: Double): Boolean {
// Normalize -0.0 to 0.0 and handle NaN equality.
return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN())
}
fun floatEquals(a: Float, b: Float): Boolean {
// Normalize -0.0 to 0.0 and handle NaN equality.
return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN())
}
fun doubleHash(d: Double): Int {
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
val normalized = if (d == 0.0) 0.0 else d
val bits = java.lang.Double.doubleToLongBits(normalized)
return (bits xor (bits ushr 32)).toInt()
}
fun floatHash(f: Float): Int {
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
val normalized = if (f == 0.0f) 0.0f else f
return java.lang.Float.floatToIntBits(normalized)
}
fun deepEquals(a: Any?, b: Any?): Boolean {
if (a === b) {
return true
}
if (a == null || b == null) {
return false
}
if (a is ByteArray && b is ByteArray) {
return a.contentEquals(b)
return a.contentEquals(b)
}
if (a is IntArray && b is IntArray) {
return a.contentEquals(b)
return a.contentEquals(b)
}
if (a is LongArray && b is LongArray) {
return a.contentEquals(b)
return a.contentEquals(b)
}
if (a is DoubleArray && b is DoubleArray) {
return a.contentEquals(b)
if (a.size != b.size) return false
for (i in a.indices) {
if (!doubleEquals(a[i], b[i])) return false
}
return true
}
if (a is FloatArray && b is FloatArray) {
if (a.size != b.size) return false
for (i in a.indices) {
if (!floatEquals(a[i], b[i])) return false
}
return true
}
if (a is Array<*> && b is Array<*>) {
return a.size == b.size &&
a.indices.all{ deepEquals(a[it], b[it]) }
if (a.size != b.size) return false
for (i in a.indices) {
if (!deepEquals(a[i], b[i])) return false
}
return true
}
if (a is List<*> && b is List<*>) {
return a.size == b.size &&
a.indices.all{ deepEquals(a[it], b[it]) }
if (a.size != b.size) return false
val iterA = a.iterator()
val iterB = b.iterator()
while (iterA.hasNext() && iterB.hasNext()) {
if (!deepEquals(iterA.next(), iterB.next())) return false
}
return true
}
if (a is Map<*, *> && b is Map<*, *>) {
return a.size == b.size && a.all {
(b as Map<Any?, Any?>).containsKey(it.key) &&
deepEquals(it.value, b[it.key])
if (a.size != b.size) return false
for (entry in a) {
val key = entry.key
var found = false
for (bEntry in b) {
if (deepEquals(key, bEntry.key)) {
if (deepEquals(entry.value, bEntry.value)) {
found = true
break
} else {
return false
}
}
}
if (!found) return false
}
return true
}
if (a is Double && b is Double) {
return doubleEquals(a, b)
}
if (a is Float && b is Float) {
return floatEquals(a, b)
}
return a == b
}
fun deepHash(value: Any?): Int {
return when (value) {
null -> 0
is ByteArray -> value.contentHashCode()
is IntArray -> value.contentHashCode()
is LongArray -> value.contentHashCode()
is DoubleArray -> {
var result = 1
for (item in value) {
result = 31 * result + doubleHash(item)
}
result
}
is FloatArray -> {
var result = 1
for (item in value) {
result = 31 * result + floatHash(item)
}
result
}
is Array<*> -> {
var result = 1
for (item in value) {
result = 31 * result + deepHash(item)
}
result
}
is List<*> -> {
var result = 1
for (item in value) {
result = 31 * result + deepHash(item)
}
result
}
is Map<*, *> -> {
var result = 0
for (entry in value) {
result += ((deepHash(entry.key) * 31) xor deepHash(entry.value))
}
result
}
is Double -> doubleHash(value)
is Float -> floatHash(value)
else -> value.hashCode()
}
}
}
/**
@ -76,7 +190,7 @@ class FlutterError (
val code: String,
override val message: String? = null,
val details: Any? = null
) : Throwable()
) : RuntimeException()
/** Generated class from Pigeon that represents data sent in messages. */
data class ClientCertData (
@ -98,15 +212,22 @@ data class ClientCertData (
)
}
override fun equals(other: Any?): Boolean {
if (other !is ClientCertData) {
if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
return NetworkPigeonUtils.deepEquals(toList(), other.toList()) }
val other = other as ClientCertData
return NetworkPigeonUtils.deepEquals(this.data, other.data) && NetworkPigeonUtils.deepEquals(this.password, other.password)
}
override fun hashCode(): Int = toList().hashCode()
override fun hashCode(): Int {
var result = javaClass.hashCode()
result = 31 * result + NetworkPigeonUtils.deepHash(this.data)
result = 31 * result + NetworkPigeonUtils.deepHash(this.password)
return result
}
}
/** Generated class from Pigeon that represents data sent in messages. */
@ -135,15 +256,24 @@ data class ClientCertPrompt (
)
}
override fun equals(other: Any?): Boolean {
if (other !is ClientCertPrompt) {
if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
return NetworkPigeonUtils.deepEquals(toList(), other.toList()) }
val other = other as ClientCertPrompt
return NetworkPigeonUtils.deepEquals(this.title, other.title) && NetworkPigeonUtils.deepEquals(this.message, other.message) && NetworkPigeonUtils.deepEquals(this.cancel, other.cancel) && NetworkPigeonUtils.deepEquals(this.confirm, other.confirm)
}
override fun hashCode(): Int = toList().hashCode()
override fun hashCode(): Int {
var result = javaClass.hashCode()
result = 31 * result + NetworkPigeonUtils.deepHash(this.title)
result = 31 * result + NetworkPigeonUtils.deepHash(this.message)
result = 31 * result + NetworkPigeonUtils.deepHash(this.cancel)
result = 31 * result + NetworkPigeonUtils.deepHash(this.confirm)
return result
}
}
private open class NetworkPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {

View File

@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
@ -46,7 +46,7 @@ class FlutterError (
val code: String,
override val message: String? = null,
val details: Any? = null
) : Throwable()
) : RuntimeException()
private open class LocalImagesPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return super.readValueOfType(type, buffer)

View File

@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")

View File

@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
@ -34,36 +34,150 @@ private object MessagesPigeonUtils {
)
}
}
fun doubleEquals(a: Double, b: Double): Boolean {
// Normalize -0.0 to 0.0 and handle NaN equality.
return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN())
}
fun floatEquals(a: Float, b: Float): Boolean {
// Normalize -0.0 to 0.0 and handle NaN equality.
return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN())
}
fun doubleHash(d: Double): Int {
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
val normalized = if (d == 0.0) 0.0 else d
val bits = java.lang.Double.doubleToLongBits(normalized)
return (bits xor (bits ushr 32)).toInt()
}
fun floatHash(f: Float): Int {
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
val normalized = if (f == 0.0f) 0.0f else f
return java.lang.Float.floatToIntBits(normalized)
}
fun deepEquals(a: Any?, b: Any?): Boolean {
if (a === b) {
return true
}
if (a == null || b == null) {
return false
}
if (a is ByteArray && b is ByteArray) {
return a.contentEquals(b)
return a.contentEquals(b)
}
if (a is IntArray && b is IntArray) {
return a.contentEquals(b)
return a.contentEquals(b)
}
if (a is LongArray && b is LongArray) {
return a.contentEquals(b)
return a.contentEquals(b)
}
if (a is DoubleArray && b is DoubleArray) {
return a.contentEquals(b)
if (a.size != b.size) return false
for (i in a.indices) {
if (!doubleEquals(a[i], b[i])) return false
}
return true
}
if (a is FloatArray && b is FloatArray) {
if (a.size != b.size) return false
for (i in a.indices) {
if (!floatEquals(a[i], b[i])) return false
}
return true
}
if (a is Array<*> && b is Array<*>) {
return a.size == b.size &&
a.indices.all{ deepEquals(a[it], b[it]) }
if (a.size != b.size) return false
for (i in a.indices) {
if (!deepEquals(a[i], b[i])) return false
}
return true
}
if (a is List<*> && b is List<*>) {
return a.size == b.size &&
a.indices.all{ deepEquals(a[it], b[it]) }
if (a.size != b.size) return false
val iterA = a.iterator()
val iterB = b.iterator()
while (iterA.hasNext() && iterB.hasNext()) {
if (!deepEquals(iterA.next(), iterB.next())) return false
}
return true
}
if (a is Map<*, *> && b is Map<*, *>) {
return a.size == b.size && a.all {
(b as Map<Any?, Any?>).containsKey(it.key) &&
deepEquals(it.value, b[it.key])
if (a.size != b.size) return false
for (entry in a) {
val key = entry.key
var found = false
for (bEntry in b) {
if (deepEquals(key, bEntry.key)) {
if (deepEquals(entry.value, bEntry.value)) {
found = true
break
} else {
return false
}
}
}
if (!found) return false
}
return true
}
if (a is Double && b is Double) {
return doubleEquals(a, b)
}
if (a is Float && b is Float) {
return floatEquals(a, b)
}
return a == b
}
fun deepHash(value: Any?): Int {
return when (value) {
null -> 0
is ByteArray -> value.contentHashCode()
is IntArray -> value.contentHashCode()
is LongArray -> value.contentHashCode()
is DoubleArray -> {
var result = 1
for (item in value) {
result = 31 * result + doubleHash(item)
}
result
}
is FloatArray -> {
var result = 1
for (item in value) {
result = 31 * result + floatHash(item)
}
result
}
is Array<*> -> {
var result = 1
for (item in value) {
result = 31 * result + deepHash(item)
}
result
}
is List<*> -> {
var result = 1
for (item in value) {
result = 31 * result + deepHash(item)
}
result
}
is Map<*, *> -> {
var result = 0
for (entry in value) {
result += ((deepHash(entry.key) * 31) xor deepHash(entry.value))
}
result
}
is Double -> doubleHash(value)
is Float -> floatHash(value)
else -> value.hashCode()
}
}
}
/**
@ -76,7 +190,7 @@ class FlutterError (
val code: String,
override val message: String? = null,
val details: Any? = null
) : Throwable()
) : RuntimeException()
enum class PlatformAssetPlaybackStyle(val raw: Int) {
UNKNOWN(0),
@ -102,7 +216,7 @@ data class PlatformAsset (
val updatedAt: Long? = null,
val width: Long? = null,
val height: Long? = null,
val durationInSeconds: Long,
val durationMs: Long,
val orientation: Long,
val isFavorite: Boolean,
val adjustmentTime: Long? = null,
@ -120,14 +234,14 @@ data class PlatformAsset (
val updatedAt = pigeonVar_list[4] as Long?
val width = pigeonVar_list[5] as Long?
val height = pigeonVar_list[6] as Long?
val durationInSeconds = pigeonVar_list[7] as Long
val durationMs = pigeonVar_list[7] as Long
val orientation = pigeonVar_list[8] as Long
val isFavorite = pigeonVar_list[9] as Boolean
val adjustmentTime = pigeonVar_list[10] as Long?
val latitude = pigeonVar_list[11] as Double?
val longitude = pigeonVar_list[12] as Double?
val playbackStyle = pigeonVar_list[13] as PlatformAssetPlaybackStyle
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite, adjustmentTime, latitude, longitude, playbackStyle)
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationMs, orientation, isFavorite, adjustmentTime, latitude, longitude, playbackStyle)
}
}
fun toList(): List<Any?> {
@ -139,7 +253,7 @@ data class PlatformAsset (
updatedAt,
width,
height,
durationInSeconds,
durationMs,
orientation,
isFavorite,
adjustmentTime,
@ -149,15 +263,34 @@ data class PlatformAsset (
)
}
override fun equals(other: Any?): Boolean {
if (other !is PlatformAsset) {
if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
val other = other as PlatformAsset
return MessagesPigeonUtils.deepEquals(this.id, other.id) && MessagesPigeonUtils.deepEquals(this.name, other.name) && MessagesPigeonUtils.deepEquals(this.type, other.type) && MessagesPigeonUtils.deepEquals(this.createdAt, other.createdAt) && MessagesPigeonUtils.deepEquals(this.updatedAt, other.updatedAt) && MessagesPigeonUtils.deepEquals(this.width, other.width) && MessagesPigeonUtils.deepEquals(this.height, other.height) && MessagesPigeonUtils.deepEquals(this.durationMs, other.durationMs) && MessagesPigeonUtils.deepEquals(this.orientation, other.orientation) && MessagesPigeonUtils.deepEquals(this.isFavorite, other.isFavorite) && MessagesPigeonUtils.deepEquals(this.adjustmentTime, other.adjustmentTime) && MessagesPigeonUtils.deepEquals(this.latitude, other.latitude) && MessagesPigeonUtils.deepEquals(this.longitude, other.longitude) && MessagesPigeonUtils.deepEquals(this.playbackStyle, other.playbackStyle)
}
override fun hashCode(): Int = toList().hashCode()
override fun hashCode(): Int {
var result = javaClass.hashCode()
result = 31 * result + MessagesPigeonUtils.deepHash(this.id)
result = 31 * result + MessagesPigeonUtils.deepHash(this.name)
result = 31 * result + MessagesPigeonUtils.deepHash(this.type)
result = 31 * result + MessagesPigeonUtils.deepHash(this.createdAt)
result = 31 * result + MessagesPigeonUtils.deepHash(this.updatedAt)
result = 31 * result + MessagesPigeonUtils.deepHash(this.width)
result = 31 * result + MessagesPigeonUtils.deepHash(this.height)
result = 31 * result + MessagesPigeonUtils.deepHash(this.durationMs)
result = 31 * result + MessagesPigeonUtils.deepHash(this.orientation)
result = 31 * result + MessagesPigeonUtils.deepHash(this.isFavorite)
result = 31 * result + MessagesPigeonUtils.deepHash(this.adjustmentTime)
result = 31 * result + MessagesPigeonUtils.deepHash(this.latitude)
result = 31 * result + MessagesPigeonUtils.deepHash(this.longitude)
result = 31 * result + MessagesPigeonUtils.deepHash(this.playbackStyle)
return result
}
}
/** Generated class from Pigeon that represents data sent in messages. */
@ -189,15 +322,25 @@ data class PlatformAlbum (
)
}
override fun equals(other: Any?): Boolean {
if (other !is PlatformAlbum) {
if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
val other = other as PlatformAlbum
return MessagesPigeonUtils.deepEquals(this.id, other.id) && MessagesPigeonUtils.deepEquals(this.name, other.name) && MessagesPigeonUtils.deepEquals(this.updatedAt, other.updatedAt) && MessagesPigeonUtils.deepEquals(this.isCloud, other.isCloud) && MessagesPigeonUtils.deepEquals(this.assetCount, other.assetCount)
}
override fun hashCode(): Int = toList().hashCode()
override fun hashCode(): Int {
var result = javaClass.hashCode()
result = 31 * result + MessagesPigeonUtils.deepHash(this.id)
result = 31 * result + MessagesPigeonUtils.deepHash(this.name)
result = 31 * result + MessagesPigeonUtils.deepHash(this.updatedAt)
result = 31 * result + MessagesPigeonUtils.deepHash(this.isCloud)
result = 31 * result + MessagesPigeonUtils.deepHash(this.assetCount)
return result
}
}
/** Generated class from Pigeon that represents data sent in messages. */
@ -226,15 +369,24 @@ data class SyncDelta (
)
}
override fun equals(other: Any?): Boolean {
if (other !is SyncDelta) {
if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
val other = other as SyncDelta
return MessagesPigeonUtils.deepEquals(this.hasChanges, other.hasChanges) && MessagesPigeonUtils.deepEquals(this.updates, other.updates) && MessagesPigeonUtils.deepEquals(this.deletes, other.deletes) && MessagesPigeonUtils.deepEquals(this.assetAlbums, other.assetAlbums)
}
override fun hashCode(): Int = toList().hashCode()
override fun hashCode(): Int {
var result = javaClass.hashCode()
result = 31 * result + MessagesPigeonUtils.deepHash(this.hasChanges)
result = 31 * result + MessagesPigeonUtils.deepHash(this.updates)
result = 31 * result + MessagesPigeonUtils.deepHash(this.deletes)
result = 31 * result + MessagesPigeonUtils.deepHash(this.assetAlbums)
return result
}
}
/** Generated class from Pigeon that represents data sent in messages. */
@ -260,15 +412,23 @@ data class HashResult (
)
}
override fun equals(other: Any?): Boolean {
if (other !is HashResult) {
if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
val other = other as HashResult
return MessagesPigeonUtils.deepEquals(this.assetId, other.assetId) && MessagesPigeonUtils.deepEquals(this.error, other.error) && MessagesPigeonUtils.deepEquals(this.hash, other.hash)
}
override fun hashCode(): Int = toList().hashCode()
override fun hashCode(): Int {
var result = javaClass.hashCode()
result = 31 * result + MessagesPigeonUtils.deepHash(this.assetId)
result = 31 * result + MessagesPigeonUtils.deepHash(this.error)
result = 31 * result + MessagesPigeonUtils.deepHash(this.hash)
return result
}
}
/** Generated class from Pigeon that represents data sent in messages. */
@ -294,15 +454,23 @@ data class CloudIdResult (
)
}
override fun equals(other: Any?): Boolean {
if (other !is CloudIdResult) {
if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
val other = other as CloudIdResult
return MessagesPigeonUtils.deepEquals(this.assetId, other.assetId) && MessagesPigeonUtils.deepEquals(this.error, other.error) && MessagesPigeonUtils.deepEquals(this.cloudId, other.cloudId)
}
override fun hashCode(): Int = toList().hashCode()
override fun hashCode(): Int {
var result = javaClass.hashCode()
result = 31 * result + MessagesPigeonUtils.deepHash(this.assetId)
result = 31 * result + MessagesPigeonUtils.deepHash(this.error)
result = 31 * result + MessagesPigeonUtils.deepHash(this.cloudId)
return result
}
}
private open class MessagesPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
@ -344,7 +512,7 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
when (value) {
is PlatformAssetPlaybackStyle -> {
stream.write(129)
writeValue(stream, value.raw)
writeValue(stream, value.raw.toLong())
}
is PlatformAsset -> {
stream.write(130)

View File

@ -178,7 +178,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
val height = c.getInt(heightColumn).toLong()
// Duration is milliseconds
val duration = if (rawMediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0L
else c.getLong(durationColumn) / 1000
else c.getLong(durationColumn)
val orientation = c.getInt(orientationColumn)
val isFavorite = if (favoriteColumn == -1) false else c.getInt(favoriteColumn) != 0

View File

@ -46,6 +46,6 @@ material = { module = "com.google.android.material:material", version.ref = "mat
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
# TODO: update to version.ref = "kotlin" when background_downloader is removed
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "1.9.22" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "2.1.0" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }

View File

@ -21,7 +21,7 @@ plugins {
id "com.android.application" version "8.11.2" apply false
id "org.jetbrains.kotlin.android" version "2.2.20" apply false
// TODO: update to match kotlin version when background_downloader is removed
id "org.jetbrains.kotlin.plugin.serialization" version "1.9.22" apply false
id "org.jetbrains.kotlin.plugin.serialization" version "2.1.0" apply false
id "com.google.devtools.ksp" version "2.2.20-2.0.3" apply false
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -20,19 +20,21 @@ PODS:
- Flutter
- flutter_udid (0.0.1):
- Flutter
- SAMKeychain
- flutter_web_auth_2 (3.0.0):
- KeychainAccess
- flutter_web_auth_2 (5.0.0):
- Flutter
- fluttertoast (0.0.2):
- Flutter
- geolocator_apple (1.2.0):
- Flutter
- FlutterMacOS
- home_widget (0.0.1):
- Flutter
- image_picker_ios (0.0.1):
- Flutter
- integration_test (0.0.1):
- Flutter
- KeychainAccess (4.2.2)
- local_auth_darwin (0.0.1):
- Flutter
- FlutterMacOS
@ -44,19 +46,13 @@ PODS:
- Flutter
- network_info_plus (0.0.1):
- Flutter
- objective_c (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- photo_manager (3.7.1):
- photo_manager (3.9.0):
- Flutter
- FlutterMacOS
- SAMKeychain (1.5.3)
- share_handler_ios (0.0.14):
- Flutter
- share_handler_ios/share_handler_ios_models (= 0.0.14)
@ -70,28 +66,6 @@ PODS:
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- sqlite3 (3.49.2):
- sqlite3/common (= 3.49.2)
- sqlite3/common (3.49.2)
- sqlite3/dbstatvtab (3.49.2):
- sqlite3/common
- sqlite3/fts5 (3.49.2):
- sqlite3/common
- sqlite3/perf-threadsafe (3.49.2):
- sqlite3/common
- sqlite3/rtree (3.49.2):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.49.1)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/perf-threadsafe
- sqlite3/rtree
- url_launcher_ios (0.0.1):
- Flutter
- wakelock_plus (0.0.1):
@ -110,7 +84,7 @@ DEPENDENCIES:
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
- flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`)
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
- home_widget (from `.symlinks/plugins/home_widget/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
@ -118,25 +92,20 @@ DEPENDENCIES:
- maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`)
- native_video_player (from `.symlinks/plugins/native_video_player/ios`)
- network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
- objective_c (from `.symlinks/plugins/objective_c/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/darwin`)
- share_handler_ios (from `.symlinks/plugins/share_handler_ios/ios`)
- share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
SPEC REPOS:
trunk:
- KeychainAccess
- MapLibre
- SAMKeychain
- sqlite3
EXTERNAL SOURCES:
background_downloader:
@ -164,7 +133,7 @@ EXTERNAL SOURCES:
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
geolocator_apple:
:path: ".symlinks/plugins/geolocator_apple/ios"
:path: ".symlinks/plugins/geolocator_apple/darwin"
home_widget:
:path: ".symlinks/plugins/home_widget/ios"
image_picker_ios:
@ -179,16 +148,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/native_video_player/ios"
network_info_plus:
:path: ".symlinks/plugins/network_info_plus/ios"
objective_c:
:path: ".symlinks/plugins/objective_c/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
photo_manager:
:path: ".symlinks/plugins/photo_manager/ios"
:path: ".symlinks/plugins/photo_manager/darwin"
share_handler_ios:
:path: ".symlinks/plugins/share_handler_ios/ios"
share_handler_ios_models:
@ -197,10 +162,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
sqlite3_flutter_libs:
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
wakelock_plus:
@ -216,32 +177,27 @@ SPEC CHECKSUMS:
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
flutter_web_auth_2: 5c8d9dcd7848b5a9efb086d24e7a9adcae979c80
flutter_udid: 92a5d31fe0526b7b6002a2318df702e12e7eb300
flutter_web_auth_2: 646fc9df97a01c59e5eea99b237da2c6360f8439
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
geolocator_apple: 1560c3c875af2a412242c7a923e15d0d401966ff
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd
maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
photo_manager: 25fd77df14f4f0ba5ef99e2c61814dde77e2bceb
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1
sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
PODFILE CHECKSUM: 938abbae4114b9c2140c550a2a0d8f7c674f5dfe

View File

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@ -751,7 +751,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
@ -760,7 +760,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.121.0;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.profile;
PRODUCT_BUNDLE_IDENTIFIER = app.futo.immich.profile;
PRODUCT_NAME = "Immich-Profile";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@ -895,7 +895,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
@ -904,7 +904,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.121.0;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.vdebug;
PRODUCT_BUNDLE_IDENTIFIER = app.futo.immich.debug;
PRODUCT_NAME = "Immich-Debug";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@ -925,7 +925,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
@ -958,7 +958,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
DEVELOPMENT_TEAM = 2F67MQ8R79;
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
@ -975,7 +975,7 @@
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.vdebug.Widget;
PRODUCT_BUNDLE_IDENTIFIER = app.futo.immich.debug.Widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
@ -1001,7 +1001,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
DEVELOPMENT_TEAM = 2F67MQ8R79;
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
@ -1041,7 +1041,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
DEVELOPMENT_TEAM = 2F67MQ8R79;
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
@ -1057,7 +1057,7 @@
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.profile.Widget;
PRODUCT_BUNDLE_IDENTIFIER = app.futo.immich.profile.Widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
@ -1081,7 +1081,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
@ -1098,7 +1098,7 @@
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.vdebug.ShareExtension;
PRODUCT_BUNDLE_IDENTIFIER = app.futo.immich.debug.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
@ -1125,7 +1125,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
@ -1166,7 +1166,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
@ -1182,7 +1182,7 @@
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.profile.ShareExtension;
PRODUCT_BUNDLE_IDENTIFIER = app.futo.immich.profile.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;

View File

@ -1,12 +1,4 @@
import BackgroundTasks
import Flutter
import native_video_player
import network_info_plus
import path_provider_foundation
import permission_handler_apple
import photo_manager
import shared_preferences_foundation
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {

View File

@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
@ -32,7 +32,7 @@ private func wrapError(_ error: Any) -> [Any?] {
}
return [
"\(error)",
"\(type(of: error))",
"\(Swift.type(of: error))",
"Stacktrace: \(Thread.callStackSymbols)",
]
}
@ -50,6 +50,19 @@ private func nilOrValue<T>(_ value: Any?) -> T? {
return value as! T?
}
private func doubleEqualsBackgroundWorker(_ lhs: Double, _ rhs: Double) -> Bool {
return (lhs.isNaN && rhs.isNaN) || lhs == rhs
}
private func doubleHashBackgroundWorker(_ value: Double, _ hasher: inout Hasher) {
if value.isNaN {
hasher.combine(0x7FF8000000000000)
} else {
// Normalize -0.0 to 0.0
hasher.combine(value == 0 ? 0 : value)
}
}
func deepEqualsBackgroundWorker(_ lhs: Any?, _ rhs: Any?) -> Bool {
let cleanLhs = nilOrValue(lhs) as Any?
let cleanRhs = nilOrValue(rhs) as Any?
@ -60,59 +73,92 @@ func deepEqualsBackgroundWorker(_ lhs: Any?, _ rhs: Any?) -> Bool {
case (nil, _), (_, nil):
return false
case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs:
return true
case is (Void, Void):
return true
case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable):
return cleanLhsHashable == cleanRhsHashable
case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]):
guard cleanLhsArray.count == cleanRhsArray.count else { return false }
for (index, element) in cleanLhsArray.enumerated() {
if !deepEqualsBackgroundWorker(element, cleanRhsArray[index]) {
case (let lhsArray, let rhsArray) as ([Any?], [Any?]):
guard lhsArray.count == rhsArray.count else { return false }
for (index, element) in lhsArray.enumerated() {
if !deepEqualsBackgroundWorker(element, rhsArray[index]) {
return false
}
}
return true
case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false }
for (key, cleanLhsValue) in cleanLhsDictionary {
guard cleanRhsDictionary.index(forKey: key) != nil else { return false }
if !deepEqualsBackgroundWorker(cleanLhsValue, cleanRhsDictionary[key]!) {
case (let lhsArray, let rhsArray) as ([Double], [Double]):
guard lhsArray.count == rhsArray.count else { return false }
for (index, element) in lhsArray.enumerated() {
if !doubleEqualsBackgroundWorker(element, rhsArray[index]) {
return false
}
}
return true
case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
guard lhsDictionary.count == rhsDictionary.count else { return false }
for (lhsKey, lhsValue) in lhsDictionary {
var found = false
for (rhsKey, rhsValue) in rhsDictionary {
if deepEqualsBackgroundWorker(lhsKey, rhsKey) {
if deepEqualsBackgroundWorker(lhsValue, rhsValue) {
found = true
break
} else {
return false
}
}
}
if !found { return false }
}
return true
case (let lhs as Double, let rhs as Double):
return doubleEqualsBackgroundWorker(lhs, rhs)
case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable):
return lhsHashable == rhsHashable
default:
// Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue.
return false
}
}
func deepHashBackgroundWorker(value: Any?, hasher: inout Hasher) {
if let valueList = value as? [AnyHashable] {
for item in valueList { deepHashBackgroundWorker(value: item, hasher: &hasher) }
return
}
if let valueDict = value as? [AnyHashable: AnyHashable] {
for key in valueDict.keys {
hasher.combine(key)
deepHashBackgroundWorker(value: valueDict[key]!, hasher: &hasher)
let cleanValue = nilOrValue(value) as Any?
if let cleanValue = cleanValue {
if let doubleValue = cleanValue as? Double {
doubleHashBackgroundWorker(doubleValue, &hasher)
} else if let valueList = cleanValue as? [Any?] {
for item in valueList {
deepHashBackgroundWorker(value: item, hasher: &hasher)
}
} else if let valueList = cleanValue as? [Double] {
for item in valueList {
doubleHashBackgroundWorker(item, &hasher)
}
} else if let valueDict = cleanValue as? [AnyHashable: Any?] {
var result = 0
for (key, value) in valueDict {
var entryKeyHasher = Hasher()
deepHashBackgroundWorker(value: key, hasher: &entryKeyHasher)
var entryValueHasher = Hasher()
deepHashBackgroundWorker(value: value, hasher: &entryValueHasher)
result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize())
}
hasher.combine(result)
} else if let hashableValue = cleanValue as? AnyHashable {
hasher.combine(hashableValue)
} else {
hasher.combine(String(describing: cleanValue))
}
return
} else {
hasher.combine(0)
}
if let hashableValue = value as? AnyHashable {
hasher.combine(hashableValue.hashValue)
}
return hasher.combine(String(describing: value))
}
/// Generated class from Pigeon that represents data sent in messages.
struct BackgroundWorkerSettings: Hashable {
@ -137,9 +183,16 @@ struct BackgroundWorkerSettings: Hashable {
]
}
static func == (lhs: BackgroundWorkerSettings, rhs: BackgroundWorkerSettings) -> Bool {
return deepEqualsBackgroundWorker(lhs.toList(), rhs.toList()) }
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return deepEqualsBackgroundWorker(lhs.requiresCharging, rhs.requiresCharging) && deepEqualsBackgroundWorker(lhs.minimumDelaySeconds, rhs.minimumDelaySeconds)
}
func hash(into hasher: inout Hasher) {
deepHashBackgroundWorker(value: toList(), hasher: &hasher)
hasher.combine("BackgroundWorkerSettings")
deepHashBackgroundWorker(value: requiresCharging, hasher: &hasher)
deepHashBackgroundWorker(value: minimumDelaySeconds, hasher: &hasher)
}
}

View File

@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
@ -32,7 +32,7 @@ private func wrapError(_ error: Any) -> [Any?] {
}
return [
"\(error)",
"\(type(of: error))",
"\(Swift.type(of: error))",
"Stacktrace: \(Thread.callStackSymbols)",
]
}

View File

@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
@ -32,7 +32,7 @@ private func wrapError(_ error: Any) -> [Any?] {
}
return [
"\(error)",
"\(type(of: error))",
"\(Swift.type(of: error))",
"Stacktrace: \(Thread.callStackSymbols)",
]
}
@ -46,6 +46,19 @@ private func nilOrValue<T>(_ value: Any?) -> T? {
return value as! T?
}
private func doubleEqualsNetwork(_ lhs: Double, _ rhs: Double) -> Bool {
return (lhs.isNaN && rhs.isNaN) || lhs == rhs
}
private func doubleHashNetwork(_ value: Double, _ hasher: inout Hasher) {
if value.isNaN {
hasher.combine(0x7FF8000000000000)
} else {
// Normalize -0.0 to 0.0
hasher.combine(value == 0 ? 0 : value)
}
}
func deepEqualsNetwork(_ lhs: Any?, _ rhs: Any?) -> Bool {
let cleanLhs = nilOrValue(lhs) as Any?
let cleanRhs = nilOrValue(rhs) as Any?
@ -56,59 +69,92 @@ func deepEqualsNetwork(_ lhs: Any?, _ rhs: Any?) -> Bool {
case (nil, _), (_, nil):
return false
case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs:
return true
case is (Void, Void):
return true
case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable):
return cleanLhsHashable == cleanRhsHashable
case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]):
guard cleanLhsArray.count == cleanRhsArray.count else { return false }
for (index, element) in cleanLhsArray.enumerated() {
if !deepEqualsNetwork(element, cleanRhsArray[index]) {
case (let lhsArray, let rhsArray) as ([Any?], [Any?]):
guard lhsArray.count == rhsArray.count else { return false }
for (index, element) in lhsArray.enumerated() {
if !deepEqualsNetwork(element, rhsArray[index]) {
return false
}
}
return true
case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false }
for (key, cleanLhsValue) in cleanLhsDictionary {
guard cleanRhsDictionary.index(forKey: key) != nil else { return false }
if !deepEqualsNetwork(cleanLhsValue, cleanRhsDictionary[key]!) {
case (let lhsArray, let rhsArray) as ([Double], [Double]):
guard lhsArray.count == rhsArray.count else { return false }
for (index, element) in lhsArray.enumerated() {
if !doubleEqualsNetwork(element, rhsArray[index]) {
return false
}
}
return true
case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
guard lhsDictionary.count == rhsDictionary.count else { return false }
for (lhsKey, lhsValue) in lhsDictionary {
var found = false
for (rhsKey, rhsValue) in rhsDictionary {
if deepEqualsNetwork(lhsKey, rhsKey) {
if deepEqualsNetwork(lhsValue, rhsValue) {
found = true
break
} else {
return false
}
}
}
if !found { return false }
}
return true
case (let lhs as Double, let rhs as Double):
return doubleEqualsNetwork(lhs, rhs)
case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable):
return lhsHashable == rhsHashable
default:
// Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue.
return false
}
}
func deepHashNetwork(value: Any?, hasher: inout Hasher) {
if let valueList = value as? [AnyHashable] {
for item in valueList { deepHashNetwork(value: item, hasher: &hasher) }
return
}
if let valueDict = value as? [AnyHashable: AnyHashable] {
for key in valueDict.keys {
hasher.combine(key)
deepHashNetwork(value: valueDict[key]!, hasher: &hasher)
let cleanValue = nilOrValue(value) as Any?
if let cleanValue = cleanValue {
if let doubleValue = cleanValue as? Double {
doubleHashNetwork(doubleValue, &hasher)
} else if let valueList = cleanValue as? [Any?] {
for item in valueList {
deepHashNetwork(value: item, hasher: &hasher)
}
} else if let valueList = cleanValue as? [Double] {
for item in valueList {
doubleHashNetwork(item, &hasher)
}
} else if let valueDict = cleanValue as? [AnyHashable: Any?] {
var result = 0
for (key, value) in valueDict {
var entryKeyHasher = Hasher()
deepHashNetwork(value: key, hasher: &entryKeyHasher)
var entryValueHasher = Hasher()
deepHashNetwork(value: value, hasher: &entryValueHasher)
result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize())
}
hasher.combine(result)
} else if let hashableValue = cleanValue as? AnyHashable {
hasher.combine(hashableValue)
} else {
hasher.combine(String(describing: cleanValue))
}
return
} else {
hasher.combine(0)
}
if let hashableValue = value as? AnyHashable {
hasher.combine(hashableValue.hashValue)
}
return hasher.combine(String(describing: value))
}
/// Generated class from Pigeon that represents data sent in messages.
struct ClientCertData: Hashable {
@ -133,9 +179,16 @@ struct ClientCertData: Hashable {
]
}
static func == (lhs: ClientCertData, rhs: ClientCertData) -> Bool {
return deepEqualsNetwork(lhs.toList(), rhs.toList()) }
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return deepEqualsNetwork(lhs.data, rhs.data) && deepEqualsNetwork(lhs.password, rhs.password)
}
func hash(into hasher: inout Hasher) {
deepHashNetwork(value: toList(), hasher: &hasher)
hasher.combine("ClientCertData")
deepHashNetwork(value: data, hasher: &hasher)
deepHashNetwork(value: password, hasher: &hasher)
}
}
@ -170,9 +223,18 @@ struct ClientCertPrompt: Hashable {
]
}
static func == (lhs: ClientCertPrompt, rhs: ClientCertPrompt) -> Bool {
return deepEqualsNetwork(lhs.toList(), rhs.toList()) }
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return deepEqualsNetwork(lhs.title, rhs.title) && deepEqualsNetwork(lhs.message, rhs.message) && deepEqualsNetwork(lhs.cancel, rhs.cancel) && deepEqualsNetwork(lhs.confirm, rhs.confirm)
}
func hash(into hasher: inout Hasher) {
deepHashNetwork(value: toList(), hasher: &hasher)
hasher.combine("ClientCertPrompt")
deepHashNetwork(value: title, hasher: &hasher)
deepHashNetwork(value: message, hasher: &hasher)
deepHashNetwork(value: cancel, hasher: &hasher)
deepHashNetwork(value: confirm, hasher: &hasher)
}
}

View File

@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
@ -32,7 +32,7 @@ private func wrapError(_ error: Any) -> [Any?] {
}
return [
"\(error)",
"\(type(of: error))",
"\(Swift.type(of: error))",
"Stacktrace: \(Thread.callStackSymbols)",
]
}

View File

@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
@ -32,7 +32,7 @@ private func wrapError(_ error: Any) -> [Any?] {
}
return [
"\(error)",
"\(type(of: error))",
"\(Swift.type(of: error))",
"Stacktrace: \(Thread.callStackSymbols)",
]
}

View File

@ -150,6 +150,27 @@
<array>
<string>INSendMessageIntent</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
@ -176,27 +197,6 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>io.flutter.embedded_views_preview</key>

View File

@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
@ -50,7 +50,7 @@ private func wrapError(_ error: Any) -> [Any?] {
}
return [
"\(error)",
"\(type(of: error))",
"\(Swift.type(of: error))",
"Stacktrace: \(Thread.callStackSymbols)",
]
}
@ -64,6 +64,19 @@ private func nilOrValue<T>(_ value: Any?) -> T? {
return value as! T?
}
private func doubleEqualsMessages(_ lhs: Double, _ rhs: Double) -> Bool {
return (lhs.isNaN && rhs.isNaN) || lhs == rhs
}
private func doubleHashMessages(_ value: Double, _ hasher: inout Hasher) {
if value.isNaN {
hasher.combine(0x7FF8000000000000)
} else {
// Normalize -0.0 to 0.0
hasher.combine(value == 0 ? 0 : value)
}
}
func deepEqualsMessages(_ lhs: Any?, _ rhs: Any?) -> Bool {
let cleanLhs = nilOrValue(lhs) as Any?
let cleanRhs = nilOrValue(rhs) as Any?
@ -74,59 +87,92 @@ func deepEqualsMessages(_ lhs: Any?, _ rhs: Any?) -> Bool {
case (nil, _), (_, nil):
return false
case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs:
return true
case is (Void, Void):
return true
case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable):
return cleanLhsHashable == cleanRhsHashable
case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]):
guard cleanLhsArray.count == cleanRhsArray.count else { return false }
for (index, element) in cleanLhsArray.enumerated() {
if !deepEqualsMessages(element, cleanRhsArray[index]) {
case (let lhsArray, let rhsArray) as ([Any?], [Any?]):
guard lhsArray.count == rhsArray.count else { return false }
for (index, element) in lhsArray.enumerated() {
if !deepEqualsMessages(element, rhsArray[index]) {
return false
}
}
return true
case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false }
for (key, cleanLhsValue) in cleanLhsDictionary {
guard cleanRhsDictionary.index(forKey: key) != nil else { return false }
if !deepEqualsMessages(cleanLhsValue, cleanRhsDictionary[key]!) {
case (let lhsArray, let rhsArray) as ([Double], [Double]):
guard lhsArray.count == rhsArray.count else { return false }
for (index, element) in lhsArray.enumerated() {
if !doubleEqualsMessages(element, rhsArray[index]) {
return false
}
}
return true
case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
guard lhsDictionary.count == rhsDictionary.count else { return false }
for (lhsKey, lhsValue) in lhsDictionary {
var found = false
for (rhsKey, rhsValue) in rhsDictionary {
if deepEqualsMessages(lhsKey, rhsKey) {
if deepEqualsMessages(lhsValue, rhsValue) {
found = true
break
} else {
return false
}
}
}
if !found { return false }
}
return true
case (let lhs as Double, let rhs as Double):
return doubleEqualsMessages(lhs, rhs)
case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable):
return lhsHashable == rhsHashable
default:
// Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue.
return false
}
}
func deepHashMessages(value: Any?, hasher: inout Hasher) {
if let valueList = value as? [AnyHashable] {
for item in valueList { deepHashMessages(value: item, hasher: &hasher) }
return
}
if let valueDict = value as? [AnyHashable: AnyHashable] {
for key in valueDict.keys {
hasher.combine(key)
deepHashMessages(value: valueDict[key]!, hasher: &hasher)
let cleanValue = nilOrValue(value) as Any?
if let cleanValue = cleanValue {
if let doubleValue = cleanValue as? Double {
doubleHashMessages(doubleValue, &hasher)
} else if let valueList = cleanValue as? [Any?] {
for item in valueList {
deepHashMessages(value: item, hasher: &hasher)
}
} else if let valueList = cleanValue as? [Double] {
for item in valueList {
doubleHashMessages(item, &hasher)
}
} else if let valueDict = cleanValue as? [AnyHashable: Any?] {
var result = 0
for (key, value) in valueDict {
var entryKeyHasher = Hasher()
deepHashMessages(value: key, hasher: &entryKeyHasher)
var entryValueHasher = Hasher()
deepHashMessages(value: value, hasher: &entryValueHasher)
result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize())
}
hasher.combine(result)
} else if let hashableValue = cleanValue as? AnyHashable {
hasher.combine(hashableValue)
} else {
hasher.combine(String(describing: cleanValue))
}
return
} else {
hasher.combine(0)
}
if let hashableValue = value as? AnyHashable {
hasher.combine(hashableValue.hashValue)
}
return hasher.combine(String(describing: value))
}
enum PlatformAssetPlaybackStyle: Int {
case unknown = 0
@ -146,7 +192,7 @@ struct PlatformAsset: Hashable {
var updatedAt: Int64? = nil
var width: Int64? = nil
var height: Int64? = nil
var durationInSeconds: Int64
var durationMs: Int64
var orientation: Int64
var isFavorite: Bool
var adjustmentTime: Int64? = nil
@ -164,7 +210,7 @@ struct PlatformAsset: Hashable {
let updatedAt: Int64? = nilOrValue(pigeonVar_list[4])
let width: Int64? = nilOrValue(pigeonVar_list[5])
let height: Int64? = nilOrValue(pigeonVar_list[6])
let durationInSeconds = pigeonVar_list[7] as! Int64
let durationMs = pigeonVar_list[7] as! Int64
let orientation = pigeonVar_list[8] as! Int64
let isFavorite = pigeonVar_list[9] as! Bool
let adjustmentTime: Int64? = nilOrValue(pigeonVar_list[10])
@ -180,7 +226,7 @@ struct PlatformAsset: Hashable {
updatedAt: updatedAt,
width: width,
height: height,
durationInSeconds: durationInSeconds,
durationMs: durationMs,
orientation: orientation,
isFavorite: isFavorite,
adjustmentTime: adjustmentTime,
@ -198,7 +244,7 @@ struct PlatformAsset: Hashable {
updatedAt,
width,
height,
durationInSeconds,
durationMs,
orientation,
isFavorite,
adjustmentTime,
@ -208,9 +254,28 @@ struct PlatformAsset: Hashable {
]
}
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return deepEqualsMessages(lhs.id, rhs.id) && deepEqualsMessages(lhs.name, rhs.name) && deepEqualsMessages(lhs.type, rhs.type) && deepEqualsMessages(lhs.createdAt, rhs.createdAt) && deepEqualsMessages(lhs.updatedAt, rhs.updatedAt) && deepEqualsMessages(lhs.width, rhs.width) && deepEqualsMessages(lhs.height, rhs.height) && deepEqualsMessages(lhs.durationMs, rhs.durationMs) && deepEqualsMessages(lhs.orientation, rhs.orientation) && deepEqualsMessages(lhs.isFavorite, rhs.isFavorite) && deepEqualsMessages(lhs.adjustmentTime, rhs.adjustmentTime) && deepEqualsMessages(lhs.latitude, rhs.latitude) && deepEqualsMessages(lhs.longitude, rhs.longitude) && deepEqualsMessages(lhs.playbackStyle, rhs.playbackStyle)
}
func hash(into hasher: inout Hasher) {
deepHashMessages(value: toList(), hasher: &hasher)
hasher.combine("PlatformAsset")
deepHashMessages(value: id, hasher: &hasher)
deepHashMessages(value: name, hasher: &hasher)
deepHashMessages(value: type, hasher: &hasher)
deepHashMessages(value: createdAt, hasher: &hasher)
deepHashMessages(value: updatedAt, hasher: &hasher)
deepHashMessages(value: width, hasher: &hasher)
deepHashMessages(value: height, hasher: &hasher)
deepHashMessages(value: durationMs, hasher: &hasher)
deepHashMessages(value: orientation, hasher: &hasher)
deepHashMessages(value: isFavorite, hasher: &hasher)
deepHashMessages(value: adjustmentTime, hasher: &hasher)
deepHashMessages(value: latitude, hasher: &hasher)
deepHashMessages(value: longitude, hasher: &hasher)
deepHashMessages(value: playbackStyle, hasher: &hasher)
}
}
@ -249,9 +314,19 @@ struct PlatformAlbum: Hashable {
]
}
static func == (lhs: PlatformAlbum, rhs: PlatformAlbum) -> Bool {
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return deepEqualsMessages(lhs.id, rhs.id) && deepEqualsMessages(lhs.name, rhs.name) && deepEqualsMessages(lhs.updatedAt, rhs.updatedAt) && deepEqualsMessages(lhs.isCloud, rhs.isCloud) && deepEqualsMessages(lhs.assetCount, rhs.assetCount)
}
func hash(into hasher: inout Hasher) {
deepHashMessages(value: toList(), hasher: &hasher)
hasher.combine("PlatformAlbum")
deepHashMessages(value: id, hasher: &hasher)
deepHashMessages(value: name, hasher: &hasher)
deepHashMessages(value: updatedAt, hasher: &hasher)
deepHashMessages(value: isCloud, hasher: &hasher)
deepHashMessages(value: assetCount, hasher: &hasher)
}
}
@ -286,9 +361,18 @@ struct SyncDelta: Hashable {
]
}
static func == (lhs: SyncDelta, rhs: SyncDelta) -> Bool {
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return deepEqualsMessages(lhs.hasChanges, rhs.hasChanges) && deepEqualsMessages(lhs.updates, rhs.updates) && deepEqualsMessages(lhs.deletes, rhs.deletes) && deepEqualsMessages(lhs.assetAlbums, rhs.assetAlbums)
}
func hash(into hasher: inout Hasher) {
deepHashMessages(value: toList(), hasher: &hasher)
hasher.combine("SyncDelta")
deepHashMessages(value: hasChanges, hasher: &hasher)
deepHashMessages(value: updates, hasher: &hasher)
deepHashMessages(value: deletes, hasher: &hasher)
deepHashMessages(value: assetAlbums, hasher: &hasher)
}
}
@ -319,9 +403,17 @@ struct HashResult: Hashable {
]
}
static func == (lhs: HashResult, rhs: HashResult) -> Bool {
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return deepEqualsMessages(lhs.assetId, rhs.assetId) && deepEqualsMessages(lhs.error, rhs.error) && deepEqualsMessages(lhs.hash, rhs.hash)
}
func hash(into hasher: inout Hasher) {
deepHashMessages(value: toList(), hasher: &hasher)
hasher.combine("HashResult")
deepHashMessages(value: assetId, hasher: &hasher)
deepHashMessages(value: error, hasher: &hasher)
deepHashMessages(value: hash, hasher: &hasher)
}
}
@ -352,9 +444,17 @@ struct CloudIdResult: Hashable {
]
}
static func == (lhs: CloudIdResult, rhs: CloudIdResult) -> Bool {
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return deepEqualsMessages(lhs.assetId, rhs.assetId) && deepEqualsMessages(lhs.error, rhs.error) && deepEqualsMessages(lhs.cloudId, rhs.cloudId)
}
func hash(into hasher: inout Hasher) {
deepHashMessages(value: toList(), hasher: &hasher)
hasher.combine("CloudIdResult")
deepHashMessages(value: assetId, hasher: &hasher)
deepHashMessages(value: error, hasher: &hasher)
deepHashMessages(value: cloudId, hasher: &hasher)
}
}

View File

@ -171,7 +171,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
id: asset.localIdentifier,
name: "",
type: 0,
durationInSeconds: 0,
durationMs: 0,
orientation: 0,
isFavorite: false,
playbackStyle: .unknown

View File

@ -21,7 +21,7 @@ extension PHAsset {
updatedAt: modificationDate.map { Int64($0.timeIntervalSince1970) },
width: Int64(pixelWidth),
height: Int64(pixelHeight),
durationInSeconds: Int64(duration),
durationMs: Int64(duration * 1000),
orientation: 0,
isFavorite: isFavorite,
adjustmentTime: adjustmentTimestamp,

View File

@ -1,35 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>IntentsSupported</key>
<array>
<string>INSendMessageIntent</string>
</array>
<key>NSExtensionActivationRule</key>
<string>SUBQUERY ( extensionItems, $extensionItem, SUBQUERY ( $extensionItem.attachments,
<dict>
<key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>IntentsSupported</key>
<array>
<string>INSendMessageIntent</string>
</array>
<key>NSExtensionActivationRule</key>
<string>SUBQUERY ( extensionItems, $extensionItem, SUBQUERY ( $extensionItem.attachments,
$attachment, ( ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.file-url"
|| ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image" || ANY
$attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.text" || ANY
$attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.movie" || ANY
$attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" ) ).@count &gt; 0
).@count &gt; 0 </string>
<key>PHSupportedMediaTypes</key>
<array>
<string>Video</string>
<string>Image</string>
</array>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
</dict>
</dict>
</plist>
<key>PHSupportedMediaTypes</key>
<array>
<string>Video</string>
<string>Image</string>
</array>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
</dict>
</dict>
</plist>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.immich.share</string>
</array>
</dict>
</plist>
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.immich.share</string>
</array>
</dict>
</plist>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.immich.share</string>
</array>
</dict>
</plist>
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.immich.share</string>
</array>
</dict>
</plist>

View File

@ -8,6 +8,7 @@ enum AlbumUserRole {
// do not change this order!
editor,
viewer,
owner,
}
// Model for an album stored in the server

View File

@ -25,7 +25,7 @@ sealed class BaseAsset {
final DateTime updatedAt;
final int? width;
final int? height;
final int? durationInSeconds;
final int? durationMs;
final bool isFavorite;
final String? livePhotoVideoId;
final bool isEdited;
@ -38,7 +38,7 @@ sealed class BaseAsset {
required this.updatedAt,
this.width,
this.height,
this.durationInSeconds,
this.durationMs,
this.isFavorite = false,
this.livePhotoVideoId,
required this.isEdited,
@ -53,15 +53,17 @@ sealed class BaseAsset {
AssetPlaybackStyle get playbackStyle {
if (isVideo) return AssetPlaybackStyle.video;
if (isMotionPhoto) return AssetPlaybackStyle.livePhoto;
if (isImage && durationInSeconds != null && durationInSeconds! > 0) return AssetPlaybackStyle.imageAnimated;
if (isImage && durationMs != null && durationMs! > 0) {
return AssetPlaybackStyle.imageAnimated;
}
if (isImage) return AssetPlaybackStyle.image;
return AssetPlaybackStyle.unknown;
}
Duration get duration {
final durationInSeconds = this.durationInSeconds;
if (durationInSeconds != null) {
return Duration(seconds: durationInSeconds);
final durationMs = this.durationMs;
if (durationMs != null) {
return Duration(milliseconds: durationMs);
}
return const Duration();
}
@ -88,7 +90,7 @@ sealed class BaseAsset {
updatedAt: $updatedAt,
width: ${width ?? "<NA>"},
height: ${height ?? "<NA>"},
durationInSeconds: ${durationInSeconds ?? "<NA>"},
durationMs: ${durationMs ?? "<NA>"},
isFavorite: $isFavorite,
isEdited: $isEdited,
}''';
@ -104,7 +106,7 @@ sealed class BaseAsset {
updatedAt == other.updatedAt &&
width == other.width &&
height == other.height &&
durationInSeconds == other.durationInSeconds &&
durationMs == other.durationMs &&
isFavorite == other.isFavorite &&
isEdited == other.isEdited;
}
@ -119,7 +121,7 @@ sealed class BaseAsset {
updatedAt.hashCode ^
width.hashCode ^
height.hashCode ^
durationInSeconds.hashCode ^
durationMs.hashCode ^
isFavorite.hashCode ^
isEdited.hashCode;
}

View File

@ -23,7 +23,7 @@ class LocalAsset extends BaseAsset {
required super.updatedAt,
super.width,
super.height,
super.durationInSeconds,
super.durationMs,
super.isFavorite = false,
super.livePhotoVideoId,
this.orientation = 0,
@ -58,7 +58,7 @@ class LocalAsset extends BaseAsset {
updatedAt: $updatedAt,
width: ${width ?? "<NA>"},
height: ${height ?? "<NA>"},
durationInSeconds: ${durationInSeconds ?? "<NA>"},
durationMs: ${durationMs ?? "<NA>"},
playbackStyle: $playbackStyle,
remoteId: ${remoteId ?? "<NA>"},
cloudId: ${cloudId ?? "<NA>"},
@ -108,7 +108,7 @@ class LocalAsset extends BaseAsset {
DateTime? updatedAt,
int? width,
int? height,
int? durationInSeconds,
int? durationMs,
bool? isFavorite,
int? orientation,
AssetPlaybackStyle? playbackStyle,
@ -128,7 +128,7 @@ class LocalAsset extends BaseAsset {
updatedAt: updatedAt ?? this.updatedAt,
width: width ?? this.width,
height: height ?? this.height,
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
durationMs: durationMs ?? this.durationMs,
isFavorite: isFavorite ?? this.isFavorite,
orientation: orientation ?? this.orientation,
playbackStyle: playbackStyle ?? this.playbackStyle,

View File

@ -22,7 +22,7 @@ class RemoteAsset extends BaseAsset {
required super.updatedAt,
super.width,
super.height,
super.durationInSeconds,
super.durationMs,
super.isFavorite = false,
this.thumbHash,
this.visibility = AssetVisibility.timeline,
@ -57,7 +57,7 @@ class RemoteAsset extends BaseAsset {
updatedAt: $updatedAt,
width: ${width ?? "<NA>"},
height: ${height ?? "<NA>"},
durationInSeconds: ${durationInSeconds ?? "<NA>"},
durationMs: ${durationMs ?? "<NA>"},
localId: ${localId ?? "<NA>"},
isFavorite: $isFavorite,
thumbHash: ${thumbHash ?? "<NA>"},
@ -102,7 +102,7 @@ class RemoteAsset extends BaseAsset {
DateTime? updatedAt,
int? width,
int? height,
int? durationInSeconds,
int? durationMs,
bool? isFavorite,
String? thumbHash,
AssetVisibility? visibility,
@ -121,7 +121,7 @@ class RemoteAsset extends BaseAsset {
updatedAt: updatedAt ?? this.updatedAt,
width: width ?? this.width,
height: height ?? this.height,
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
durationMs: durationMs ?? this.durationMs,
isFavorite: isFavorite ?? this.isFavorite,
thumbHash: thumbHash ?? this.thumbHash,
visibility: visibility ?? this.visibility,
@ -146,7 +146,7 @@ class RemoteAssetExif extends RemoteAsset {
required super.updatedAt,
super.width,
super.height,
super.durationInSeconds,
super.durationMs,
super.isFavorite = false,
super.thumbHash,
super.visibility = AssetVisibility.timeline,
@ -178,7 +178,7 @@ class RemoteAssetExif extends RemoteAsset {
DateTime? updatedAt,
int? width,
int? height,
int? durationInSeconds,
int? durationMs,
bool? isFavorite,
String? thumbHash,
AssetVisibility? visibility,
@ -198,7 +198,7 @@ class RemoteAssetExif extends RemoteAsset {
updatedAt: updatedAt ?? this.updatedAt,
width: width ?? this.width,
height: height ?? this.height,
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
durationMs: durationMs ?? this.durationMs,
isFavorite: isFavorite ?? this.isFavorite,
thumbHash: thumbHash ?? this.thumbHash,
visibility: visibility ?? this.visibility,

View File

@ -29,7 +29,7 @@ class ExifInfo {
bool get hasCoordinates => latitude != null && longitude != null && latitude != 0 && longitude != 0;
String get exposureTime {
if (exposureSeconds == null) {
if (exposureSeconds == null || exposureSeconds! <= 0 || exposureSeconds!.isNaN) {
return "";
}
if (exposureSeconds! < 1) {

View File

@ -337,7 +337,7 @@ class LocalSyncService {
a.createdAt.isAtSameMomentAs(b.createdAt) &&
a.width == b.width &&
a.height == b.height &&
a.durationInSeconds == b.durationInSeconds;
a.durationMs == b.durationMs;
}
final firstAdjustment = a.adjustmentTime?.millisecondsSinceEpoch ?? 0;
@ -346,7 +346,7 @@ class LocalSyncService {
a.createdAt.isAtSameMomentAs(b.createdAt) &&
a.width == b.width &&
a.height == b.height &&
a.durationInSeconds == b.durationInSeconds &&
a.durationMs == b.durationMs &&
a.latitude == b.latitude &&
a.longitude == b.longitude;
}
@ -432,7 +432,7 @@ extension PlatformToLocalAsset on PlatformAsset {
updatedAt: tryFromSecondsSinceEpoch(updatedAt, isUtc: true) ?? DateTime.timestamp(),
width: width,
height: height,
durationInSeconds: durationInSeconds,
durationMs: durationMs,
isFavorite: isFavorite,
orientation: orientation,
playbackStyle: _toPlaybackStyle(playbackStyle),

View File

@ -7,8 +7,8 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
class RemoteAlbumService {
final DriftRemoteAlbumRepository _repository;
@ -28,10 +28,6 @@ class RemoteAlbumService {
return _repository.get(albumId);
}
Future<RemoteAlbum?> getByName(String albumName, String ownerId) {
return _repository.getByName(albumName, ownerId);
}
Future<List<RemoteAlbum>> sortAlbums(
List<RemoteAlbum> albums,
AlbumSortMode sortMode, {
@ -86,8 +82,18 @@ class RemoteAlbumService {
return filtered;
}
Future<RemoteAlbum> createAlbum({required String title, required List<String> assetIds, String? description}) async {
final album = await _albumApiRepository.createDriftAlbum(title, description: description, assetIds: assetIds);
Future<RemoteAlbum> createAlbum({
required String title,
required UserDto owner,
required List<String> assetIds,
String? description,
}) async {
final album = await _albumApiRepository.createDriftAlbum(
title,
owner,
description: description,
assetIds: assetIds,
);
await _repository.create(album, assetIds);
return album;
@ -101,8 +107,10 @@ class RemoteAlbumService {
bool? isActivityEnabled,
AlbumAssetOrder? order,
}) async {
final owner = await _repository.getOwner(albumId);
final updatedAlbum = await _albumApiRepository.updateAlbum(
albumId,
owner,
name: name,
description: description,
thumbnailAssetId: thumbnailAssetId,

View File

@ -1,8 +1,11 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/store.provider.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:logging/logging.dart';
@ -12,6 +15,7 @@ final syncLinkedAlbumServiceProvider = Provider(
ref.watch(localAlbumRepository),
ref.watch(remoteAlbumRepository),
ref.watch(driftAlbumApiRepositoryProvider),
ref.watch(storeServiceProvider),
),
);
@ -19,8 +23,14 @@ class SyncLinkedAlbumService {
final DriftLocalAlbumRepository _localAlbumRepository;
final DriftRemoteAlbumRepository _remoteAlbumRepository;
final DriftAlbumApiRepository _albumApiRepository;
final StoreService _storeService;
SyncLinkedAlbumService(this._localAlbumRepository, this._remoteAlbumRepository, this._albumApiRepository);
SyncLinkedAlbumService(
this._localAlbumRepository,
this._remoteAlbumRepository,
this._albumApiRepository,
this._storeService,
);
final _log = Logger("SyncLinkedAlbumService");
@ -103,7 +113,11 @@ class SyncLinkedAlbumService {
/// Creates a new remote album and links it to the local album
Future<void> _createAndLinkNewRemoteAlbum(LocalAlbum localAlbum) async {
dPrint(() => "Creating new remote album for local album: ${localAlbum.name}");
final newRemoteAlbum = await _albumApiRepository.createDriftAlbum(localAlbum.name, assetIds: []);
final newRemoteAlbum = await _albumApiRepository.createDriftAlbum(
localAlbum.name,
_storeService.get(StoreKey.currentUser),
assetIds: [],
);
await _remoteAlbumRepository.create(newRemoteAlbum, []);
return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, newRemoteAlbum.id);
}

View File

@ -3,6 +3,7 @@
import 'dart:async';
import 'dart:convert';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
@ -191,17 +192,22 @@ class SyncStreamService {
case SyncEntityType.assetV1:
final remoteSyncAssets = data.cast<SyncAssetV1>();
await _syncStreamRepository.updateAssetsV1(remoteSyncAssets);
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
final hasPermission = await _localFilesManager.hasManageMediaPermission();
if (hasPermission) {
await _handleRemoteTrashed(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.checksum));
await _runWithManageMediaPermission(
logContext: "Trashed Assets",
action: () async {
await _handleRemoteDeleted(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.id));
await _applyRemoteRestoreToLocal();
} else {
_logger.warning("sync Trashed Assets cannot proceed because MANAGE_MEDIA permission is missing");
}
}
},
);
return;
case SyncEntityType.assetDeleteV1:
await _runWithManageMediaPermission(
logContext: "Deleted Assets",
action: () async {
final remoteSyncAssets = data.cast<SyncAssetDeleteV1>();
await _handleRemoteDeleted(remoteSyncAssets.map((e) => e.assetId));
},
);
return _syncStreamRepository.deleteAssetsV1(data.cast());
case SyncEntityType.assetExifV1:
return _syncStreamRepository.updateAssetsExifV1(data.cast());
@ -225,6 +231,8 @@ class SyncStreamService {
return _syncStreamRepository.updateAssetsExifV1(data.cast(), debugLabel: 'partner backfill');
case SyncEntityType.albumV1:
return _syncStreamRepository.updateAlbumsV1(data.cast());
case SyncEntityType.albumV2:
return _syncStreamRepository.updateAlbumsV2(data.cast());
case SyncEntityType.albumDeleteV1:
return _syncStreamRepository.deleteAlbumsV1(data.cast());
case SyncEntityType.albumUserV1:
@ -380,28 +388,32 @@ class SyncStreamService {
}
}
Future<void> _handleRemoteTrashed(Iterable<String> checksums) async {
if (checksums.isEmpty) {
Future<void> _handleRemoteDeleted(Iterable<String> remoteIds) async {
if (remoteIds.isEmpty) {
return Future.value();
} else {
final localAssetsToTrash = await _localAssetRepository.getAssetsFromBackupAlbums(checksums);
final localAssetsToTrash = await _localAssetRepository.getAssetsFromBackupAlbums(remoteIds);
if (localAssetsToTrash.isNotEmpty) {
final mediaUrls = await Future.wait(
localAssetsToTrash.values
.expand((e) => e)
.map((localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl())),
);
_logger.info("Moving to trash ${mediaUrls.join(", ")} assets");
final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
if (result) {
await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash);
}
await _trashLocalAssets(localAssetsToTrash);
} else {
_logger.info("No assets found in backup-enabled albums for assets: $checksums");
_logger.info("No assets found in backup-enabled albums for remote assets: $remoteIds");
}
}
}
Future<void> _trashLocalAssets(Map<String, List<LocalAsset>> localAssetsToTrash) async {
final mediaUrls = await Future.wait(
localAssetsToTrash.values
.expand((e) => e)
.map((localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl())),
);
_logger.info("Moving to trash ${mediaUrls.join(", ")} assets");
final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
if (result) {
await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash);
}
}
Future<void> _applyRemoteRestoreToLocal() async {
final assetsToRestore = await _trashedLocalAssetRepository.getToRestore();
if (assetsToRestore.isNotEmpty) {
@ -411,4 +423,21 @@ class SyncStreamService {
_logger.info("No remote assets found for restoration");
}
}
Future<void> _runWithManageMediaPermission({
required String logContext,
required Future<void> Function() action,
}) async {
if (!CurrentPlatform.isAndroid || !Store.get(StoreKey.manageLocalMediaAndroid, false)) {
return;
}
final hasPermission = await _localFilesManager.hasManageMediaPermission();
if (!hasPermission) {
_logger.warning("sync $logContext cannot proceed because MANAGE_MEDIA permission is missing");
return;
}
await action();
}
}

View File

@ -14,7 +14,7 @@ extension DTOToAsset on api.AssetResponseDto {
updatedAt: updatedAt,
ownerId: ownerId,
visibility: visibility.toAssetVisibility(),
durationInSeconds: duration?.toDuration()?.inSeconds ?? 0,
durationMs: duration?.toDuration()?.inMilliseconds ?? 0,
height: height?.toInt(),
width: width?.toInt(),
isFavorite: isFavorite,
@ -36,7 +36,7 @@ extension DTOToAsset on api.AssetResponseDto {
updatedAt: updatedAt,
ownerId: ownerId,
visibility: visibility.toAssetVisibility(),
durationInSeconds: duration?.toDuration()?.inSeconds ?? 0,
durationMs: duration?.toDuration()?.inMilliseconds ?? 0,
height: height?.toInt(),
width: width?.toInt(),
isFavorite: isFavorite,

View File

@ -7,11 +7,16 @@ extension StringExtension on String {
}
extension DurationExtension on String {
/// Parses and returns the string of format HH:MM:SS as a duration object else null
/// Parses and returns the string of format HH:MM:SS.ffffff as a duration object else null
Duration? toDuration() {
try {
final parts = split(':').map((e) => double.parse(e).toInt()).toList(growable: false);
return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);
final parts = split(':');
final hours = double.parse(parts[0]).toInt();
final minutes = double.parse(parts[1]).toInt();
final secondsParts = parts[2].split('.');
final seconds = int.parse(secondsParts[0]);
final milliseconds = secondsParts.length > 1 ? (double.parse('0.${secondsParts[1]}') * 1000).round() : 0;
return Duration(hours: hours, minutes: minutes, seconds: seconds, milliseconds: milliseconds);
} catch (e) {
return null;
}

View File

@ -39,7 +39,7 @@ extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
type: type,
createdAt: createdAt,
updatedAt: updatedAt,
durationInSeconds: durationInSeconds,
durationMs: durationMs,
isFavorite: isFavorite,
height: height,
width: width,

View File

@ -16,7 +16,7 @@ typedef $$LocalAssetEntityTableCreateCompanionBuilder =
i0.Value<DateTime> updatedAt,
i0.Value<int?> width,
i0.Value<int?> height,
i0.Value<int?> durationInSeconds,
i0.Value<int?> durationMs,
required String id,
i0.Value<String?> checksum,
i0.Value<bool> isFavorite,
@ -35,7 +35,7 @@ typedef $$LocalAssetEntityTableUpdateCompanionBuilder =
i0.Value<DateTime> updatedAt,
i0.Value<int?> width,
i0.Value<int?> height,
i0.Value<int?> durationInSeconds,
i0.Value<int?> durationMs,
i0.Value<String> id,
i0.Value<String?> checksum,
i0.Value<bool> isFavorite,
@ -87,8 +87,8 @@ class $$LocalAssetEntityTableFilterComposer
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<int> get durationInSeconds => $composableBuilder(
column: $table.durationInSeconds,
i0.ColumnFilters<int> get durationMs => $composableBuilder(
column: $table.durationMs,
builder: (column) => i0.ColumnFilters(column),
);
@ -182,8 +182,8 @@ class $$LocalAssetEntityTableOrderingComposer
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<int> get durationInSeconds => $composableBuilder(
column: $table.durationInSeconds,
i0.ColumnOrderings<int> get durationMs => $composableBuilder(
column: $table.durationMs,
builder: (column) => i0.ColumnOrderings(column),
);
@ -260,8 +260,8 @@ class $$LocalAssetEntityTableAnnotationComposer
i0.GeneratedColumn<int> get height =>
$composableBuilder(column: $table.height, builder: (column) => column);
i0.GeneratedColumn<int> get durationInSeconds => $composableBuilder(
column: $table.durationInSeconds,
i0.GeneratedColumn<int> get durationMs => $composableBuilder(
column: $table.durationMs,
builder: (column) => column,
);
@ -348,7 +348,7 @@ class $$LocalAssetEntityTableTableManager
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<int?> width = const i0.Value.absent(),
i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
i0.Value<int?> durationMs = const i0.Value.absent(),
i0.Value<String> id = const i0.Value.absent(),
i0.Value<String?> checksum = const i0.Value.absent(),
i0.Value<bool> isFavorite = const i0.Value.absent(),
@ -366,7 +366,7 @@ class $$LocalAssetEntityTableTableManager
updatedAt: updatedAt,
width: width,
height: height,
durationInSeconds: durationInSeconds,
durationMs: durationMs,
id: id,
checksum: checksum,
isFavorite: isFavorite,
@ -385,7 +385,7 @@ class $$LocalAssetEntityTableTableManager
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<int?> width = const i0.Value.absent(),
i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
i0.Value<int?> durationMs = const i0.Value.absent(),
required String id,
i0.Value<String?> checksum = const i0.Value.absent(),
i0.Value<bool> isFavorite = const i0.Value.absent(),
@ -403,7 +403,7 @@ class $$LocalAssetEntityTableTableManager
updatedAt: updatedAt,
width: width,
height: height,
durationInSeconds: durationInSeconds,
durationMs: durationMs,
id: id,
checksum: checksum,
isFavorite: isFavorite,
@ -522,17 +522,17 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
);
static const i0.VerificationMeta _durationInSecondsMeta =
const i0.VerificationMeta('durationInSeconds');
static const i0.VerificationMeta _durationMsMeta = const i0.VerificationMeta(
'durationMs',
);
@override
late final i0.GeneratedColumn<int> durationInSeconds =
i0.GeneratedColumn<int>(
'duration_in_seconds',
aliasedName,
true,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
);
late final i0.GeneratedColumn<int> durationMs = i0.GeneratedColumn<int>(
'duration_ms',
aliasedName,
true,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
);
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
@override
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
@ -645,7 +645,7 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
updatedAt,
width,
height,
durationInSeconds,
durationMs,
id,
checksum,
isFavorite,
@ -700,13 +700,10 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
height.isAcceptableOrUnknown(data['height']!, _heightMeta),
);
}
if (data.containsKey('duration_in_seconds')) {
if (data.containsKey('duration_ms')) {
context.handle(
_durationInSecondsMeta,
durationInSeconds.isAcceptableOrUnknown(
data['duration_in_seconds']!,
_durationInSecondsMeta,
),
_durationMsMeta,
durationMs.isAcceptableOrUnknown(data['duration_ms']!, _durationMsMeta),
);
}
if (data.containsKey('id')) {
@ -800,9 +797,9 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
i0.DriftSqlType.int,
data['${effectivePrefix}height'],
),
durationInSeconds: attachedDatabase.typeMapping.read(
durationMs: attachedDatabase.typeMapping.read(
i0.DriftSqlType.int,
data['${effectivePrefix}duration_in_seconds'],
data['${effectivePrefix}duration_ms'],
),
id: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
@ -870,7 +867,7 @@ class LocalAssetEntityData extends i0.DataClass
final DateTime updatedAt;
final int? width;
final int? height;
final int? durationInSeconds;
final int? durationMs;
final String id;
final String? checksum;
final bool isFavorite;
@ -887,7 +884,7 @@ class LocalAssetEntityData extends i0.DataClass
required this.updatedAt,
this.width,
this.height,
this.durationInSeconds,
this.durationMs,
required this.id,
this.checksum,
required this.isFavorite,
@ -915,8 +912,8 @@ class LocalAssetEntityData extends i0.DataClass
if (!nullToAbsent || height != null) {
map['height'] = i0.Variable<int>(height);
}
if (!nullToAbsent || durationInSeconds != null) {
map['duration_in_seconds'] = i0.Variable<int>(durationInSeconds);
if (!nullToAbsent || durationMs != null) {
map['duration_ms'] = i0.Variable<int>(durationMs);
}
map['id'] = i0.Variable<String>(id);
if (!nullToAbsent || checksum != null) {
@ -958,7 +955,7 @@ class LocalAssetEntityData extends i0.DataClass
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
width: serializer.fromJson<int?>(json['width']),
height: serializer.fromJson<int?>(json['height']),
durationInSeconds: serializer.fromJson<int?>(json['durationInSeconds']),
durationMs: serializer.fromJson<int?>(json['durationMs']),
id: serializer.fromJson<String>(json['id']),
checksum: serializer.fromJson<String?>(json['checksum']),
isFavorite: serializer.fromJson<bool>(json['isFavorite']),
@ -984,7 +981,7 @@ class LocalAssetEntityData extends i0.DataClass
'updatedAt': serializer.toJson<DateTime>(updatedAt),
'width': serializer.toJson<int?>(width),
'height': serializer.toJson<int?>(height),
'durationInSeconds': serializer.toJson<int?>(durationInSeconds),
'durationMs': serializer.toJson<int?>(durationMs),
'id': serializer.toJson<String>(id),
'checksum': serializer.toJson<String?>(checksum),
'isFavorite': serializer.toJson<bool>(isFavorite),
@ -1006,7 +1003,7 @@ class LocalAssetEntityData extends i0.DataClass
DateTime? updatedAt,
i0.Value<int?> width = const i0.Value.absent(),
i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
i0.Value<int?> durationMs = const i0.Value.absent(),
String? id,
i0.Value<String?> checksum = const i0.Value.absent(),
bool? isFavorite,
@ -1023,9 +1020,7 @@ class LocalAssetEntityData extends i0.DataClass
updatedAt: updatedAt ?? this.updatedAt,
width: width.present ? width.value : this.width,
height: height.present ? height.value : this.height,
durationInSeconds: durationInSeconds.present
? durationInSeconds.value
: this.durationInSeconds,
durationMs: durationMs.present ? durationMs.value : this.durationMs,
id: id ?? this.id,
checksum: checksum.present ? checksum.value : this.checksum,
isFavorite: isFavorite ?? this.isFavorite,
@ -1046,9 +1041,9 @@ class LocalAssetEntityData extends i0.DataClass
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
width: data.width.present ? data.width.value : this.width,
height: data.height.present ? data.height.value : this.height,
durationInSeconds: data.durationInSeconds.present
? data.durationInSeconds.value
: this.durationInSeconds,
durationMs: data.durationMs.present
? data.durationMs.value
: this.durationMs,
id: data.id.present ? data.id.value : this.id,
checksum: data.checksum.present ? data.checksum.value : this.checksum,
isFavorite: data.isFavorite.present
@ -1078,7 +1073,7 @@ class LocalAssetEntityData extends i0.DataClass
..write('updatedAt: $updatedAt, ')
..write('width: $width, ')
..write('height: $height, ')
..write('durationInSeconds: $durationInSeconds, ')
..write('durationMs: $durationMs, ')
..write('id: $id, ')
..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite, ')
@ -1100,7 +1095,7 @@ class LocalAssetEntityData extends i0.DataClass
updatedAt,
width,
height,
durationInSeconds,
durationMs,
id,
checksum,
isFavorite,
@ -1121,7 +1116,7 @@ class LocalAssetEntityData extends i0.DataClass
other.updatedAt == this.updatedAt &&
other.width == this.width &&
other.height == this.height &&
other.durationInSeconds == this.durationInSeconds &&
other.durationMs == this.durationMs &&
other.id == this.id &&
other.checksum == this.checksum &&
other.isFavorite == this.isFavorite &&
@ -1141,7 +1136,7 @@ class LocalAssetEntityCompanion
final i0.Value<DateTime> updatedAt;
final i0.Value<int?> width;
final i0.Value<int?> height;
final i0.Value<int?> durationInSeconds;
final i0.Value<int?> durationMs;
final i0.Value<String> id;
final i0.Value<String?> checksum;
final i0.Value<bool> isFavorite;
@ -1158,7 +1153,7 @@ class LocalAssetEntityCompanion
this.updatedAt = const i0.Value.absent(),
this.width = const i0.Value.absent(),
this.height = const i0.Value.absent(),
this.durationInSeconds = const i0.Value.absent(),
this.durationMs = const i0.Value.absent(),
this.id = const i0.Value.absent(),
this.checksum = const i0.Value.absent(),
this.isFavorite = const i0.Value.absent(),
@ -1176,7 +1171,7 @@ class LocalAssetEntityCompanion
this.updatedAt = const i0.Value.absent(),
this.width = const i0.Value.absent(),
this.height = const i0.Value.absent(),
this.durationInSeconds = const i0.Value.absent(),
this.durationMs = const i0.Value.absent(),
required String id,
this.checksum = const i0.Value.absent(),
this.isFavorite = const i0.Value.absent(),
@ -1196,7 +1191,7 @@ class LocalAssetEntityCompanion
i0.Expression<DateTime>? updatedAt,
i0.Expression<int>? width,
i0.Expression<int>? height,
i0.Expression<int>? durationInSeconds,
i0.Expression<int>? durationMs,
i0.Expression<String>? id,
i0.Expression<String>? checksum,
i0.Expression<bool>? isFavorite,
@ -1214,7 +1209,7 @@ class LocalAssetEntityCompanion
if (updatedAt != null) 'updated_at': updatedAt,
if (width != null) 'width': width,
if (height != null) 'height': height,
if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds,
if (durationMs != null) 'duration_ms': durationMs,
if (id != null) 'id': id,
if (checksum != null) 'checksum': checksum,
if (isFavorite != null) 'is_favorite': isFavorite,
@ -1234,7 +1229,7 @@ class LocalAssetEntityCompanion
i0.Value<DateTime>? updatedAt,
i0.Value<int?>? width,
i0.Value<int?>? height,
i0.Value<int?>? durationInSeconds,
i0.Value<int?>? durationMs,
i0.Value<String>? id,
i0.Value<String?>? checksum,
i0.Value<bool>? isFavorite,
@ -1252,7 +1247,7 @@ class LocalAssetEntityCompanion
updatedAt: updatedAt ?? this.updatedAt,
width: width ?? this.width,
height: height ?? this.height,
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
durationMs: durationMs ?? this.durationMs,
id: id ?? this.id,
checksum: checksum ?? this.checksum,
isFavorite: isFavorite ?? this.isFavorite,
@ -1288,8 +1283,8 @@ class LocalAssetEntityCompanion
if (height.present) {
map['height'] = i0.Variable<int>(height.value);
}
if (durationInSeconds.present) {
map['duration_in_seconds'] = i0.Variable<int>(durationInSeconds.value);
if (durationMs.present) {
map['duration_ms'] = i0.Variable<int>(durationMs.value);
}
if (id.present) {
map['id'] = i0.Variable<String>(id.value);
@ -1334,7 +1329,7 @@ class LocalAssetEntityCompanion
..write('updatedAt: $updatedAt, ')
..write('width: $width, ')
..write('height: $height, ')
..write('durationInSeconds: $durationInSeconds, ')
..write('durationMs: $durationMs, ')
..write('id: $id, ')
..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite, ')

View File

@ -14,7 +14,7 @@ SELECT
rae.updated_at,
rae.width,
rae.height,
rae.duration_in_seconds,
rae.duration_ms,
rae.is_favorite,
rae.thumb_hash,
rae.checksum,
@ -52,7 +52,7 @@ SELECT
lae.updated_at,
lae.width,
lae.height,
lae.duration_in_seconds,
lae.duration_ms,
lae.is_favorite,
NULL as thumb_hash,
lae.checksum,

View File

@ -29,7 +29,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
);
$arrayStartIndex += generatedlimit.amountOfVariables;
return customSelect(
'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime, rae.is_edited, 0 AS playback_style FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time, 0 AS is_edited, lae.playback_style FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_ms, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime, rae.is_edited, 0 AS playback_style FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_ms, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time, 0 AS is_edited, lae.playback_style FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
variables: [
for (var $ in userIds) i0.Variable<String>($),
...generatedlimit.introducedVariables,
@ -54,7 +54,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
updatedAt: row.read<DateTime>('updated_at'),
width: row.readNullable<int>('width'),
height: row.readNullable<int>('height'),
durationInSeconds: row.readNullable<int>('duration_in_seconds'),
durationMs: row.readNullable<int>('duration_ms'),
isFavorite: row.read<bool>('is_favorite'),
thumbHash: row.readNullable<String>('thumb_hash'),
checksum: row.readNullable<String>('checksum'),
@ -127,7 +127,7 @@ class MergedAssetResult {
final DateTime updatedAt;
final int? width;
final int? height;
final int? durationInSeconds;
final int? durationMs;
final bool isFavorite;
final String? thumbHash;
final String? checksum;
@ -150,7 +150,7 @@ class MergedAssetResult {
required this.updatedAt,
this.width,
this.height,
this.durationInSeconds,
this.durationMs,
required this.isFavorite,
this.thumbHash,
this.checksum,

View File

@ -1,10 +1,8 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)')
class RemoteAlbumEntity extends Table with DriftDefaultsMixin {
const RemoteAlbumEntity();
@ -18,8 +16,6 @@ class RemoteAlbumEntity extends Table with DriftDefaultsMixin {
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
TextColumn get ownerId => text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
TextColumn get thumbnailAssetId =>
text().references(RemoteAssetEntity, #id, onDelete: KeyAction.setNull).nullable()();

View File

@ -7,11 +7,9 @@ import 'package:immich_mobile/domain/models/album/album.model.dart' as i2;
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'
as i3;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
as i5;
import 'package:drift/internal/modular.dart' as i6;
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
as i7;
typedef $$RemoteAlbumEntityTableCreateCompanionBuilder =
i1.RemoteAlbumEntityCompanion Function({
@ -20,7 +18,6 @@ typedef $$RemoteAlbumEntityTableCreateCompanionBuilder =
i0.Value<String> description,
i0.Value<DateTime> createdAt,
i0.Value<DateTime> updatedAt,
required String ownerId,
i0.Value<String?> thumbnailAssetId,
i0.Value<bool> isActivityEnabled,
required i2.AlbumAssetOrder order,
@ -32,7 +29,6 @@ typedef $$RemoteAlbumEntityTableUpdateCompanionBuilder =
i0.Value<String> description,
i0.Value<DateTime> createdAt,
i0.Value<DateTime> updatedAt,
i0.Value<String> ownerId,
i0.Value<String?> thumbnailAssetId,
i0.Value<bool> isActivityEnabled,
i0.Value<i2.AlbumAssetOrder> order,
@ -51,42 +47,10 @@ final class $$RemoteAlbumEntityTableReferences
super.$_typedResult,
);
static i5.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
.createAlias(
i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
.resultSet<i1.$RemoteAlbumEntityTable>('remote_album_entity')
.ownerId,
i6.ReadDatabaseContainer(
db,
).resultSet<i5.$UserEntityTable>('user_entity').id,
),
);
i5.$$UserEntityTableProcessedTableManager get ownerId {
final $_column = $_itemColumn<String>('owner_id')!;
final manager = i5
.$$UserEntityTableTableManager(
$_db,
i6.ReadDatabaseContainer(
$_db,
).resultSet<i5.$UserEntityTable>('user_entity'),
)
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_ownerIdTable($_db));
if (item == null) return manager;
return i0.ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]),
);
}
static i7.$RemoteAssetEntityTable _thumbnailAssetIdTable(
static i5.$RemoteAssetEntityTable _thumbnailAssetIdTable(
i0.GeneratedDatabase db,
) => i6.ReadDatabaseContainer(db)
.resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity')
.resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity')
.createAlias(
i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
@ -94,19 +58,19 @@ final class $$RemoteAlbumEntityTableReferences
.thumbnailAssetId,
i6.ReadDatabaseContainer(
db,
).resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity').id,
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity').id,
),
);
i7.$$RemoteAssetEntityTableProcessedTableManager? get thumbnailAssetId {
i5.$$RemoteAssetEntityTableProcessedTableManager? get thumbnailAssetId {
final $_column = $_itemColumn<String>('thumbnail_asset_id');
if ($_column == null) return null;
final manager = i7
final manager = i5
.$$RemoteAssetEntityTableTableManager(
$_db,
i6.ReadDatabaseContainer(
$_db,
).resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity'),
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
)
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_thumbnailAssetIdTable($_db));
@ -162,51 +126,24 @@ class $$RemoteAlbumEntityTableFilterComposer
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
);
i5.$$UserEntityTableFilterComposer get ownerId {
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.ownerId,
referencedTable: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => i5.$$UserEntityTableFilterComposer(
$db: $db,
$table: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return composer;
}
i7.$$RemoteAssetEntityTableFilterComposer get thumbnailAssetId {
final i7.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
i5.$$RemoteAssetEntityTableFilterComposer get thumbnailAssetId {
final i5.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.thumbnailAssetId,
referencedTable: i6.ReadDatabaseContainer(
$db,
).resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity'),
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => i7.$$RemoteAssetEntityTableFilterComposer(
}) => i5.$$RemoteAssetEntityTableFilterComposer(
$db: $db,
$table: i6.ReadDatabaseContainer(
$db,
).resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity'),
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@ -261,52 +198,25 @@ class $$RemoteAlbumEntityTableOrderingComposer
builder: (column) => i0.ColumnOrderings(column),
);
i5.$$UserEntityTableOrderingComposer get ownerId {
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.ownerId,
referencedTable: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => i5.$$UserEntityTableOrderingComposer(
$db: $db,
$table: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return composer;
}
i7.$$RemoteAssetEntityTableOrderingComposer get thumbnailAssetId {
final i7.$$RemoteAssetEntityTableOrderingComposer composer =
i5.$$RemoteAssetEntityTableOrderingComposer get thumbnailAssetId {
final i5.$$RemoteAssetEntityTableOrderingComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.thumbnailAssetId,
referencedTable: i6.ReadDatabaseContainer(
$db,
).resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity'),
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => i7.$$RemoteAssetEntityTableOrderingComposer(
}) => i5.$$RemoteAssetEntityTableOrderingComposer(
$db: $db,
$table: i6.ReadDatabaseContainer(
$db,
).resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity'),
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@ -351,52 +261,25 @@ class $$RemoteAlbumEntityTableAnnotationComposer
i0.GeneratedColumnWithTypeConverter<i2.AlbumAssetOrder, int> get order =>
$composableBuilder(column: $table.order, builder: (column) => column);
i5.$$UserEntityTableAnnotationComposer get ownerId {
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.ownerId,
referencedTable: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => i5.$$UserEntityTableAnnotationComposer(
$db: $db,
$table: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return composer;
}
i7.$$RemoteAssetEntityTableAnnotationComposer get thumbnailAssetId {
final i7.$$RemoteAssetEntityTableAnnotationComposer composer =
i5.$$RemoteAssetEntityTableAnnotationComposer get thumbnailAssetId {
final i5.$$RemoteAssetEntityTableAnnotationComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.thumbnailAssetId,
referencedTable: i6.ReadDatabaseContainer(
$db,
).resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity'),
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => i7.$$RemoteAssetEntityTableAnnotationComposer(
}) => i5.$$RemoteAssetEntityTableAnnotationComposer(
$db: $db,
$table: i6.ReadDatabaseContainer(
$db,
).resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity'),
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@ -420,7 +303,7 @@ class $$RemoteAlbumEntityTableTableManager
$$RemoteAlbumEntityTableUpdateCompanionBuilder,
(i1.RemoteAlbumEntityData, i1.$$RemoteAlbumEntityTableReferences),
i1.RemoteAlbumEntityData,
i0.PrefetchHooks Function({bool ownerId, bool thumbnailAssetId})
i0.PrefetchHooks Function({bool thumbnailAssetId})
> {
$$RemoteAlbumEntityTableTableManager(
i0.GeneratedDatabase db,
@ -445,7 +328,6 @@ class $$RemoteAlbumEntityTableTableManager
i0.Value<String> description = const i0.Value.absent(),
i0.Value<DateTime> createdAt = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<String> ownerId = const i0.Value.absent(),
i0.Value<String?> thumbnailAssetId = const i0.Value.absent(),
i0.Value<bool> isActivityEnabled = const i0.Value.absent(),
i0.Value<i2.AlbumAssetOrder> order = const i0.Value.absent(),
@ -455,7 +337,6 @@ class $$RemoteAlbumEntityTableTableManager
description: description,
createdAt: createdAt,
updatedAt: updatedAt,
ownerId: ownerId,
thumbnailAssetId: thumbnailAssetId,
isActivityEnabled: isActivityEnabled,
order: order,
@ -467,7 +348,6 @@ class $$RemoteAlbumEntityTableTableManager
i0.Value<String> description = const i0.Value.absent(),
i0.Value<DateTime> createdAt = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
required String ownerId,
i0.Value<String?> thumbnailAssetId = const i0.Value.absent(),
i0.Value<bool> isActivityEnabled = const i0.Value.absent(),
required i2.AlbumAssetOrder order,
@ -477,7 +357,6 @@ class $$RemoteAlbumEntityTableTableManager
description: description,
createdAt: createdAt,
updatedAt: updatedAt,
ownerId: ownerId,
thumbnailAssetId: thumbnailAssetId,
isActivityEnabled: isActivityEnabled,
order: order,
@ -490,7 +369,7 @@ class $$RemoteAlbumEntityTableTableManager
),
)
.toList(),
prefetchHooksCallback: ({ownerId = false, thumbnailAssetId = false}) {
prefetchHooksCallback: ({thumbnailAssetId = false}) {
return i0.PrefetchHooks(
db: db,
explicitlyWatchedTables: [],
@ -510,21 +389,6 @@ class $$RemoteAlbumEntityTableTableManager
dynamic
>
>(state) {
if (ownerId) {
state =
state.withJoin(
currentTable: table,
currentColumn: table.ownerId,
referencedTable: i1
.$$RemoteAlbumEntityTableReferences
._ownerIdTable(db),
referencedColumn: i1
.$$RemoteAlbumEntityTableReferences
._ownerIdTable(db)
.id,
)
as T;
}
if (thumbnailAssetId) {
state =
state.withJoin(
@ -564,12 +428,8 @@ typedef $$RemoteAlbumEntityTableProcessedTableManager =
$$RemoteAlbumEntityTableUpdateCompanionBuilder,
(i1.RemoteAlbumEntityData, i1.$$RemoteAlbumEntityTableReferences),
i1.RemoteAlbumEntityData,
i0.PrefetchHooks Function({bool ownerId, bool thumbnailAssetId})
i0.PrefetchHooks Function({bool thumbnailAssetId})
>;
i0.Index get idxRemoteAlbumOwnerId => i0.Index(
'idx_remote_album_owner_id',
'CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)',
);
class $RemoteAlbumEntityTable extends i3.RemoteAlbumEntity
with i0.TableInfo<$RemoteAlbumEntityTable, i1.RemoteAlbumEntityData> {
@ -636,20 +496,6 @@ class $RemoteAlbumEntityTable extends i3.RemoteAlbumEntity
requiredDuringInsert: false,
defaultValue: i4.currentDateAndTime,
);
static const i0.VerificationMeta _ownerIdMeta = const i0.VerificationMeta(
'ownerId',
);
@override
late final i0.GeneratedColumn<String> ownerId = i0.GeneratedColumn<String>(
'owner_id',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE',
),
);
static const i0.VerificationMeta _thumbnailAssetIdMeta =
const i0.VerificationMeta('thumbnailAssetId');
@override
@ -698,7 +544,6 @@ class $RemoteAlbumEntityTable extends i3.RemoteAlbumEntity
description,
createdAt,
updatedAt,
ownerId,
thumbnailAssetId,
isActivityEnabled,
order,
@ -749,14 +594,6 @@ class $RemoteAlbumEntityTable extends i3.RemoteAlbumEntity
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta),
);
}
if (data.containsKey('owner_id')) {
context.handle(
_ownerIdMeta,
ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta),
);
} else if (isInserting) {
context.missing(_ownerIdMeta);
}
if (data.containsKey('thumbnail_asset_id')) {
context.handle(
_thumbnailAssetIdMeta,
@ -807,10 +644,6 @@ class $RemoteAlbumEntityTable extends i3.RemoteAlbumEntity
i0.DriftSqlType.dateTime,
data['${effectivePrefix}updated_at'],
)!,
ownerId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}owner_id'],
)!,
thumbnailAssetId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}thumbnail_asset_id'],
@ -850,7 +683,6 @@ class RemoteAlbumEntityData extends i0.DataClass
final String description;
final DateTime createdAt;
final DateTime updatedAt;
final String ownerId;
final String? thumbnailAssetId;
final bool isActivityEnabled;
final i2.AlbumAssetOrder order;
@ -860,7 +692,6 @@ class RemoteAlbumEntityData extends i0.DataClass
required this.description,
required this.createdAt,
required this.updatedAt,
required this.ownerId,
this.thumbnailAssetId,
required this.isActivityEnabled,
required this.order,
@ -873,7 +704,6 @@ class RemoteAlbumEntityData extends i0.DataClass
map['description'] = i0.Variable<String>(description);
map['created_at'] = i0.Variable<DateTime>(createdAt);
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
map['owner_id'] = i0.Variable<String>(ownerId);
if (!nullToAbsent || thumbnailAssetId != null) {
map['thumbnail_asset_id'] = i0.Variable<String>(thumbnailAssetId);
}
@ -897,7 +727,6 @@ class RemoteAlbumEntityData extends i0.DataClass
description: serializer.fromJson<String>(json['description']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
ownerId: serializer.fromJson<String>(json['ownerId']),
thumbnailAssetId: serializer.fromJson<String?>(json['thumbnailAssetId']),
isActivityEnabled: serializer.fromJson<bool>(json['isActivityEnabled']),
order: i1.$RemoteAlbumEntityTable.$converterorder.fromJson(
@ -914,7 +743,6 @@ class RemoteAlbumEntityData extends i0.DataClass
'description': serializer.toJson<String>(description),
'createdAt': serializer.toJson<DateTime>(createdAt),
'updatedAt': serializer.toJson<DateTime>(updatedAt),
'ownerId': serializer.toJson<String>(ownerId),
'thumbnailAssetId': serializer.toJson<String?>(thumbnailAssetId),
'isActivityEnabled': serializer.toJson<bool>(isActivityEnabled),
'order': serializer.toJson<int>(
@ -929,7 +757,6 @@ class RemoteAlbumEntityData extends i0.DataClass
String? description,
DateTime? createdAt,
DateTime? updatedAt,
String? ownerId,
i0.Value<String?> thumbnailAssetId = const i0.Value.absent(),
bool? isActivityEnabled,
i2.AlbumAssetOrder? order,
@ -939,7 +766,6 @@ class RemoteAlbumEntityData extends i0.DataClass
description: description ?? this.description,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
ownerId: ownerId ?? this.ownerId,
thumbnailAssetId: thumbnailAssetId.present
? thumbnailAssetId.value
: this.thumbnailAssetId,
@ -955,7 +781,6 @@ class RemoteAlbumEntityData extends i0.DataClass
: this.description,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId,
thumbnailAssetId: data.thumbnailAssetId.present
? data.thumbnailAssetId.value
: this.thumbnailAssetId,
@ -974,7 +799,6 @@ class RemoteAlbumEntityData extends i0.DataClass
..write('description: $description, ')
..write('createdAt: $createdAt, ')
..write('updatedAt: $updatedAt, ')
..write('ownerId: $ownerId, ')
..write('thumbnailAssetId: $thumbnailAssetId, ')
..write('isActivityEnabled: $isActivityEnabled, ')
..write('order: $order')
@ -989,7 +813,6 @@ class RemoteAlbumEntityData extends i0.DataClass
description,
createdAt,
updatedAt,
ownerId,
thumbnailAssetId,
isActivityEnabled,
order,
@ -1003,7 +826,6 @@ class RemoteAlbumEntityData extends i0.DataClass
other.description == this.description &&
other.createdAt == this.createdAt &&
other.updatedAt == this.updatedAt &&
other.ownerId == this.ownerId &&
other.thumbnailAssetId == this.thumbnailAssetId &&
other.isActivityEnabled == this.isActivityEnabled &&
other.order == this.order);
@ -1016,7 +838,6 @@ class RemoteAlbumEntityCompanion
final i0.Value<String> description;
final i0.Value<DateTime> createdAt;
final i0.Value<DateTime> updatedAt;
final i0.Value<String> ownerId;
final i0.Value<String?> thumbnailAssetId;
final i0.Value<bool> isActivityEnabled;
final i0.Value<i2.AlbumAssetOrder> order;
@ -1026,7 +847,6 @@ class RemoteAlbumEntityCompanion
this.description = const i0.Value.absent(),
this.createdAt = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
this.ownerId = const i0.Value.absent(),
this.thumbnailAssetId = const i0.Value.absent(),
this.isActivityEnabled = const i0.Value.absent(),
this.order = const i0.Value.absent(),
@ -1037,13 +857,11 @@ class RemoteAlbumEntityCompanion
this.description = const i0.Value.absent(),
this.createdAt = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
required String ownerId,
this.thumbnailAssetId = const i0.Value.absent(),
this.isActivityEnabled = const i0.Value.absent(),
required i2.AlbumAssetOrder order,
}) : id = i0.Value(id),
name = i0.Value(name),
ownerId = i0.Value(ownerId),
order = i0.Value(order);
static i0.Insertable<i1.RemoteAlbumEntityData> custom({
i0.Expression<String>? id,
@ -1051,7 +869,6 @@ class RemoteAlbumEntityCompanion
i0.Expression<String>? description,
i0.Expression<DateTime>? createdAt,
i0.Expression<DateTime>? updatedAt,
i0.Expression<String>? ownerId,
i0.Expression<String>? thumbnailAssetId,
i0.Expression<bool>? isActivityEnabled,
i0.Expression<int>? order,
@ -1062,7 +879,6 @@ class RemoteAlbumEntityCompanion
if (description != null) 'description': description,
if (createdAt != null) 'created_at': createdAt,
if (updatedAt != null) 'updated_at': updatedAt,
if (ownerId != null) 'owner_id': ownerId,
if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId,
if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled,
if (order != null) 'order': order,
@ -1075,7 +891,6 @@ class RemoteAlbumEntityCompanion
i0.Value<String>? description,
i0.Value<DateTime>? createdAt,
i0.Value<DateTime>? updatedAt,
i0.Value<String>? ownerId,
i0.Value<String?>? thumbnailAssetId,
i0.Value<bool>? isActivityEnabled,
i0.Value<i2.AlbumAssetOrder>? order,
@ -1086,7 +901,6 @@ class RemoteAlbumEntityCompanion
description: description ?? this.description,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
ownerId: ownerId ?? this.ownerId,
thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId,
isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled,
order: order ?? this.order,
@ -1111,9 +925,6 @@ class RemoteAlbumEntityCompanion
if (updatedAt.present) {
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
}
if (ownerId.present) {
map['owner_id'] = i0.Variable<String>(ownerId.value);
}
if (thumbnailAssetId.present) {
map['thumbnail_asset_id'] = i0.Variable<String>(thumbnailAssetId.value);
}
@ -1136,7 +947,6 @@ class RemoteAlbumEntityCompanion
..write('description: $description, ')
..write('createdAt: $createdAt, ')
..write('updatedAt: $updatedAt, ')
..write('ownerId: $ownerId, ')
..write('thumbnailAssetId: $thumbnailAssetId, ')
..write('isActivityEnabled: $isActivityEnabled, ')
..write('order: $order')

View File

@ -66,7 +66,7 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
type: type,
createdAt: createdAt,
updatedAt: updatedAt,
durationInSeconds: durationInSeconds,
durationMs: durationMs,
isFavorite: isFavorite,
height: height,
width: width,

View File

@ -19,7 +19,7 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder =
i0.Value<DateTime> updatedAt,
i0.Value<int?> width,
i0.Value<int?> height,
i0.Value<int?> durationInSeconds,
i0.Value<int?> durationMs,
required String id,
required String checksum,
i0.Value<bool> isFavorite,
@ -41,7 +41,7 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder =
i0.Value<DateTime> updatedAt,
i0.Value<int?> width,
i0.Value<int?> height,
i0.Value<int?> durationInSeconds,
i0.Value<int?> durationMs,
i0.Value<String> id,
i0.Value<String> checksum,
i0.Value<bool> isFavorite,
@ -142,8 +142,8 @@ class $$RemoteAssetEntityTableFilterComposer
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<int> get durationInSeconds => $composableBuilder(
column: $table.durationInSeconds,
i0.ColumnFilters<int> get durationMs => $composableBuilder(
column: $table.durationMs,
builder: (column) => i0.ColumnFilters(column),
);
@ -270,8 +270,8 @@ class $$RemoteAssetEntityTableOrderingComposer
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<int> get durationInSeconds => $composableBuilder(
column: $table.durationInSeconds,
i0.ColumnOrderings<int> get durationMs => $composableBuilder(
column: $table.durationMs,
builder: (column) => i0.ColumnOrderings(column),
);
@ -385,8 +385,8 @@ class $$RemoteAssetEntityTableAnnotationComposer
i0.GeneratedColumn<int> get height =>
$composableBuilder(column: $table.height, builder: (column) => column);
i0.GeneratedColumn<int> get durationInSeconds => $composableBuilder(
column: $table.durationInSeconds,
i0.GeneratedColumn<int> get durationMs => $composableBuilder(
column: $table.durationMs,
builder: (column) => column,
);
@ -499,7 +499,7 @@ class $$RemoteAssetEntityTableTableManager
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<int?> width = const i0.Value.absent(),
i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
i0.Value<int?> durationMs = const i0.Value.absent(),
i0.Value<String> id = const i0.Value.absent(),
i0.Value<String> checksum = const i0.Value.absent(),
i0.Value<bool> isFavorite = const i0.Value.absent(),
@ -520,7 +520,7 @@ class $$RemoteAssetEntityTableTableManager
updatedAt: updatedAt,
width: width,
height: height,
durationInSeconds: durationInSeconds,
durationMs: durationMs,
id: id,
checksum: checksum,
isFavorite: isFavorite,
@ -542,7 +542,7 @@ class $$RemoteAssetEntityTableTableManager
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<int?> width = const i0.Value.absent(),
i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
i0.Value<int?> durationMs = const i0.Value.absent(),
required String id,
required String checksum,
i0.Value<bool> isFavorite = const i0.Value.absent(),
@ -562,7 +562,7 @@ class $$RemoteAssetEntityTableTableManager
updatedAt: updatedAt,
width: width,
height: height,
durationInSeconds: durationInSeconds,
durationMs: durationMs,
id: id,
checksum: checksum,
isFavorite: isFavorite,
@ -724,17 +724,17 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
);
static const i0.VerificationMeta _durationInSecondsMeta =
const i0.VerificationMeta('durationInSeconds');
static const i0.VerificationMeta _durationMsMeta = const i0.VerificationMeta(
'durationMs',
);
@override
late final i0.GeneratedColumn<int> durationInSeconds =
i0.GeneratedColumn<int>(
'duration_in_seconds',
aliasedName,
true,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
);
late final i0.GeneratedColumn<int> durationMs = i0.GeneratedColumn<int>(
'duration_ms',
aliasedName,
true,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
);
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
@override
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
@ -886,7 +886,7 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
updatedAt,
width,
height,
durationInSeconds,
durationMs,
id,
checksum,
isFavorite,
@ -944,13 +944,10 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
height.isAcceptableOrUnknown(data['height']!, _heightMeta),
);
}
if (data.containsKey('duration_in_seconds')) {
if (data.containsKey('duration_ms')) {
context.handle(
_durationInSecondsMeta,
durationInSeconds.isAcceptableOrUnknown(
data['duration_in_seconds']!,
_durationInSecondsMeta,
),
_durationMsMeta,
durationMs.isAcceptableOrUnknown(data['duration_ms']!, _durationMsMeta),
);
}
if (data.containsKey('id')) {
@ -1066,9 +1063,9 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
i0.DriftSqlType.int,
data['${effectivePrefix}height'],
),
durationInSeconds: attachedDatabase.typeMapping.read(
durationMs: attachedDatabase.typeMapping.read(
i0.DriftSqlType.int,
data['${effectivePrefix}duration_in_seconds'],
data['${effectivePrefix}duration_ms'],
),
id: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
@ -1148,7 +1145,7 @@ class RemoteAssetEntityData extends i0.DataClass
final DateTime updatedAt;
final int? width;
final int? height;
final int? durationInSeconds;
final int? durationMs;
final String id;
final String checksum;
final bool isFavorite;
@ -1168,7 +1165,7 @@ class RemoteAssetEntityData extends i0.DataClass
required this.updatedAt,
this.width,
this.height,
this.durationInSeconds,
this.durationMs,
required this.id,
required this.checksum,
required this.isFavorite,
@ -1199,8 +1196,8 @@ class RemoteAssetEntityData extends i0.DataClass
if (!nullToAbsent || height != null) {
map['height'] = i0.Variable<int>(height);
}
if (!nullToAbsent || durationInSeconds != null) {
map['duration_in_seconds'] = i0.Variable<int>(durationInSeconds);
if (!nullToAbsent || durationMs != null) {
map['duration_ms'] = i0.Variable<int>(durationMs);
}
map['id'] = i0.Variable<String>(id);
map['checksum'] = i0.Variable<String>(checksum);
@ -1247,7 +1244,7 @@ class RemoteAssetEntityData extends i0.DataClass
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
width: serializer.fromJson<int?>(json['width']),
height: serializer.fromJson<int?>(json['height']),
durationInSeconds: serializer.fromJson<int?>(json['durationInSeconds']),
durationMs: serializer.fromJson<int?>(json['durationMs']),
id: serializer.fromJson<String>(json['id']),
checksum: serializer.fromJson<String>(json['checksum']),
isFavorite: serializer.fromJson<bool>(json['isFavorite']),
@ -1276,7 +1273,7 @@ class RemoteAssetEntityData extends i0.DataClass
'updatedAt': serializer.toJson<DateTime>(updatedAt),
'width': serializer.toJson<int?>(width),
'height': serializer.toJson<int?>(height),
'durationInSeconds': serializer.toJson<int?>(durationInSeconds),
'durationMs': serializer.toJson<int?>(durationMs),
'id': serializer.toJson<String>(id),
'checksum': serializer.toJson<String>(checksum),
'isFavorite': serializer.toJson<bool>(isFavorite),
@ -1301,7 +1298,7 @@ class RemoteAssetEntityData extends i0.DataClass
DateTime? updatedAt,
i0.Value<int?> width = const i0.Value.absent(),
i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
i0.Value<int?> durationMs = const i0.Value.absent(),
String? id,
String? checksum,
bool? isFavorite,
@ -1321,9 +1318,7 @@ class RemoteAssetEntityData extends i0.DataClass
updatedAt: updatedAt ?? this.updatedAt,
width: width.present ? width.value : this.width,
height: height.present ? height.value : this.height,
durationInSeconds: durationInSeconds.present
? durationInSeconds.value
: this.durationInSeconds,
durationMs: durationMs.present ? durationMs.value : this.durationMs,
id: id ?? this.id,
checksum: checksum ?? this.checksum,
isFavorite: isFavorite ?? this.isFavorite,
@ -1349,9 +1344,9 @@ class RemoteAssetEntityData extends i0.DataClass
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
width: data.width.present ? data.width.value : this.width,
height: data.height.present ? data.height.value : this.height,
durationInSeconds: data.durationInSeconds.present
? data.durationInSeconds.value
: this.durationInSeconds,
durationMs: data.durationMs.present
? data.durationMs.value
: this.durationMs,
id: data.id.present ? data.id.value : this.id,
checksum: data.checksum.present ? data.checksum.value : this.checksum,
isFavorite: data.isFavorite.present
@ -1384,7 +1379,7 @@ class RemoteAssetEntityData extends i0.DataClass
..write('updatedAt: $updatedAt, ')
..write('width: $width, ')
..write('height: $height, ')
..write('durationInSeconds: $durationInSeconds, ')
..write('durationMs: $durationMs, ')
..write('id: $id, ')
..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite, ')
@ -1409,7 +1404,7 @@ class RemoteAssetEntityData extends i0.DataClass
updatedAt,
width,
height,
durationInSeconds,
durationMs,
id,
checksum,
isFavorite,
@ -1433,7 +1428,7 @@ class RemoteAssetEntityData extends i0.DataClass
other.updatedAt == this.updatedAt &&
other.width == this.width &&
other.height == this.height &&
other.durationInSeconds == this.durationInSeconds &&
other.durationMs == this.durationMs &&
other.id == this.id &&
other.checksum == this.checksum &&
other.isFavorite == this.isFavorite &&
@ -1456,7 +1451,7 @@ class RemoteAssetEntityCompanion
final i0.Value<DateTime> updatedAt;
final i0.Value<int?> width;
final i0.Value<int?> height;
final i0.Value<int?> durationInSeconds;
final i0.Value<int?> durationMs;
final i0.Value<String> id;
final i0.Value<String> checksum;
final i0.Value<bool> isFavorite;
@ -1476,7 +1471,7 @@ class RemoteAssetEntityCompanion
this.updatedAt = const i0.Value.absent(),
this.width = const i0.Value.absent(),
this.height = const i0.Value.absent(),
this.durationInSeconds = const i0.Value.absent(),
this.durationMs = const i0.Value.absent(),
this.id = const i0.Value.absent(),
this.checksum = const i0.Value.absent(),
this.isFavorite = const i0.Value.absent(),
@ -1497,7 +1492,7 @@ class RemoteAssetEntityCompanion
this.updatedAt = const i0.Value.absent(),
this.width = const i0.Value.absent(),
this.height = const i0.Value.absent(),
this.durationInSeconds = const i0.Value.absent(),
this.durationMs = const i0.Value.absent(),
required String id,
required String checksum,
this.isFavorite = const i0.Value.absent(),
@ -1523,7 +1518,7 @@ class RemoteAssetEntityCompanion
i0.Expression<DateTime>? updatedAt,
i0.Expression<int>? width,
i0.Expression<int>? height,
i0.Expression<int>? durationInSeconds,
i0.Expression<int>? durationMs,
i0.Expression<String>? id,
i0.Expression<String>? checksum,
i0.Expression<bool>? isFavorite,
@ -1544,7 +1539,7 @@ class RemoteAssetEntityCompanion
if (updatedAt != null) 'updated_at': updatedAt,
if (width != null) 'width': width,
if (height != null) 'height': height,
if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds,
if (durationMs != null) 'duration_ms': durationMs,
if (id != null) 'id': id,
if (checksum != null) 'checksum': checksum,
if (isFavorite != null) 'is_favorite': isFavorite,
@ -1567,7 +1562,7 @@ class RemoteAssetEntityCompanion
i0.Value<DateTime>? updatedAt,
i0.Value<int?>? width,
i0.Value<int?>? height,
i0.Value<int?>? durationInSeconds,
i0.Value<int?>? durationMs,
i0.Value<String>? id,
i0.Value<String>? checksum,
i0.Value<bool>? isFavorite,
@ -1588,7 +1583,7 @@ class RemoteAssetEntityCompanion
updatedAt: updatedAt ?? this.updatedAt,
width: width ?? this.width,
height: height ?? this.height,
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
durationMs: durationMs ?? this.durationMs,
id: id ?? this.id,
checksum: checksum ?? this.checksum,
isFavorite: isFavorite ?? this.isFavorite,
@ -1627,8 +1622,8 @@ class RemoteAssetEntityCompanion
if (height.present) {
map['height'] = i0.Variable<int>(height.value);
}
if (durationInSeconds.present) {
map['duration_in_seconds'] = i0.Variable<int>(durationInSeconds.value);
if (durationMs.present) {
map['duration_ms'] = i0.Variable<int>(durationMs.value);
}
if (id.present) {
map['id'] = i0.Variable<String>(id.value);
@ -1680,7 +1675,7 @@ class RemoteAssetEntityCompanion
..write('updatedAt: $updatedAt, ')
..write('width: $width, ')
..write('height: $height, ')
..write('durationInSeconds: $durationInSeconds, ')
..write('durationMs: $durationMs, ')
..write('id: $id, ')
..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite, ')

View File

@ -42,7 +42,7 @@ extension TrashedLocalAssetEntityDataDomainExtension on TrashedLocalAssetEntityD
type: type,
createdAt: createdAt,
updatedAt: updatedAt,
durationInSeconds: durationInSeconds,
durationMs: durationMs,
isFavorite: isFavorite,
height: height,
width: width,

View File

@ -16,7 +16,7 @@ typedef $$TrashedLocalAssetEntityTableCreateCompanionBuilder =
i0.Value<DateTime> updatedAt,
i0.Value<int?> width,
i0.Value<int?> height,
i0.Value<int?> durationInSeconds,
i0.Value<int?> durationMs,
required String id,
required String albumId,
i0.Value<String?> checksum,
@ -33,7 +33,7 @@ typedef $$TrashedLocalAssetEntityTableUpdateCompanionBuilder =
i0.Value<DateTime> updatedAt,
i0.Value<int?> width,
i0.Value<int?> height,
i0.Value<int?> durationInSeconds,
i0.Value<int?> durationMs,
i0.Value<String> id,
i0.Value<String> albumId,
i0.Value<String?> checksum,
@ -84,8 +84,8 @@ class $$TrashedLocalAssetEntityTableFilterComposer
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<int> get durationInSeconds => $composableBuilder(
column: $table.durationInSeconds,
i0.ColumnFilters<int> get durationMs => $composableBuilder(
column: $table.durationMs,
builder: (column) => i0.ColumnFilters(column),
);
@ -171,8 +171,8 @@ class $$TrashedLocalAssetEntityTableOrderingComposer
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<int> get durationInSeconds => $composableBuilder(
column: $table.durationInSeconds,
i0.ColumnOrderings<int> get durationMs => $composableBuilder(
column: $table.durationMs,
builder: (column) => i0.ColumnOrderings(column),
);
@ -240,8 +240,8 @@ class $$TrashedLocalAssetEntityTableAnnotationComposer
i0.GeneratedColumn<int> get height =>
$composableBuilder(column: $table.height, builder: (column) => column);
i0.GeneratedColumn<int> get durationInSeconds => $composableBuilder(
column: $table.durationInSeconds,
i0.GeneratedColumn<int> get durationMs => $composableBuilder(
column: $table.durationMs,
builder: (column) => column,
);
@ -326,7 +326,7 @@ class $$TrashedLocalAssetEntityTableTableManager
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<int?> width = const i0.Value.absent(),
i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
i0.Value<int?> durationMs = const i0.Value.absent(),
i0.Value<String> id = const i0.Value.absent(),
i0.Value<String> albumId = const i0.Value.absent(),
i0.Value<String?> checksum = const i0.Value.absent(),
@ -342,7 +342,7 @@ class $$TrashedLocalAssetEntityTableTableManager
updatedAt: updatedAt,
width: width,
height: height,
durationInSeconds: durationInSeconds,
durationMs: durationMs,
id: id,
albumId: albumId,
checksum: checksum,
@ -359,7 +359,7 @@ class $$TrashedLocalAssetEntityTableTableManager
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<int?> width = const i0.Value.absent(),
i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
i0.Value<int?> durationMs = const i0.Value.absent(),
required String id,
required String albumId,
i0.Value<String?> checksum = const i0.Value.absent(),
@ -375,7 +375,7 @@ class $$TrashedLocalAssetEntityTableTableManager
updatedAt: updatedAt,
width: width,
height: height,
durationInSeconds: durationInSeconds,
durationMs: durationMs,
id: id,
albumId: albumId,
checksum: checksum,
@ -498,17 +498,17 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
);
static const i0.VerificationMeta _durationInSecondsMeta =
const i0.VerificationMeta('durationInSeconds');
static const i0.VerificationMeta _durationMsMeta = const i0.VerificationMeta(
'durationMs',
);
@override
late final i0.GeneratedColumn<int> durationInSeconds =
i0.GeneratedColumn<int>(
'duration_in_seconds',
aliasedName,
true,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
);
late final i0.GeneratedColumn<int> durationMs = i0.GeneratedColumn<int>(
'duration_ms',
aliasedName,
true,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
);
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
@override
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
@ -599,7 +599,7 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
updatedAt,
width,
height,
durationInSeconds,
durationMs,
id,
albumId,
checksum,
@ -652,13 +652,10 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
height.isAcceptableOrUnknown(data['height']!, _heightMeta),
);
}
if (data.containsKey('duration_in_seconds')) {
if (data.containsKey('duration_ms')) {
context.handle(
_durationInSecondsMeta,
durationInSeconds.isAcceptableOrUnknown(
data['duration_in_seconds']!,
_durationInSecondsMeta,
),
_durationMsMeta,
durationMs.isAcceptableOrUnknown(data['duration_ms']!, _durationMsMeta),
);
}
if (data.containsKey('id')) {
@ -733,9 +730,9 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
i0.DriftSqlType.int,
data['${effectivePrefix}height'],
),
durationInSeconds: attachedDatabase.typeMapping.read(
durationMs: attachedDatabase.typeMapping.read(
i0.DriftSqlType.int,
data['${effectivePrefix}duration_in_seconds'],
data['${effectivePrefix}duration_ms'],
),
id: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
@ -800,7 +797,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass
final DateTime updatedAt;
final int? width;
final int? height;
final int? durationInSeconds;
final int? durationMs;
final String id;
final String albumId;
final String? checksum;
@ -815,7 +812,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass
required this.updatedAt,
this.width,
this.height,
this.durationInSeconds,
this.durationMs,
required this.id,
required this.albumId,
this.checksum,
@ -841,8 +838,8 @@ class TrashedLocalAssetEntityData extends i0.DataClass
if (!nullToAbsent || height != null) {
map['height'] = i0.Variable<int>(height);
}
if (!nullToAbsent || durationInSeconds != null) {
map['duration_in_seconds'] = i0.Variable<int>(durationInSeconds);
if (!nullToAbsent || durationMs != null) {
map['duration_ms'] = i0.Variable<int>(durationMs);
}
map['id'] = i0.Variable<String>(id);
map['album_id'] = i0.Variable<String>(albumId);
@ -880,7 +877,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
width: serializer.fromJson<int?>(json['width']),
height: serializer.fromJson<int?>(json['height']),
durationInSeconds: serializer.fromJson<int?>(json['durationInSeconds']),
durationMs: serializer.fromJson<int?>(json['durationMs']),
id: serializer.fromJson<String>(json['id']),
albumId: serializer.fromJson<String>(json['albumId']),
checksum: serializer.fromJson<String?>(json['checksum']),
@ -905,7 +902,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass
'updatedAt': serializer.toJson<DateTime>(updatedAt),
'width': serializer.toJson<int?>(width),
'height': serializer.toJson<int?>(height),
'durationInSeconds': serializer.toJson<int?>(durationInSeconds),
'durationMs': serializer.toJson<int?>(durationMs),
'id': serializer.toJson<String>(id),
'albumId': serializer.toJson<String>(albumId),
'checksum': serializer.toJson<String?>(checksum),
@ -929,7 +926,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass
DateTime? updatedAt,
i0.Value<int?> width = const i0.Value.absent(),
i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
i0.Value<int?> durationMs = const i0.Value.absent(),
String? id,
String? albumId,
i0.Value<String?> checksum = const i0.Value.absent(),
@ -944,9 +941,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass
updatedAt: updatedAt ?? this.updatedAt,
width: width.present ? width.value : this.width,
height: height.present ? height.value : this.height,
durationInSeconds: durationInSeconds.present
? durationInSeconds.value
: this.durationInSeconds,
durationMs: durationMs.present ? durationMs.value : this.durationMs,
id: id ?? this.id,
albumId: albumId ?? this.albumId,
checksum: checksum.present ? checksum.value : this.checksum,
@ -965,9 +960,9 @@ class TrashedLocalAssetEntityData extends i0.DataClass
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
width: data.width.present ? data.width.value : this.width,
height: data.height.present ? data.height.value : this.height,
durationInSeconds: data.durationInSeconds.present
? data.durationInSeconds.value
: this.durationInSeconds,
durationMs: data.durationMs.present
? data.durationMs.value
: this.durationMs,
id: data.id.present ? data.id.value : this.id,
albumId: data.albumId.present ? data.albumId.value : this.albumId,
checksum: data.checksum.present ? data.checksum.value : this.checksum,
@ -993,7 +988,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass
..write('updatedAt: $updatedAt, ')
..write('width: $width, ')
..write('height: $height, ')
..write('durationInSeconds: $durationInSeconds, ')
..write('durationMs: $durationMs, ')
..write('id: $id, ')
..write('albumId: $albumId, ')
..write('checksum: $checksum, ')
@ -1013,7 +1008,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass
updatedAt,
width,
height,
durationInSeconds,
durationMs,
id,
albumId,
checksum,
@ -1032,7 +1027,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass
other.updatedAt == this.updatedAt &&
other.width == this.width &&
other.height == this.height &&
other.durationInSeconds == this.durationInSeconds &&
other.durationMs == this.durationMs &&
other.id == this.id &&
other.albumId == this.albumId &&
other.checksum == this.checksum &&
@ -1050,7 +1045,7 @@ class TrashedLocalAssetEntityCompanion
final i0.Value<DateTime> updatedAt;
final i0.Value<int?> width;
final i0.Value<int?> height;
final i0.Value<int?> durationInSeconds;
final i0.Value<int?> durationMs;
final i0.Value<String> id;
final i0.Value<String> albumId;
final i0.Value<String?> checksum;
@ -1065,7 +1060,7 @@ class TrashedLocalAssetEntityCompanion
this.updatedAt = const i0.Value.absent(),
this.width = const i0.Value.absent(),
this.height = const i0.Value.absent(),
this.durationInSeconds = const i0.Value.absent(),
this.durationMs = const i0.Value.absent(),
this.id = const i0.Value.absent(),
this.albumId = const i0.Value.absent(),
this.checksum = const i0.Value.absent(),
@ -1081,7 +1076,7 @@ class TrashedLocalAssetEntityCompanion
this.updatedAt = const i0.Value.absent(),
this.width = const i0.Value.absent(),
this.height = const i0.Value.absent(),
this.durationInSeconds = const i0.Value.absent(),
this.durationMs = const i0.Value.absent(),
required String id,
required String albumId,
this.checksum = const i0.Value.absent(),
@ -1101,7 +1096,7 @@ class TrashedLocalAssetEntityCompanion
i0.Expression<DateTime>? updatedAt,
i0.Expression<int>? width,
i0.Expression<int>? height,
i0.Expression<int>? durationInSeconds,
i0.Expression<int>? durationMs,
i0.Expression<String>? id,
i0.Expression<String>? albumId,
i0.Expression<String>? checksum,
@ -1117,7 +1112,7 @@ class TrashedLocalAssetEntityCompanion
if (updatedAt != null) 'updated_at': updatedAt,
if (width != null) 'width': width,
if (height != null) 'height': height,
if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds,
if (durationMs != null) 'duration_ms': durationMs,
if (id != null) 'id': id,
if (albumId != null) 'album_id': albumId,
if (checksum != null) 'checksum': checksum,
@ -1135,7 +1130,7 @@ class TrashedLocalAssetEntityCompanion
i0.Value<DateTime>? updatedAt,
i0.Value<int?>? width,
i0.Value<int?>? height,
i0.Value<int?>? durationInSeconds,
i0.Value<int?>? durationMs,
i0.Value<String>? id,
i0.Value<String>? albumId,
i0.Value<String?>? checksum,
@ -1151,7 +1146,7 @@ class TrashedLocalAssetEntityCompanion
updatedAt: updatedAt ?? this.updatedAt,
width: width ?? this.width,
height: height ?? this.height,
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
durationMs: durationMs ?? this.durationMs,
id: id ?? this.id,
albumId: albumId ?? this.albumId,
checksum: checksum ?? this.checksum,
@ -1185,8 +1180,8 @@ class TrashedLocalAssetEntityCompanion
if (height.present) {
map['height'] = i0.Variable<int>(height.value);
}
if (durationInSeconds.present) {
map['duration_in_seconds'] = i0.Variable<int>(durationInSeconds.value);
if (durationMs.present) {
map['duration_ms'] = i0.Variable<int>(durationMs.value);
}
if (id.present) {
map['id'] = i0.Variable<String>(id.value);
@ -1227,7 +1222,7 @@ class TrashedLocalAssetEntityCompanion
..write('updatedAt: $updatedAt, ')
..write('width: $width, ')
..write('height: $height, ')
..write('durationInSeconds: $durationInSeconds, ')
..write('durationMs: $durationMs, ')
..write('id: $id, ')
..write('albumId: $albumId, ')
..write('checksum: $checksum, ')

View File

@ -1,5 +1,3 @@
// ignore_for_file: experimental_member_use
import 'dart:async';
import 'package:drift/drift.dart';
@ -12,6 +10,7 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.dart';
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
@ -20,10 +19,12 @@ import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.dart';
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.drift.dart';
@ -83,7 +84,7 @@ class Drift extends $Drift {
}
@override
int get schemaVersion => 22;
int get schemaVersion => 24;
@override
MigrationStrategy get migration => MigrationStrategy(
@ -224,6 +225,31 @@ class Drift extends $Drift {
await m.createTable(v22.assetEditEntity);
await m.createIndex(v22.idxAssetEditAssetId);
},
from22To23: (m, v23) async {
await m.renameColumn(v23.localAssetEntity, 'duration_in_seconds', v23.localAssetEntity.durationMs);
await m.renameColumn(v23.remoteAssetEntity, 'duration_in_seconds', v23.remoteAssetEntity.durationMs);
await m.renameColumn(
v23.trashedLocalAssetEntity,
'duration_in_seconds',
v23.trashedLocalAssetEntity.durationMs,
);
await localAssetEntity.update().write(
LocalAssetEntityCompanion.custom(durationMs: v23.localAssetEntity.durationMs * const Constant(1000)),
);
await remoteAssetEntity.update().write(
RemoteAssetEntityCompanion.custom(durationMs: v23.remoteAssetEntity.durationMs * const Constant(1000)),
);
await trashedLocalAssetEntity.update().write(
TrashedLocalAssetEntityCompanion.custom(
durationMs: v23.trashedLocalAssetEntity.durationMs * const Constant(1000),
),
);
},
from23To24: (m, v24) async {
await customStatement('DROP INDEX IF EXISTS idx_remote_album_owner_id');
await m.alterTable(TableMigration(v24.remoteAlbumEntity));
},
),
);

View File

@ -105,7 +105,6 @@ abstract class $Drift extends i0.GeneratedDatabase {
localAlbumEntity,
localAlbumAssetEntity,
i7.idxLocalAlbumAssetAlbumAsset,
i5.idxRemoteAlbumOwnerId,
i4.idxLocalAssetChecksum,
i4.idxLocalAssetCloudId,
i3.idxStackPrimaryAssetId,
@ -160,15 +159,6 @@ abstract class $Drift extends i0.GeneratedDatabase {
),
result: [i0.TableUpdate('stack_entity', kind: i0.UpdateKind.delete)],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName(
'user_entity',
limitUpdateKind: i0.UpdateKind.delete,
),
result: [
i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName(
'remote_asset_entity',

File diff suppressed because it is too large Load Diff

View File

@ -297,7 +297,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
updatedAt: Value(asset.updatedAt),
width: Value(asset.width),
height: Value(asset.height),
durationInSeconds: Value(asset.durationInSeconds),
durationMs: Value(asset.durationMs),
id: asset.id,
orientation: Value(asset.orientation),
isFavorite: Value(asset.isFavorite),
@ -329,7 +329,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
updatedAt: Value(asset.updatedAt),
width: Value(asset.width),
height: Value(asset.height),
durationInSeconds: Value(asset.durationInSeconds),
durationMs: Value(asset.durationMs),
id: asset.id,
checksum: const Value(null),
orientation: Value(asset.orientation),

View File

@ -109,31 +109,40 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
return query.map((localAlbum) => localAlbum.toDto()).get();
}
Future<Map<String, List<LocalAsset>>> getAssetsFromBackupAlbums(Iterable<String> checksums) async {
if (checksums.isEmpty) {
Future<Map<String, List<LocalAsset>>> getAssetsFromBackupAlbums(Iterable<String> remoteIds) async {
if (remoteIds.isEmpty) {
return {};
}
final result = <String, List<LocalAsset>>{};
for (final slice in checksums.toSet().slices(kDriftMaxChunk)) {
for (final slice in remoteIds.toSet().slices(kDriftMaxChunk)) {
final rows =
await (_db.select(_db.localAlbumAssetEntity).join([
innerJoin(_db.localAlbumEntity, _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id)),
innerJoin(
_db.localAlbumEntity,
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
useColumns: false,
),
innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)),
innerJoin(
_db.remoteAssetEntity,
_db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum),
useColumns: false,
),
])..where(
_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) &
_db.localAssetEntity.checksum.isIn(slice),
_db.remoteAssetEntity.id.isIn(slice),
))
.get();
for (final row in rows) {
final albumId = row.readTable(_db.localAlbumAssetEntity).albumId;
final assetData = row.readTable(_db.localAssetEntity);
final asset = assetData.toDto();
final asset = row.readTable(_db.localAssetEntity).toDto();
(result[albumId] ??= <LocalAsset>[]).add(asset);
}
}
return result;
}

View File

@ -32,17 +32,23 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId),
useColumns: false,
),
leftOuterJoin(_db.userEntity, _db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId), useColumns: false),
leftOuterJoin(
_db.remoteAlbumUserEntity,
_db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id),
useColumns: false,
),
innerJoin(
_db.userEntity,
_db.userEntity.id.equalsExp(_db.remoteAlbumUserEntity.userId) &
_db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id) &
_db.remoteAlbumUserEntity.role.equalsValue(AlbumUserRole.owner),
useColumns: false,
),
]);
query
..where(_db.remoteAssetEntity.deletedAt.isNull())
..addColumns([assetCount])
..addColumns([_db.userEntity.name])
..addColumns([_db.userEntity.name, _db.userEntity.id])
..addColumns([_db.remoteAlbumUserEntity.userId.count(distinct: true)])
..groupBy([_db.remoteAlbumEntity.id]);
@ -63,6 +69,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
.readTable(_db.remoteAlbumEntity)
.toDto(
assetCount: row.read(assetCount) ?? 0,
ownerId: row.read(_db.userEntity.id)!,
ownerName: row.read(_db.userEntity.name)!,
isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 0,
),
@ -85,20 +92,22 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId),
useColumns: false,
),
leftOuterJoin(
_db.userEntity,
_db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId),
useColumns: false,
),
leftOuterJoin(
_db.remoteAlbumUserEntity,
_db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id),
useColumns: false,
),
innerJoin(
_db.userEntity,
_db.userEntity.id.equalsExp(_db.remoteAlbumUserEntity.userId) &
_db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id) &
_db.remoteAlbumUserEntity.role.equalsValue(AlbumUserRole.owner),
useColumns: false,
),
])
..where(_db.remoteAlbumEntity.id.equals(albumId) & _db.remoteAssetEntity.deletedAt.isNull())
..addColumns([assetCount])
..addColumns([_db.userEntity.name])
..addColumns([_db.userEntity.name, _db.userEntity.id])
..addColumns([_db.remoteAlbumUserEntity.userId.count(distinct: true)])
..groupBy([_db.remoteAlbumEntity.id]);
@ -108,6 +117,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
.readTable(_db.remoteAlbumEntity)
.toDto(
assetCount: row.read(assetCount) ?? 0,
ownerId: row.read(_db.userEntity.id)!,
ownerName: row.read(_db.userEntity.name)!,
isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 0,
),
@ -116,12 +126,29 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
}
Future<RemoteAlbum?> getByName(String albumName, String ownerId) {
final query = _db.remoteAlbumEntity.select()
..where((row) => row.name.equals(albumName) & row.ownerId.equals(ownerId))
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
final query = _db.remoteAlbumEntity.select().join([
innerJoin(
_db.remoteAlbumUserEntity,
_db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id) &
_db.remoteAlbumUserEntity.userId.equals(ownerId) &
_db.remoteAlbumUserEntity.role.equalsValue(AlbumUserRole.owner),
useColumns: false,
),
]);
query
..addColumns([_db.remoteAlbumUserEntity.userId])
..where(_db.remoteAlbumEntity.name.equals(albumName))
..orderBy([OrderingTerm.desc(_db.remoteAlbumEntity.createdAt)])
..limit(1);
return query.map((row) => row.toDto(ownerName: '', isShared: false)).getSingleOrNull();
return query
.map(
(row) => row
.readTable(_db.remoteAlbumEntity)
.toDto(ownerId: row.read(_db.remoteAlbumUserEntity.userId)!, ownerName: '', isShared: false),
)
.getSingleOrNull();
}
Future<void> create(RemoteAlbum album, List<String> assetIds) async {
@ -129,7 +156,6 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
final entity = RemoteAlbumEntityCompanion(
id: Value(album.id),
name: Value(album.name),
ownerId: Value(album.ownerId),
createdAt: Value(album.createdAt),
updatedAt: Value(album.updatedAt),
description: Value(album.description),
@ -140,6 +166,14 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
await _db.remoteAlbumEntity.insertOne(entity);
await _db.remoteAlbumUserEntity.insertOne(
RemoteAlbumUserEntityCompanion(
albumId: Value(album.id),
userId: Value(album.ownerId),
role: const Value(AlbumUserRole.owner),
),
);
if (assetIds.isNotEmpty) {
final albumAssets = assetIds.map(
(assetId) => RemoteAlbumAssetEntityCompanion(albumId: Value(album.id), assetId: Value(assetId)),
@ -157,7 +191,6 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
RemoteAlbumEntityCompanion(
id: Value(album.id),
name: Value(album.name),
ownerId: Value(album.ownerId),
createdAt: Value(album.createdAt),
updatedAt: Value(album.updatedAt),
description: Value(album.description),
@ -197,7 +230,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
Future<List<UserDto>> getSharedUsers(String albumId) async {
final albumUserRows = await (_db.select(
_db.remoteAlbumUserEntity,
)..where((row) => row.albumId.equals(albumId))).get();
)..where((row) => row.albumId.equals(albumId) & row.role.isNotValue(AlbumUserRole.owner.index))).get();
if (albumUserRows.isEmpty) {
return [];
@ -295,19 +328,21 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId),
useColumns: false,
),
leftOuterJoin(
_db.userEntity,
_db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId),
useColumns: false,
),
leftOuterJoin(
_db.remoteAlbumUserEntity,
_db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id),
useColumns: false,
),
innerJoin(
_db.userEntity,
_db.userEntity.id.equalsExp(_db.remoteAlbumUserEntity.userId) &
_db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id) &
_db.remoteAlbumUserEntity.role.equalsValue(AlbumUserRole.owner),
useColumns: false,
),
])
..where(_db.remoteAlbumEntity.id.equals(albumId))
..addColumns([_db.userEntity.name])
..addColumns([_db.userEntity.name, _db.userEntity.id])
..addColumns([_db.remoteAlbumUserEntity.userId.count(distinct: true)])
..groupBy([_db.remoteAlbumEntity.id]);
@ -315,6 +350,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
final album = row
.readTable(_db.remoteAlbumEntity)
.toDto(
ownerId: row.read(_db.userEntity.id)!,
ownerName: row.read(_db.userEntity.name)!,
isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 0,
);
@ -355,6 +391,37 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
return _db.managers.remoteAlbumEntity.count();
}
Future<UserDto> getOwner(String albumId) {
final query =
_db.userEntity.select().join([
innerJoin(
_db.remoteAlbumUserEntity,
_db.userEntity.id.equalsExp(_db.remoteAlbumUserEntity.userId),
useColumns: false,
),
])..where(
_db.remoteAlbumUserEntity.albumId.equals(albumId) &
_db.remoteAlbumUserEntity.role.equalsValue(AlbumUserRole.owner),
);
return query
.map(
(row) => UserDto(
id: row.read(_db.userEntity.id)!,
email: row.read(_db.userEntity.email)!,
name: row.read(_db.userEntity.name)!,
memoryEnabled: true,
inTimeline: false,
isPartnerSharedBy: false,
isPartnerSharedWith: false,
profileChangedAt: row.read(_db.userEntity.profileChangedAt)!,
hasProfileImage: row.read(_db.userEntity.hasProfileImage)!,
avatarColor: AvatarColor.values[row.read(_db.userEntity.avatarColor)!],
),
)
.getSingle();
}
Future<List<String>> getLinkedAssetIds(String userId, String localAlbumId, String remoteAlbumId) async {
// Find remote asset ids that:
// 1. Belong to the provided local album (via local_album_asset_entity)
@ -416,21 +483,23 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId),
useColumns: false,
),
leftOuterJoin(
_db.userEntity,
_db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId),
useColumns: false,
),
leftOuterJoin(
_db.remoteAlbumUserEntity,
_db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id),
useColumns: false,
),
innerJoin(
_db.userEntity,
_db.userEntity.id.equalsExp(_db.remoteAlbumUserEntity.userId) &
_db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id) &
_db.remoteAlbumUserEntity.role.equalsValue(AlbumUserRole.owner),
useColumns: false,
),
])
..where(_db.remoteAlbumEntity.id.isIn(albumIds) & _db.remoteAssetEntity.deletedAt.isNull())
..addColumns([assetCount])
..addColumns([_db.remoteAlbumUserEntity.userId.count(distinct: true)])
..addColumns([_db.userEntity.name])
..addColumns([_db.userEntity.name, _db.userEntity.id])
..groupBy([_db.remoteAlbumEntity.id]);
return query
@ -438,6 +507,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
(row) => row
.readTable(_db.remoteAlbumEntity)
.toDto(
ownerId: row.read(_db.userEntity.id)!,
ownerName: row.read(_db.userEntity.name) ?? '',
isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 0,
assetCount: row.read(assetCount) ?? 0,
@ -448,7 +518,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
}
extension on RemoteAlbumEntityData {
RemoteAlbum toDto({int assetCount = 0, required String ownerName, required bool isShared}) {
RemoteAlbum toDto({int assetCount = 0, required String ownerName, required String ownerId, required bool isShared}) {
return RemoteAlbum(
id: id,
name: name,

View File

@ -53,7 +53,10 @@ class SyncApiRepository {
SyncRequestType.partnersV1,
SyncRequestType.partnerAssetsV1,
SyncRequestType.partnerAssetExifsV1,
SyncRequestType.albumsV1,
if (serverVersion < const SemVer(major: 3, minor: 0, patch: 0))
SyncRequestType.albumsV1
else
SyncRequestType.albumsV2,
SyncRequestType.albumUsersV1,
SyncRequestType.albumAssetsV1,
SyncRequestType.albumAssetExifsV1,
@ -162,6 +165,7 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.partnerAssetExifV1: SyncAssetExifV1.fromJson,
SyncEntityType.partnerAssetExifBackfillV1: SyncAssetExifV1.fromJson,
SyncEntityType.albumV1: SyncAlbumV1.fromJson,
SyncEntityType.albumV2: SyncAlbumV2.fromJson,
SyncEntityType.albumDeleteV1: SyncAlbumDeleteV1.fromJson,
SyncEntityType.albumUserV1: SyncAlbumUserV1.fromJson,
SyncEntityType.albumUserBackfillV1: SyncAlbumUserV1.fromJson,

View File

@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/models/asset_edit.model.dart';
import 'package:immich_mobile/domain/models/memory.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart';
@ -29,7 +30,7 @@ import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole, UserMetadataKey, AssetEditAction;
import 'package:openapi/api.dart' hide AlbumUserRole, UserMetadataKey, AssetEditAction, AssetVisibility;
import 'package:openapi/api.dart' hide UserMetadataKey, AssetEditAction, AssetVisibility, AlbumUserRole;
class SyncStreamRepository extends DriftDatabaseRepository {
final Logger _logger = Logger('DriftSyncStreamRepository');
@ -190,7 +191,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
type: Value(asset.type.toAssetType()),
createdAt: Value.absentIfNull(asset.fileCreatedAt),
updatedAt: Value.absentIfNull(asset.fileModifiedAt),
durationInSeconds: Value(asset.duration?.toDuration()?.inSeconds ?? 0),
durationMs: Value(asset.duration?.toDuration()?.inMilliseconds ?? 0),
checksum: Value(asset.checksum),
isFavorite: Value(asset.isFavorite),
ownerId: Value(asset.ownerId),
@ -396,6 +397,47 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
Future<void> updateAlbumsV1(Iterable<SyncAlbumV1> data) async {
try {
await _db.transaction(() async {
await _db.batch((batch) {
for (final album in data) {
final companion = RemoteAlbumEntityCompanion(
name: Value(album.name),
description: Value(album.description),
isActivityEnabled: Value(album.isActivityEnabled),
order: Value(album.order.toAlbumAssetOrder()),
thumbnailAssetId: Value(album.thumbnailAssetId),
createdAt: Value(album.createdAt),
updatedAt: Value(album.updatedAt),
);
batch.insert(
_db.remoteAlbumEntity,
companion.copyWith(id: Value(album.id)),
onConflict: DoUpdate((_) => companion),
);
}
});
await _db.batch((batch) {
for (final album in data) {
final companion = RemoteAlbumUserEntityCompanion(
albumId: Value(album.id),
userId: Value(album.ownerId),
role: const Value(AlbumUserRole.owner),
);
batch.insert(_db.remoteAlbumUserEntity, companion, onConflict: DoUpdate((_) => companion));
}
});
});
} catch (error, stack) {
_logger.severe('Error: updateAlbumsV1', error, stack);
rethrow;
}
}
Future<void> updateAlbumsV2(Iterable<SyncAlbumV2> data) async {
try {
await _db.batch((batch) {
for (final album in data) {
@ -405,7 +447,6 @@ class SyncStreamRepository extends DriftDatabaseRepository {
isActivityEnabled: Value(album.isActivityEnabled),
order: Value(album.order.toAlbumAssetOrder()),
thumbnailAssetId: Value(album.thumbnailAssetId),
ownerId: Value(album.ownerId),
createdAt: Value(album.createdAt),
updatedAt: Value(album.updatedAt),
);
@ -418,7 +459,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
});
} catch (error, stack) {
_logger.severe('Error: updateAlbumsV1', error, stack);
_logger.severe('Error: updateAlbumsV2', error, stack);
rethrow;
}
}
@ -820,6 +861,7 @@ extension on api.AlbumUserRole {
AlbumUserRole toAlbumUserRole() => switch (this) {
api.AlbumUserRole.editor => AlbumUserRole.editor,
api.AlbumUserRole.viewer => AlbumUserRole.viewer,
api.AlbumUserRole.owner => AlbumUserRole.owner,
_ => throw Exception('Unknown AlbumUserRole value: $this'),
};
}
@ -843,18 +885,6 @@ extension on api.UserMetadataKey {
};
}
extension on String {
Duration? toDuration() {
try {
final parts = split(':').map((e) => double.parse(e).toInt()).toList(growable: false);
return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);
} catch (_) {
return null;
}
}
}
extension on UserAvatarColor {
AvatarColor? toAvatarColor() => AvatarColor.values.firstWhereOrNull((c) => c.name == value);
}

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